gofront 1.5.1 → 1.6.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 (91) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +288 -128
  4. package/src/backend/js/index.js +138 -56
  5. package/src/backend/js/runtime.js +1055 -53
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +151 -83
  8. package/src/backend/js/stdlib/base64.js +12 -0
  9. package/src/backend/js/stdlib/binary.js +102 -0
  10. package/src/backend/js/stdlib/bits.js +15 -1
  11. package/src/backend/js/stdlib/builder.js +28 -9
  12. package/src/backend/js/stdlib/bytes.js +75 -18
  13. package/src/backend/js/stdlib/errors.js +22 -3
  14. package/src/backend/js/stdlib/fmt.js +95 -20
  15. package/src/backend/js/stdlib/hex.js +17 -0
  16. package/src/backend/js/stdlib/html.js +1 -1
  17. package/src/backend/js/stdlib/index.js +19 -0
  18. package/src/backend/js/stdlib/io.js +105 -3
  19. package/src/backend/js/stdlib/maps.js +1 -1
  20. package/src/backend/js/stdlib/math.js +13 -1
  21. package/src/backend/js/stdlib/path.js +20 -6
  22. package/src/backend/js/stdlib/rand.js +10 -5
  23. package/src/backend/js/stdlib/sha256.js +12 -0
  24. package/src/backend/js/stdlib/shared.js +17 -0
  25. package/src/backend/js/stdlib/slices.js +53 -27
  26. package/src/backend/js/stdlib/sort.js +1 -1
  27. package/src/backend/js/stdlib/strconv.js +52 -11
  28. package/src/backend/js/stdlib/strings.js +152 -16
  29. package/src/backend/js/stdlib/sync.js +13 -0
  30. package/src/backend/js/stdlib/testing.js +2 -2
  31. package/src/backend/js/stdlib/time.js +93 -22
  32. package/src/backend/js/stdlib/unicode.js +2 -2
  33. package/src/backend/js/templ.js +1 -1
  34. package/src/backend/wasm/boundary.js +1677 -58
  35. package/src/backend/wasm/const-fold.js +205 -0
  36. package/src/backend/wasm/emit-builtins.js +1230 -0
  37. package/src/backend/wasm/emit-exprs.js +2096 -0
  38. package/src/backend/wasm/emit-maps.js +1999 -0
  39. package/src/backend/wasm/emit-shared.js +519 -0
  40. package/src/backend/wasm/emit-stdlib.js +13 -0
  41. package/src/backend/wasm/emit-stmts.js +2466 -0
  42. package/src/backend/wasm/emit.js +656 -5031
  43. package/src/backend/wasm/encode.js +259 -85
  44. package/src/backend/wasm/glue.js +19 -5
  45. package/src/backend/wasm/index.js +2432 -86
  46. package/src/backend/wasm/monomorph.js +711 -0
  47. package/src/backend/wasm/optimize.js +236 -0
  48. package/src/backend/wasm/stdlib/bytes.js +5039 -0
  49. package/src/backend/wasm/stdlib/core.js +8903 -0
  50. package/src/backend/wasm/stdlib/index.js +40 -0
  51. package/src/backend/wasm/stdlib/io.js +2161 -0
  52. package/src/backend/wasm/stdlib/packages.js +2400 -0
  53. package/src/backend/wasm/stdlib/pure.js +720 -0
  54. package/src/backend/wasm/stdlib/time.js +1200 -0
  55. package/src/backend/wasm/types.js +312 -36
  56. package/src/backend/wasm/wat.js +49 -2
  57. package/src/cli-core.js +276 -73
  58. package/src/compiler.js +162 -25
  59. package/src/dts-parser.js +79 -126
  60. package/src/index.js +88 -121
  61. package/src/lexer.js +80 -29
  62. package/src/lower/boxing.js +4 -6
  63. package/src/lower/captures.js +12 -23
  64. package/src/lower/escape.js +4 -9
  65. package/src/lower/functions.js +277 -5
  66. package/src/lower/globals.js +7 -15
  67. package/src/lower/index.js +5 -2
  68. package/src/lower/ownership.js +35 -19
  69. package/src/lower/range.js +2 -5
  70. package/src/lower/walk.js +25 -0
  71. package/src/minifier.js +20 -68
  72. package/src/parser/declarations.js +3 -6
  73. package/src/parser/expressions.js +3 -8
  74. package/src/parser/index.js +18 -3
  75. package/src/parser/statements.js +25 -38
  76. package/src/parser/types.js +1 -2
  77. package/src/resolver.js +3 -27
  78. package/src/templ-lexer.js +29 -63
  79. package/src/templ-parser.js +22 -45
  80. package/src/test-runner.js +20 -0
  81. package/src/typechecker/assignability.js +123 -14
  82. package/src/typechecker/expressions.js +244 -4
  83. package/src/typechecker/index.js +27 -1
  84. package/src/typechecker/resolve.js +71 -12
  85. package/src/typechecker/statements.js +0 -7
  86. package/src/typechecker/stdlib/core.js +292 -103
  87. package/src/typechecker/stdlib/extended.js +471 -4
  88. package/src/typechecker/stdlib/shared.js +53 -0
  89. package/src/typechecker/stdlib.js +2 -0
  90. package/src/typechecker/types.js +545 -8
  91. package/src/vendor.js +2 -2
