gofront 0.0.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/package.json +7 -4
  3. package/src/cli-core.js +65 -0
  4. package/src/codegen/expressions.js +523 -1717
  5. package/src/codegen/index.js +576 -0
  6. package/src/codegen/runtime.js +151 -0
  7. package/src/codegen/statements.js +308 -296
  8. package/src/codegen/stdlib/builder.js +39 -0
  9. package/src/codegen/stdlib/bytes.js +65 -0
  10. package/src/codegen/stdlib/errors.js +20 -0
  11. package/src/codegen/stdlib/fmt.js +104 -0
  12. package/src/codegen/stdlib/gom.js +155 -0
  13. package/src/codegen/stdlib/html.js +20 -0
  14. package/src/codegen/stdlib/index.js +98 -0
  15. package/src/codegen/stdlib/io.js +36 -0
  16. package/src/codegen/stdlib/maps.js +45 -0
  17. package/src/codegen/stdlib/math.js +56 -0
  18. package/src/codegen/stdlib/os.js +13 -0
  19. package/src/codegen/stdlib/path.js +34 -0
  20. package/src/codegen/stdlib/rand.js +27 -0
  21. package/src/codegen/stdlib/regexp.js +54 -0
  22. package/src/codegen/stdlib/slices.js +51 -0
  23. package/src/codegen/stdlib/sort.js +31 -0
  24. package/src/codegen/stdlib/strconv.js +33 -0
  25. package/src/codegen/stdlib/strings.js +86 -0
  26. package/src/codegen/stdlib/time.js +57 -0
  27. package/src/codegen/stdlib/unicode.js +25 -0
  28. package/src/codegen/stdlib/utf8.js +26 -0
  29. package/src/codegen/templ.js +284 -0
  30. package/src/compiler.js +142 -77
  31. package/src/dts-parser.js +355 -371
  32. package/src/index.js +32 -162
  33. package/src/lexer.js +186 -272
  34. package/src/minifier.js +305 -318
  35. package/src/{parser.js → parser/declarations.js} +18 -175
  36. package/src/parser/expressions.js +51 -34
  37. package/src/parser/index.js +161 -0
  38. package/src/parser/statements.js +24 -34
  39. package/src/parser/types.js +31 -29
  40. package/src/templ-parser.js +2 -2
  41. package/src/tokens.js +109 -0
  42. package/src/typechecker/assignability.js +261 -0
  43. package/src/typechecker/expressions.js +568 -549
  44. package/src/typechecker/index.js +556 -0
  45. package/src/typechecker/resolve.js +428 -0
  46. package/src/typechecker/statements.js +270 -280
  47. package/src/typechecker/stdlib/core.js +592 -0
  48. package/src/typechecker/stdlib/extended.js +332 -0
  49. package/src/typechecker/stdlib.js +9 -1060
  50. package/src/typechecker/termination.js +86 -0
  51. package/src/typechecker/types.js +1 -0
  52. package/src/codegen.js +0 -882
  53. package/src/typechecker.js +0 -1299
