gofront 1.5.1 → 1.6.0

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 (60) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +50 -19
  4. package/src/backend/js/index.js +100 -55
  5. package/src/backend/js/runtime.js +102 -0
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +96 -16
  8. package/src/backend/js/stdlib/errors.js +2 -2
  9. package/src/backend/js/stdlib/fmt.js +26 -8
  10. package/src/backend/js/stdlib/index.js +3 -0
  11. package/src/backend/js/stdlib/maps.js +1 -1
  12. package/src/backend/js/stdlib/path.js +2 -2
  13. package/src/backend/js/stdlib/shared.js +17 -0
  14. package/src/backend/js/stdlib/slices.js +1 -1
  15. package/src/backend/js/stdlib/sort.js +1 -1
  16. package/src/backend/js/stdlib/strconv.js +34 -9
  17. package/src/backend/js/stdlib/strings.js +4 -1
  18. package/src/backend/js/stdlib/testing.js +2 -2
  19. package/src/backend/js/stdlib/time.js +3 -3
  20. package/src/backend/js/templ.js +1 -1
  21. package/src/backend/wasm/boundary.js +486 -33
  22. package/src/backend/wasm/emit-builtins.js +1103 -0
  23. package/src/backend/wasm/emit-exprs.js +1908 -0
  24. package/src/backend/wasm/emit-maps.js +1863 -0
  25. package/src/backend/wasm/emit-shared.js +519 -0
  26. package/src/backend/wasm/emit-stdlib.js +3006 -0
  27. package/src/backend/wasm/emit-stmts.js +2472 -0
  28. package/src/backend/wasm/emit.js +606 -5009
  29. package/src/backend/wasm/encode.js +212 -64
  30. package/src/backend/wasm/glue.js +19 -5
  31. package/src/backend/wasm/index.js +1426 -49
  32. package/src/backend/wasm/monomorph.js +711 -0
  33. package/src/backend/wasm/optimize.js +227 -0
  34. package/src/backend/wasm/types.js +159 -35
  35. package/src/backend/wasm/wat.js +49 -2
  36. package/src/cli-core.js +275 -72
  37. package/src/compiler.js +144 -21
  38. package/src/index.js +86 -115
  39. package/src/lower/boxing.js +3 -4
  40. package/src/lower/captures.js +10 -17
  41. package/src/lower/escape.js +4 -9
  42. package/src/lower/functions.js +277 -5
  43. package/src/lower/globals.js +7 -15
  44. package/src/lower/index.js +5 -2
  45. package/src/lower/ownership.js +5 -10
  46. package/src/lower/range.js +2 -5
  47. package/src/lower/walk.js +25 -0
  48. package/src/resolver.js +2 -1
  49. package/src/test-runner.js +20 -0
  50. package/src/typechecker/assignability.js +42 -1
  51. package/src/typechecker/expressions.js +142 -2
  52. package/src/typechecker/index.js +27 -1
  53. package/src/typechecker/resolve.js +35 -6
  54. package/src/typechecker/statements.js +0 -7
  55. package/src/typechecker/stdlib/core.js +17 -19
  56. package/src/typechecker/stdlib/extended.js +18 -3
  57. package/src/typechecker/stdlib/shared.js +53 -0
  58. package/src/typechecker/stdlib.js +2 -0
  59. package/src/typechecker/types.js +206 -7
  60. package/src/vendor.js +2 -2
package/src/compiler.js CHANGED
@@ -18,9 +18,13 @@
18
18
  // de-qualifies it because the dependency is inlined.
19
19
 
20
20
  import { readdirSync, readFileSync, statSync } from "node:fs";
21
- import { basename, dirname, join, relative, resolve } from "node:path";
22
- import { buildSourceMap, CodeGen } from "./backend/js/index.js";
23
- import { compileWasmModule } from "./backend/wasm/index.js";
21
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
22
+ import {
23
+ buildSourceMap,
24
+ buildWasmSourceMap,
25
+ CodeGen,
26
+ } from "./backend/js/index.js";
27
+ import { compileWasmModule, optimizeWasm } from "./backend/wasm/index.js";
24
28
  import { log } from "./colors.js";
