gofront 1.6.1 → 1.8.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 (132) hide show
  1. package/README.md +45 -49
  2. package/package.json +12 -8
  3. package/src/backend/js/expressions.js +1627 -148
  4. package/src/backend/js/fmt-runtime.js +573 -0
  5. package/src/backend/js/index.js +508 -102
  6. package/src/backend/js/runtime.js +1997 -106
  7. package/src/backend/js/statements.js +931 -87
  8. package/src/backend/js/stdlib/bits.js +87 -14
  9. package/src/backend/js/stdlib/builder.js +41 -6
  10. package/src/backend/js/stdlib/bytes.js +253 -7
  11. package/src/backend/js/stdlib/errors.js +7 -1
  12. package/src/backend/js/stdlib/fmt.js +349 -10
  13. package/src/backend/js/stdlib/index.js +19 -2
  14. package/src/backend/js/stdlib/json.js +1167 -0
  15. package/src/backend/js/stdlib/maps.js +0 -8
  16. package/src/backend/js/stdlib/math.js +17 -1
  17. package/src/backend/js/stdlib/simd.js +34 -0
  18. package/src/backend/js/stdlib/slices.js +36 -6
  19. package/src/backend/js/stdlib/strconv.js +10 -5
  20. package/src/backend/js/stdlib/strings.js +83 -4
  21. package/src/backend/js/stdlib/testing.js +7 -0
  22. package/src/backend/js/stdlib/time.js +79 -55
  23. package/src/backend/js/stdlib/unicode.js +1 -1
  24. package/src/backend/js/stdlib/utf8.js +1 -1
  25. package/src/backend/js/templ.js +38 -76
  26. package/src/backend/js/type-info.js +272 -0
  27. package/src/backend/shared/fmt-types.js +188 -0
  28. package/src/backend/wasm/boundary.js +446 -72
  29. package/src/backend/wasm/emit-builtins.js +320 -13
  30. package/src/backend/wasm/emit-exprs.js +1404 -199
  31. package/src/backend/wasm/emit-maps.js +61 -13
  32. package/src/backend/wasm/emit-stmts.js +1359 -164
  33. package/src/backend/wasm/emit.js +518 -74
  34. package/src/backend/wasm/encode.js +128 -13
  35. package/src/backend/wasm/globals.js +452 -0
  36. package/src/backend/wasm/index.js +194 -2489
  37. package/src/backend/wasm/method-expr.js +381 -0
  38. package/src/backend/wasm/module-emitter.js +2240 -0
  39. package/src/backend/wasm/monomorph.js +62 -4
  40. package/src/backend/wasm/optimize.js +4 -2
  41. package/src/backend/wasm/prescan.js +397 -0
  42. package/src/backend/wasm/stdlib/bytes.js +1804 -127
  43. package/src/backend/wasm/stdlib/core.js +1574 -199
  44. package/src/backend/wasm/stdlib/index.js +3 -5
  45. package/src/backend/wasm/stdlib/io.js +1567 -150
  46. package/src/backend/wasm/stdlib/packages.js +2 -0
  47. package/src/backend/wasm/stdlib/simd.js +499 -0
  48. package/src/backend/wasm/stdlib/time.js +1041 -12
  49. package/src/backend/wasm/types.js +221 -15
  50. package/src/backend/wasm/variadic.js +93 -0
  51. package/src/backend/wasm/wat.js +84 -20
  52. package/src/{cli-core.js → cli/cli-core.js} +236 -10
  53. package/src/{test-runner.js → cli/test-runner.js} +332 -39
  54. package/src/{vendor.js → cli/vendor.js} +89 -15
  55. package/src/compiler.js +107 -269
  56. package/src/dts-parser.js +71 -14
  57. package/src/index.js +15 -169
  58. package/src/{lexer.js → lexer/lexer.js} +102 -55
  59. package/src/{tokens.js → lexer/tokens.js} +1 -0
  60. package/src/link/sourcemap.js +97 -0
  61. package/src/link/stdlib.js +515 -0
  62. package/src/link/wasm.js +294 -0
  63. package/src/lower/bounds.js +394 -0
  64. package/src/lower/boxing.js +1 -6
  65. package/src/lower/captures.js +176 -32
  66. package/src/lower/embedding.js +2 -1
  67. package/src/lower/index.js +26 -18
  68. package/src/lower/json-codecs.js +1398 -0
  69. package/src/lower/ownership.js +22 -1
  70. package/src/lower/range.js +86 -1
  71. package/src/lower/reachability.js +132 -0
  72. package/src/parser/declarations.js +58 -13
  73. package/src/parser/expressions.js +29 -7
  74. package/src/parser/index.js +2 -1
  75. package/src/parser/statements.js +37 -22
  76. package/src/parser/struct-tag.js +144 -0
  77. package/src/{templ-parser.js → parser/templ.js} +138 -15
  78. package/src/parser/types.js +130 -13
  79. package/src/resolver.js +22 -1
  80. package/src/typechecker/assignability.js +215 -11
  81. package/src/typechecker/const-fold.js +356 -0
  82. package/src/typechecker/constants.js +256 -0
  83. package/src/typechecker/expressions.js +960 -99
  84. package/src/typechecker/index.js +491 -72
  85. package/src/typechecker/init-order.js +330 -0
  86. package/src/typechecker/resolve.js +596 -52
  87. package/src/typechecker/statements.js +157 -17
  88. package/src/typechecker/stdlib/core.js +333 -55
  89. package/src/typechecker/stdlib/excluded.js +898 -0
  90. package/src/typechecker/stdlib/extended.js +359 -26
  91. package/src/typechecker/{stdlib.js → stdlib/index.js} +6 -4
  92. package/src/typechecker/stdlib/simd.js +123 -0
  93. package/src/typechecker/termination.js +1 -1
  94. package/src/typechecker/types.js +165 -26
  95. package/stdlib/bytes/iter.go +192 -0
  96. package/stdlib/bytes/search.go +240 -0
  97. package/stdlib/bytes/split.go +218 -0
  98. package/stdlib/cmp/cmp.go +65 -0
  99. package/stdlib/encoding/json/errors.go +31 -0
  100. package/stdlib/encoding/json/indent.go +71 -0
  101. package/stdlib/encoding/json/number.go +12 -0
  102. package/stdlib/encoding/json/stream.go +173 -0
  103. package/stdlib/errors/overlay.go +12 -0
  104. package/stdlib/hash/hash.go +53 -0
  105. package/stdlib/internal/jsonrt/dec.go +738 -0
  106. package/stdlib/internal/jsonrt/enc.go +366 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +649 -0
  111. package/stdlib/io/io.go +346 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/iter.go +65 -0
  114. package/stdlib/math/extra.go +270 -0
  115. package/stdlib/slices/extra.go +73 -0
  116. package/stdlib/slices/iter.go +113 -0
  117. package/stdlib/sort/sort.go +561 -0
  118. package/stdlib/strconv/quote.go +341 -0
  119. package/stdlib/strings/extra.go +35 -0
  120. package/stdlib/strings/iter.go +122 -0
  121. package/stdlib/sync/extra.go +105 -0
  122. package/stdlib/unicode/unicode.go +112 -0
  123. package/stdlib/unicode/utf8/bytes.go +239 -0
  124. package/src/backend/wasm/const-fold.js +0 -205
  125. package/src/backend/wasm/emit-stdlib.js +0 -13
  126. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  127. /package/src/{colors.js → cli/colors.js} +0 -0
  128. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  129. /package/src/{minifier.js → cli/minifier.js} +0 -0
  130. /package/src/{project-config.js → cli/project-config.js} +0 -0
  131. /package/src/{pwa.js → cli/pwa.js} +0 -0
  132. /package/src/{templ-lexer.js → lexer/templ-lexer.js} +0 -0
