gofront 1.4.0 → 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 (77) hide show
  1. package/README.md +174 -403
  2. package/package.json +16 -19
  3. package/src/{codegen → backend/js}/expressions.js +265 -77
  4. package/src/{codegen → backend/js}/index.js +120 -89
  5. package/src/{codegen → backend/js}/runtime.js +102 -0
  6. package/src/{codegen → backend/js}/source-map.js +32 -0
  7. package/src/{codegen → backend/js}/statements.js +139 -64
  8. package/src/backend/js/stdlib/bits.js +32 -0
  9. package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
  10. package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
  11. package/src/{codegen → backend/js}/stdlib/index.js +6 -0
  12. package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
  13. package/src/{codegen → backend/js}/stdlib/path.js +2 -2
  14. package/src/backend/js/stdlib/shared.js +17 -0
  15. package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
  16. package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
  17. package/src/backend/js/stdlib/strconv.js +58 -0
  18. package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
  19. package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
  20. package/src/{codegen → backend/js}/stdlib/time.js +3 -3
  21. package/src/{codegen → backend/js}/templ.js +3 -3
  22. package/src/backend/wasm/boundary.js +1798 -0
  23. package/src/backend/wasm/emit-builtins.js +1103 -0
  24. package/src/backend/wasm/emit-exprs.js +1908 -0
  25. package/src/backend/wasm/emit-maps.js +1863 -0
  26. package/src/backend/wasm/emit-shared.js +519 -0
  27. package/src/backend/wasm/emit-stdlib.js +3006 -0
  28. package/src/backend/wasm/emit-stmts.js +2472 -0
  29. package/src/backend/wasm/emit.js +1376 -0
  30. package/src/backend/wasm/encode.js +937 -0
  31. package/src/backend/wasm/glue.js +144 -0
  32. package/src/backend/wasm/index.js +2790 -0
  33. package/src/backend/wasm/monomorph.js +711 -0
  34. package/src/backend/wasm/optimize.js +227 -0
  35. package/src/backend/wasm/types.js +544 -0
  36. package/src/backend/wasm/wat.js +302 -0
  37. package/src/cli-core.js +323 -75
  38. package/src/compiler.js +447 -14
  39. package/src/dev-server.js +56 -15
  40. package/src/index.js +99 -120
  41. package/src/lexer.js +18 -0
  42. package/src/lower/boxing.js +35 -0
  43. package/src/lower/captures.js +172 -0
  44. package/src/lower/embedding.js +30 -0
  45. package/src/lower/escape.js +164 -0
  46. package/src/lower/functions.js +302 -0
  47. package/src/lower/globals.js +316 -0
  48. package/src/lower/index.js +118 -0
  49. package/src/lower/ownership.js +137 -0
  50. package/src/lower/range.js +67 -0
  51. package/src/lower/walk.js +25 -0
  52. package/src/minifier.js +2 -0
  53. package/src/parser/index.js +8 -3
  54. package/src/resolver.js +3 -1
  55. package/src/test-runner.js +94 -20
  56. package/src/typechecker/assignability.js +42 -1
  57. package/src/typechecker/expressions.js +182 -3
  58. package/src/typechecker/index.js +261 -5
  59. package/src/typechecker/resolve.js +35 -6
  60. package/src/typechecker/stdlib/core.js +34 -19
  61. package/src/typechecker/stdlib/extended.js +18 -3
  62. package/src/typechecker/stdlib/shared.js +53 -0
  63. package/src/typechecker/stdlib.js +2 -0
  64. package/src/typechecker/types.js +259 -6
  65. package/src/vendor.js +2 -2
  66. package/src/codegen/stdlib/strconv.js +0 -33
  67. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  68. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  69. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  70. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  71. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  72. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  73. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  74. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  75. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  76. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  77. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/src/compiler.js CHANGED
@@ -18,17 +18,28 @@
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 { CodeGen } from "./codegen/index.js";
23
- import { buildSourceMap } from "./codegen/source-map.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";
31
+ import { lower } from "./lower/index.js";
27
32
  import { Parser } from "./parser/index.js";
28
- import { isLocalPath, resolveAll, resolveGwDir } from "./resolver.js";
33
+ import {
34
+ isBuiltinPackage,
35
+ isLocalPath,
36
+ resolveAll,
37
+ resolveGwDir,
38
+ } from "./resolver.js";
29
39
  import { TemplLexer } from "./templ-lexer.js";