@@ -0,0 +1,284 @@
1
+ // Templ (.templ file) code-generation mixin for CodeGen.
2
+ // Mounts an HTML/expression tree onto a parent DOM node by emitting plain
3
+ // document.createElement / appendChild / setAttribute calls.
4
+
5
+ import { Parser } from "../parser/index.js";
6
+ import { T, Token } from "../tokens.js";
7
+
8
+ /** @typedef {import('./index.js').CodeGen} CodeGen */
9
+
10
+ /** @type {ThisType<CodeGen>} */
11
+ export const templGenMethods = {
12
+ genTemplDecl(decl) {
13
+ const params = decl.params
14
+ .map((p, i) =>
15
+ p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
16
+ )
17
+ .join(", ");
18
+ this.line(`function ${decl.name}(${params}) {`);
19
+ this.indented(() => {
20
+ this.line(`return {Mount(___p) {`);
21
+ this.indented(() => {
22
+ this._genTemplNodes(decl.body, "___p");
23
+ });
24
+ this.line(`}};`);
25
+ });
26
+ this.line("}");
27
+ },
28
+
29
+ // Emit mount statements for an array of TemplNodes into parent variable `p`.
30
+ _genTemplNodes(nodes, p) {
31
+ if (!nodes || nodes.length === 0) return;
32
+ if (nodes.length === 1) {
33
+ this._genTemplNodeMount(nodes[0], p);
34
+ return;
35
+ }
36
+ // Wrap multiple root nodes in a fragment div? No — emit each directly.
37
+ for (const node of nodes) {
38
+ this._genTemplNodeMount(node, p);
39
+ }
40
+ },
41
+
42
+ // Emit JS to mount a single TemplNode onto parent `p`.
43
+ _genTemplNodeMount(node, p) {
44
+ switch (node.kind) {
45
+ case "TemplElement":
46
+ return this._genTemplElement(node, p);
47
+ case "TemplText":
48
+ return this.line(
49
+ `${p}.appendChild(document.createTextNode(${JSON.stringify(node.value)}));`,
50
+ );
51
+ case "TemplExpr":
52
+ return this.line(
53
+ `${p}.appendChild(document.createTextNode(String(${this._genTemplTokenExpr(node.tokens)})));`,
54
+ );
55
+ case "TemplComponent":
56
+ return this._genTemplComponentMount(node, p);
57
+ case "TemplChildren":
58
+ // children is a variadic gom.Node param — mount each child
59
+ return this.line(
60
+ `if(typeof children!=="undefined")(Array.isArray(children)?children:[...children]).forEach(___c=>___c?.Mount?.(${p}));`,
61
+ );
62
+ case "TemplIf":
63
+ return this._genTemplIf(node, p);
64
+ case "TemplFor":
65
+ return this._genTemplFor(node, p);
66
+ case "TemplSwitch":
67
+ return this._genTemplSwitch(node, p);
68
+ default:
69
+ break;
70
+ }
71
+ },
72
+
73
+ _genTemplComponentMount(node, p) {
74
+ // Special case: @templ.Raw(expr) → insertAdjacentHTML
75
+ const toks = node.tokens;
76
+ if (
77
+ toks.length >= 4 &&
78
+ toks[0]?.value === "templ" &&
79
+ toks[1]?.type === T.DOT &&
80
+ toks[2]?.value === "Raw" &&
81
+ toks[3]?.type === T.LPAREN
82
+ ) {
83
+ const argTokens = toks.slice(4, toks.length - 1);
84
+ const argJs = this._genTemplTokenExpr(argTokens);
85
+ return this.line(`${p}.insertAdjacentHTML("beforeend", ${argJs});`);
86
+ }
87
+ return this.line(
88
+ `(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p});`,
89
+ );
90
+ },
91
+
92
+ _genTemplElement(node, p) {
93
+ const { tag, attrs, children } = node;
94
+ const elVar = this._freshVar("e");
95
+ this.line(
96
+ `const ${elVar} = document.createElement(${JSON.stringify(tag)});`,
97
+ );
98
+ for (const attr of attrs) {
99
+ this._genTemplAttr(attr, elVar);
100
+ }
101
+ this._genTemplNodes(children, elVar);
102
+ this.line(`${p}.appendChild(${elVar});`);
103
+ },
104
+
105
+ _genTemplAttr(attr, el) {
106
+ const { name } = attr;
107
+ if (attr.kind === "static") {
108
+ if (name === "class") {
109
+ this.line(`${el}.className = ${JSON.stringify(attr.value)};`);
110
+ } else {
111
+ this.line(
112
+ `${el}.setAttribute(${JSON.stringify(name)}, ${JSON.stringify(attr.value)});`,
113
+ );
114
+ }
115
+ } else if (attr.kind === "expr") {
116
+ const exprJs = this._genTemplTokenExpr(attr.tokens);
117
+ if (name === "class") {
118
+ this.line(`${el}.className = ${exprJs};`);
119
+ } else {
120
+ this.line(
121
+ `${el}.setAttribute(${JSON.stringify(name)}, String(${exprJs}));`,
122
+ );
123
+ }
124
+ } else if (attr.kind === "bool") {
125
+ this.line(`${el}.setAttribute(${JSON.stringify(name)}, "");`);
126
+ } else if (attr.kind === "cond-bool") {
127
+ const exprJs = this._genTemplTokenExpr(attr.tokens);
128
+ this.line(
129
+ `if(${exprJs})${el}.setAttribute(${JSON.stringify(name)}, "");`,
130
+ );
131
+ }
132
+ },
133
+
134
+ _genTemplIf(node, p) {
135
+ const condJs = this._genTemplCondTokens(node.condTokens);
136
+ this.line(`if (${condJs}) {`);
137
+ this.indented(() => this._genTemplNodes(node.then, p));
138
+ this._genTemplIfElse(node.else_, p);
139
+ },
140
+
141
+ _genTemplIfElse(else_, p) {
142
+ if (!else_ || else_.length === 0) {
143
+ this.line(`}`);
144
+ return;
145
+ }
146
+ if (else_.length === 1 && else_[0].kind === "TemplIf") {
147
+ const elseIf = else_[0];
148
+ this.line(`} else if (${this._genTemplCondTokens(elseIf.condTokens)}) {`);
149
+ this.indented(() => this._genTemplNodes(elseIf.then, p));
150
+ this._genTemplIfElse(elseIf.else_, p);
151
+ } else {
152
+ this.line(`} else {`);
153
+ this.indented(() => this._genTemplNodes(else_, p));
154
+ this.line(`}`);
155
+ }
156
+ },
157
+
158
+ _genTemplSwitch(node, p) {
159
+ const { exprTokens, cases } = node;
160
+ if (cases.length === 0) return;
161
+ const exprJs = this._genTemplTokenExpr(exprTokens);
162
+ this.line(`switch (${exprJs}) {`);
163
+ this.indented(() => {
164
+ for (const c of cases) {
165
+ const label =
166
+ c.caseTokens === null
167
+ ? "default"
168
+ : `case ${this._genTemplTokenExpr(c.caseTokens)}`;
169
+ this.line(`${label}: {`);
170
+ this.indented(() => this._genTemplNodes(c.body, p));
171
+ this.line(`break; }`);
172
+ }
173
+ });
174
+ this.line(`}`);
175
+ },
176
+
177
+ _genTemplFor(node, p) {
178
+ // stmtTokens include the `for` keyword plus the range clause (no body).
179
+ // Strip any trailing semicolons (inserted by the Go lexer on newlines),
180
+ // then add a fake empty body `{ }` so the parser can complete the ForStmt.
181
+ let tokens = node.stmtTokens;
182
+ while (tokens.length > 0 && tokens[tokens.length - 1].type === T.SEMICOLON)
183
+ tokens = tokens.slice(0, -1);
184
+ const eofTok = new Token(T.EOF, "", 1, 1);
185
+ const lbrace = new Token(T.LBRACE, "{", 1, 1);
186
+ const rbrace = new Token(T.RBRACE, "}", 1, 1);
187
+ const semi = new Token(T.SEMICOLON, ";", 1, 1);
188
+ const withFor =
189
+ tokens[0]?.type === T.FOR
190
+ ? [...tokens, lbrace, rbrace, semi, eofTok]
191
+ : [
192
+ new Token(T.FOR, "for", 1, 1),
193
+ ...tokens,
194
+ lbrace,
195
+ rbrace,
196
+ semi,
197
+ eofTok,
198
+ ];
199
+ try {
200
+ const p2 = new Parser(withFor);
201
+ const forStmt = p2.parseFor();
202
+ this._genTemplRangeFor(forStmt, node.body, p);
203
+ } catch {
204
+ // Fallback for non-range loops: emit a comment + skip
205
+ this.line(`/* templ for: ${this._tokensToSource(tokens)} */`);
206
+ }
207
+ },
208
+
209
+ _genTemplRangeFor(forStmt, templBody, mountParent) {
210
+ const init = forStmt.init;
211
+ if (!init?.rhs?.[0] || init.rhs[0].kind !== "RangeExpr") {
212
+ // Plain for loop (cond-only or three-clause)
213
+ const condJs = forStmt.cond ? this.genExpr(forStmt.cond) : "true";
214
+ this.line(`while (${condJs}) {`);
215
+ this.indented(() => this._genTemplNodes(templBody, mountParent));
216
+ this.line("}");
217
+ return;
218
+ }
219
+ // Range for: for key, val := range iter
220
+ const range = init.rhs[0];
221
+ const iterJs = this.genExpr(range.expr);
222
+ const lhs = init.lhs.map((e) => e.name ?? "_");
223
+ const keyName = lhs[0] ?? "_";
224
+ const valName = lhs[1] ?? null;
225
+
226
+ const loopHeader = this._genTemplRangeHeader(keyName, valName, iterJs);
227
+ this.line(loopHeader);
228
+ this.indented(() => this._genTemplNodes(templBody, mountParent));
229
+ this.line("}");
230
+ },
231
+
232
+ _genTemplRangeHeader(keyName, valName, iterJs) {
233
+ if (valName === null || valName === undefined) {
234
+ return `for (const ${keyName === "_" ? "_$" : keyName} of ${iterJs}) {`;
235
+ }
236
+ if (keyName === "_" && valName === "_")
237
+ return `for (const _$ of ${iterJs}) {`;
238
+ if (keyName === "_") return `for (const ${valName} of ${iterJs}) {`;
239
+ if (valName === "_")
240
+ return `for (const [${keyName}] of Object.entries(${iterJs})) {`;
241
+ // Default to .entries() for arrays, Object.entries for objects
242
+ this._usesSliceGuard = true;
243
+ return `for (const [${keyName}, ${valName}] of __s(${iterJs}).entries()) {`;
244
+ },
245
+
246
+ // Parse a token array as a Go expression and emit JS.
247
+ _genTemplTokenExpr(tokens) {
248
+ if (!tokens || tokens.length === 0) return '""';
249
+ const eofTok = new Token(T.EOF, "", 1, 1);
250
+ const p = new Parser([...tokens, eofTok]);
251
+ const ast = p.parseExpr();
252
+ return this.genExpr(ast);
253
+ },
254
+
255
+ // Parse a token array as a Go call expression and emit JS.
256
+ _genTemplTokenCallExpr(tokens) {
257
+ if (!tokens || tokens.length === 0) return "null";
258
+ const eofTok = new Token(T.EOF, "", 1, 1);
259
+ const p = new Parser([...tokens, eofTok]);
260
+ const ast = p.parseExpr();
261
+ return this.genExpr(ast);
262
+ },
263
+
264
+ // Extract condition JS from condTokens (which may start with `if`).
265
+ _genTemplCondTokens(tokens) {
266
+ if (!tokens || tokens.length === 0) return "false";
267
+ let toks = tokens;
268
+ if (toks[0]?.type === T.IF) toks = toks.slice(1); // skip `if` keyword
269
+ const eofTok = new Token(T.EOF, "", 1, 1);
270
+ const p = new Parser([...toks, eofTok]);
271
+ const ast = p.parseExpr();
272
+ return this.genExpr(ast);
273
+ },
274
+
275
+ _tokensToSource(tokens) {
276
+ return tokens.map((t) => t.value).join(" ");
277
+ },
278
+
279
+ // Fresh variable name counter
280
+ _freshVar(prefix = "v") {
281
+ this._templVarCount = (this._templVarCount ?? 0) + 1;
282
+ return `___${prefix}${this._templVarCount}`;
283
+ },
284
+ };
package/src/compiler.js CHANGED
@@ -1,9 +1,11 @@
1
- // Multi-file GoFront package compiler.
1
+ // GoFront package compiler.
2
2
  //
