gofront 1.3.12 → 1.5.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 (63) hide show
  1. package/README.md +90 -20
  2. package/package.json +2 -1
  3. package/src/{codegen → backend/js}/expressions.js +217 -60
  4. package/src/{codegen → backend/js}/index.js +47 -36
  5. package/src/{codegen → backend/js}/runtime.js +11 -0
  6. package/src/{codegen → backend/js}/statements.js +43 -48
  7. package/src/backend/js/stdlib/bits.js +32 -0
  8. package/src/{codegen → backend/js}/stdlib/index.js +3 -0
  9. package/src/{codegen → backend/js}/templ.js +31 -2
  10. package/src/backend/wasm/boundary.js +1345 -0
  11. package/src/backend/wasm/emit.js +5779 -0
  12. package/src/backend/wasm/encode.js +789 -0
  13. package/src/backend/wasm/glue.js +130 -0
  14. package/src/backend/wasm/index.js +1413 -0
  15. package/src/backend/wasm/types.js +420 -0
  16. package/src/backend/wasm/wat.js +255 -0
  17. package/src/cli-core.js +52 -7
  18. package/src/compiler.js +328 -12
  19. package/src/dev-server.js +56 -15
  20. package/src/index.js +17 -9
  21. package/src/lexer.js +18 -0
  22. package/src/lower/boxing.js +36 -0
  23. package/src/lower/captures.js +179 -0
  24. package/src/lower/embedding.js +30 -0
  25. package/src/lower/escape.js +169 -0
  26. package/src/lower/functions.js +30 -0
  27. package/src/lower/globals.js +324 -0
  28. package/src/lower/index.js +115 -0
  29. package/src/lower/ownership.js +142 -0
  30. package/src/lower/range.js +70 -0
  31. package/src/minifier.js +2 -0
  32. package/src/parser/index.js +8 -3
  33. package/src/resolver.js +4 -1
  34. package/src/templ-lexer.js +107 -3
  35. package/src/templ-parser.js +29 -0
  36. package/src/test-runner.js +74 -20
  37. package/src/typechecker/expressions.js +40 -1
  38. package/src/typechecker/index.js +301 -6
  39. package/src/typechecker/statements.js +7 -0
  40. package/src/typechecker/stdlib/core.js +17 -0
  41. package/src/typechecker/types.js +56 -2
  42. /package/src/{codegen → backend/js}/source-map.js +0 -0
  43. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  44. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  45. /package/src/{codegen → backend/js}/stdlib/errors.js +0 -0
  46. /package/src/{codegen → backend/js}/stdlib/fmt.js +0 -0
  47. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  48. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  49. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  50. /package/src/{codegen → backend/js}/stdlib/maps.js +0 -0
  51. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  52. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  53. /package/src/{codegen → backend/js}/stdlib/path.js +0 -0
  54. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  55. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  56. /package/src/{codegen → backend/js}/stdlib/slices.js +0 -0
  57. /package/src/{codegen → backend/js}/stdlib/sort.js +0 -0
  58. /package/src/{codegen → backend/js}/stdlib/strconv.js +0 -0
  59. /package/src/{codegen → backend/js}/stdlib/strings.js +0 -0
  60. /package/src/{codegen → backend/js}/stdlib/testing.js +0 -0
  61. /package/src/{codegen → backend/js}/stdlib/time.js +0 -0
  62. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  63. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/src/cli-core.js CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  sep,
24
24
  } from "node:path";
25
25
  import { copyAssets } from "./asset-manager.js";
26
+ import { isGoFrontWasm } from "./backend/wasm/index.js";
26
27
  import { colors, log } from "./colors.js";
27
28
  import { compileDir, compileSingleFile } from "./compiler.js";
28
29
  import { createDevServer } from "./dev-server.js";
@@ -42,11 +43,12 @@ export function runCompile(inputPath, isDir, options) {
42
43
  outputFile = null,
43
44
  dumpTokens = false,
44
45
  dumpAst = false,
46
+ emitWat = false,
45
47
  } = options ?? {};
46
48
 
47
49
  if (isDir) {
48
50
  const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
49
- return compileDir(inputPath, { sourceMap, outputDir });
51
+ return compileDir(inputPath, { sourceMap, outputDir, emitWat });
50
52
  }