25
29
  import { parseDts } from "./dts-parser.js";
26
30
  import { Lexer } from "./lexer.js";
@@ -67,6 +71,9 @@ function parseGoFrontFile(filePath) {
67
71
  ? new TemplParser(tokens, filename, source).parse()
68
72
  : new Parser(tokens, filename, source).parse();
69
73
  ast._source = source;
74
+ ast._path = resolve(filePath);
75
+ // compileFiles overwrites `target` with the resolved one; keep the directive.
76
+ ast._declaredTarget = ast.target;
70
77
  _parseCache.set(filePath, { mtime, ast });
71
78
  return ast;
72
79
  }
@@ -163,6 +170,48 @@ function attachSourceMap(merged, outputDir) {
163
170
  merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
164
171
  }
165
172
 
173
+ const WASM_MAP_URL = "app.wasm.map";
174
+
175
+ // Function-level `app.wasm.map` (sources relative to the output dir).
176
+ function buildWasmMap(info, outputDir) {
177
+ if (!info || info.mappings.length === 0) return null;
178
+ return buildWasmSourceMap(
179
+ info.sources.map((f) => relative(outputDir, f)),
180
+ info.mappings,
181
+ );
182
+ }
183
+
184
+ // Shared tail of compileFiles / compileSingleFile: link `app.wasm`, run
185
+ // wasm-opt when requested and produce the wasm source map.
186
+ function finishWasm(options, merged, pkgTarget, outputDir) {
187
+ const wantMap = Boolean(options.sourceMap && !options.isDependency);
188
+ let { wasm, wat, sourceMapInfo } = linkIntoBundle(
189
+ options,
190
+ merged,
191
+ pkgTarget,
192
+ {
193
+ sourceMappingURL: wantMap ? WASM_MAP_URL : null,
194
+ },
195
+ );
196
+ let wasmOptInfo = null;
197
+ let wasmSourceMap =
198
+ wantMap && wasm ? buildWasmMap(sourceMapInfo, outputDir) : null;
199
+
200
+ if (wasm && options.wasmOpt) {
201
+ const opt = optimizeWasm(wasm, {
202
+ sourceMap: Boolean(wasmSourceMap),
203
+ inputSourceMap: wasmSourceMap,
204
+ sourceMapUrl: WASM_MAP_URL,
205
+ emitWat: Boolean(options.emitWat),
206
+ });
207
+ wasm = opt.wasm;
208
+ if (opt.wat) wat = opt.wat;
209
+ wasmOptInfo = opt;
210
+ if (opt.sourceMap) wasmSourceMap = opt.sourceMap;
211
+ }
212
+ return { wasm, wat, wasmOptInfo, wasmSourceMap };
213
+ }
214
+
166
215
  // ── WASM linking ──────────────────────────────────────────────
167
216
  //
168
217
  // Every `wasm`/`both` package reached during a build registers a unit in the
@@ -205,7 +254,10 @@ function registerPackageUnit(options, unit) {
205
254
  return emitTarget;
206
255
  }
207
256
 
208
- function linkWasmUnits(units, { emitWat = false, rootTarget = "js" } = {}) {
257
+ function linkWasmUnits(
258
+ units,
259
+ { emitWat = false, rootTarget = "js", sourceMappingURL = null } = {},
260
+ ) {
209
261
  if (!units.some((u) => u.target === "wasm")) return null;
210
262
  checkLinkCollisions(units);
211
263
 
@@ -224,6 +276,7 @@ function linkWasmUnits(units, { emitWat = false, rootTarget = "js" } = {}) {
224
276
  bundledPackages,
225
277
  emitWat,
226
278
  callMain: rootTarget === "wasm",
279
+ sourceMappingURL,
227
280
  });
228
281
  }
229
282
 
@@ -291,17 +344,23 @@ function spliceFacade(merged, facade) {
291
344
  }
292
345
 
293
346
  // Root compiles only: link every registered unit into app.wasm and splice the