3
- // compileDir(dir, options) — compile all *.go in a directory as one package
4
- // compileFiles(files, options) — compile an explicit list of .go files
3
+ // compileSingleFile(path, options) — compile one .go file
4
+ // compileDir(dir, options) — compile all *.go in a directory as one package
5
+ // compileFiles(files, options) — compile an explicit list of .go files
5
6
  //
6
- // Both return:
7
+ // compileSingleFile returns: { js } (or { tokens } / { ast } for debug modes)
8
+ // compileDir / compileFiles return:
7
9
  // {
8
10
  // pkgName: string,
9
11
  // js: string, // generated JS bundle (may include dep preamble)
@@ -17,14 +19,14 @@
17
19
 
18
20
  import { readdirSync, readFileSync, statSync } from "node:fs";
19
21
  import { basename, dirname, join, relative, resolve } from "node:path";
20
- import { CodeGen } from "./codegen.js";
22
+ import { CodeGen } from "./codegen/index.js";
21
23
  import { parseDts } from "./dts-parser.js";
22
24
  import { Lexer } from "./lexer.js";
23
- import { Parser } from "./parser.js";
25
+ import { Parser } from "./parser/index.js";
24
26
  import { isLocalPath, resolveAll, resolveGwDir } from "./resolver.js";
25
27
  import { TemplLexer } from "./templ-lexer.js";