51
53
 
52
54
  return compileSingleFile(inputPath, {
@@ -54,9 +56,49 @@ export function runCompile(inputPath, isDir, options) {
54
56
  outputFile,
55
57
  dumpTokens,
56
58
  dumpAst,
59
+ emitWat,
57
60
  });
58
61
  }
59
62
 
63
+ // Writes the JS bundle plus, when the project links wasm packages, the
64
+ // sibling `app.wasm` (and `app.wat` when requested). Stale wasm artifacts
65
+ // are removed so a project that drops its wasm packages serves clean output —
66
+ // but only when the existing file is one GoFront produced (recognised by its
67
+ // `gofront` custom section), so a hand-placed `app.wasm` is left alone.
68
+ export function writeCompileOutput(outputFile, result, js = result.js) {
69
+ mkdirSync(dirname(outputFile), { recursive: true });
70
+ writeFileSync(outputFile, `${js}\n`);
71
+ const wasmFile = join(dirname(outputFile), "app.wasm");
72
+ const watFile = join(dirname(outputFile), "app.wat");
73
+ const written = [outputFile];
74
+ let ownsWasm = Boolean(result.wasm);
75
+ if (result.wasm) {
76
+ writeFileSync(wasmFile, result.wasm);
77
+ written.push(wasmFile);
78
+ } else if (existsSync(wasmFile)) {
79
+ if (isGoFrontWasm(readFileSync(wasmFile))) {
80
+ rmSync(wasmFile);
81
+ ownsWasm = true;
82
+ }
83
+ }
84
+ if (result.wat) {
85
+ writeFileSync(watFile, result.wat);
86
+ written.push(watFile);
87
+ } else if (ownsWasm && existsSync(watFile)) {
88
+ rmSync(watFile);
89
+ }
90
+ return written;
91
+ }
92
+
93
+ export function formatWrittenDesc(written, outputFile = written?.[0] ?? "") {
94
+ if (!written || written.length <= 1) return colors.cyan(outputFile);
95
+ const extras = written
96
+ .slice(1)
97
+ .map((p) => colors.cyan(basename(p)))
98
+ .join(" + ");
99
+ return `${colors.cyan(outputFile)} + ${extras}`;
100
+ }
101
+
60
102
  export function maybeMinify(js, options) {
61
103
  const {
62
104
  minify: doMinify = false,
@@ -460,6 +502,7 @@ export function parseBuildArgs(argv) {
460
502
  const sourceMap = argv.includes("--source-map");
461
503
  const noMinify = argv.includes("--no-minify");
462
504
  const noMangle = argv.includes("--no-mangle");
505
+ const emitWat = argv.includes("--emit-wat");
463
506
 
464
507
  for (let i = 0; i < argv.length; i++) {
465
508
  const arg = argv[i];
@@ -477,6 +520,7 @@ export function parseBuildArgs(argv) {
477
520
  outDir,
478
521
  pwa,
479
522
  sourceMap,
523
+ emitWat,
480
524
  minify: !noMinify,
481
525
  mangle: !noMangle && !noMinify,
482
526
  };
@@ -667,6 +711,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
667
711
  const compileResult = runCompile(srcDir, isDir, {
668
712
  outputFile,
669
713
  sourceMap: options.sourceMap ?? false,
714
+ emitWat: options.emitWat ?? false,
670
715
  });
671
716
 
672
717
  const doMinify = options.minify ?? true;
@@ -676,7 +721,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
676
721
  mangle: doMangle,
677
722
  sourceMap: options.sourceMap ?? false,
678
723
  });
679
- writeFileSync(outputFile, `${js}\n`);
724
+ writeCompileOutput(outputFile, compileResult, js);
680
725
 
681
726
  let vendor = null;
682
727
  try {
@@ -752,12 +797,11 @@ function buildDevOnce(
752
797
  outputFile,
753
798
  sourceMap: sourceMap ?? true,
754
799
  });
755
- mkdirSync(dirname(outputFile), { recursive: true });
756
- writeFileSync(outputFile, `${result.js}\n`);
800
+ const written = writeCompileOutput(outputFile, result);
757
801
  const elapsedMs = (performance.now() - startMs).toFixed(0);
758
802
  const note = changedFile ? ` — ${changedFile} changed` : "";
759
803
  log.ok(
760
- `— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
804
+ `— wrote ${formatWrittenDesc(written, outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
761
805
  );
762
806
  devServer.notify();
763
807
  } catch (e) {
@@ -878,13 +922,13 @@ export async function handleDev(targetDir = ".", options = {}) {
878
922
  });
879
923
  let initialError = null;
880
924
 
925
+ let written = null;
881
926
  try {
882
927
  const result = runCompile(srcDir, isDir, {
883
928
  outputFile,
884
929
  sourceMap: options.sourceMap ?? true,
885
930
  });
886
- mkdirSync(dirname(outputFile), { recursive: true });
887
- writeFileSync(outputFile, `${result.js}\n`);
931
+ written = writeCompileOutput(outputFile, result);
888
932
  } catch (err) {
889
933
  initialError = err;
890
934
  devServer.notifyError(err);
@@ -914,6 +958,7 @@ export async function handleDev(targetDir = ".", options = {}) {
914
958
  srcDir,
915
959
  outputFile,
916
960
  initialError,
961
+ written,
917
962
  close: async () => {
918
963
  watcherController?.close();
919
964
  await devServer.close();
package/src/compiler.js CHANGED
@@ -19,16 +19,23 @@
19
19
 
20
20
  import { readdirSync, readFileSync, statSync } from "node:fs";
21
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";
22
+ import { buildSourceMap, CodeGen } from "./backend/js/index.js";
23
+ import { compileWasmModule } from "./backend/wasm/index.js";
24
24
  import { log } from "./colors.js";
25
25
  import { parseDts } from "./dts-parser.js";
26
26
  import { Lexer } from "./lexer.js";
27
+ import { lower } from "./lower/index.js";
27
28
  import { Parser } from "./parser/index.js";
28
- import { isLocalPath, resolveAll, resolveGwDir } from "./resolver.js";
29
+ import {
30
+ isBuiltinPackage,
31
+ isLocalPath,
32
+ resolveAll,
33
+ resolveGwDir,
34
+ } from "./resolver.js";
29
35
  import { TemplLexer } from "./templ-lexer.js";
30
36
  import { TemplParser } from "./templ-parser.js";
31
37
  import { TypeChecker } from "./typechecker/index.js";
38
+ import { WASM_SUPPORTED_STDLIB } from "./typechecker/types.js";
32
39
 
33
40
  // ── Parse cache ──────────────────────────────────────────────
34
41
 
@@ -156,6 +163,147 @@ function attachSourceMap(merged, outputDir) {
156
163
  merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
157
164
  }
158
165
 
166
+ // ── WASM linking ──────────────────────────────────────────────
167
+ //
168
+ // Every `wasm`/`both` package reached during a build registers a unit in the
169
+ // shared `options.wasmUnits` array. The root compile links all units into a
170
+ // single `app.wasm` and splices the generated JS facade into the bundle where
171
+ // the first wasm package's JS would have been.
172
+
173
+ const WASM_MARKER_RE = /^\/\*__GOFRONT_WASM_UNIT:[^*]*\*\/$/m;
174
+
175
+ function wasmMarker(pkgName) {
176
+ return `/*__GOFRONT_WASM_UNIT:${pkgName}*/`;
177
+ }
178
+
179
+ function registerWasmUnit(options, unit) {
180
+ const units = options.wasmUnits;
181
+ if (!units) return;
182
+ if (units.some((u) => u.key === unit.key)) return;
183
+ units.push(unit);
184
+ }
185
+
186
+ // Registers a `wasm`/`both` package as a link unit and returns the target the
187
+ // package's own code is emitted for. `options.wasmTests` runs a `both`
188
+ // package's tests through the wasm backend (the package is then treated as
189
+ // `wasm` for linking so its test functions are exposed by the facade).
190
+ function registerPackageUnit(options, unit) {
191
+ const { pkgTarget, programs } = unit;
192
+ if (pkgTarget !== "wasm" && pkgTarget !== "both") return pkgTarget;
193
+ const emitTarget =
194
+ options.wasmTests && options.isTest && pkgTarget === "both"
195
+ ? "wasm"
196
+ : pkgTarget;
197
+ registerWasmUnit(options, {
198
+ ...unit,
199
+ target: emitTarget,
200
+ programs:
201
+ emitTarget === pkgTarget
202
+ ? programs
203
+ : programs.map((p) => ({ ...p, target: emitTarget })),
204
+ });
205
+ return emitTarget;
206
+ }
207
+
208
+ function linkWasmUnits(units, { emitWat = false, rootTarget = "js" } = {}) {
209
+ if (!units.some((u) => u.target === "wasm")) return null;
210
+ checkLinkCollisions(units);
211
+
212
+ const types = new Map();
213
+ const bundledPackages = new Set();
214
+ const programs = [];
215
+ for (const u of units) {
216
+ for (const [k, v] of u.types) types.set(k, v);
217
+ for (const b of u.bundledPackages) bundledPackages.add(b);
218
+ programs.push(...u.programs);
219
+ }
220
+ const checker = { types };
221
+ const lowerRes = lower(programs, checker);
222
+ return compileWasmModule(programs, checker, lowerRes, {
223
+ boundary: true,
224
+ bundledPackages,
225
+ emitWat,
226
+ callMain: rootTarget === "wasm",
227
+ });
228
+ }
229
+
230
+ // The linked module is one flat namespace (like the JS bundle), so a top-level
231
+ // name declared by two different packages would silently resolve to whichever
232
+ // was emitted last. Reject it up front.
233
+ function topLevelDeclNames(decl) {
234
+ switch (decl.kind) {
235
+ case "FuncDecl":
236
+ return [decl.name];
237
+ case "MethodDecl":
238
+ return [`${decl.recvType?.name ?? "?"}.${decl.name}`];
239
+ case "TypeDecl":
240
+ return [decl.name];
241
+ case "VarDecl":
242
+ case "ConstDecl":
243
+ return (decl.decls ?? []).flatMap((spec) => spec.names ?? []);
244
+ default:
245
+ return [];
246
+ }
247
+ }
248
+
249
+ function checkLinkCollisions(units) {
250
+ const owner = new Map(); // name -> pkgName
251
+ const errors = [];
252
+ for (const u of units) {
253
+ for (const p of u.programs) {
254
+ for (const d of p.decls ?? []) {
255
+ for (const name of topLevelDeclNames(d)) {
256
+ if (name === "_") continue;
257
+ const prev = owner.get(name);
258
+ if (prev === undefined) owner.set(name, u.pkgName);
259
+ else if (prev !== u.pkgName)
260
+ errors.push(
261
+ `cannot link wasm packages: '${name}' is declared in both '${prev}' and '${u.pkgName}' (linked wasm packages share one namespace)`,
262
+ );
263
+ }
264
+ }
265
+ }
266
+ }
267
+ if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
268
+ }
269
+
270
+ // Replaces the first wasm marker with the facade (dropping the others) and
271
+ // shifts source-map lines that follow it.
272
+ function spliceFacade(merged, facade) {
273
+ const lines = merged.js.split("\n");
274
+ const first = lines.findIndex((l) => WASM_MARKER_RE.test(l));
275
+ if (first < 0) {
276
+ merged.js = `${facade}\n${merged.js}`;
277
+ const shift = facade.split("\n").length;
278
+ for (const m of merged.mappings) m.genLine += shift;
279
+ return;
280
+ }
281
+ const facadeLines = facade.split("\n");
282
+ const delta = facadeLines.length - 1;
283
+ for (const m of merged.mappings) {
284
+ if (m.genLine > first) m.genLine += delta;
285
+ }
286
+ const out = [...lines.slice(0, first), ...facadeLines];
287
+ for (let i = first + 1; i < lines.length; i++) {
288
+ if (!WASM_MARKER_RE.test(lines[i])) out.push(lines[i]);
289
+ }
290
+ merged.js = out.join("\n");
291
+ }
292
+
293
+ // 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 };
298
+ const linked = linkWasmUnits(options.wasmUnits, {
299
+ emitWat: options.emitWat,
300
+ rootTarget: pkgTarget,
301
+ });
302
+ if (!linked) return { wasm: null, wat: null };
303
+ spliceFacade(merged, linked.facade);
304
+ return { wasm: linked.wasm, wat: linked.wat ?? null };
305
+ }
306
+
159
307
  // ── Import resolution ─────────────────────────────────────────
160
308
  //
161
309
  // Shared by compileFiles (multi-file) and the single-file path in index.js.
@@ -173,6 +321,55 @@ export function resolveImports(
173
321
  ) {
174
322
  const fromDir = dirname(resolve(fromFile));
175
323
  const allImports = programs.flatMap((p) => p.imports);
324
+ const pkgTarget = checker.target ?? "js";
325
+ const pkgName = checker.pkgName ?? programs[0]?.pkg?.name ?? "main";
326
+
327
+ // Validate target rules on non-local imports
328
+ if (pkgTarget === "wasm" || pkgTarget === "both") {
329
+ for (const p of programs) {
330
+ const saved = [checker._currentFile, checker._currentSource];
331
+ checker._currentFile = p._filename ?? null;
332
+ checker._currentSource = p._source ?? null;
333
+ for (const imp of p.imports) {
334
+ for (const { path, _line, _col, line, col } of imp.imports) {
335
+ const impNode = {
336
+ _line,
337
+ _col,
338
+ line: line ?? _line,
339
+ col: col ?? _col ?? 1,
340
+ };
341
+ if (path.startsWith("js:")) {
342
+ checker.recordBlocker("js: import", path);
343
+ checker.err(
344
+ "js: imports are not allowed in wasm packages",
345
+ impNode,
346
+ );
347
+ } else if (path === "gom") {
348
+ checker.recordBlocker("gom usage", "gom");
349
+ checker.err(
350
+ "package 'gom' is not available in wasm packages",
351
+ impNode,
352
+ );
353
+ } else if (!isLocalPath(path)) {
354
+ if (!isBuiltinPackage(path)) {
355
+ checker.recordBlocker("js: import", path);
356
+ checker.err(
357
+ "js: imports are not allowed in wasm packages",
358
+ impNode,
359
+ );
360
+ } else if (!WASM_SUPPORTED_STDLIB.has(path)) {
361
+ checker.recordBlocker("unsupported stdlib import", path);
362
+ checker.err(
363
+ `'${path}' is not yet available in wasm packages`,
364
+ impNode,
365
+ );
366
+ }
367
+ }
368
+ }
369
+ }
370
+ [checker._currentFile, checker._currentSource] = saved;
371
+ }
372
+ }
176
373
 
177
374
  // js: prefix — local .d.ts files
178
375
  for (const imp of allImports) {
@@ -205,7 +402,7 @@ export function resolveImports(
205
402
  const seenLocalPaths = new Set();
206
403
  for (const p of programs) {
207
404
  for (const imp of p.imports) {
208
- for (const { path, alias, _line } of imp.imports) {
405
+ for (const { path, alias, _line, _col, line, col } of imp.imports) {
209
406
  if (!isLocalPath(path) || seenLocalPaths.has(path)) continue;
210
407
  seenLocalPaths.add(path);
211
408
  const depDir = resolveGwDir(path, fromFile);
@@ -226,6 +423,44 @@ export function resolveImports(
226
423
  isDependency: true,
227
424
  });
228
425
  if (!alreadyBundled) preambles.push(dep);
426
+
427
+ // Target import rules for local packages
428
+ const impNode = {
429
+ _line,
430
+ _col,
431
+ line: line ?? _line,
432
+ col: col ?? _col ?? 1,
433
+ };
434
+ if (pkgTarget === "wasm" && dep.target === "js") {
435
+ const saved = [checker._currentFile, checker._currentSource];
436
+ checker._currentFile = p._filename ?? null;
437
+ checker._currentSource = p._source ?? null;
438
+ checker.recordBlocker("js package import", dep.pkgName);
439
+ checker.err(
440
+ `package '${pkgName}' (wasm) cannot import '${dep.pkgName}' (js)`,
441
+ impNode,
442
+ );
443
+ [checker._currentFile, checker._currentSource] = saved;
444
+ } else if (pkgTarget === "both") {
445
+ const saved = [checker._currentFile, checker._currentSource];
446
+ checker._currentFile = p._filename ?? null;
447
+ checker._currentSource = p._source ?? null;
448
+ if (dep.target === "js") {
449
+ checker.recordBlocker("js package import", dep.pkgName);
450
+ checker.err(
451
+ `package '${pkgName}' (both) cannot import '${dep.pkgName}' (js)`,
452
+ impNode,
453
+ );
454
+ } else if (dep.target === "wasm") {
455
+ checker.recordBlocker("wasm package import", dep.pkgName);
456
+ checker.err(
457
+ `package '${pkgName}' (both) can only import 'both' packages; '${dep.pkgName}' is wasm`,
458
+ impNode,
459
+ );
460
+ }
461
+ [checker._currentFile, checker._currentSource] = saved;
462
+ }
463
+
229
464
  if (alias === "_") continue;
230
465
  if (alias === ".") {
231
466
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
@@ -253,6 +488,7 @@ export function compileSingleFile(inputPath, options = {}) {
253
488
  dumpTokens = false,
254
489
  dumpAst = false,
255
490
  } = options;
491
+ options = { ...options, wasmUnits: options.wasmUnits ?? [] };
256
492
 
257
493
  let source;
258
494
  try {
@@ -277,6 +513,11 @@ export function compileSingleFile(inputPath, options = {}) {
277
513
  const bundledPackages = new Set();
278
514
  const preambles = [];
279
515
 
516
+ const pkgTarget = ast.target ?? options.target ?? "js";
517
+ ast.target = pkgTarget;
518
+ checker.target = pkgTarget;
519
+ checker.pkgName = ast.pkg?.name ?? "main";
520
+
280
521
  resolveImports(
281
522
  [ast],
282
523
  inputPath,
@@ -294,23 +535,40 @@ export function compileSingleFile(inputPath, options = {}) {
294
535
  throw new Error(errors.map((e) => e.message).join("\n"));
295
536
  }
296
537
 
297
- const cg = new CodeGen(checker, jsImports, bundledPackages);
298
- const mainJs = cg.generate(ast);
538
+ if (pkgTarget === "wasm" || pkgTarget === "both") {
539
+ registerWasmUnit(options, {
540
+ key: resolve(inputPath),
541
+ pkgName: checker.pkgName,
542
+ target: pkgTarget,
543
+ programs: [ast],
544
+ types: checker.types,
545
+ bundledPackages,
546
+ });
547
+ }
548
+
549
+ const isStrict = Boolean(options.strict || pkgTarget === "both");
550
+ const cg = new CodeGen(checker, jsImports, bundledPackages, {
551
+ target: pkgTarget,
552
+ strict: isStrict,
553
+ });
554
+ const mainJs =
555
+ pkgTarget === "wasm" ? wasmMarker(checker.pkgName) : cg.generate(ast);
299
556
 
300
557
  const mainChunk = {
301
558
  js: mainJs,
302
559
  sourceFiles: [resolve(inputPath)],
303
560
  sourcesContent: [source],
304
- mappings: cg.getMappings(),
561
+ mappings: pkgTarget === "wasm" ? [] : cg.getMappings(),
305
562
  };
306
563
 
307
564
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
565
+ const { wasm, wat } = linkIntoBundle(options, merged, pkgTarget);
308
566
 
309
567
  if (sourceMap && !options.isDependency) {
310
568
  attachSourceMap(merged, outputDir);
311
569
  }
312
570
 
313
- return { js: merged.js };
571
+ return { js: merged.js, css: cg.getCss(), target: pkgTarget, wasm, wat };
314
572
  }
315
573
 
316
574
  export function compileDir(dir, options = {}) {
@@ -325,7 +583,15 @@ export function compilePackageTests(dir, options = {}) {
325
583
  return compileFiles(files, { ...options, fromDir: dir, isTest: true });
326
584
  }
327
585
 
586
+ // Compiles a `both` package's tests through the wasm backend (the JS run uses
587
+ // compilePackageTests). Returns null for packages that are not `both`.
588
+ export function compilePackageTestsWasm(dir, options = {}) {
589
+ const res = compilePackageTests(dir, { ...options, wasmTests: true });
590
+ return res.target === "both" ? res : null;
591
+ }
592
+
328
593
  export function compileFiles(files, options = {}) {
594
+ options = { ...options, wasmUnits: options.wasmUnits ?? [] };
329
595
  const fromDir = options.fromDir ?? dirname(resolve(files[0]));
330
596
  const outputDir =
331
597
  options.outputDir ??
@@ -353,8 +619,32 @@ export function compileFiles(files, options = {}) {
353
619
  );
354
620
  const pkgName = pkgNames[0];
355
621
 
622
+ // Validate target directives across files in this package
623
+ const declaredTargets = new Map();
624
+ 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);
628
+ }
629
+ }
630
+ if (declaredTargets.size > 1) {
631
+ const list = [...declaredTargets.entries()]
632
+ .map(([t, fList]) => `${t} (${fList.join(", ")})`)
633
+ .join(" vs ");
634
+ throw new Error(
635
+ `Conflicting //gofront:target directives in package '${pkgName}': ${list}`,
636
+ );
637
+ }
638
+ const pkgTarget =
639
+ declaredTargets.size === 1
640
+ ? declaredTargets.keys().next().value
641
+ : (options.target ?? "js");
642
+ for (const p of programs) p.target = pkgTarget;
643
+
356
644
  // ── 2. Resolve imports ────────────────────────────────────────
357
645
  const checker = new TypeChecker();
646
+ checker.target = pkgTarget;
647
+ checker.pkgName = pkgName;
358
648
  const jsImports = new Map(); // npm imports → exported names (for ESM emit)
359
649
  const bundledPackages = new Set(); // package names whose code is inlined
360
650
  const preambles = []; // JS code / chunks from compiled sub-packages
@@ -380,27 +670,53 @@ export function compileFiles(files, options = {}) {
380
670
  }
381
671
 
382
672
  // ── 4. Code generation ────────────────────────────────────────
383
- const codegen = new CodeGen(checker, jsImports, bundledPackages);
384
- const mainJs = codegen.generateAll(programs, {
385
- isTest: options.isTest ?? false,
673
+ const emitTarget = registerPackageUnit(options, {
674
+ key: resolve(fromDir),
675
+ pkgName,
676
+ pkgTarget,
677
+ programs,
678
+ types: checker.types,
679
+ bundledPackages,
680
+ });
681
+
682
+ const isStrict = Boolean(options.strict || pkgTarget === "both");
683
+ const codegen = new CodeGen(checker, jsImports, bundledPackages, {
684
+ target: pkgTarget,
685
+ strict: isStrict,
386
686
  });
687
+ const mainJs =
688
+ emitTarget === "wasm"
689
+ ? wasmMarker(pkgName)
690
+ : codegen.generateAll(programs, {
691
+ isTest: options.isTest ?? false,
692
+ });
387
693
 
388
694
  const mainChunk = {
389
695
  js: mainJs,
390
696
  sourceFiles: files.map((f) => resolve(f)),
391
697
  sourcesContent: files.map((f) => readFileSync(f, "utf8")),
392
- mappings: codegen.getMappings(),
698
+ mappings: emitTarget === "wasm" ? [] : codegen.getMappings(),
393
699
  };
394
700
 
395
701
  const merged = mergeCompilationChunks([...preambles, mainChunk]);
702
+ const { wasm, wat } = linkIntoBundle(options, merged, pkgTarget);
396
703
 
397
704
  if (options.sourceMap && !options.isDependency) {
398
705
  attachSourceMap(merged, outputDir);
399
706
  }
400
707
 
708
+ const mainCss = codegen.getCss();
709
+ const allCss = [...preambles.map((p) => p.css).filter(Boolean), mainCss]
710
+ .filter(Boolean)
711
+ .join("\n\n");
712
+
401
713
  return {
402
714
  pkgName,
715
+ target: pkgTarget,
403
716
  js: merged.js,
717
+ css: allCss,
718
+ wasm,
719
+ wat,
404
720
  programs,
405
721
  exportedSymbols: checker.getExportedSymbols(),
406
722
  exportedTypes: checker.getExportedTypes(),