30
40
  import { TemplParser } from "./templ-parser.js";
31
41
  import { TypeChecker } from "./typechecker/index.js";
42
+ import { WASM_SUPPORTED_STDLIB } from "./typechecker/types.js";
32
43
 
33
44
  // ── Parse cache ──────────────────────────────────────────────
34
45
 
@@ -60,6 +71,9 @@ function parseGoFrontFile(filePath) {
60
71
  ? new TemplParser(tokens, filename, source).parse()
61
72
  : new Parser(tokens, filename, source).parse();
62
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;
63
77
  _parseCache.set(filePath, { mtime, ast });
64
78
  return ast;
65
79
  }
@@ -156,12 +170,205 @@ function attachSourceMap(merged, outputDir) {
156
170
  merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
157
171
  }
158
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
+
215
+ // ── WASM linking ──────────────────────────────────────────────
216
+ //
217
+ // Every `wasm`/`both` package reached during a build registers a unit in the
218
+ // shared `options.wasmUnits` array. The root compile links all units into a
219
+ // single `app.wasm` and splices the generated JS facade into the bundle where
220
+ // the first wasm package's JS would have been.
221
+
222
+ const WASM_MARKER_RE = /^\/\*__GOFRONT_WASM_UNIT:[^*]*\*\/$/m;
223
+
224
+ function wasmMarker(pkgName) {
225
+ return `/*__GOFRONT_WASM_UNIT:${pkgName}*/`;
226
+ }
227
+
228
+ function registerWasmUnit(options, unit) {
229
+ const units = options.wasmUnits;
230
+ if (!units) return;
231
+ if (units.some((u) => u.key === unit.key)) return;
232
+ units.push(unit);
233
+ }
234
+
235
+ // Registers a `wasm`/`both` package as a link unit and returns the target the
236
+ // package's own code is emitted for. `options.wasmTests` runs a `both`
237
+ // package's tests through the wasm backend (the package is then treated as
238
+ // `wasm` for linking so its test functions are exposed by the facade).
239
+ function registerPackageUnit(options, unit) {
240
+ const { pkgTarget, programs } = unit;
241
+ if (pkgTarget !== "wasm" && pkgTarget !== "both") return pkgTarget;
242
+ const emitTarget =
243
+ options.wasmTests && options.isTest && pkgTarget === "both"
244
+ ? "wasm"
245
+ : pkgTarget;
246
+ registerWasmUnit(options, {
247
+ ...unit,
248
+ target: emitTarget,
249
+ programs:
250
+ emitTarget === pkgTarget
251
+ ? programs
252
+ : programs.map((p) => ({ ...p, target: emitTarget })),
253
+ });
254
+ return emitTarget;
255
+ }
256
+
257
+ function linkWasmUnits(
258
+ units,
259
+ { emitWat = false, rootTarget = "js", sourceMappingURL = null } = {},
260
+ ) {
261
+ if (!units.some((u) => u.target === "wasm")) return null;
262
+ checkLinkCollisions(units);
263
+
264
+ const types = new Map();
265
+ const bundledPackages = new Set();
266
+ const programs = [];
267
+ for (const u of units) {
268
+ for (const [k, v] of u.types) types.set(k, v);
269
+ for (const b of u.bundledPackages) bundledPackages.add(b);
270
+ programs.push(...u.programs);
271
+ }
272
+ const checker = { types };
273
+ const lowerRes = lower(programs, checker);
274
+ return compileWasmModule(programs, checker, lowerRes, {
275
+ boundary: true,
276
+ bundledPackages,
277
+ emitWat,
278
+ callMain: rootTarget === "wasm",
279
+ sourceMappingURL,
280
+ });
281
+ }
282
+
283
+ // The linked module is one flat namespace (like the JS bundle), so a top-level
284
+ // name declared by two different packages would silently resolve to whichever
285
+ // was emitted last. Reject it up front.
286
+ function topLevelDeclNames(decl) {
287
+ switch (decl.kind) {
288
+ case "FuncDecl":
289
+ return [decl.name];
290
+ case "MethodDecl":
291
+ return [`${decl.recvType?.name ?? "?"}.${decl.name}`];
292
+ case "TypeDecl":
293
+ return [decl.name];
294
+ case "VarDecl":
295
+ case "ConstDecl":
296
+ return (decl.decls ?? []).flatMap((spec) => spec.names ?? []);
297
+ default:
298
+ return [];
299
+ }
300
+ }
301
+
302
+ function checkLinkCollisions(units) {
303
+ const owner = new Map(); // name -> pkgName
304
+ const errors = [];
305
+ for (const u of units) {
306
+ for (const p of u.programs) {
307
+ for (const d of p.decls ?? []) {
308
+ for (const name of topLevelDeclNames(d)) {
309
+ if (name === "_") continue;
310
+ const prev = owner.get(name);
311
+ if (prev === undefined) owner.set(name, u.pkgName);
312
+ else if (prev !== u.pkgName)
313
+ errors.push(
314
+ `cannot link wasm packages: '${name}' is declared in both '${prev}' and '${u.pkgName}' (linked wasm packages share one namespace)`,
315
+ );
316
+ }
317
+ }
318
+ }
319
+ }
320
+ if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
321
+ }
322
+
323
+ // Replaces the first wasm marker with the facade (dropping the others) and
324
+ // shifts source-map lines that follow it.
325
+ function spliceFacade(merged, facade) {
326
+ const lines = merged.js.split("\n");
327
+ const first = lines.findIndex((l) => WASM_MARKER_RE.test(l));
328
+ if (first < 0) {
329
+ merged.js = `${facade}\n${merged.js}`;
330
+ const shift = facade.split("\n").length;
331
+ for (const m of merged.mappings) m.genLine += shift;
332
+ return;
333
+ }
334
+ const facadeLines = facade.split("\n");
335
+ const delta = facadeLines.length - 1;
336
+ for (const m of merged.mappings) {
337
+ if (m.genLine > first) m.genLine += delta;
338
+ }
339
+ const out = [...lines.slice(0, first), ...facadeLines];
340
+ for (let i = first + 1; i < lines.length; i++) {
341
+ if (!WASM_MARKER_RE.test(lines[i])) out.push(lines[i]);
342
+ }
343
+ merged.js = out.join("\n");
344
+ }
345
+
346
+ // Root compiles only: link every registered unit into app.wasm and splice the
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;
352
+ const linked = linkWasmUnits(options.wasmUnits, {
353
+ emitWat: options.emitWat,
354
+ rootTarget: pkgTarget,
355
+ sourceMappingURL,
356
+ });
357
+ if (!linked) return none;
358
+ spliceFacade(merged, linked.facade);
359
+ return {
360
+ wasm: linked.wasm,
361
+ wat: linked.wat ?? null,
362
+ sourceMapInfo: linked.sourceMapInfo ?? null,
363
+ };
364
+ }
365
+
159
366
  // ── Import resolution ─────────────────────────────────────────