26
28
  import { TemplParser } from "./templ-parser.js";
27
- import { TypeChecker } from "./typechecker.js";
29
+ import { TypeChecker } from "./typechecker/index.js";
28
30
 
29
31
  // ── Parse cache ──────────────────────────────────────────────
30
32
 
@@ -41,29 +43,20 @@ export function parseCacheSize() {
41
43
 
42
44
  // ── Helpers ──────────────────────────────────────────────────
43
45
 
44
- function parseFile(filePath) {
46
+ function parseGoFrontFile(filePath) {
45
47
  const mtime = statSync(filePath).mtimeMs;
46
48
  const cached = _parseCache.get(filePath);
47
49
  if (cached && cached.mtime === mtime) return cached.ast;
48
50
 
49
51
  const source = readFileSync(filePath, "utf8");
50
52
  const filename = basename(filePath);
51
- const tokens = new Lexer(source, filename).tokenize();
52
- const ast = new Parser(tokens, filename, source).parse();
53
- ast._source = source;
54
- _parseCache.set(filePath, { mtime, ast });
55
- return ast;
56
- }
57
-
58
- function parseTemplFile(filePath) {
59
- const mtime = statSync(filePath).mtimeMs;
60
- const cached = _parseCache.get(filePath);
61
- if (cached && cached.mtime === mtime) return cached.ast;
62
-
63
- const source = readFileSync(filePath, "utf8");
64
- const filename = basename(filePath);
65
- const tokens = new TemplLexer(source, filename).tokenize();
66
- const ast = new TemplParser(tokens, filename, source).parse();
53
+ const isTempl = filePath.endsWith(".templ");
54
+ const tokens = isTempl
55
+ ? new TemplLexer(source, filename).tokenize()
56
+ : new Lexer(source, filename).tokenize();
57
+ const ast = isTempl
58
+ ? new TemplParser(tokens, filename, source).parse()
59
+ : new Parser(tokens, filename, source).parse();
67
60
  ast._source = source;
68
61
  _parseCache.set(filePath, { mtime, ast });
69
62
  return ast;
@@ -76,53 +69,27 @@ function gwFilesIn(dir) {
76
69
  .map((f) => join(dir, f));
77
70
  }
78
71
 
79
- // ── Main entry points ─────────────────────────────────────────
80
-
81
- export function compileDir(dir, options = {}) {
82
- const files = gwFilesIn(dir);
83
- if (files.length === 0) throw new Error(`No .go files found in ${dir}`);
84
- return compileFiles(files, { ...options, fromDir: dir });
85
- }
86
-
87
- export function compileFiles(files, options = {}) {
88
- const fromDir = options.fromDir ?? dirname(resolve(files[0]));
89
-
90
- // ── 1. Parse ─────────────────────────────────────────────────
91
- const parseErrors = [];
92
- const programs = [];
93
- for (const f of files) {
94
- try {
95
- programs.push(f.endsWith(".templ") ? parseTemplFile(f) : parseFile(f));
96
- } catch (e) {
97
- parseErrors.push(e.message);
98
- }
99
- }
100
- if (parseErrors.length > 0) {
101
- throw new Error(parseErrors.join("\n"));
102
- }
103
-
104
- // Validate consistent package name
105
- const pkgNames = [...new Set(programs.map((p) => p.pkg.name))];
106
- if (pkgNames.length > 1)
107
- throw new Error(
108
- `Mixed package names in ${fromDir}: ${pkgNames.join(", ")}`,
109
- );
110
- const pkgName = pkgNames[0];
111
-
112
- // ── 2. Resolve imports ────────────────────────────────────────
113
- const checker = new TypeChecker();
114
- const jsImports = new Map(); // npm imports → exported names (for ESM emit)
115
- const bundledPackages = new Set(); // package names whose code is inlined
116
- const preambles = []; // JS code from compiled sub-packages
72
+ // ── Import resolution ─────────────────────────────────────────
73
+ //
74
+ // Shared by compileFiles (multi-file) and the single-file path in index.js.
75
+ // Mutates checker, jsImports, bundledPackages, and preambles in place.
117
76
 
77
+ export function resolveImports(
78
+ programs,
79
+ fromFile,
80
+ checker,
81
+ jsImports,
82
+ bundledPackages,
83
+ preambles,
84
+ ) {
85
+ const fromDir = dirname(resolve(fromFile));
118
86
  const allImports = programs.flatMap((p) => p.imports);
119
- const dummyFromFile = join(fromDir, "_dummy.go");
120
87
 
121
88
  // js: prefix — local .d.ts files
122
89
  for (const imp of allImports) {
123
90
  for (const { path, alias } of imp.imports) {
124
91
  if (!path.startsWith("js:")) continue;
125
- if (alias === "_") continue; // side-effect import — no type definitions
92
+ if (alias === "_") continue;
126
93
  const dtsPath = join(fromDir, path.slice(3));
127
94
  try {
128
95
  const { types, values } = parseDts(readFileSync(dtsPath, "utf8"));
@@ -133,12 +100,12 @@ export function compileFiles(files, options = {}) {
133
100
  }
134
101
  }
135
102
 
136
- // npm packages — exclude side-effect imports (alias "_") from type resolution
103
+ // npm packages — exclude side-effect imports from type resolution
137
104
  const allImportsNoSideEffect = allImports.map((imp) => ({
138
105
  ...imp,
139
106
  imports: imp.imports.filter(({ alias }) => alias !== "_"),
140
107
  }));
141
- const resolved = resolveAll(allImportsNoSideEffect, dummyFromFile, parseDts);
108
+ const resolved = resolveAll(allImportsNoSideEffect, fromFile, parseDts);
142
109
  for (const [path, info] of resolved) {
143
110
  if (!info) continue;
144
111
  checker.addDefinitions(info.types, info.values);
@@ -152,29 +119,20 @@ export function compileFiles(files, options = {}) {
152
119
  for (const { path, alias, _line } of imp.imports) {
153
120
  if (!isLocalPath(path) || seenLocalPaths.has(path)) continue;
154
121
  seenLocalPaths.add(path);
155
-
156
- const depDir = resolveGwDir(path, dummyFromFile);
122
+ const depDir = resolveGwDir(path, fromFile);
157
123
  if (!depDir) {
158
124
  console.error(
159
125
  `gofront: warning: cannot find local package '${path}' relative to ${fromDir}`,
160
126
  );
161
127
  continue;
162
128
  }
163
-
164
- // Recursively compile dependency
165
129
  const dep = compileDir(depDir);
166
130
  preambles.push(dep.js);
167
- if (alias === "_") continue; // side-effect import — bundle code but don't expose namespace
168
-
169
- // Dot import: flatten all exported symbols into global scope
131
+ if (alias === "_") continue;
170
132
  if (alias === ".") {
171
133
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
172
- // All dep symbols must be treated as bundled so codegen
173
- // can emit them without a qualifier. We DON'T add a
174
- // namespace, so SelectorExpr never appears for these.
175
134
  continue;
176
135
  }
177
-
178
136
  const nameUsed = alias ?? dep.pkgName;
179
137
  bundledPackages.add(nameUsed);
180
138
  checker.addPackageNamespace(
@@ -186,6 +144,113 @@ export function compileFiles(files, options = {}) {
186
144
  }
187
145
  }
188
146
  }
147
+ }
148
+
149
+ // ── Main entry points ─────────────────────────────────────────
150
+
151
+ export function compileSingleFile(inputPath, options = {}) {
152
+ const {
153
+ sourceMap = false,
154
+ outputFile = null,
155
+ dumpTokens = false,
156
+ dumpAst = false,
157
+ } = options;
158
+
159
+ let source;
160
+ try {
161
+ source = readFileSync(inputPath, "utf8");
162
+ } catch (e) {
163
+ throw new Error(`cannot read '${inputPath}': ${e.message}`);
164
+ }
165
+
166
+ const tokens = new Lexer(source, basename(inputPath)).tokenize();
167
+ if (dumpTokens) return { tokens };
168
+
169
+ const ast = new Parser(tokens, basename(inputPath), source).parse();
170
+ ast._source = source;
171
+ if (dumpAst) return { ast };
172
+
173
+ const checker = new TypeChecker();
174
+ const jsImports = new Map();
175
+ const bundledPackages = new Set();
176
+ const preambles = [];
177
+
178
+ resolveImports(
179
+ [ast],
180
+ inputPath,
181
+ checker,
182
+ jsImports,
183
+ bundledPackages,
184
+ preambles,
185
+ );
186
+
187
+ const errors = checker.check(ast);
188
+ checker.reportUnusedImports();
189
+ if (errors.length > 0) {
190
+ throw new Error(errors.map((e) => e.message).join("\n"));
191
+ }
192
+
193
+ const cg = new CodeGen(checker, jsImports, bundledPackages);
194
+ let js = cg.generate(ast);
195
+
196
+ if (sourceMap) {
197
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
198
+ const srcPath = relative(outputDir, inputPath);
199
+ const map = cg.getSourceMap(srcPath, [source]);
200
+ const b64 = Buffer.from(map).toString("base64");
201
+ js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
202
+ }
203
+
204
+ const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
205
+ return { js: output };
206
+ }
207
+
208
+ export function compileDir(dir, options = {}) {
209
+ const files = gwFilesIn(dir);
210
+ if (files.length === 0) throw new Error(`No .go files found in ${dir}`);
211
+ return compileFiles(files, { ...options, fromDir: dir });
212
+ }
213
+
214
+ export function compileFiles(files, options = {}) {
215
+ const fromDir = options.fromDir ?? dirname(resolve(files[0]));
216
+
217
+ // ── 1. Parse ─────────────────────────────────────────────────
218
+ const parseErrors = [];
219
+ const programs = [];
220
+ for (const f of files) {
221
+ try {
222
+ programs.push(parseGoFrontFile(f));
223
+ } catch (e) {
224
+ parseErrors.push(e.message);
225
+ }
226
+ }
227
+ if (parseErrors.length > 0) {
228
+ throw new Error(parseErrors.join("\n"));
229
+ }
230
+
231
+ // Validate consistent package name
232
+ const pkgNames = [...new Set(programs.map((p) => p.pkg.name))];
233
+ if (pkgNames.length > 1)
234
+ throw new Error(
235
+ `Mixed package names in ${fromDir}: ${pkgNames.join(", ")}`,
236
+ );
237
+ const pkgName = pkgNames[0];
238
+
239
+ // ── 2. Resolve imports ────────────────────────────────────────
240
+ const checker = new TypeChecker();
241
+ const jsImports = new Map(); // npm imports → exported names (for ESM emit)
242
+ const bundledPackages = new Set(); // package names whose code is inlined
243
+ const preambles = []; // JS code from compiled sub-packages
244
+
245
+ const dummyFromFile = join(fromDir, "_dummy.go");
246
+ resolveImports(
247
+ programs,
248
+ dummyFromFile,
249
+ checker,
250
+ jsImports,
251
+ bundledPackages,
252
+ preambles,
253
+ );
189
254
 
190
255
  // ── 3. Type-check ─────────────────────────────────────────────
191
256
  const errors = checker.checkAll(programs);