@gomani/dsl
v0.12.0
Published
The .goma authoring DSL: a compile-time template + TypeScript signals, with provable zero-JS-outside-islands and template-level type safety.
Maintainers
Readme
@gomani/dsl
The .goma authoring DSL. A compile-time template for
structure, TypeScript signals for logic, and the island boundary as a compiler-owned keyword —
so Gomani's core guarantees (zero-JS-by-default, island isolation) are provable, not inferred (thesis
§6.8). The compiler lowers into the already-proven island/signals runtime — one runtime, not two
— so constrained JSX remains a permanent fallback.
This package ships the complete
.gomaDSL: the parser + compiler (with the zero-JS proof), template-level type safety, editor tooling (a formatter + a language service + a reference VS Code extension intooling/gomani-vscode), coexistence (the.gomabundler seam shared bygomani buildandgomani dev), and first-class routes (a.gomaroute is a route module with a co-locatedloader, and@gomani/benchproves a.gomabuild ships ≤ the JSX build's client JS).
The .goma file
Frontmatter (a TypeScript logic block, with an optional island directive) delimited by ---, then a
template:
---
interface Props { data: { title: string; items: string[] } }
const { data } = props;
---
<main class="sv-container">
<h1>{data.title}</h1>
<ul>{#each data.items as item}<li>{item}</li>{/each}</ul>
</main>An island (interactive) declares itself with the island keyword; it is the only source of client
JavaScript:
---
island counter interaction
import { signal } from '@gomani/signals';
const count = signal(0);
---
<button class="sv-btn" onClick={() => count.value++}>Tapped {count} times</button>A route is a first-class route module: a frontmatter export const loader (or staticPaths) is
hoisted to module scope, so it co-locates its server data exactly like a .tsx route — fetched on the
server, rendered into the HTML, never a client waterfall:
---
interface Props { data: { items: string[] } }
export const loader = () => ({ items: ['drum', 'cloth'] });
const { data } = props;
---
<ul>{#each data.items as item}<li>{item}</li>{/each}</ul>What the compiler guarantees
compile(source)lowers a.gomato a TypeScript module calling@gomani/core'sjsx()/island()— the exact runtime a hand-written constrained-JSX component uses (proven by byte-identical render output).{#if}/{#each}become reactive-region thunks inside an island, static expressions in a route.analyze(source)→ the zero-JS proof. A route (noislanddirective) that contains anon*event handler is rejected — interactivity is dead on a static page. A route that passes ships zero client JavaScript, guaranteed (not inferred). This is the machine-checkable report.check(source)→ template-level type safety (a hard requirement). Template expressions are type-checked against the logic block and props:{data.titel}when the field istitlefails with a precise diagnostic located in the.gomafile, at the template line. Correct usage type-checks.
Editor tooling
formatGomani(source)— deterministic, idempotent formatting: the template is reprinted from the AST with consistent indentation (all-inline runs on one line, empty elements self-closed), and the frontmatter is reassembled.createGomaniLanguageService()— the brains an editor/LSP calls:getDiagnostics(type errors + zero-JS violations, located in the.goma),getCompletionsandgetHoverthat work inside template expressions (the cursor is mapped through the source map to TypeScript, which answers). It keeps a persistentLanguageService, so it is fast across edits.tooling/gomani-vscode— the reference VS Code extension: the TextMate grammar (embeds TypeScript in the frontmatter and{…}), the language configuration, and a thin LSP server that adapts the service. The intelligence stays framework-side and unit-tested; the editor shell is replaceable.
Coexistence with constrained JSX
.goma and hand-written constrained JSX lower to the same @gomani/core VNodes, so the two surfaces
mix in one project — adopt .goma file by file, keep JSX as a real fallback.
transformGomani(source, filename)+GOMANI_FILTER— the one place a bundler turns a.gomaimport into ajsx()/island()module.gomani build(esbuild) andgomani dev(Vite) each wrap it in a ~12-line plugin (gomaniEsbuildPluginfrom@gomani/build,gomaniVitePluginfrom@gomani/dev), so both paths compile.gomaidentically. Routes, layouts, islands, and components can be either surface.A
.gomausing a JSX island just works: import it in the frontmatter and use<Counter/>— a Capitalised tag lowers to the imported identifier.A
.tsxusing a.gomacomponent needs the ambient module once, so the import type-checks:/// <reference types="@gomani/dsl/gomani-env" />Then
import Card from './Card.goma'resolves as a component. Seeexamples/coexistfor an app that exercises every direction and builds within budget.