294
- // facade into the merged bundle. Returns `{ wasm, wat }` (nulls when there
295
- // is nothing to link).
296
- function linkIntoBundle(options, merged, pkgTarget) {
297
- if (options.isDependency) return { wasm: null, wat: null };
347
+ // facade into the merged bundle. Returns `{ wasm, wat, sourceMapInfo }`
348
+ // (nulls when there is nothing to link).
349
+ function linkIntoBundle(options, merged, pkgTarget, { sourceMappingURL } = {}) {
350
+ const none = { wasm: null, wat: null, sourceMapInfo: null };
351
+ if (options.isDependency) return none;
298
352
  const linked = linkWasmUnits(options.wasmUnits, {
299
353
  emitWat: options.emitWat,
300
354
  rootTarget: pkgTarget,
355
+ sourceMappingURL,
301
356
  });
302
- if (!linked) return { wasm: null, wat: null };
357
+ if (!linked) return none;
303
358
  spliceFacade(merged, linked.facade);
304
- return { wasm: linked.wasm, wat: linked.wat ?? null };
359
+ return {
360
+ wasm: linked.wasm,
361
+ wat: linked.wat ?? null,
362
+ sourceMapInfo: linked.sourceMapInfo ?? null,
363
+ };
305
364
  }
306
365
 
307
366
  // ── Import resolution ─────────────────────────────────────────
@@ -309,7 +368,7 @@ function linkIntoBundle(options, merged, pkgTarget) {
309
368
  // Shared by compileFiles (multi-file) and the single-file path in index.js.
310
369
  // Mutates checker, jsImports, bundledPackages, and preambles in place.
311
370
 
312
- export function resolveImports(
371
+ function resolveImports(
313
372
  programs,
314
373
  fromFile,
315
374
  checker,
@@ -350,6 +409,11 @@ export function resolveImports(
350
409
  "package 'gom' is not available in wasm packages",
351
410
  impNode,
352
411
  );
412
+ } else if (path === "gofront/shared" && pkgTarget === "both") {
413
+ checker.err(
414
+ "'gofront/shared' requires a wasm-only package (//gofront:target wasm): a both package has no linear memory on the JS side",
415
+ impNode,
416
+ );
353
417
  } else if (!isLocalPath(path)) {
354
418
  if (!isBuiltinPackage(path)) {
355
419
  checker.recordBlocker("js: import", path);
@@ -472,6 +536,7 @@ export function resolveImports(
472
536
  nameUsed,
473
537
  dep.exportedSymbols,
474
538
  dep.exportedTypes,
539
+ dep.target,
475
540
  );
476
541
  checker.trackImport(nameUsed, { _line }, p._filename, p._source);
477
542
  }
@@ -481,6 +546,29 @@ export function resolveImports(
481
546
 
482
547
  // ── Main entry points ─────────────────────────────────────────
483
548
 
549
+ // Package target after build-time overrides (Task H6.3): `forceTarget`
550
+ // (`--js-only`) compiles every package to JS; `targetOverrides` maps package
551
+ // directories relative to the root source dir (`"engine/physics"`, `"."` for
552
+ // the root) to a target, replacing the `//gofront:target` directive.
553
+ const PACKAGE_TARGETS = new Set(["js", "wasm", "both"]);
554
+
555
+ export function resolvePackageTarget(declared, pkgDir, options = {}) {
556
+ if (options.forceTarget) return options.forceTarget;
557
+ const overrides = options.targetOverrides;
558
+ if (!overrides || !options.rootDir) return declared;
559
+ const rel = relative(resolve(options.rootDir), resolve(pkgDir))
560
+ .split(sep)
561
+ .join("/");
562
+ const key = rel === "" ? "." : rel;
563
+ const t = overrides[key] ?? (key === "." ? overrides[""] : undefined);
564
+ if (t === undefined) return declared;
565
+ if (!PACKAGE_TARGETS.has(t))
566
+ throw new Error(
567
+ `gofront.json: invalid target '${t}' for package '${key}' (expected js, wasm or both)`,
568
+ );
569
+ return t;
570
+ }
571
+
484
572
  export function compileSingleFile(inputPath, options = {}) {
485
573
  const {
486
574
  sourceMap = false,
@@ -502,6 +590,7 @@ export function compileSingleFile(inputPath, options = {}) {
502
590
 
503
591
  const ast = new Parser(tokens, basename(inputPath), source).parse();
504
592
  ast._source = source;
593
+ ast._path = resolve(inputPath);
505
594
  if (dumpAst) return { ast };
506
595
 
507
596
  const outputDir =
@@ -513,7 +602,11 @@ export function compileSingleFile(inputPath, options = {}) {
513
602
  const bundledPackages = new Set();
514
603
  const preambles = [];
515
604
 
516
- const pkgTarget = ast.target ?? options.target ?? "js";
605
+ const pkgTarget = resolvePackageTarget(
606
+ ast.target ?? options.target ?? "js",
607
+ dirname(resolve(inputPath)),
608
+ options,
609
+ );
517
610
  ast.target = pkgTarget;
518
611
  checker.target = pkgTarget;
519
612
  checker.pkgName = ast.pkg?.name ?? "main";
@@ -534,6 +627,7 @@ export function compileSingleFile(inputPath, options = {}) {
534
627
  if (errors.length > 0) {
535
628
  throw new Error(errors.map((e) => e.message).join("\n"));
536
629
  }
630
+ for (const w of checker.warnings) log.warn(w);
537
631
 
538
632
  if (pkgTarget === "wasm" || pkgTarget === "both") {
539
633
  registerWasmUnit(options, {
@@ -562,13 +656,26 @@ export function compileSingleFile(inputPath, options = {}) {
562
656
  };
563
657
 
564
658
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
565
- const { wasm, wat } = linkIntoBundle(options, merged, pkgTarget);
659
+ const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
660
+ { ...options, sourceMap },
661
+ merged,
662
+ pkgTarget,
663
+ outputDir,
664
+ );
566
665
 
567
666
  if (sourceMap && !options.isDependency) {
568
667
  attachSourceMap(merged, outputDir);
569
668
  }
570
669
 
571
- return { js: merged.js, css: cg.getCss(), target: pkgTarget, wasm, wat };
670
+ return {
671
+ js: merged.js,
672
+ css: cg.getCss(),
673
+ target: pkgTarget,
674
+ wasm,
675
+ wat,
676
+ wasmOptInfo,
677
+ wasmSourceMap,
678
+ };
572
679
  }
573
680
 
574
681
  export function compileDir(dir, options = {}) {
@@ -591,8 +698,12 @@ export function compilePackageTestsWasm(dir, options = {}) {
591
698
  }
592
699
 
593
700
  export function compileFiles(files, options = {}) {
594
- options = { ...options, wasmUnits: options.wasmUnits ?? [] };
595
701
  const fromDir = options.fromDir ?? dirname(resolve(files[0]));
702
+ options = {
703
+ ...options,
704
+ wasmUnits: options.wasmUnits ?? [],
705
+ rootDir: options.rootDir ?? fromDir,
706
+ };
596
707
  const outputDir =
597
708
  options.outputDir ??
598
709
  (options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
@@ -622,9 +733,10 @@ export function compileFiles(files, options = {}) {
622
733
  // Validate target directives across files in this package
623
734
  const declaredTargets = new Map();
624
735
  for (const p of programs) {
625
- if (p.target) {
626
- if (!declaredTargets.has(p.target)) declaredTargets.set(p.target, []);
627
- declaredTargets.get(p.target).push(p._filename);
736
+ const declared = p._declaredTarget;
737
+ if (declared) {
738
+ if (!declaredTargets.has(declared)) declaredTargets.set(declared, []);
739
+ declaredTargets.get(declared).push(p._filename);
628
740
  }
629
741
  }
630
742
  if (declaredTargets.size > 1) {
@@ -635,10 +747,13 @@ export function compileFiles(files, options = {}) {
635
747
  `Conflicting //gofront:target directives in package '${pkgName}': ${list}`,
636
748
  );
637
749
  }
638
- const pkgTarget =
750
+ const pkgTarget = resolvePackageTarget(
639
751
  declaredTargets.size === 1
640
752
  ? declaredTargets.keys().next().value
641
- : (options.target ?? "js");
753
+ : (options.target ?? "js"),
754
+ fromDir,
755
+ options,
756
+ );
642
757
  for (const p of programs) p.target = pkgTarget;
643
758
 
644
759
  // ── 2. Resolve imports ────────────────────────────────────────
@@ -668,6 +783,7 @@ export function compileFiles(files, options = {}) {
668
783
  const msgs = errors.map((e) => e.message).join("\n");
669
784
  throw new Error(msgs);
670
785
  }
786
+ for (const w of checker.warnings) log.warn(w);
671
787
 
672
788
  // ── 4. Code generation ────────────────────────────────────────
673
789
  const emitTarget = registerPackageUnit(options, {
@@ -699,7 +815,12 @@ export function compileFiles(files, options = {}) {
699
815
  };
700
816
 
701
817
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
702
- const { wasm, wat } = linkIntoBundle(options, merged, pkgTarget);
818
+ const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
819
+ options,
820
+ merged,
821
+ pkgTarget,
822
+ outputDir,
823
+ );
703
824
 
704
825
  if (options.sourceMap && !options.isDependency) {
705
826
  attachSourceMap(merged, outputDir);
@@ -717,6 +838,8 @@ export function compileFiles(files, options = {}) {
717
838
  css: allCss,
718
839
  wasm,
719
840
  wat,
841
+ wasmOptInfo,
842
+ wasmSourceMap,
720
843
  programs,
721
844
  exportedSymbols: checker.getExportedSymbols(),
722
845
  exportedTypes: checker.getExportedTypes(),
package/src/index.js CHANGED
@@ -18,7 +18,7 @@ import { createRequire } from "node:module";
18
18
  const _require = createRequire(import.meta.url);
19
19
  const { version } = _require("../package.json");
20
20
 
21
- import { statSync, watch } from "node:fs";
21
+ import { statSync } from "node:fs";
22
22
  import { dirname, relative, resolve } from "node:path";
23
23
  import { copyAssets } from "./asset-manager.js";
24
24
  import {
@@ -35,13 +35,13 @@ import {
35
35
  parseBuildArgs,
36
36
  parseCheckArgs,
37
37
  parseDevArgs,
38
+ parseLegacyArgs,
38
39
  parsePrepArgs,
39
40
  parseTestArgs,
40
41
  runCompile,
41
42
  writeCompileOutput,
42
43
  } from "./cli-core.js";
43
44
  import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
44
- import { createDevServer } from "./dev-server.js";
45
45
 
46
46
  // ── Parse CLI args ───────────────────────────────────────────
47
47
 
@@ -54,11 +54,11 @@ if (args[0] === "--version" || args[0] === "-v") {
54
54
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
55
55
  console.log(
56
56
  `
57
- GoFront — a Go-inspired language that compiles to JavaScript
57
+ GoFront — a Go-inspired language that compiles to JavaScript and WebAssembly
58
58
 
59
59
  Usage:
60
60
  gofront dev [dir] [options] Start dev server with live reload (default port 3000)
61
- gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat)
61
+ gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat, --release, --wasm-opt, --js-only)
62
62
  gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
63
63
  gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
64
64
  gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
@@ -66,6 +66,8 @@ Usage:
66
66
  gofront <file.go> Compile single file and print to stdout
67
67
  gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
68
68
  gofront <input> -o out.js Compile and write to file (wasm packages also emit app.wasm)
69
+ gofront <input> --release Production build with minification and Binaryen wasm optimization
70
+ gofront <input> --wasm-opt Optimize wasm output with Binaryen (-O3 + GUFA)
69
71
  gofront <input> --check Type-check only
70
72
  gofront <input> --watch Watch for changes and recompile
71
73
  gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
@@ -131,9 +133,9 @@ if (args[0] === "test") {
131
133
  // ── check subcommand ──────────────────────────────────────────
132
134
 
133
135
  if (args[0] === "check") {
134
- const { targetDir } = parseCheckArgs(args.slice(1));
136
+ const { targetDir, forceTarget } = parseCheckArgs(args.slice(1));
135
137
  try {
136
- const { elapsedMs, packages } = handleCheck(targetDir);
138
+ const { elapsedMs, packages } = handleCheck(targetDir, { forceTarget });
137
139
  const okLine = (label, t) =>
138
140
  log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
139
141
  if (packages) {
@@ -202,20 +204,22 @@ if (args[0] === "dev") {
202
204
  }
203
205
  }
204
206
 
205
- const inputArg = args[0];
206
- const outputFlag = args.indexOf("-o");
207
- const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
208
- const checkOnly = args.includes("--check");
209
- const dumpAst = args.includes("--ast");
210
- const dumpTokens = args.includes("--tokens");
211
- const sourceMap = args.includes("--source-map");
212
- const serveMode = args.includes("--serve");
213
- const watchMode = args.includes("--watch") || serveMode;
214
- const copyAssetsFlag = args.includes("--copy-assets");
215
- const minifyOutput = args.includes("--minify");
216
- const mangleOutput = args.includes("--mangle");
217
- const portFlag = args.indexOf("--port");
218
- const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
207
+ const {
208
+ inputArg,
209
+ outputFile,
210
+ checkOnly,
211
+ dumpAst,
212
+ dumpTokens,
213
+ sourceMap,
214
+ serve: serveMode,
215
+ watch: watchMode,
216
+ copyAssets: copyAssetsFlag,
217
+ wasmOpt,
218
+ minify: minifyOutput,
219
+ mangle: mangleOutput,
220
+ port: servePort,
221
+ forceTarget,
222
+ } = parseLegacyArgs(args);
219
223
 
220
224
  // ── Determine input mode ─────────────────────────────────────
221
225
 
@@ -228,6 +232,17 @@ try {
228
232
  process.exit(1);
229
233
  }
230
234
 
235
+ function copyProjectAssets(verbose) {
236
+ try {
237
+ const { copied, skipped } = copyAssets(resolve("."));
238
+ if (verbose && (copied > 0 || skipped > 0)) {
239
+ log.info(`copied ${copied} assets (${skipped} skipped)`);
240
+ }
241
+ } catch (e) {
242
+ log.warn(`asset copy failed: ${e.message}`);
243
+ }
244
+ }
245
+
231
246
  // ── Single-shot mode ─────────────────────────────────────────
232
247
 
233
248
  if (!watchMode) {
@@ -239,6 +254,8 @@ if (!watchMode) {
239
254
  outputFile,
240
255
  dumpTokens,
241
256
  dumpAst,
257
+ wasmOpt,
258
+ forceTarget,
242
259
  });
243
260
  } catch (e) {
244
261
  log.fail(e.message);
@@ -282,6 +299,13 @@ if (!watchMode) {
282
299
  log.info(
283
300
  `wrote ${formatWrittenDesc(written, outputFile)} ${ms(elapsedMs)}`,
284
301
  );
302
+ if (result.wasmOptInfo && result.wasmOptInfo.originalSize > 0) {
303
+ const origKb = (result.wasmOptInfo.originalSize / 1024).toFixed(1);
304
+ const optKb = (result.wasmOptInfo.optimizedSize / 1024).toFixed(1);
305
+ log.info(
306
+ `wasm-opt: app.wasm ${origKb} kB → ${optKb} kB (-${result.wasmOptInfo.percentSaved}%) [${result.wasmOptInfo.engine}]`,
307
+ );
308
+ }
285
309
  } catch (e) {
286
310
  log.fail(`cannot write '${outputFile}': ${e.message}`);
287
311
  process.exit(1);
@@ -295,110 +319,57 @@ if (!watchMode) {
295
319
  }
296
320
  }
297
321
 
298
- if (copyAssetsFlag) {
299
- try {
300
- const projectDir = resolve(".");
301
- const { copied, skipped } = copyAssets(projectDir);
302
- if (copied > 0 || skipped > 0) {
303
- log.info(`copied ${copied} assets (${skipped} skipped)`);
304
- }
305
- } catch (e) {
306
- log.warn(`asset copy failed: ${e.message}`);
307
- }
308
- }
322
+ if (copyAssetsFlag) copyProjectAssets(true);
309
323
 
310
324
  process.exit(0);
311
325
  }
312
326
 
313
327
  // ── Watch mode ───────────────────────────────────────────────
314
328
 
315
- // Start dev server before first build so the browser can connect immediately
316
- let devServer = null;
317
- if (serveMode) {
318
- if (!outputFile) {
319
- log.fail("--serve requires -o <output file>");
320
- process.exit(1);
321
- }
322
- const serveDir = dirname(resolve(outputFile));
323
- devServer = createDevServer(serveDir, servePort);
329
+ if (serveMode && !outputFile) {
330
+ log.fail("--serve requires -o <output file>");
331
+ process.exit(1);
324
332
  }
325
333
 
326
- function buildOnce(changedFile = null) {
327
- try {
328
- const startMs = performance.now();
329
- const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
330
- const js = maybeMinify(result.js, {
331
- minify: minifyOutput,
332
- mangle: mangleOutput,
333
- sourceMap,
334
- });
335
- const elapsedMs = (performance.now() - startMs).toFixed(0);
336
- const changeNote = changedFile ? ` — ${changedFile} changed` : "";
337
- const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
338
- if (outputFile) {
339
- const written = writeCompileOutput(resolve(outputFile), result, js);
340
- console.error(
341
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
342
- );
343
- } else {
344
- // Clear screen then print
345
- process.stdout.write("\x1Bc");
346
- console.log(js);
347
- console.error(
348
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
349
- );
350
- }
334
+ const prefix = () => `${stamp()} ${colors.bold("gofront:")}`;
351
335
 
352
- if (copyAssetsFlag) {
353
- try {
354
- copyAssets(resolve("."));
355
- } catch (e) {
356
- log.warn(`asset copy failed: ${e.message}`);
336
+ try {
337
+ await handleDev(".", {
338
+ srcDir: inputPath,
339
+ outputFile,
340
+ serve: serveMode,
341
+ serveDir: outputFile ? dirname(resolve(outputFile)) : undefined,
342
+ port: servePort,
343
+ silent: false,
344
+ sourceMap,
345
+ minify: minifyOutput,
346
+ mangle: mangleOutput,
347
+ copyAssets: copyAssetsFlag,
348
+ forceTarget,
349
+ onBuild: ({ js, written, elapsedMs, changedFile }) => {
350
+ const changeNote = changedFile ? ` — ${changedFile} changed` : "";
351
+ const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
352
+ if (outputFile) {
353
+ console.error(
354
+ `${prefix()} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
355
+ );
356
+ } else {
357
+ // Clear screen then print
358
+ process.stdout.write("\x1Bc");
359
+ console.log(js);
360
+ console.error(`${prefix()} ${colors.green("OK")} ${timing}`);
357
361
  }
358
- }
359
-
360
- devServer?.notify();
361
- } catch (e) {
362
- console.error(
363
- `${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
364
- );
365
- for (const line of formatDiagnostic(e.message).split("\n"))
366
- console.error(` ${line}`);
367
- devServer?.notifyError?.(e);
368
- }
369
- }
370
-
371
- // Initial build
372
- buildOnce();
373
-
374
- // Determine what to watch
375
- const watchTarget = isDir ? inputPath : dirname(inputPath);
376
-
377
- function handleCssWatch(filename) {
378
- if (copyAssetsFlag) {
379
- try {
380
- copyAssets(resolve("."));
381
- } catch (e) {
382
- log.warn(`asset copy failed: ${e.message}`);
383
- }
384
- }
385
- devServer?.notifyCss?.(filename);
362
+ if (copyAssetsFlag) copyProjectAssets(false);
363
+ },
364
+ onError: (e) => {
365
+ console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
366
+ for (const line of formatDiagnostic(e.message).split("\n"))
367
+ console.error(` ${line}`);
368
+ },
369
+ });
370
+ } catch (e) {
371
+ log.fail(e.message);
372
+ process.exit(1);
386
373
  }
387
374
 
388
- let debounce = null;
389
- let cssDebounce = null;
390
- watch(watchTarget, { recursive: true }, (_event, filename) => {
391
- if (!filename) return;
392
- if (filename.endsWith(".css")) {
393
- clearTimeout(cssDebounce);
394
- cssDebounce = setTimeout(() => handleCssWatch(filename), 50);
395
- return;
396
- }
397
- if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
398
- clearTimeout(debounce);
399
- debounce = setTimeout(() => buildOnce(filename), 80);
400
- });
401
-
402
- console.error(
403
- `${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
404
- );
375
+ console.error(`${prefix()} watching ${colors.cyan(inputArg)} ...`);
@@ -1,6 +1,8 @@
1
1
  // src/lower/boxing.js
2
2
  // Identifies address-taken scalar variables that require heap boxing ({ value: x }).
3
3
 
4
+ import { forEachChild } from "./walk.js";
5
+
4
6
  export function isReferenceType(t) {
5
7
  if (!t) return false;
6
8
  const base = t.kind === "named" ? t.underlying : t;
@@ -28,9 +30,6 @@ export function scanAddressTaken(node, boxedVars = new Set()) {
28
30
  }
29
31
  }
30
32
  // Recurse into FuncLit too — closures may take address of outer vars
31
- for (const key of Object.keys(node)) {
32
- if (key.startsWith("_")) continue;
33
- scanAddressTaken(node[key], boxedVars);
34
- }
33
+ forEachChild(node, (child) => scanAddressTaken(child, boxedVars));
35
34
  return boxedVars;
36
35
  }
@@ -3,6 +3,7 @@
3
3
  // are mutated and thus require heap-allocated environment cells for WASM closures.
4
4
 
5
5
  import { collectMutatedVars, nodeMutatesVar } from "./ownership.js";
6
+ import { forEachChild } from "./walk.js";
6
7
 
7
8
  /**
8
9
  * Result of capture analysis on a function or closure.
@@ -41,17 +42,14 @@ export function collectDeclaredNames(node, names = new Set()) {
41
42
  // Do not descend into nested functions when collecting local declarations
42
43
  if (node.kind === "FuncLit") return names;
43
44
 
44
- for (const key of Object.keys(node)) {
45
- if (key.startsWith("_")) continue;
46
- collectDeclaredNames(node[key], names);
47
- }
45
+ forEachChild(node, (child) => collectDeclaredNames(child, names));
48
46
  return names;
49
47
  }
50
48
 
51
49
  /**
52
50
  * Finds all identifier references in a node that are not shadowed by inner declarations.
53
51
  */
54
- export function collectReferencedNames(node, localDecls, refs = new Set()) {
52
+ function collectReferencedNames(node, localDecls, refs = new Set()) {
55
53
  if (!node || typeof node !== "object") return refs;
56
54
  if (Array.isArray(node)) {
57
55
  for (const item of node) collectReferencedNames(item, localDecls, refs);
@@ -73,10 +71,9 @@ export function collectReferencedNames(node, localDecls, refs = new Set()) {
73
71
  if (r.name && r.name !== "_") innerLocals.add(r.name);
74
72
  }
75
73
  collectDeclaredNames(node.body, innerLocals);
76
- for (const key of Object.keys(node)) {
77
- if (key.startsWith("_")) continue;
78
- collectReferencedNames(node[key], innerLocals, refs);
79
- }
74
+ forEachChild(node, (child) =>
75
+ collectReferencedNames(child, innerLocals, refs),
76
+ );
80
77
  return refs;
81
78
  }
82
79
 
@@ -89,10 +86,9 @@ export function collectReferencedNames(node, localDecls, refs = new Set()) {
89
86
  }
90
87
 
91
88
  // For nested closures, free variables are also referenced
92
- for (const key of Object.keys(node)) {
93
- if (key.startsWith("_")) continue;
94
- collectReferencedNames(node[key], localDecls, refs);
95
- }
89
+ forEachChild(node, (child) =>
90
+ collectReferencedNames(child, localDecls, refs),
91
+ );
96
92
  return refs;
97
93
  }
98
94
 
@@ -168,10 +164,7 @@ export function analyzeCaptures(fnDecl) {
168
164
  return;
169
165
  }
170
166
 
171
- for (const key of Object.keys(node)) {
172
- if (key.startsWith("_")) continue;
173
- walkClosures(node[key], scopeStack);
174
- }
167
+ forEachChild(node, (child) => walkClosures(child, scopeStack));
175
168
  };
176
169
 
177
170
  walkClosures(fnDecl.body, [rootScope]);