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/cli-core.js CHANGED
@@ -44,11 +44,25 @@ export function runCompile(inputPath, isDir, options) {
44
44
  dumpTokens = false,
45
45
  dumpAst = false,
46
46
  emitWat = false,
47
+ wasmOpt = false,
48
+ forceTarget = null,
49
+ targetOverrides = null,
50
+ rootDir = null,
47
51
  } = options ?? {};
52
+ const targetOpts = {};
53
+ if (forceTarget) targetOpts.forceTarget = forceTarget;
54
+ if (targetOverrides) targetOpts.targetOverrides = targetOverrides;
55
+ if (rootDir) targetOpts.rootDir = rootDir;
48
56
 
49
57
  if (isDir) {
50
58
  const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
51
- return compileDir(inputPath, { sourceMap, outputDir, emitWat });
59
+ return compileDir(inputPath, {
60
+ sourceMap,
61
+ outputDir,
62
+ emitWat,
63
+ wasmOpt,
64
+ ...targetOpts,
65
+ });
52
66
  }
53
67
 
54
68
  return compileSingleFile(inputPath, {
@@ -57,9 +71,68 @@ export function runCompile(inputPath, isDir, options) {
57
71
  dumpTokens,
58
72
  dumpAst,
59
73
  emitWat,
74
+ wasmOpt,
75
+ ...targetOpts,
60
76
  });
61
77
  }
62
78
 
79
+ // `--js-only` / `--target js` compile every package to JavaScript (A/B
80
+ // benchmarking, fallback builds for browsers without WasmGC).
81
+ export function parseTargetFlag(argv) {
82
+ if (argv.includes("--js-only")) return "js";
83
+ for (let i = 0; i < argv.length; i++) {
84
+ const arg = argv[i];
85
+ const value =
86
+ arg === "--target"
87
+ ? argv[i + 1]
88
+ : arg.startsWith("--target=")
89
+ ? arg.slice("--target=".length)
90
+ : null;
91
+ if (value === null || value === undefined) continue;
92
+ if (value !== "js")
93
+ throw new Error(
94
+ `unsupported --target '${value}' (only 'js' is supported; package targets come from //gofront:target directives or gofront.json "targets")`,
95
+ );
96
+ return "js";
97
+ }
98
+ return null;
99
+ }
100
+
101
+ // Per-package target overrides from `gofront.json` (`"targets": { "engine/physics": "wasm" }`).
102
+ function projectTargetOverrides(project) {
103
+ const t = project?.config?.targets;
104
+ return t && typeof t === "object" ? t : null;
105
+ }
106
+
107
+ // Override keys are relative to the project's source root, so every command
108
+ // (build, dev, check, test — including `dir/...`) must resolve them against
109
+ // the same `rootDir` or a sub-package would see itself as ".".
110
+ function projectTargetOptions(project, options = {}) {
111
+ const targetOverrides =
112
+ options.targetOverrides ?? projectTargetOverrides(project);
113
+ if (!targetOverrides) return {};
114
+ return {
115
+ targetOverrides,
116
+ rootDir: options.rootDir ?? project.srcDir,
117
+ };
118
+ }
119
+
120
+ // Removes `--target <value>` / `--target=<value>` / `--js-only` so the value
121
+ // is not mistaken for a positional argument.
122
+ function stripTargetFlag(argv) {
123
+ const out = [];
124
+ for (let i = 0; i < argv.length; i++) {
125
+ const arg = argv[i];
126
+ if (arg === "--target") {
127
+ i++;
128
+ continue;
129
+ }
130
+ if (arg.startsWith("--target=") || arg === "--js-only") continue;
131
+ out.push(arg);
132
+ }
133
+ return out;
134
+ }
135
+
63
136
  // Writes the JS bundle plus, when the project links wasm packages, the
64
137
  // sibling `app.wasm` (and `app.wat` when requested). Stale wasm artifacts
65
138
  // are removed so a project that drops its wasm packages serves clean output —