160
367
  //
161
368
  // Shared by compileFiles (multi-file) and the single-file path in index.js.
162
369
  // Mutates checker, jsImports, bundledPackages, and preambles in place.
163
370
 
164
- export function resolveImports(
371
+ function resolveImports(
165
372
  programs,
166
373
  fromFile,
167
374
  checker,
@@ -173,6 +380,60 @@ export function resolveImports(
173
380
  ) {
174
381
  const fromDir = dirname(resolve(fromFile));
175
382
  const allImports = programs.flatMap((p) => p.imports);
383
+ const pkgTarget = checker.target ?? "js";
384
+ const pkgName = checker.pkgName ?? programs[0]?.pkg?.name ?? "main";
385
+
386
+ // Validate target rules on non-local imports
387
+ if (pkgTarget === "wasm" || pkgTarget === "both") {
388
+ for (const p of programs) {
389
+ const saved = [checker._currentFile, checker._currentSource];
390
+ checker._currentFile = p._filename ?? null;
391
+ checker._currentSource = p._source ?? null;
392
+ for (const imp of p.imports) {
393
+ for (const { path, _line, _col, line, col } of imp.imports) {
394
+ const impNode = {
395
+ _line,
396
+ _col,
397
+ line: line ?? _line,
398
+ col: col ?? _col ?? 1,
399
+ };
400
+ if (path.startsWith("js:")) {
401
+ checker.recordBlocker("js: import", path);
402
+ checker.err(
403
+ "js: imports are not allowed in wasm packages",
404
+ impNode,
405
+ );
406
+ } else if (path === "gom") {
407
+ checker.recordBlocker("gom usage", "gom");
408
+ checker.err(
409
+ "package 'gom' is not available in wasm packages",
410
+ impNode,
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
+ );
417
+ } else if (!isLocalPath(path)) {
418
+ if (!isBuiltinPackage(path)) {
419
+ checker.recordBlocker("js: import", path);
420
+ checker.err(
421
+ "js: imports are not allowed in wasm packages",
422
+ impNode,
423
+ );
424
+ } else if (!WASM_SUPPORTED_STDLIB.has(path)) {
425
+ checker.recordBlocker("unsupported stdlib import", path);
426
+ checker.err(
427
+ `'${path}' is not yet available in wasm packages`,
428
+ impNode,
429
+ );
430
+ }
431
+ }
432
+ }
433
+ }
434
+ [checker._currentFile, checker._currentSource] = saved;
435
+ }
436
+ }
176
437
 
177
438
  // js: prefix — local .d.ts files
178
439
  for (const imp of allImports) {
@@ -205,7 +466,7 @@ export function resolveImports(
205
466
  const seenLocalPaths = new Set();
206
467
  for (const p of programs) {
207
468
  for (const imp of p.imports) {
208
- for (const { path, alias, _line } of imp.imports) {
469
+ for (const { path, alias, _line, _col, line, col } of imp.imports) {
209
470
  if (!isLocalPath(path) || seenLocalPaths.has(path)) continue;
210
471
  seenLocalPaths.add(path);
211
472
  const depDir = resolveGwDir(path, fromFile);
@@ -226,6 +487,44 @@ export function resolveImports(
226
487
  isDependency: true,
227
488
  });
228
489
  if (!alreadyBundled) preambles.push(dep);
490
+
491
+ // Target import rules for local packages
492
+ const impNode = {
493
+ _line,
494
+ _col,
495
+ line: line ?? _line,
496
+ col: col ?? _col ?? 1,
497
+ };
498
+ if (pkgTarget === "wasm" && dep.target === "js") {
499
+ const saved = [checker._currentFile, checker._currentSource];
500
+ checker._currentFile = p._filename ?? null;
501
+ checker._currentSource = p._source ?? null;
502
+ checker.recordBlocker("js package import", dep.pkgName);
503
+ checker.err(
504
+ `package '${pkgName}' (wasm) cannot import '${dep.pkgName}' (js)`,
505
+ impNode,
506
+ );
507
+ [checker._currentFile, checker._currentSource] = saved;
508
+ } else if (pkgTarget === "both") {
509
+ const saved = [checker._currentFile, checker._currentSource];
510
+ checker._currentFile = p._filename ?? null;
511
+ checker._currentSource = p._source ?? null;
512
+ if (dep.target === "js") {
513
+ checker.recordBlocker("js package import", dep.pkgName);
514
+ checker.err(
515
+ `package '${pkgName}' (both) cannot import '${dep.pkgName}' (js)`,
516
+ impNode,
517
+ );
518
+ } else if (dep.target === "wasm") {
519
+ checker.recordBlocker("wasm package import", dep.pkgName);
520
+ checker.err(
521
+ `package '${pkgName}' (both) can only import 'both' packages; '${dep.pkgName}' is wasm`,
522
+ impNode,
523
+ );
524
+ }
525
+ [checker._currentFile, checker._currentSource] = saved;
526
+ }
527
+
229
528
  if (alias === "_") continue;
230
529
  if (alias === ".") {
231
530
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
@@ -237,6 +536,7 @@ export function resolveImports(
237
536
  nameUsed,
238
537
  dep.exportedSymbols,
239
538
  dep.exportedTypes,
539
+ dep.target,
240
540
  );
241
541
  checker.trackImport(nameUsed, { _line }, p._filename, p._source);
242
542
  }
@@ -246,6 +546,29 @@ export function resolveImports(
246
546
 
247
547
  // ── Main entry points ─────────────────────────────────────────
248
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
+
249
572
  export function compileSingleFile(inputPath, options = {}) {
250
573
  const {
251
574
  sourceMap = false,
@@ -253,6 +576,7 @@ export function compileSingleFile(inputPath, options = {}) {
253
576
  dumpTokens = false,
254
577
  dumpAst = false,
255
578
  } = options;
579
+ options = { ...options, wasmUnits: options.wasmUnits ?? [] };
256
580
 
257
581
  let source;
258
582
  try {
@@ -266,6 +590,7 @@ export function compileSingleFile(inputPath, options = {}) {
266
590
 
267
591
  const ast = new Parser(tokens, basename(inputPath), source).parse();
268
592
  ast._source = source;
593
+ ast._path = resolve(inputPath);
269
594
  if (dumpAst) return { ast };
270
595
 
271
596
  const outputDir =
@@ -277,6 +602,15 @@ export function compileSingleFile(inputPath, options = {}) {
277
602
  const bundledPackages = new Set();
278
603
  const preambles = [];
279
604
 
605
+ const pkgTarget = resolvePackageTarget(
606
+ ast.target ?? options.target ?? "js",
607
+ dirname(resolve(inputPath)),
608
+ options,
609
+ );
610
+ ast.target = pkgTarget;
611
+ checker.target = pkgTarget;
612
+ checker.pkgName = ast.pkg?.name ?? "main";
613
+
280
614
  resolveImports(
281
615
  [ast],
282
616
  inputPath,
@@ -293,24 +627,55 @@ export function compileSingleFile(inputPath, options = {}) {
293
627
  if (errors.length > 0) {
294
628
  throw new Error(errors.map((e) => e.message).join("\n"));
295
629
  }
630
+ for (const w of checker.warnings) log.warn(w);
631
+
632
+ if (pkgTarget === "wasm" || pkgTarget === "both") {
633
+ registerWasmUnit(options, {
634
+ key: resolve(inputPath),
635
+ pkgName: checker.pkgName,
636
+ target: pkgTarget,
637
+ programs: [ast],
638
+ types: checker.types,
639
+ bundledPackages,
640
+ });
641
+ }
296
642
 
297
- const cg = new CodeGen(checker, jsImports, bundledPackages);
298
- const mainJs = cg.generate(ast);
643
+ const isStrict = Boolean(options.strict || pkgTarget === "both");
644
+ const cg = new CodeGen(checker, jsImports, bundledPackages, {
645
+ target: pkgTarget,
646
+ strict: isStrict,
647
+ });
648
+ const mainJs =
649
+ pkgTarget === "wasm" ? wasmMarker(checker.pkgName) : cg.generate(ast);
299
650
 
300
651
  const mainChunk = {
301
652
  js: mainJs,
302
653
  sourceFiles: [resolve(inputPath)],
303
654
  sourcesContent: [source],
304
- mappings: cg.getMappings(),
655
+ mappings: pkgTarget === "wasm" ? [] : cg.getMappings(),
305
656
  };
306
657
 
307
658
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
659
+ const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
660
+ { ...options, sourceMap },
661
+ merged,
662
+ pkgTarget,
663
+ outputDir,
664
+ );
308
665
 
309
666
  if (sourceMap && !options.isDependency) {
310
667
  attachSourceMap(merged, outputDir);
311
668
  }
312
669
 
313
- return { js: merged.js, css: cg.getCss() };
670
+ return {
671
+ js: merged.js,
672
+ css: cg.getCss(),
673
+ target: pkgTarget,
674
+ wasm,
675
+ wat,
676
+ wasmOptInfo,
677
+ wasmSourceMap,
678
+ };
314
679
  }
315
680
 
316
681
  export function compileDir(dir, options = {}) {
@@ -325,8 +690,20 @@ export function compilePackageTests(dir, options = {}) {
325
690
  return compileFiles(files, { ...options, fromDir: dir, isTest: true });
326
691
  }
327
692
 
693
+ // Compiles a `both` package's tests through the wasm backend (the JS run uses
694
+ // compilePackageTests). Returns null for packages that are not `both`.
695
+ export function compilePackageTestsWasm(dir, options = {}) {
696
+ const res = compilePackageTests(dir, { ...options, wasmTests: true });
697
+ return res.target === "both" ? res : null;
698
+ }
699
+
328
700
  export function compileFiles(files, options = {}) {
329
701
  const fromDir = options.fromDir ?? dirname(resolve(files[0]));
702
+ options = {
703
+ ...options,
704
+ wasmUnits: options.wasmUnits ?? [],
705
+ rootDir: options.rootDir ?? fromDir,
706
+ };
330
707
  const outputDir =
331
708
  options.outputDir ??
332
709
  (options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
@@ -353,8 +730,36 @@ export function compileFiles(files, options = {}) {
353
730
  );
354
731
  const pkgName = pkgNames[0];
355
732
 
733
+ // Validate target directives across files in this package
734
+ const declaredTargets = new Map();
735
+ for (const p of programs) {
736
+ const declared = p._declaredTarget;
737
+ if (declared) {
738
+ if (!declaredTargets.has(declared)) declaredTargets.set(declared, []);
739
+ declaredTargets.get(declared).push(p._filename);
740
+ }
741
+ }
742
+ if (declaredTargets.size > 1) {
743
+ const list = [...declaredTargets.entries()]
744
+ .map(([t, fList]) => `${t} (${fList.join(", ")})`)
745
+ .join(" vs ");
746
+ throw new Error(
747
+ `Conflicting //gofront:target directives in package '${pkgName}': ${list}`,
748
+ );
749
+ }
750
+ const pkgTarget = resolvePackageTarget(
751
+ declaredTargets.size === 1
752
+ ? declaredTargets.keys().next().value
753
+ : (options.target ?? "js"),
754
+ fromDir,
755
+ options,
756
+ );
757
+ for (const p of programs) p.target = pkgTarget;
758
+
356
759
  // ── 2. Resolve imports ────────────────────────────────────────
357
760
  const checker = new TypeChecker();
761
+ checker.target = pkgTarget;
762
+ checker.pkgName = pkgName;
358
763
  const jsImports = new Map(); // npm imports → exported names (for ESM emit)
359
764
  const bundledPackages = new Set(); // package names whose code is inlined
360
765
  const preambles = []; // JS code / chunks from compiled sub-packages
@@ -378,21 +783,44 @@ export function compileFiles(files, options = {}) {
378
783
  const msgs = errors.map((e) => e.message).join("\n");
379
784
  throw new Error(msgs);
380
785
  }
786
+ for (const w of checker.warnings) log.warn(w);
381
787
 
382
788
  // ── 4. Code generation ────────────────────────────────────────
383
- const codegen = new CodeGen(checker, jsImports, bundledPackages);
384
- const mainJs = codegen.generateAll(programs, {
385
- isTest: options.isTest ?? false,
789
+ const emitTarget = registerPackageUnit(options, {
790
+ key: resolve(fromDir),
791
+ pkgName,
792
+ pkgTarget,
793
+ programs,
794
+ types: checker.types,
795
+ bundledPackages,
386
796
  });
387
797
 
798
+ const isStrict = Boolean(options.strict || pkgTarget === "both");
799
+ const codegen = new CodeGen(checker, jsImports, bundledPackages, {
800
+ target: pkgTarget,
801
+ strict: isStrict,
802
+ });
803
+ const mainJs =
804
+ emitTarget === "wasm"
805
+ ? wasmMarker(pkgName)
806
+ : codegen.generateAll(programs, {
807
+ isTest: options.isTest ?? false,
808
+ });
809
+
388
810
  const mainChunk = {
389
811
  js: mainJs,
390
812
  sourceFiles: files.map((f) => resolve(f)),
391
813
  sourcesContent: files.map((f) => readFileSync(f, "utf8")),
392
- mappings: codegen.getMappings(),
814
+ mappings: emitTarget === "wasm" ? [] : codegen.getMappings(),
393
815
  };
394
816
 
395
817
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
818
+ const { wasm, wat, wasmOptInfo, wasmSourceMap } = finishWasm(
819
+ options,
820
+ merged,
821
+ pkgTarget,
822
+ outputDir,
823
+ );
396
824
 
397
825
  if (options.sourceMap && !options.isDependency) {
398
826
  attachSourceMap(merged, outputDir);
@@ -405,8 +833,13 @@ export function compileFiles(files, options = {}) {
405
833
 
406
834
  return {
407
835
  pkgName,
836
+ target: pkgTarget,
408
837
  js: merged.js,
409
838
  css: allCss,
839
+ wasm,
840
+ wat,
841
+ wasmOptInfo,
842
+ wasmSourceMap,
410
843
  programs,
411
844
  exportedSymbols: checker.getExportedSymbols(),
412
845
  exportedTypes: checker.getExportedTypes(),