@@ -6,13 +6,13 @@ import { statSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join, resolve } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
- import { HELPER_SPRINTF, HELPER_TESTING } from "./backend/js/runtime.js";
10
- import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
9
+ import { HELPER_SPRINTF, HELPER_TESTING } from "../backend/js/runtime.js";
11
10
  import {
12
11
  compilePackageTests,
13
12
  compilePackageTestsWasm,
14
13
  gwFilesIn,
15
- } from "./compiler.js";
14
+ } from "../compiler.js";
15
+ import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
16
16
 
17
17
  // WasmGC + exception handling (what the wasm backend emits) ship enabled by
18
18
  // default from Node 22 (V8 11.9+). Older runtimes fail at instantiate time
@@ -30,11 +30,55 @@ export function checkWasmRuntime(version = process.version) {
30
30
  export function isTestFunc(decl) {
31
31
  if (decl.kind !== "FuncDecl") return false;
32
32
  if (!decl.name.startsWith("Test")) return false;
33
+ if (decl.name === "TestMain") return false;
33
34
  if (decl.params?.length !== 1) return false;
34
35
  const pType = decl.params[0].type;
35
- if (pType?.kind === "PointerType" && pType.base?.name === "testing.T")
36
+ if (
37
+ pType?.kind === "PointerType" &&
38
+ (pType.base?.name === "testing.T" || pType.base?.name === "T")
39
+ )
40
+ return true;
41
+ if (
42
+ pType?.kind === "TypeName" &&
43
+ (pType.name === "testing.T" || pType.name === "T")
44
+ )
45
+ return true;
46
+ return false;
47
+ }
48
+
49
+ export function isBenchmarkFunc(decl) {
50
+ if (decl.kind !== "FuncDecl") return false;
51
+ if (!decl.name.startsWith("Benchmark")) return false;
52
+ if (decl.params?.length !== 1) return false;
53
+ const pType = decl.params[0].type;
54
+ if (
55
+ pType?.kind === "PointerType" &&
56
+ (pType.base?.name === "testing.B" || pType.base?.name === "B")
57
+ )
58
+ return true;
59
+ if (
60
+ pType?.kind === "TypeName" &&
61
+ (pType.name === "testing.B" || pType.name === "B")
62
+ )
63
+ return true;
64
+ return false;
65
+ }
66
+
67
+ export function isTestMainFunc(decl) {
68
+ if (decl.kind !== "FuncDecl") return false;
69
+ if (decl.name !== "TestMain") return false;
70
+ if (decl.params?.length !== 1) return false;
71
+ const pType = decl.params[0].type;
72
+ if (
73
+ pType?.kind === "PointerType" &&
74
+ (pType.base?.name === "testing.M" || pType.base?.name === "M")
75
+ )
76
+ return true;
77
+ if (
78
+ pType?.kind === "TypeName" &&
79
+ (pType.name === "testing.M" || pType.name === "M")
80
+ )
36
81
  return true;
37
- if (pType?.kind === "TypeName" && pType.name === "testing.T") return true;
38
82
  return false;
39
83
  }
40
84
 
@@ -53,8 +97,38 @@ export function discoverTests(programs) {
53
97
  return testNames;
54
98
  }
55
99
 
100
+ export function discoverBenchmarks(programs) {
101
+ const benchNames = [];
102
+ for (const p of programs) {
103
+ if (p._filename && !p._filename.endsWith("_test.go")) continue;
104
+ for (const d of p.decls) {
105
+ if (isBenchmarkFunc(d) && !benchNames.includes(d.name)) {
106
+ benchNames.push(d.name);
107
+ }
108
+ }
109
+ }
110
+ return benchNames;
111
+ }
112
+
113
+ export function discoverTestMain(programs) {
114
+ for (const p of programs) {
115
+ if (p._filename && !p._filename.endsWith("_test.go")) continue;
116
+ for (const d of p.decls) {
117
+ if (isTestMainFunc(d)) return d.name;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
56
123
  export function generateTestHarness(bundleJs, testNames, options = {}) {
57
124
  const runRegex = options.run ? options.run : null;
125
+ const benchRegex = options.bench
126
+ ? options.bench === true
127
+ ? "."
128
+ : options.bench
129
+ : null;
130
+ const benchNames = options.benchNames ?? [];
131
+ const testMainName = options.testMain ?? null;
58
132
  const verbose = Boolean(options.verbose);
59
133
  const dom = Boolean(options.dom);
60
134
  const color = Boolean(options.color);
@@ -65,6 +139,14 @@ export function generateTestHarness(bundleJs, testNames, options = {}) {
65
139
  ? pathToFileURL(options.jsdomPath).href
66
140
  : "jsdom";
67
141
 
142
+ const allFuncNames = [
143
+ ...new Set([
144
+ ...testNames,
145
+ ...benchNames,
146
+ ...(testMainName ? [testMainName] : []),
147
+ ]),
148
+ ];
149
+
68
150
  return `
69
151
  ${
70
152
  dom
@@ -112,7 +194,10 @@ ${bundleJs}
112
194
 
113
195
  async function __runGoFrontSuite() {
114
196
  const allTests = ${JSON.stringify(testNames)};
197
+ const allBenchmarks = ${JSON.stringify(benchNames)};
198
+ const testMainName = ${JSON.stringify(testMainName)};
115
199
  const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
200
+ const benchFilter = ${benchRegex ? `new RegExp(${JSON.stringify(benchRegex)})` : "null"};
116
201
  const verbose = ${verbose};
117
202
  const pkgName = ${JSON.stringify(pkgName)};
118
203
  const paint = ${color}
@@ -126,19 +211,23 @@ async function __runGoFrontSuite() {
126
211
  ? allTests.filter((name) => runFilter.test(name))
127
212
  : allTests;
128
213
 
129
- if (testsToRun.length === 0) {
214
+ const benchesToRun = benchFilter
215
+ ? allBenchmarks.filter((name) => benchFilter.test(name))
216
+ : [];
217
+
218
+ if (testsToRun.length === 0 && benchesToRun.length === 0) {
130
219
  if (allTests.length > 0 && runFilter) {
131
220
  console.log(yellow("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)}));
132
221
  }
222
+ if (allBenchmarks.length > 0 && benchFilter) {
223
+ console.log(yellow("testing: warning: no benchmarks to run matching " + ${JSON.stringify(benchRegex)}));
224
+ }
133
225
  console.log(PASS);
134
226
  okLine(pkgName, "0.000");
135
227
  process.exit(0);
136
228
  }
137
229
 
138
- let totalFailures = 0;
139
- let totalSkipped = 0;
140
230
  const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
141
- const suiteStart = now();
142
231
 
143
232
  // Go-style streaming report. In non-verbose mode only failures and skips are
144
233
  // shown, so the "=== RUN" header is emitted late for failures.
@@ -157,6 +246,28 @@ async function __runGoFrontSuite() {
157
246
  }
158
247
  };
159
248
 
249
+ const reportBenchmark = (b) => {
250
+ if (b.skipped) {
251
+ console.log("--- " + SKIP + ": " + b._name);
252
+ for (const log of b._logs) console.log(" " + log);
253
+ return;
254
+ }
255
+ if (b.failed) {
256
+ console.log("--- " + FAIL + ": " + b._name);
257
+ for (const log of b._logs) console.log(" " + red(log));
258
+ return;
259
+ }
260
+ const elapsedNs = Number(b.Elapsed());
261
+ const nsPerOp = b.N > 0 ? (elapsedNs / b.N).toFixed(0) : "0";
262
+ let line = \`\${b._name.padEnd(20)}\\t\${String(b.N).padStart(8)}\\t\${nsPerOp.padStart(10)} ns/op\`;
263
+ if (b._bytes > 0n && elapsedNs > 0) {
264
+ const mbPerSec = (Number(b._bytes * BigInt(b.N)) / (elapsedNs / 1e9) / (1024 * 1024)).toFixed(2);
265
+ line += \`\\t\${mbPerSec.padStart(8)} MB/s\`;
266
+ }
267
+ console.log(line);
268
+ for (const log of b._logs) console.log(" " + log);
269
+ };
270
+
160
271
  // FailNow/SkipNow are control flow; anything else is a panic → failure.
161
272
  const recordTestError = (t, e) => {
162
273
  if (e instanceof __GoFront_FailNow || e instanceof __GoFront_SkipNow) return;
@@ -170,43 +281,221 @@ async function __runGoFrontSuite() {
170
281
  globalThis.__onSubtestEnd = report;
171
282
 
172
283
  const __testRegistry = {
173
- ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ? ${n} : null`).join(",\n")}
284
+ ${allFuncNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ? ${n} : null`).join(",\n")}
174
285
  };
175
286
 
176
- for (const name of testsToRun) {
177
- const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
178
- if (typeof fn !== "function") {
179
- console.error(red("testing: cannot find test function " + name));
180
- totalFailures++;
181
- continue;
287
+ function runAll() {
288
+ let totalFailures = 0;
289
+ let totalSkipped = 0;
290
+ const suiteStart = now();
291
+
292
+ const finish = () => {
293
+ const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
294
+ if (totalFailures > 0) {
295
+ console.log(FAIL);
296
+ console.log(FAIL + "\\t" + pkgName + "\\t" + dim(suiteElapsed + "s"));
297
+ return 1;
298
+ } else {
299
+ console.log(PASS);
300
+ okLine(pkgName, suiteElapsed);
301
+ return 0;
302
+ }
303
+ };
304
+
305
+ const runOneTest = (name) => {
306
+ const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
307
+ if (typeof fn !== "function") {
308
+ console.error(red("testing: cannot find test function " + name));
309
+ totalFailures++;
310
+ return null;
311
+ }
312
+
313
+ const t = new __GoFront_T(name);
314
+ if (verbose) console.log(dim("=== RUN ") + name);
315
+
316
+ let res;
317
+ try {
318
+ res = fn(t);
319
+ } catch (e) {
320
+ recordTestError(t, e);
321
+ t._runCleanups();
322
+ if (t.skipped) totalSkipped++;
323
+ else if (t.failed) totalFailures++;
324
+ report(t);
325
+ return null;
326
+ }
327
+
328
+ if (res && typeof res.then === "function") {
329
+ return res
330
+ .catch((e) => { recordTestError(t, e); })
331
+ .finally(() => {
332
+ t._runCleanups();
333
+ if (t.skipped) totalSkipped++;
334
+ else if (t.failed) totalFailures++;
335
+ report(t);
336
+ });
337
+ }
338
+
339
+ t._runCleanups();
340
+ if (t.skipped) totalSkipped++;
341
+ else if (t.failed) totalFailures++;
342
+ report(t);
343
+ return null;
344
+ };
345
+
346
+ const runOneBench = (name) => {
347
+ const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
348
+ if (typeof fn !== "function") {
349
+ console.error(red("testing: cannot find benchmark function " + name));
350
+ totalFailures++;
351
+ return null;
352
+ }
353
+ if (verbose) console.log(dim("=== RUN ") + name);
354
+ const b = new __GoFront_B(name);
355
+ b._benchDurationNs = 1e8; // 100ms
356
+ const runIters = () => {
357
+ b.N = 1;
358
+ b.ResetTimer();
359
+ const res = fn(b);
360
+ if (res && typeof res.then === "function") {
361
+ return res.then(() => loopBench(b, fn));
362
+ }
363
+ return loopBench(b, fn);
364
+ };
365
+ const loopBench = (b, fn) => {
366
+ if (!b._loopStarted && !b.failed && !b.skipped) {
367
+ b.StopTimer();
368
+ b._runCleanups();
369
+ let n = 1;
370
+ while (!b.failed && !b.skipped) {
371
+ const elapsedNs = Number(b.Elapsed());
372
+ if (elapsedNs >= b._benchDurationNs || n >= 1e9) break;
373
+ let nextN;
374
+ if (elapsedNs === 0) nextN = n * 100;
375
+ else nextN = Math.max(n + 1, Math.min(1e9, Math.floor(n * (b._benchDurationNs / elapsedNs) * 1.2)));
376
+ if (nextN <= n) break;
377
+ n = nextN;
378
+ const nextB = new __GoFront_B(name);
379
+ nextB._benchDurationNs = b._benchDurationNs;
380
+ nextB.N = n;
381
+ nextB.ResetTimer();
382
+ const res = fn(nextB);
383
+ if (res && typeof res.then === "function") {
384
+ return res.then(() => {
385
+ nextB.StopTimer();
386
+ nextB._runCleanups();
387
+ b.N = nextB.N;
388
+ b._elapsedMs = nextB._elapsedMs;
389
+ b._timerRunning = false;
390
+ b.failed = nextB.failed;
391
+ b.skipped = nextB.skipped;
392
+ b._logs = nextB._logs;
393
+ });
394
+ }
395
+ nextB.StopTimer();
396
+ nextB._runCleanups();
397
+ b.N = nextB.N;
398
+ b._elapsedMs = nextB._elapsedMs;
399
+ b._timerRunning = false;
400
+ b.failed = nextB.failed;
401
+ b.skipped = nextB.skipped;
402
+ b._logs = nextB._logs;
403
+ }
404
+ }
405
+ };
406
+
407
+ let res;
408
+ try {
409
+ res = runIters();
410
+ } catch (e) {
411
+ recordTestError(b, e);
412
+ b._runCleanups();
413
+ if (b.failed) totalFailures++;
414
+ else if (b.skipped) totalSkipped++;
415
+ reportBenchmark(b);
416
+ return null;
417
+ }
418
+
419
+ if (res && typeof res.then === "function") {
420
+ return res
421
+ .catch((e) => { recordTestError(b, e); })
422
+ .finally(() => {
423
+ b._runCleanups();
424
+ if (b.failed) totalFailures++;
425
+ else if (b.skipped) totalSkipped++;
426
+ reportBenchmark(b);
427
+ });
428
+ }
429
+
430
+ b._runCleanups();
431
+ if (b.failed) totalFailures++;
432
+ else if (b.skipped) totalSkipped++;
433
+ reportBenchmark(b);
434
+ return null;
435
+ };
436
+
437
+ let ti = 0;
438
+ while (ti < testsToRun.length) {
439
+ const p = runOneTest(testsToRun[ti++]);
440
+ if (p) {
441
+ return p.then(async () => {
442
+ while (ti < testsToRun.length) {
443
+ const nextP = runOneTest(testsToRun[ti++]);
444
+ if (nextP) await nextP;
445
+ }
446
+ for (const bName of benchesToRun) {
447
+ const nextP = runOneBench(bName);
448
+ if (nextP) await nextP;
449
+ }
450
+ return finish();
451
+ });
452
+ }
182
453
  }
183
454
 
184
- const t = new __GoFront_T(name);
185
- if (verbose) console.log(dim("=== RUN ") + name);
186
-
187
- try {
188
- const res = fn(t);
189
- if (res && typeof res.then === "function") await res;
190
- } catch (e) {
191
- recordTestError(t, e);
455
+ let bi = 0;
456
+ while (bi < benchesToRun.length) {
457
+ const p = runOneBench(benchesToRun[bi++]);
458
+ if (p) {
459
+ return p.then(async () => {
460
+ while (bi < benchesToRun.length) {
461
+ const nextP = runOneBench(benchesToRun[bi++]);
462
+ if (nextP) await nextP;
463
+ }
464
+ return finish();
465
+ });
466
+ }
192
467
  }
193
468
 
194
- if (t.skipped) totalSkipped++;
195
- else if (t.failed) totalFailures++;
196
- report(t);
469
+ return finish();
197
470
  }
198
471
 
199
- const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
200
-
201
- if (totalFailures > 0) {
202
- console.log(FAIL);
203
- console.log(FAIL + "\\t" + pkgName + "\\t" + dim(suiteElapsed + "s"));
204
- process.exit(1);
205
- } else {
206
- console.log(PASS);
207
- okLine(pkgName, suiteElapsed);
208
- process.exit(0);
472
+ if (testMainName) {
473
+ const mainFn = __testRegistry[testMainName] ?? (typeof globalThis[testMainName] === "function" ? globalThis[testMainName] : null);
474
+ if (typeof mainFn === "function") {
475
+ let exitCode = 0;
476
+ const m = new __GoFront_M(() => {
477
+ const res = runAll();
478
+ if (res && typeof res.then === "function") {
479
+ return res.then((code) => { exitCode = code; return code; });
480
+ }
481
+ exitCode = res;
482
+ return exitCode;
483
+ });
484
+ try {
485
+ const res = mainFn(m);
486
+ if (res && typeof res.then === "function") await res;
487
+ } catch (e) {
488
+ console.error(red("panic in TestMain: " + (e?.message ?? String(e))));
489
+ process.exit(1);
490
+ }
491
+ if (m._promise) await m._promise;
492
+ process.exit(exitCode);
493
+ }
209
494
  }
495
+
496
+ const res = runAll();
497
+ const exitCode = res && typeof res.then === "function" ? await res : res;
498
+ process.exit(exitCode);
210
499
  }
211
500
 
212
501
  __runGoFrontSuite();
@@ -374,8 +663,10 @@ export async function runTests(targetDir, options = {}) {
374
663
 
375
664
  const { pkgName, js, programs } = compiled;
376
665
  const testNames = discoverTests(programs);
666
+ const benchNames = discoverBenchmarks(programs);
667
+ const testMain = discoverTestMain(programs);
377
668
 
378
- if (testNames.length === 0) {
669
+ if (testNames.length === 0 && benchNames.length === 0 && !testMain) {
379
670
  return reportEmptyTests(pkgName, "no tests to run", options);
380
671
  }
381
672
 
@@ -384,12 +675,14 @@ export async function runTests(targetDir, options = {}) {
384
675
  resolvedDir,
385
676
  generateTestHarness(bundleJs, testNames, {
386
677
  ...options,
678
+ benchNames,
679
+ testMain,
387
680
  pkgName: label,
388
681
  jsdomPath,
389
682
  wasmBytes,
390
683
  color: colorsFor(options, options.stdout ?? process.stdout).enabled,
391
684
  }),
392
- testNames.length,
685
+ testNames.length + benchNames.length,
393
686
  options,
394
687
  );
395
688
 
@@ -19,12 +19,17 @@ export function getExportNames(pkgName, globals = {}) {
19
19
  const clean = base.replace(/[^a-zA-Z0-9_]/g, "_");
20
20
  const names = new Set([pkgName, base, clean]);
21
21
  const extra = globals[pkgName];
22
- for (const name of Array.isArray(extra) ? extra : extra ? [extra] : [])
23
- names.add(name);
22
+ for (const name of Array.isArray(extra)
23
+ ? extra
24
+ : typeof extra === "string"
25
+ ? [extra]
26
+ : []) {
27
+ if (typeof name === "string") names.add(name);
28
+ }
24
29
  return [...names];
25
30
  }
26
31
 
27
- export function generateVendorEntry(packages, globals = {}) {
32
+ export function generateVendorEntry(packages, globals = {}, namedExports = {}) {
28
33
  const imports = [];
29
34
  const assignments = [];
30
35
  const exports = [];
@@ -40,6 +45,46 @@ export function generateVendorEntry(packages, globals = {}) {
40
45
  );
41
46
  }
42
47
 
48
+ // Named export global assignments (e.g. window.h = preact.h)
49
+ const pkgNamed = Object.create(null);
50
+ const rawNamed = namedExports[pkg];
51
+ const isSafeKey = (k) =>
52
+ typeof k === "string" &&
53
+ k !== "__proto__" &&
54
+ k !== "prototype" &&
55
+ k !== "constructor";
56
+
57
+ if (Array.isArray(rawNamed)) {
58
+ for (const name of rawNamed) {
59
+ if (isSafeKey(name)) pkgNamed[name] = name;
60
+ }
61
+ } else if (rawNamed && typeof rawNamed === "object") {
62
+ for (const [k, v] of Object.entries(rawNamed)) {
63
+ if (isSafeKey(k) && isSafeKey(v)) pkgNamed[k] = v;
64
+ }
65
+ }
66
+
67
+ if (typeof globals[pkg] === "object" && !Array.isArray(globals[pkg])) {
68
+ for (const [k, v] of Object.entries(globals[pkg])) {
69
+ if (isSafeKey(k) && isSafeKey(v)) pkgNamed[k] = v;
70
+ }
71
+ } else if (Array.isArray(globals[pkg])) {
72
+ for (const item of globals[pkg]) {
73
+ if (typeof item === "object" && item !== null) {
74
+ for (const [k, v] of Object.entries(item)) {
75
+ if (isSafeKey(k) && isSafeKey(v)) pkgNamed[k] = v;
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ for (const [globalName, exportName] of Object.entries(pkgNamed)) {
82
+ if (!isSafeKey(globalName) || !isSafeKey(exportName)) continue;
83
+ assignments.push(
84
+ ` window[${JSON.stringify(globalName)}] = ${id}[${JSON.stringify(exportName)}] !== undefined ? ${id}[${JSON.stringify(exportName)}] : (${id}.default ? ${id}.default[${JSON.stringify(exportName)}] : undefined);`,
85
+ );
86
+ }
87
+
43
88
  const cleanId = pkg
44
89
  .replace(/^@[^/]+\//, "")
45
90
  .replace(/[^a-zA-Z0-9_$]/g, "_");
@@ -82,8 +127,26 @@ function findBundler(projectDir) {
82
127
  return null;
83
128
  }
84
129
 
85
- // Normalises `vendor.globals` ({ pkg: string | string[] }) to { pkg: string[] }.
86
- function normalizeGlobals(raw) {
130
+ export function normalizeNamedExports(raw) {
131
+ if (raw === undefined || raw === null) return {};
132
+ if (typeof raw !== "object" || Array.isArray(raw)) return {};
133
+ const out = {};
134
+ for (const [pkg, value] of Object.entries(raw)) {
135
+ if (Array.isArray(value)) {
136
+ const map = {};
137
+ for (const name of value) {
138
+ if (typeof name === "string") map[name] = name;
139
+ }
140
+ out[pkg] = map;
141
+ } else if (typeof value === "object" && value !== null) {
142
+ out[pkg] = { ...value };
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+
148
+ // Normalises `vendor.globals` ({ pkg: string | string[] | object })
149
+ export function normalizeGlobals(raw) {
87
150
  if (raw === undefined || raw === null) return {};
88
151
  if (typeof raw !== "object" || Array.isArray(raw)) {
89
152
  throw new Error(
@@ -92,13 +155,17 @@ function normalizeGlobals(raw) {
92
155
  }
93
156
  const out = {};
94
157
  for (const [pkg, value] of Object.entries(raw)) {
95
- const list = Array.isArray(value) ? value : [value];
96
- if (list.some((v) => typeof v !== "string")) {
158
+ if (typeof value === "string") {
159
+ out[pkg] = [value];
160
+ } else if (Array.isArray(value)) {
161
+ out[pkg] = value;
162
+ } else if (typeof value === "object" && value !== null) {
163
+ out[pkg] = value;
164
+ } else {
97
165
  throw new Error(
98
- `gofront: vendor.globals[${JSON.stringify(pkg)}] must be a string or array of strings`,
166
+ `gofront: vendor.globals[${JSON.stringify(pkg)}] must be a string, array, or object`,
99
167
  );
100
168
  }
101
- out[pkg] = list;
102
169
  }
103
170
  return out;
104
171
  }
@@ -119,6 +186,7 @@ export function loadVendorConfig(projectDir) {
119
186
  let packages = dependencies;
120
187
  let minify = false;
121
188
  let globals = {};
189
+ let namedExports = {};
122
190
 
123
191
  if (typeof vendorConfig === "string") {
124
192
  dest = vendorConfig;
@@ -127,13 +195,16 @@ export function loadVendorConfig(projectDir) {
127
195
  if (Array.isArray(vendorConfig.packages)) packages = vendorConfig.packages;
128
196
  if (typeof vendorConfig.minify === "boolean") minify = vendorConfig.minify;
129
197
  globals = normalizeGlobals(vendorConfig.globals);
198
+ namedExports = normalizeNamedExports(
199
+ vendorConfig.namedExports ?? vendorConfig.exports,
200
+ );
130
201
  }
131
202
 
132
203
  if (!dest) {
133
204
  dest = existsSync(join(projectDir, "app")) ? "app/vendor.js" : "vendor.js";
134
205
  }
135
206
 
136
- return { dest, packages, minify, globals };
207
+ return { dest, packages, minify, globals, namedExports };
137
208
  }
138
209
 
139
210
  function resolveDestinationPaths(projectRoot, dest) {
@@ -299,6 +370,10 @@ export async function bundleVendor(projectDir = ".", options = {}) {
299
370
  const dest = options.dest ?? config.dest;
300
371
  const minify = options.minify ?? config.minify ?? false;
301
372
  const globals = { ...config.globals, ...normalizeGlobals(options.globals) };
373
+ const namedExports = {
374
+ ...config.namedExports,
375
+ ...normalizeNamedExports(options.namedExports ?? options.exports),
376
+ };
302
377
 
303
378
  if (!packages || packages.length === 0) {
304
379
  return {
@@ -366,11 +441,10 @@ export async function bundleVendor(projectDir = ".", options = {}) {
366
441
  };
367
442
  }
368
443
 
369
- const entryCode = generateVendorEntry(packages, globals);
370
- const entryFile = join(
371
- projectRoot,
372
- `.gofront-vendor-entry-${Date.now()}.mjs`,
373
- );
444
+ const entryCode = generateVendorEntry(packages, globals, namedExports);
445
+ // A fixed name: the bundler records the entry path in vendor.js, so a
446
+ // per-run name would make every prep produce different bytes.
447
+ const entryFile = join(projectRoot, ".gofront-vendor-entry.mjs");
374
448
  writeFileSync(entryFile, entryCode);
375
449
 
376
450
  try {