@@ -70,15 +143,23 @@ export function writeCompileOutput(outputFile, result, js = result.js) {
70
143
  writeFileSync(outputFile, `${js}\n`);
71
144
  const wasmFile = join(dirname(outputFile), "app.wasm");
72
145
  const watFile = join(dirname(outputFile), "app.wat");
146
+ const wasmMapFile = join(dirname(outputFile), "app.wasm.map");
73
147
  const written = [outputFile];
74
148
  let ownsWasm = Boolean(result.wasm);
75
149
  if (result.wasm) {
76
150
  writeFileSync(wasmFile, result.wasm);
77
151
  written.push(wasmFile);
152
+ if (result.wasmSourceMap) {
153
+ writeFileSync(wasmMapFile, result.wasmSourceMap);
154
+ written.push(wasmMapFile);
155
+ } else if (existsSync(wasmMapFile)) {
156
+ rmSync(wasmMapFile);
157
+ }
78
158
  } else if (existsSync(wasmFile)) {
79
159
  if (isGoFrontWasm(readFileSync(wasmFile))) {
80
160
  rmSync(wasmFile);
81
161
  ownsWasm = true;
162
+ if (existsSync(wasmMapFile)) rmSync(wasmMapFile);
82
163
  }
83
164
  }
84
165
  if (result.wat) {
@@ -254,10 +335,11 @@ export function formatPrepSummary({ assets, vendor }) {
254
335
  export function parseTestArgs(argv) {
255
336
  let run = null;
256
337
  const positional = [];
257
- for (let i = 0; i < argv.length; i++) {
258
- const arg = argv[i];
338
+ const rest = stripTargetFlag(argv);
339
+ for (let i = 0; i < rest.length; i++) {
340
+ const arg = rest[i];
259
341
  if (arg === "-run" || arg === "--run") {
260
- run = argv[++i] ?? null;
342
+ run = rest[++i] ?? null;
261
343
  } else if (arg.startsWith("-run=") || arg.startsWith("--run=")) {
262
344
  run = arg.slice(arg.indexOf("=") + 1);
263
345
  } else if (!arg.startsWith("-")) {
@@ -269,16 +351,24 @@ export function parseTestArgs(argv) {
269
351
  verbose: argv.includes("-v"),
270
352
  dom: argv.includes("--dom"),
271
353
  run,
354
+ forceTarget: parseTargetFlag(argv),
272
355
  };
273
356
  }
274
357
 
275
358
  export function handleTest(targetDir = ".", options = {}) {
276
359
  const pattern = parsePackagePattern(targetDir);
277
- if (pattern) return runTestsRecursive(pattern, options);
360
+ if (pattern) {
361
+ const project = detectProject(pattern);
362
+ return runTestsRecursive(pattern, {
363
+ ...options,
364
+ ...projectTargetOptions(project, options),
365
+ });
366
+ }
278
367
 
279
368
  let testTarget = targetDir;
369
+ let project = null;
280
370
  try {
281
- const project = detectProject(targetDir);
371
+ project = detectProject(targetDir);
282
372
  const targetPath = resolve(targetDir);
283
373
  if (
284
374
  statSync(targetPath).isDirectory() &&
@@ -290,7 +380,10 @@ export function handleTest(targetDir = ".", options = {}) {
290
380
  : dirname(project.srcDir);
291
381
  }
292
382
  } catch {}
293
- return runTests(testTarget, options);
383
+ return runTests(testTarget, {
384
+ ...options,
385
+ ...projectTargetOptions(project, options),
386
+ });
294
387
  }
295
388
 
296
389
  async function runTestsRecursive(rootDir, options) {
@@ -318,7 +411,7 @@ async function runTestsRecursive(rootDir, options) {
318
411
 
319
412
  // Returns the root directory of a Go-style recursive pattern (`./...`,
320
413
  // `app/src/...`), or null when the argument is a plain path.
321
- export function parsePackagePattern(arg) {
414
+ function parsePackagePattern(arg) {
322
415
  if (arg === "...") return resolve(".");
323
416
  if (!arg.endsWith("/...")) return null;
324
417
  return resolve(arg.slice(0, -4) || ".");
@@ -328,7 +421,7 @@ const SKIPPED_DIRS = new Set(["node_modules", "dist", "public"]);
328
421
 
329
422
  // Lists every directory under root (inclusive) that contains .go or .templ
330
423
  // files, in sorted order, skipping hidden directories and build output.
331
- export function findPackageDirs(root) {
424
+ function findPackageDirs(root) {
332
425
  const out = [];
333
426
  const walk = (dir) => {
334
427
  let entries;
@@ -418,7 +511,7 @@ function resolveDevOutputFile(projectRoot, serveDir, config) {
418
511
  return join(serveDir, "app.js");
419
512
  }
420
513
 
421
- export function detectProject(dir = ".") {
514
+ function detectProject(dir = ".") {
422
515
  const projectRoot = resolve(dir);
423
516
  const config = loadProjectConfig(projectRoot);
424
517
  const srcDir = resolveSrcDir(projectRoot, config);
@@ -441,9 +534,10 @@ export function detectProject(dir = ".") {
441
534
  // ── check command ─────────────────────────────────────────────
442
535
 
443
536
  export function parseCheckArgs(argv) {
444
- const positional = argv.filter((a) => !a.startsWith("-"));
537
+ const positional = stripTargetFlag(argv).filter((a) => !a.startsWith("-"));
445
538
  return {
446
539
  targetDir: positional[0] ?? ".",
540
+ forceTarget: parseTargetFlag(argv),
447
541
  };
448
542
  }
449
543
 
@@ -454,11 +548,12 @@ export function handleCheck(targetDir = ".", options = {}) {
454
548
  if (dirs.length === 0) {
455
549
  throw new Error(`No .go files found under ${targetDir}`);
456
550
  }
551
+ const targetOpts = projectTargetOptions(detectProject(pattern), options);
457
552
  const startMs = performance.now();
458
553
  const packages = [];
459
554
  for (const dir of dirs) {
460
555
  const pkgStart = performance.now();
461
- runCompile(dir, true, { ...options });
556
+ runCompile(dir, true, { ...options, ...targetOpts });
462
557
  packages.push({
463
558
  dir,
464
559
  elapsedMs: (performance.now() - pkgStart).toFixed(0),
@@ -488,7 +583,10 @@ export function handleCheck(targetDir = ".", options = {}) {
488
583
 
489
584
  const targetIsDir = statSync(compileTarget).isDirectory();
490
585
  const startMs = performance.now();
491
- runCompile(compileTarget, targetIsDir, { ...options });
586
+ runCompile(compileTarget, targetIsDir, {
587
+ ...options,
588
+ ...projectTargetOptions(project, options),
589
+ });
492
590
  const elapsedMs = (performance.now() - startMs).toFixed(0);
493
591
  return { target: targetDir, compileTarget, elapsedMs };
494
592
  }
@@ -503,6 +601,10 @@ export function parseBuildArgs(argv) {
503
601
  const noMinify = argv.includes("--no-minify");
504
602
  const noMangle = argv.includes("--no-mangle");
505
603
  const emitWat = argv.includes("--emit-wat");
604
+ const release = argv.includes("--release");
605
+ const noWasmOpt = argv.includes("--no-wasm-opt");
606
+ const wasmOpt = !noWasmOpt && (argv.includes("--wasm-opt") || release);
607
+ const forceTarget = parseTargetFlag(argv);
506
608
 
507
609
  for (let i = 0; i < argv.length; i++) {
508
610
  const arg = argv[i];
@@ -510,6 +612,8 @@ export function parseBuildArgs(argv) {
510
612
  outDir = argv[++i] ?? null;
511
613
  } else if (arg.startsWith("--out-dir=")) {
512
614
  outDir = arg.slice(arg.indexOf("=") + 1);
615
+ } else if (arg === "--target") {
616
+ i++;
513
617
  } else if (!arg.startsWith("-")) {
514
618
  positional.push(arg);
515
619
  }
@@ -523,6 +627,9 @@ export function parseBuildArgs(argv) {
523
627
  emitWat,
524
628
  minify: !noMinify,
525
629
  mangle: !noMangle && !noMinify,
630
+ release,
631
+ wasmOpt,
632
+ forceTarget,
526
633
  };
527
634
  }
528
635
 
@@ -684,12 +791,19 @@ function collectOutputFiles(outDir) {
684
791
  return files;
685
792
  }
686
793
 
687
- export function formatBuildSummary({ outDir, elapsedMs, files, pwa }) {
794
+ export function formatBuildSummary({ outDir, elapsedMs, files, pwa, wasmOpt }) {
688
795
  const lines = [`build complete in ${elapsedMs}ms → ${outDir}`];
689
796
  for (const f of files) {
690
797
  const sizeKb = (f.size / 1024).toFixed(1);
691
798
  lines.push(` ${f.name} (${sizeKb} kB)`);
692
799
  }
800
+ if (wasmOpt && wasmOpt.originalSize > 0) {
801
+ const origKb = (wasmOpt.originalSize / 1024).toFixed(1);
802
+ const optKb = (wasmOpt.optimizedSize / 1024).toFixed(1);
803
+ lines.push(
804
+ ` wasm-opt: app.wasm ${origKb} kB → ${optKb} kB (-${wasmOpt.percentSaved}%) [${wasmOpt.engine}]`,
805
+ );
806
+ }
693
807
  if (pwa) {
694
808
  lines.push(
695
809
  ` pwa: generated sw.js (${pwa.assetsCount} cached assets, ${pwa.version})`,
@@ -712,6 +826,9 @@ export async function handleBuild(targetDir = ".", options = {}) {
712
826
  outputFile,
713
827
  sourceMap: options.sourceMap ?? false,
714
828
  emitWat: options.emitWat ?? false,
829
+ wasmOpt: options.wasmOpt ?? false,
830
+ forceTarget: options.forceTarget ?? null,
831
+ ...projectTargetOptions(project, options),
715
832
  });
716
833
 
717
834
  const doMinify = options.minify ?? true;
@@ -757,16 +874,45 @@ export async function handleBuild(targetDir = ".", options = {}) {
757
874
  assets,
758
875
  vendor,
759
876
  pwa,
877
+ wasmOpt: compileResult.wasmOptInfo,
760
878
  };
761
879
  }
762
880
 
763
881
  export { generatePwa };
764
882
 
883
+ // ── legacy `gofront <input> [flags]` command ──────────────────
884
+
885
+ export function parseLegacyArgs(argv) {
886
+ const outputFlag = argv.indexOf("-o");
887
+ const portFlag = argv.indexOf("--port");
888
+ const serve = argv.includes("--serve");
889
+ const release = argv.includes("--release");
890
+ return {
891
+ inputArg: argv[0],
892
+ outputFile: outputFlag !== -1 ? (argv[outputFlag + 1] ?? null) : null,
893
+ checkOnly: argv.includes("--check"),
894
+ dumpAst: argv.includes("--ast"),
895
+ dumpTokens: argv.includes("--tokens"),
896
+ sourceMap: argv.includes("--source-map"),
897
+ forceTarget: parseTargetFlag(argv),
898
+ serve,
899
+ watch: argv.includes("--watch") || serve,
900
+ copyAssets: argv.includes("--copy-assets"),
901
+ release,
902
+ wasmOpt:
903
+ !argv.includes("--no-wasm-opt") &&
904
+ (argv.includes("--wasm-opt") || release),
905
+ minify: argv.includes("--minify") || release,
906
+ mangle: argv.includes("--mangle") || release,
907
+ port: portFlag !== -1 ? parseInt(argv[portFlag + 1], 10) : 3000,
908
+ };
909
+ }
910
+
765
911
  // ── dev command ───────────────────────────────────────────────
766
912
 
767
913
  export function parseDevArgs(argv) {
768
914
  let port = null;
769
- let outputFile = null;
915
+ let outputFile; // undefined → fall back to project.devOutputFile
770
916
  const positional = [];
771
917
  for (let i = 0; i < argv.length; i++) {
772
918
  const arg = argv[i];
@@ -775,7 +921,9 @@ export function parseDevArgs(argv) {
775
921
  } else if (arg.startsWith("--port=")) {
776
922
  port = parseInt(arg.slice(arg.indexOf("=") + 1), 10);
777
923
  } else if (arg === "-o" || arg === "--output") {
778
- outputFile = argv[++i] ?? null;
924
+ outputFile = argv[++i];
925
+ } else if (arg === "--target") {
926
+ i++;
779
927
  } else if (!arg.startsWith("-")) {
780
928
  positional.push(arg);
781
929
  }
@@ -784,29 +932,58 @@ export function parseDevArgs(argv) {
784
932
  targetDir: positional[0] ?? ".",
785
933
  port: Number.isNaN(port) ? null : port,
786
934
  outputFile,
935
+ forceTarget: parseTargetFlag(argv),
787
936
  };
788
937
  }
789
938
 
790
- function buildDevOnce(
791
- { srcDir, isDir, outputFile, sourceMap, devServer },
939
+ // Compiles once and writes the output when an output file is configured.
940
+ // Returns the build info; throws on compile errors.
941
+ function runDevBuild(
942
+ {
943
+ srcDir,
944
+ isDir,
945
+ outputFile,
946
+ sourceMap,
947
+ minify,
948
+ mangle,
949
+ forceTarget,
950
+ targetOverrides,
951
+ rootDir,
952
+ },
792
953
  changedFile = null,
793
954
  ) {
955
+ const startMs = performance.now();
956
+ const result = runCompile(srcDir, isDir, {
957
+ outputFile,
958
+ sourceMap,
959
+ forceTarget,
960
+ targetOverrides,
961
+ rootDir,
962
+ });
963
+ const js = maybeMinify(result.js, { minify, mangle, sourceMap });
964
+ const written = outputFile
965
+ ? writeCompileOutput(outputFile, result, js)
966
+ : null;
967
+ const elapsedMs = (performance.now() - startMs).toFixed(0);
968
+ return { result, js, written, outputFile, elapsedMs, changedFile };
969
+ }
970
+
971
+ function reportDevBuild({ written, outputFile, elapsedMs, changedFile }) {
972
+ const note = changedFile ? ` — ${changedFile} changed` : "";
973
+ log.ok(
974
+ `— wrote ${formatWrittenDesc(written, outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
975
+ );
976
+ }
977
+
978
+ function buildDevOnce(config, changedFile = null) {
794
979
  try {
795
- const startMs = performance.now();
796
- const result = runCompile(srcDir, isDir, {
797
- outputFile,
798
- sourceMap: sourceMap ?? true,
799
- });
800
- const written = writeCompileOutput(outputFile, result);
801
- const elapsedMs = (performance.now() - startMs).toFixed(0);
802
- const note = changedFile ? ` — ${changedFile} changed` : "";
803
- log.ok(
804
- `— wrote ${formatWrittenDesc(written, outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
805
- );
806
- devServer.notify();
980
+ const info = runDevBuild(config, changedFile);
981
+ (config.onBuild ?? reportDevBuild)(info);
982
+ config.devServer?.notify();
807
983
  } catch (e) {
808
- log.error(e.message);
809
- devServer.notifyError(e);
984
+ if (config.onError) config.onError(e);
985
+ else log.error(e.message);
986
+ config.devServer?.notifyError(e);
810
987
  }
811
988
  }
812
989
 
@@ -849,10 +1026,12 @@ function setupDevWatchers(config) {
849
1026
  let htmlDebounce = null;
850
1027
 
851
1028
  const handleCss = (filename) => {
852
- try {
853
- copyAssets(config.project.projectRoot, config.assetConfig);
854
- } catch {}
855
- config.devServer.notifyCss(filename);
1029
+ if (config.copyAssets) {
1030
+ try {
1031
+ copyAssets(config.project.projectRoot, config.assetConfig);
1032
+ } catch {}
1033
+ }
1034
+ config.devServer?.notifyCss(filename);
856
1035
  };
857
1036
 
858
1037
  const watchers = [];
@@ -869,22 +1048,24 @@ function setupDevWatchers(config) {
869
1048
  );
870
1049
  if (srcWatcher) watchers.push(srcWatcher);
871
1050
 
872
- const serveWatcher = createServeWatcher(
873
- config.serveDir,
874
- config.watchTarget,
875
- (fn) => {
876
- clearTimeout(cssDebounce);
877
- cssDebounce = setTimeout(() => handleCss(fn), 50);
878
- },
879
- () => {
880
- clearTimeout(htmlDebounce);
881
- // Typed reload keeps a pending build error visible after the page reloads.
882
- htmlDebounce = setTimeout(
883
- () => config.devServer.notify({ type: "reload" }),
884
- 50,
885
- );
886
- },
887
- );
1051
+ const serveWatcher = config.devServer
1052
+ ? createServeWatcher(
1053
+ config.serveDir,
1054
+ config.watchTarget,
1055
+ (fn) => {
1056
+ clearTimeout(cssDebounce);
1057
+ cssDebounce = setTimeout(() => handleCss(fn), 50);
1058
+ },
1059
+ () => {
1060
+ clearTimeout(htmlDebounce);
1061
+ // Typed reload keeps a pending build error visible after the page reloads.
1062
+ htmlDebounce = setTimeout(
1063
+ () => config.devServer.notify({ type: "reload" }),
1064
+ 50,
1065
+ );
1066
+ },
1067
+ )
1068
+ : null;
888
1069
  if (serveWatcher) watchers.push(serveWatcher);
889
1070
 
890
1071
  return {
@@ -902,6 +1083,12 @@ function setupDevWatchers(config) {
902
1083
  };
903
1084
  }
904
1085
 
1086
+ // Options beyond the `dev` subcommand's own (used by the legacy `--watch` path):
1087
+ // serve: false → no dev server (watch + rebuild only)
1088
+ // outputFile: null → never write; the compiled JS is passed to `onBuild`
1089
+ // copyAssets: false → skip the initial and per-CSS-change asset copy
1090
+ // minify / mangle → applied to every build
1091
+ // onBuild / onError → replace the default log lines
905
1092
  export async function handleDev(targetDir = ".", options = {}) {
906
1093
  const project = detectProject(targetDir);
907
1094
  const port = options.port ?? project.port ?? 3000;
@@ -909,43 +1096,59 @@ export async function handleDev(targetDir = ".", options = {}) {
909
1096
  ? resolve(options.serveDir)
910
1097
  : project.serveDir;
911
1098
  const srcDir = options.srcDir ? resolve(options.srcDir) : project.srcDir;
912
- const outputFile = options.outputFile
913
- ? resolve(options.outputFile)
914
- : project.devOutputFile;
1099
+ const outputFile =
1100
+ options.outputFile === null
1101
+ ? null
1102
+ : options.outputFile
1103
+ ? resolve(options.outputFile)
1104
+ : project.devOutputFile;
1105
+ const doCopyAssets = options.copyAssets ?? true;
915
1106
 
916
- copyAssets(project.projectRoot, options.assetConfig);
1107
+ if (doCopyAssets) copyAssets(project.projectRoot, options.assetConfig);
917
1108
 
918
1109
  const isDir = statSync(srcDir).isDirectory();
919
- const devServer = createDevServer(serveDir, port, {
920
- ...options,
921
- silent: true,
922
- });
1110
+ const devServer =
1111
+ options.serve === false
1112
+ ? null
1113
+ : createDevServer(serveDir, port, {
1114
+ ...options,
1115
+ silent: options.silent ?? true,
1116
+ });
1117
+ const buildConfig = {
1118
+ srcDir,
1119
+ isDir,
1120
+ outputFile,
1121
+ sourceMap: options.sourceMap ?? true,
1122
+ minify: options.minify ?? false,
1123
+ mangle: options.mangle ?? false,
1124
+ forceTarget: options.forceTarget ?? null,
1125
+ ...projectTargetOptions(project, options),
1126
+ devServer,
1127
+ onBuild: options.onBuild,
1128
+ onError: options.onError,
1129
+ };
923
1130
  let initialError = null;
924
1131
 
925
1132
  let written = null;
926
1133
  try {
927
- const result = runCompile(srcDir, isDir, {
928
- outputFile,
929
- sourceMap: options.sourceMap ?? true,
930
- });
931
- written = writeCompileOutput(outputFile, result);
1134
+ const info = runDevBuild(buildConfig);
1135
+ written = info.written;
1136
+ options.onBuild?.(info);
932
1137
  } catch (err) {
933
1138
  initialError = err;
934
- devServer.notifyError(err);
1139
+ options.onError?.(err);
1140
+ devServer?.notifyError(err);
935
1141
  }
936
1142
 
937
1143
  let watcherController = null;
938
1144
  if (options.watch !== false) {
939
1145
  const watchTarget = isDir ? srcDir : dirname(srcDir);
940
1146
  watcherController = setupDevWatchers({
1147
+ ...buildConfig,
941
1148
  watchTarget,
942
- srcDir,
943
- isDir,
944
- outputFile,
945
- sourceMap: options.sourceMap,
946
- devServer,
947
1149
  project,
948
1150
  serveDir,
1151
+ copyAssets: doCopyAssets,
949
1152
  assetConfig: options.assetConfig,
950
1153
  });
951
1154
  }
@@ -961,7 +1164,7 @@ export async function handleDev(targetDir = ".", options = {}) {
961
1164
  written,
962
1165
  close: async () => {
963
1166
  watcherController?.close();
964
- await devServer.close();
1167
+ await devServer?.close();
965
1168
  },
966
1169
  };
967
1170
  }