@@ -261,8 +261,7 @@ export const declarationParseMethods = {
261
261
  },
262
262
 
263
263
  parseVarSpec() {
264
- const names = [this.expect(T.IDENT).value];
265
- while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
264
+ const names = this.parseIdentList();
266
265
  let type = null;
267
266
  let value = null;
268
267
  if (
@@ -284,8 +283,7 @@ export const declarationParseMethods = {
284
283
  let prevRawExprs = null; // raw AST (with iota Idents) from the previous spec
285
284
  let prevType = null;
286
285
  while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
287
- const names = [this.expect(T.IDENT).value];
288
- while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
286
+ const names = this.parseIdentList();
289
287
  let type = null;
290
288
  let rawExprs; // before iota substitution
291
289
  if (this.check(T.ASSIGN)) {
@@ -315,8 +313,7 @@ export const declarationParseMethods = {
315
313
  }
316
314
  this.expect(T.RPAREN);
317
315
  } else {
318
- const names = [this.expect(T.IDENT).value];
319
- while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
316
+ const names = this.parseIdentList();
320
317
  let type = null;
321
318
  if (!this.check(T.ASSIGN)) type = this.parseType();
322
319
  this.expect(T.ASSIGN);
@@ -25,6 +25,8 @@ const PREC = {
25
25
  [T.AND_NOT]: 5,
26
26
  };
27
27
 
28
+ const UNARY_OPS = new Set([T.NOT, T.MINUS, T.PLUS, T.CARET, T.AMP, T.STAR]);
29
+
28
30
  const TYPE_KEYWORDS = new Set([
29
31
  "int",
30
32
  "int8",
@@ -128,14 +130,7 @@ export const expressionParserMethods = {
128
130
 
129
131
  parseUnary(noCompLit = false) {
130
132
  const t = this.peek();
131
- if (
132
- t.type === T.NOT ||
133
- t.type === T.MINUS ||
134
- t.type === T.PLUS ||
135
- t.type === T.CARET ||
136
- t.type === T.AMP ||
137
- t.type === T.STAR
138
- ) {
133
+ if (UNARY_OPS.has(t.type)) {
139
134
  const op = this.advance().value;
140
135
  return { kind: "UnaryExpr", op, operand: this.parseUnary(noCompLit) };
141
136
  }
@@ -73,9 +73,18 @@ export class Parser {
73
73
  return this.peek2().type === type;
74
74
  }
75
75
 
76
- match(...types) {
77
- for (const type of types) {
78
- if (this.check(type)) {
76
+ match(t1, t2, t3, ...rest) {
77
+ const cur = this.peek().type;
78
+ if (
79
+ cur === t1 ||
80
+ (t2 !== undefined && cur === t2) ||
81
+ (t3 !== undefined && cur === t3)
82
+ ) {
83
+ this.advance();
84
+ return true;
85
+ }
86
+ for (let i = 0; i < rest.length; i++) {
87
+ if (cur === rest[i]) {
79
88
  this.advance();
80
89
  return true;
81
90
  }
@@ -83,6 +92,12 @@ export class Parser {
83
92
  return false;
84
93
  }
85
94
 
95
+ parseIdentList() {
96
+ const names = [this.expect(T.IDENT).value];
97
+ while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
98
+ return names;
99
+ }
100
+
86
101
  expect(type) {
87
102
  if (!this.check(type))
88
103
  throw new ParseError(`expected '${type}'`, this.peek());
@@ -15,6 +15,20 @@ const PARSE_STMT_DELEGATE = {
15
15
  [T.LBRACE]: "parseBlock",
16
16
  };
17
17
 
18
+ const ASSIGN_OPS = new Set([
19
+ T.ASSIGN,
20
+ T.PLUS_ASSIGN,
21
+ T.MINUS_ASSIGN,
22
+ T.STAR_ASSIGN,
23
+ T.SLASH_ASSIGN,
24
+ T.PERCENT_ASSIGN,
25
+ T.AMP_ASSIGN,
26
+ T.PIPE_ASSIGN,
27
+ T.CARET_ASSIGN,
28
+ T.LSHIFT_ASSIGN,
29
+ T.RSHIFT_ASSIGN,
30
+ ]);
31
+
18
32
  export const statementParserMethods = {
19
33
  parseBlock() {
20
34
  this.expect(T.LBRACE);
@@ -206,22 +220,13 @@ export const statementParserMethods = {
206
220
  this.expect(T.LBRACE);
207
221
  const cases = [];
208
222
  while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
209
- if (this.check(T.CASE)) {
210
- this.advance();
211
- const list = this.parseExprList();
212
- this.expect(T.COLON);
213
- cases.push({ kind: "CaseClause", list, stmts: this._parseCaseBody() });
214
- } else if (this.check(T.DEFAULT)) {
215
- this.advance();
216
- this.expect(T.COLON);
217
- cases.push({
218
- kind: "CaseClause",
219
- list: null,
220
- stmts: this._parseCaseBody(),
221
- });
222
- } else {
223
+ const isCase = this.match(T.CASE);
224
+ if (!isCase && !this.match(T.DEFAULT)) {
223
225
  this.err("Expected case or default");
224
226
  }
227
+ const list = isCase ? this.parseExprList() : null;
228
+ this.expect(T.COLON);
229
+ cases.push({ kind: "CaseClause", list, stmts: this._parseCaseBody() });
225
230
  }
226
231
  this.expect(T.RBRACE);
227
232
  return { kind: "SwitchStmt", init, tag, cases };
@@ -244,18 +249,13 @@ export const statementParserMethods = {
244
249
  this.expect(T.LBRACE);
245
250
  const cases = [];
246
251
  while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
247
- if (this.check(T.CASE)) {
248
- this.advance();
249
- const types = this._parseTypeSwitchTypeList();
250
- this.expect(T.COLON);
251
- cases.push({ types, stmts: this._parseCaseBody() });
252
- } else if (this.check(T.DEFAULT)) {
253
- this.advance();
254
- this.expect(T.COLON);
255
- cases.push({ types: null, stmts: this._parseCaseBody() });
256
- } else {
252
+ const isCase = this.match(T.CASE);
253
+ if (!isCase && !this.match(T.DEFAULT)) {
257
254
  this.err("Expected case or default");
258
255
  }
256
+ const types = isCase ? this._parseTypeSwitchTypeList() : null;
257
+ this.expect(T.COLON);
258
+ cases.push({ types, stmts: this._parseCaseBody() });
259
259
  }
260
260
  this.expect(T.RBRACE);
261
261
  return { kind: "TypeSwitchStmt", assign, expr, cases };
@@ -305,20 +305,7 @@ export const statementParserMethods = {
305
305
  }
306
306
 
307
307
  // x = expr or x += expr etc.
308
- const assignOps = [
309
- T.ASSIGN,
310
- T.PLUS_ASSIGN,
311
- T.MINUS_ASSIGN,
312
- T.STAR_ASSIGN,
313
- T.SLASH_ASSIGN,
314
- T.PERCENT_ASSIGN,
315
- T.AMP_ASSIGN,
316
- T.PIPE_ASSIGN,
317
- T.CARET_ASSIGN,
318
- T.LSHIFT_ASSIGN,
319
- T.RSHIFT_ASSIGN,
320
- ];
321
- if (assignOps.includes(t.type)) {
308
+ if (ASSIGN_OPS.has(t.type)) {
322
309
  const op = this.advance().value;
323
310
  const rhs = this.parseExprList();
324
311
  return { kind: "AssignStmt", lhs: [first], op, rhs };
@@ -175,8 +175,7 @@ export const typeParserMethods = {
175
175
  this.expect(T.LBRACE);
176
176
  const fields = [];
177
177
  while (!this.check(T.RBRACE) && !this.check(T.EOF)) {
178
- const names = [this.expect(T.IDENT).value];
179
- while (this.match(T.COMMA)) names.push(this.expect(T.IDENT).value);
178
+ const names = this.parseIdentList();
180
179
  // Check if this is an embedded field (just a type name)
181
180
  // e.g., type T struct { Base }
182
181
  if (this.check(T.SEMICOLON) || this.check(T.RBRACE)) {
package/src/resolver.js CHANGED
@@ -25,31 +25,7 @@ function findNodeModules(startDir) {
25
25
  }
26
26
  }
27
27
 
28
- // Built-in stdlib package paths that are always available as globals — no resolution needed.
29
- const BUILTIN_PACKAGES = new Set([
30
- "fmt",
31
- "strings",
32
- "bytes",
33
- "strconv",
34
- "sort",
35
- "math",
36
- "math/bits",
37
- "math/rand",
38
- "errors",
39
- "time",
40
- "unicode",
41
- "unicode/utf8",
42
- "os",
43
- "slices",
44
- "html",
45
- "io",
46
- "gom",
47
- "maps",
48
- "regexp",
49
- "path",
50
- "path/filepath",
51
- "testing",
52
- ]);
28
+ import { STDLIB_PACKAGES } from "./typechecker/types.js";
53
29
 
54
30
  // Returns true for local relative paths (start with ./ or ../)
55
31
  export function isLocalPath(importPath) {
@@ -57,7 +33,7 @@ export function isLocalPath(importPath) {
57
33
  }
58
34
 
59
35
  export function isBuiltinPackage(importPath) {
60
- return BUILTIN_PACKAGES.has(importPath);
36
+ return STDLIB_PACKAGES.has(importPath);
61
37
  }
62
38
 
63
39
  // Resolve a local relative import path to an absolute directory of .go files.
@@ -72,7 +48,7 @@ export function resolveGwDir(importPath, fromFile) {
72
48
  return gwFiles.length > 0 ? dir : null;
73
49
  }
74
50
 
75
- export function resolveImport(importPath, fromFile) {
51
+ function resolveImport(importPath, fromFile) {
76
52
  const fromDir = dirname(resolve(fromFile));
77
53
  const nodeModules = findNodeModules(fromDir);
78
54
 
@@ -118,17 +118,15 @@ export class TemplLexer {
118
118
  continue;
119
119
  }
120
120
 
121
- // Detect `templ` keyword at identifier boundaries
121
+ // Detect `templ` or `css` keyword at identifier boundaries
122
122
  if (this._matchKeyword("templ")) {
123
123
  this.tokens.push(new Token(TT.TEMPL_KW, "templ", this.line, this.col));
124
- this._lexTemplDecl();
124
+ this._lexBlockDecl("templ");
125
125
  continue;
126
126
  }
127
-
128
- // Detect `css` keyword at identifier boundaries
129
127
  if (this._matchKeyword("css")) {
130
128
  this.tokens.push(new Token(TT.CSS_KW, "css", this.line, this.col));
131
- this._lexCssDecl();
129
+ this._lexBlockDecl("css");
132
130
  continue;
133
131
  }
134
132
 
@@ -201,24 +199,25 @@ export class TemplLexer {
201
199
  for (const t of this._goTokens(chunk)) this.tokens.push(t);
202
200
  }
203
201
 
204
- // ── Templ declaration lexing ─────────────────────────────────
202
+ // ── Templ / CSS declaration lexing ───────────────────────────
205
203
 
206
- // Called right after consuming `templ` keyword.
207
- // Lexes: Name(params) { <HTML body> }
208
- _lexTemplDecl() {
204
+ // Called right after consuming `templ` or `css` keyword.
205
+ // Lexes: Name(params) { <body> }
206
+ _lexBlockDecl(kw) {
209
207
  this.skipWhitespace();
210
208
 
211
- // Name
212
- const nameStart = this.pos;
213
- while (!this.eof() && /\w/.test(this.peek())) this.advance();
214
- const name = this.src.slice(nameStart, this.pos);
215
- if (!name) this.err("expected component name after 'templ'");
209
+ const name = this._readWord();
210
+ if (!name) {
211
+ this.err(
212
+ `expected ${kw === "templ" ? "component name" : "name"} after '${kw}'`,
213
+ );
214
+ }
216
215
  this.tokens.push(new Token(T.IDENT, name, this.line, this.col));
217
216
 
218
217
  this.skipWhitespace();
219
218
 
220
219
  // Parameter list ( ... ) — lex as Go
221
- if (this.peek() !== "(") this.err("expected '(' after templ name");
220
+ if (this.peek() !== "(") this.err(`expected '(' after ${kw} name`);
222
221
  const paramStart = this.pos;
223
222
  this._skipBalanced("(", ")");
224
223
  const paramSrc = this.src.slice(paramStart, this.pos);
@@ -227,43 +226,12 @@ export class TemplLexer {
227
226
  this.skipWhitespace();
228
227
 
229
228
  // Opening { of body
230
- if (this.peek() !== "{") this.err("expected '{' after templ params");
229
+ if (this.peek() !== "{") this.err(`expected '{' after ${kw} params`);
231
230
  this.advance(); // consume {
232
231
  this.tokens.push(new Token(T.LBRACE, "{", this.line, this.col));
233
232
 
234
- // HTML body
235
- this._lexHtmlBody(1);
236
- }
237
-
238
- // ── CSS declaration lexing ───────────────────────────────────
239
-
240
- _lexCssDecl() {
241
- this.skipWhitespace();
242
-
243
- // Name (identifier)
244
- const nameStart = this.pos;
245
- while (!this.eof() && /\w/.test(this.peek())) this.advance();
246
- const name = this.src.slice(nameStart, this.pos);
247
- if (!name) this.err("expected name after 'css'");
248
- this.tokens.push(new Token(T.IDENT, name, this.line, this.col));
249
-
250
- this.skipWhitespace();
251
-
252
- // Parameter list (balanced parens)
253
- if (this.peek() !== "(") this.err("expected '(' after css name");
254
- const paramStart = this.pos;
255
- this._skipBalanced("(", ")");
256
- const paramSrc = this.src.slice(paramStart, this.pos);
257
- for (const t of this._goTokens(paramSrc)) this.tokens.push(t);
258
-
259
- this.skipWhitespace();
260
-
261
- // Opening { of body
262
- if (this.peek() !== "{") this.err("expected '{' after css params");
263
- this.advance(); // consume {
264
- this.tokens.push(new Token(T.LBRACE, "{", this.line, this.col));
265
-
266
- this._lexCssBody();
233
+ if (kw === "templ") this._lexHtmlBody(1);
234
+ else this._lexCssBody();
267
235
  }
268
236
 
269
237
  _skipCssString(quote) {
@@ -660,7 +628,7 @@ export class TemplLexer {
660
628
  // ── HTML text content ─────────────────────────────────────────
661
629
 
662
630
  _lexHtmlText() {
663
- let text = "";
631
+ const start = this.pos;
664
632
  const line = this.line;
665
633
  while (!this.eof()) {
666
634
  const c = this.peek();
@@ -674,9 +642,9 @@ export class TemplLexer {
674
642
  (c === "d" && this._peekKeyword("default"))
675
643
  )
676
644
  break;
677
- text += this.advance();
645
+ this.advance();
678
646
  }
679
- const trimmed = text.trim();
647
+ const trimmed = this.src.slice(start, this.pos).trim();
680
648
  if (trimmed)
681
649
  this.tokens.push(new Token(TT.HTML_TEXT, trimmed, line, this.col));
682
650
  }
@@ -729,17 +697,15 @@ export class TemplLexer {
729
697
  }
730
698
 
731
699
  _readHtmlName() {
732
- let name = "";
733
- while (!this.eof() && /[a-zA-Z0-9_-]/.test(this.peek()))
734
- name += this.advance();
735
- return name;
700
+ const start = this.pos;
701
+ while (!this.eof() && /[a-zA-Z0-9_-]/.test(this.peek())) this.advance();
702
+ return this.src.slice(start, this.pos);
736
703
  }
737
704
 
738
705
  _readHtmlAttrName() {
739
- let name = "";
740
- while (!this.eof() && /[a-zA-Z0-9_:.-]/.test(this.peek()))
741
- name += this.advance();
742
- return name;
706
+ const start = this.pos;
707
+ while (!this.eof() && /[a-zA-Z0-9_:.-]/.test(this.peek())) this.advance();
708
+ return this.src.slice(start, this.pos);
743
709
  }
744
710
 
745
711
  _readQuotedString() {
@@ -756,9 +722,9 @@ export class TemplLexer {
756
722
  }
757
723
 
758
724
  _readWord() {
759
- let w = "";
760
- while (!this.eof() && /\w/.test(this.peek())) w += this.advance();
761
- return w;
725
+ const start = this.pos;
726
+ while (!this.eof() && /\w/.test(this.peek())) this.advance();
727
+ return this.src.slice(start, this.pos);
762
728
  }
763
729
 
764
730
  _skipToOpenBrace() {
@@ -48,55 +48,31 @@ export class TemplParser extends Parser {
48
48
 
49
49
  // ── CssDecl ───────────────────────────────────────────────────
50
50
 
51
- parseCssDecl() {
51
+ _parseDecl(kind, bodyFn) {
52
52
  const _line = this.peek().line;
53
- this.advance(); // consume CSS_KW
54
-
53
+ this.advance();
55
54
  const name = this.expect(T.IDENT).value;
56
-
57
- // Parse parameter list (standard Go params: LPAREN...RPAREN)
58
55
  const params = this.parseParamList();
59
-
60
- // Optional semicolon between ) and {
61
56
  this.semi();
62
-
63
- // Consume opening {
64
57
  this.expect(T.LBRACE);
65
-
66
- // Consume CSS_BODY
67
- const bodyToken = this.expect(TT.CSS_BODY);
68
- const cssText = bodyToken.value;
69
-
70
- // Consume closing }
71
- this.expect(T.RBRACE);
58
+ const extra = bodyFn();
72
59
  this.semi();
73
-
74
- return { kind: "CssDecl", name, params, cssText, _line };
60
+ return { kind, name, params, ...extra, _line };
75
61
  }
76
62
 
77
- // ── TemplDecl ─────────────────────────────────────────────────
63
+ parseCssDecl() {
64
+ return this._parseDecl("CssDecl", () => {
65
+ const bodyToken = this.expect(TT.CSS_BODY);
66
+ const cssText = bodyToken.value;
67
+ this.expect(T.RBRACE);
68
+ return { cssText };
69
+ });
70
+ }
78
71
 
79
72
  parseTemplDecl() {
80
- const _line = this.peek().line;
81
- this.advance(); // consume TEMPL_KW
82
-
83
- const name = this.expect(T.IDENT).value;
84
-
85
- // Parse parameter list (standard Go params: LPAREN...RPAREN)
86
- const params = this.parseParamList();
87
-
88
- // Optional semicolon between ) and {
89
- this.semi();
90
-
91
- // Consume the opening { of the HTML body
92
- this.expect(T.LBRACE);
93
-
94
- // Parse the HTML body; parseHtmlChildren consumes the closing RBRACE
95
- const body = this.parseHtmlChildren();
96
-
97
- this.semi();
98
-
99
- return { kind: "TemplDecl", name, params, body, _line };
73
+ return this._parseDecl("TemplDecl", () => ({
74
+ body: this.parseHtmlChildren(),
75
+ }));
100
76
  }
101
77
 
102
78
  // ── HTML node parsing ─────────────────────────────────────────
@@ -229,14 +205,15 @@ export class TemplParser extends Parser {
229
205
  !this.check(T.RBRACE) &&
230
206
  !this.check(T.EOF)
231
207
  ) {
232
- if (this.check(TT.TEMPL_CASE)) {
208
+ if (this.check(TT.TEMPL_CASE) || this.check(TT.TEMPL_DEFAULT)) {
209
+ const isDefault = this.check(TT.TEMPL_DEFAULT);
233
210
  const caseTok = this.advance();
234
211
  const body = this.parseTemplCaseBody();
235
- cases.push({ kind: "TemplCase", caseTokens: caseTok.value, body });
236
- } else if (this.check(TT.TEMPL_DEFAULT)) {
237
- this.advance();
238
- const body = this.parseTemplCaseBody();
239
- cases.push({ kind: "TemplCase", caseTokens: null, body });
212
+ cases.push({
213
+ kind: "TemplCase",
214
+ caseTokens: isDefault ? null : caseTok.value,
215
+ body,
216
+ });
240
217
  } else {
241
218
  this.advance(); // skip unexpected
242
219
  }
@@ -14,6 +14,19 @@ import {
14
14
  gwFilesIn,
15
15
  } from "./compiler.js";
16
16
 
17
+ // WasmGC + exception handling (what the wasm backend emits) ship enabled by
18
+ // default from Node 22 (V8 11.9+). Older runtimes fail at instantiate time
19
+ // with an opaque CompileError, so check up front and explain.
20
+ export const MIN_WASM_NODE_MAJOR = 22;
21
+
22
+ export function checkWasmRuntime(version = process.version) {
23
+ const major = Number.parseInt(version.replace(/^v/, ""), 10);
24
+ if (Number.isNaN(major) || major >= MIN_WASM_NODE_MAJOR) return;
25
+ throw new Error(
26
+ `running wasm packages requires Node ${MIN_WASM_NODE_MAJOR}+ (WasmGC); found ${version}\n hint: upgrade Node, or compile with --js-only to run the JS backend instead`,
27
+ );
28
+ }
29
+
17
30
  export function isTestFunc(decl) {
18
31
  if (decl.kind !== "FuncDecl") return false;
19
32
  if (!decl.name.startsWith("Test")) return false;
@@ -382,6 +395,13 @@ export async function runTests(targetDir, options = {}) {
382
395
 
383
396
  // `wasm` packages run inside the linked app.wasm; `js` packages that import
384
397
  // wasm packages need the linked module too (hybrid packages).
398
+ if (compiled.target === "both" || compiled.wasm) {
399
+ try {
400
+ checkWasmRuntime();
401
+ } catch (err) {
402
+ return reportBuildFailure(err, resolvedDir, options);
403
+ }
404
+ }
385
405
  if (compiled.target === "both") {
386
406
  return runDualTarget(compiled, resolvedDir, runHarness, options);
387
407
  }