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
@@ -114,6 +114,8 @@ export class Parser {
114
114
  pkg,
115
115
  imports,
116
116
  decls,
117
+ target: this.tokens?.target ?? null,
118
+ _targetDirective: this.tokens?.targetDirective ?? null,
117
119
  _filename: this.filename,
118
120
  _source: this.source,
119
121
  };
@@ -130,7 +132,9 @@ export class Parser {
130
132
  this.expect(T.IMPORT);
131
133
  const imports = [];
132
134
  const parseOne = () => {
133
- const _line = this.peek().line;
135
+ const tok = this.peek();
136
+ const _line = tok.line;
137
+ const _col = tok.col;
134
138
  let alias = null;
135
139
  if (this.check(T.IDENT)) {
136
140
  alias = this.advance().value;
@@ -138,8 +142,9 @@ export class Parser {
138
142
  alias = ".";
139
143
  this.advance();
140
144
  }
141
- const path = this.expect(T.STRING).value;
142
- return { path, alias, _line };
145
+ const pathTok = this.expect(T.STRING);
146
+ const path = pathTok.value;
147
+ return { path, alias, _line, _col, line: pathTok.line, col: pathTok.col };
143
148
  };
144
149
  if (this.match(T.LPAREN)) {
145
150
  while (!this.check(T.RPAREN) && !this.check(T.EOF)) {
package/src/resolver.js CHANGED
@@ -33,6 +33,7 @@ const BUILTIN_PACKAGES = new Set([
33
33
  "strconv",
34
34
  "sort",
35
35
  "math",
36
+ "math/bits",
36
37
  "math/rand",
37
38
  "errors",
38
39
  "time",
@@ -65,7 +66,9 @@ export function resolveGwDir(importPath, fromFile) {
65
66
  const fromDir = dirname(resolve(fromFile));
66
67
  const dir = resolve(fromDir, importPath);
67
68
  if (!existsSync(dir)) return null;
68
- const gwFiles = readdirSync(dir).filter((f) => f.endsWith(".go"));
69
+ const gwFiles = readdirSync(dir).filter(
70
+ (f) => f.endsWith(".go") || f.endsWith(".templ"),
71
+ );
69
72
  return gwFiles.length > 0 ? dir : null;
70
73
  }
71
74
 
@@ -35,6 +35,8 @@ export const TT = {
35
35
  TEMPL_CASE: "TEMPL_CASE", // case expr:
36
36
  TEMPL_DEFAULT: "TEMPL_DEFAULT", // default:
37
37
  TEMPL_END: "TEMPL_END", // closing } of if/for/switch/templ body
38
+ CSS_KW: "css",
39
+ CSS_BODY: "CSS_BODY",
38
40
  };
39
41
 
40
42
  // Describes a parsed HTML attribute
@@ -123,6 +125,13 @@ export class TemplLexer {
123
125
  continue;
124
126
  }
125
127
 
128
+ // Detect `css` keyword at identifier boundaries
129
+ if (this._matchKeyword("css")) {
130
+ this.tokens.push(new Token(TT.CSS_KW, "css", this.line, this.col));
131
+ this._lexCssDecl();
132
+ continue;
133
+ }
134
+
126
135
  // Everything else: scan to end of top-level declaration using the
127
136
  // raw Go lexer, accumulating tokens until the next `templ` keyword
128
137
  // or EOF.
@@ -135,7 +144,7 @@ export class TemplLexer {
135
144
 
136
145
  // ── Go chunk lexing ──────────────────────────────────────────
137
146
 
138
- // Scan ahead to find the next `templ ` keyword at top level (not inside
147
+ // Scan ahead to find the next `templ ` or `css ` keyword at top level (not inside
139
148
  // braces/parens/strings/comments). Collect all source text up to that
140
149
  // point, lex it as normal Go, and append the tokens.
141
150
  _lexGoChunk() {
@@ -143,8 +152,12 @@ export class TemplLexer {
143
152
  let depth = 0; // brace depth
144
153
 
145
154
  while (!this.eof()) {
146
- // Check for `templ` keyword at top level (depth === 0)
147
- if (depth === 0 && this._peekKeyword("templ")) break;
155
+ // Check for `templ` or `css` keyword at top level (depth === 0)
156
+ if (
157
+ depth === 0 &&
158
+ (this._peekKeyword("templ") || this._peekKeyword("css"))
159
+ )
160
+ break;
148
161
 
149
162
  const ch = this.peek();
150
163
 
@@ -222,6 +235,97 @@ export class TemplLexer {
222
235
  this._lexHtmlBody(1);
223
236
  }
224
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();
267
+ }
268
+
269
+ _skipCssString(quote) {
270
+ this.advance(); // consume opening quote
271
+ while (!this.eof()) {
272
+ const ch = this.peek();
273
+ if (ch === "\\") {
274
+ this.advance();
275
+ if (!this.eof()) this.advance();
276
+ continue;
277
+ }
278
+ if (ch === quote) {
279
+ this.advance();
280
+ return;
281
+ }
282
+ if (ch === "\n") {
283
+ this.err("unterminated string in css block");
284
+ }
285
+ this.advance();
286
+ }
287
+ this.err("unterminated string in css block");
288
+ }
289
+
290
+ _lexCssBody() {
291
+ const start = this.pos;
292
+ let depth = 1;
293
+ while (!this.eof()) {
294
+ const ch = this.peek();
295
+ // Skip CSS comments /* ... */
296
+ if (ch === "/" && this.peek(1) === "*") {
297
+ this.advance();
298
+ this.advance();
299
+ this.skipBlockComment();
300
+ continue;
301
+ }
302
+ // Skip strings
303
+ if (ch === '"' || ch === "'") {
304
+ this._skipCssString(ch);
305
+ continue;
306
+ }
307
+ if (ch === "{") {
308
+ depth++;
309
+ this.advance();
310
+ } else if (ch === "}") {
311
+ depth--;
312
+ if (depth === 0) {
313
+ const cssText = this.src.slice(start, this.pos);
314
+ this.tokens.push(
315
+ new Token(TT.CSS_BODY, cssText, this.line, this.col),
316
+ );
317
+ this.advance(); // consume closing }
318
+ this.tokens.push(new Token(T.RBRACE, "}", this.line, this.col));
319
+ return;
320
+ }
321
+ this.advance();
322
+ } else {
323
+ this.advance();
324
+ }
325
+ }
326
+ this.err("unclosed css block");
327
+ }
328
+
225
329
  // Lex the HTML body of a templ declaration.
226
330
  // depth: current brace depth (starts at 1 for the opening { of templ)
227
331
  _lexHtmlBody(depth) {
@@ -42,9 +42,38 @@ export class TemplParser extends Parser {
42
42
 
43
43
  parseTopDecl() {
44
44
  if (this.check(TT.TEMPL_KW)) return this.parseTemplDecl();
45
+ if (this.check(TT.CSS_KW)) return this.parseCssDecl();
45
46
  return super.parseTopDecl();
46
47
  }
47
48
 
49
+ // ── CssDecl ───────────────────────────────────────────────────
50
+
51
+ parseCssDecl() {
52
+ const _line = this.peek().line;
53
+ this.advance(); // consume CSS_KW
54
+
55
+ const name = this.expect(T.IDENT).value;
56
+
57
+ // Parse parameter list (standard Go params: LPAREN...RPAREN)
58
+ const params = this.parseParamList();
59
+
60
+ // Optional semicolon between ) and {
61
+ this.semi();
62
+
63
+ // Consume opening {
64
+ 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);
72
+ this.semi();
73
+
74
+ return { kind: "CssDecl", name, params, cssText, _line };
75
+ }
76
+
48
77
  // ── TemplDecl ─────────────────────────────────────────────────
49
78
 
50
79
  parseTemplDecl() {
@@ -6,8 +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";
9
10
  import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
10
- import { compilePackageTests, gwFilesIn } from "./compiler.js";
11
+ import {
12
+ compilePackageTests,
13
+ compilePackageTestsWasm,
14
+ gwFilesIn,
15
+ } from "./compiler.js";
11
16
 
12
17
  export function isTestFunc(decl) {
13
18
  if (decl.kind !== "FuncDecl") return false;
@@ -78,7 +83,16 @@ globalThis.document = globalThis.document ?? null;
78
83
  `
79
84
  }
80
85
  globalThis.__gofront_verbose = ${verbose};
81
-
86
+ ${
87
+ options.wasmBytes
88
+ ? `
89
+ // ── WASM module (tests run inside app.wasm; *testing.T stays in JS) ──
90
+ ${HELPER_SPRINTF}
91
+ ${HELPER_TESTING}
92
+ globalThis.__GOFRONT_WASM_BYTES = Uint8Array.from(atob(${JSON.stringify(Buffer.from(options.wasmBytes).toString("base64"))}), (c) => c.charCodeAt(0));
93
+ `
94
+ : ""
95
+ }
82
96
  // ── Compiled Package Bundle ──
83
97
  ${bundleJs}
84
98
  // ─────────────────────────────
@@ -281,6 +295,44 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
281
295
  });
282
296
  }
283
297
 
298
+ function reportBuildFailure(err, resolvedDir, options) {
299
+ const msg = err?.message ?? String(err);
300
+ if (options.captureOutput) {
301
+ const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
302
+ return { exitCode: 1, stdout: "", stderr: out };
303
+ }
304
+ const dest = options.stderr ?? process.stderr;
305
+ const c = colorsFor(options, dest);
306
+ dest.write(
307
+ `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
308
+ );
309
+ return { exitCode: 1 };
310
+ }
311
+
312
+ // `both` packages run twice (JS and wasm) so the two backends are kept in
313
+ // agreement; output of both runs is concatenated.
314
+ async function runDualTarget(compiled, resolvedDir, runHarness, options) {
315
+ let wasmCompiled;
316
+ try {
317
+ wasmCompiled = compilePackageTestsWasm(resolvedDir, options);
318
+ } catch (err) {
319
+ return reportBuildFailure(err, resolvedDir, options);
320
+ }
321
+ const label = compiled.pkgName;
322
+ const jsRun = await runHarness(compiled.js, `${label} [js]`, null);
323
+ const wasmRun = await runHarness(
324
+ wasmCompiled.js,
325
+ `${label} [wasm]`,
326
+ wasmCompiled.wasm,
327
+ );
328
+ return {
329
+ exitCode: jsRun.exitCode !== 0 ? jsRun.exitCode : wasmRun.exitCode,
330
+ stdout: jsRun.stdout + wasmRun.stdout,
331
+ stderr: jsRun.stderr + wasmRun.stderr,
332
+ testCount: jsRun.testCount + wasmRun.testCount,
333
+ };
334
+ }
335
+
284
336
  export async function runTests(targetDir, options = {}) {
285
337
  let resolvedDir = resolve(targetDir);
286
338
  try {
@@ -304,17 +356,7 @@ export async function runTests(targetDir, options = {}) {
304
356
  try {
305
357
  compiled = compilePackageTests(resolvedDir, options);
306
358
  } catch (err) {
307
- const msg = err?.message ?? String(err);
308
- if (options.captureOutput) {
309
- const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
310
- return { exitCode: 1, stdout: "", stderr: out };
311
- }
312
- const dest = options.stderr ?? process.stderr;
313
- const c = colorsFor(options, dest);
314
- dest.write(
315
- `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
316
- );
317
- return { exitCode: 1 };
359
+ return reportBuildFailure(err, resolvedDir, options);
318
360
  }
319
361
 
320
362
  const { pkgName, js, programs } = compiled;
@@ -324,12 +366,24 @@ export async function runTests(targetDir, options = {}) {
324
366
  return reportEmptyTests(pkgName, "no tests to run", options);
325
367
  }
326
368
 
327
- const harnessJs = generateTestHarness(js, testNames, {
328
- ...options,
329
- pkgName,
330
- jsdomPath,
331
- color: colorsFor(options, options.stdout ?? process.stdout).enabled,
332
- });
369
+ const runHarness = (bundleJs, label, wasmBytes) =>
370
+ spawnTestRunner(
371
+ resolvedDir,
372
+ generateTestHarness(bundleJs, testNames, {
373
+ ...options,
374
+ pkgName: label,
375
+ jsdomPath,
376
+ wasmBytes,
377
+ color: colorsFor(options, options.stdout ?? process.stdout).enabled,
378
+ }),
379
+ testNames.length,
380
+ options,
381
+ );
333
382
 
334
- return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
383
+ // `wasm` packages run inside the linked app.wasm; `js` packages that import
384
+ // wasm packages need the linked module too (hybrid packages).
385
+ if (compiled.target === "both") {
386
+ return runDualTarget(compiled, resolvedDir, runHarness, options);
387
+ }
388
+ return runHarness(js, pkgName, compiled.wasm ?? null);
335
389
  }
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  ANY,
5
5
  BOOL,
6
+ BROWSER_GLOBALS,
6
7
  COMPLEX128,
7
8
  ERROR,
8
9
  FLOAT64,
@@ -126,6 +127,26 @@ export const expressionCheckMethods = {
126
127
  const t = scope.lookup(expr.name);
127
128
  if (!t) return this.err(`Undefined: '${expr.name}'`, expr);
128
129
  if (this.types.has(expr.name)) expr._isTypeRef = true;
130
+ if (
131
+ (this.target === "wasm" || this.target === "both") &&
132
+ BROWSER_GLOBALS.has(expr.name) &&
133
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
134
+ ) {
135
+ this.recordBlocker("browser global", expr.name);
136
+ this.err(
137
+ `'${expr.name}' is not available in wasm packages`,
138
+ expr,
139
+ `(package '${this.pkgName}' is //gofront:target ${this.target} — browser globals are only available in js packages)`,
140
+ );
141
+ }
142
+ if (
143
+ (this.target === "wasm" || this.target === "both") &&
144
+ expr.name === "gom" &&
145
+ scope.lookup(expr.name) === this.globals.lookup(expr.name)
146
+ ) {
147
+ this.recordBlocker("gom usage", "gom");
148
+ this.err("package 'gom' is not available in wasm packages", expr);
149
+ }
129
150
  return t;
130
151
  }
131
152
  case "BinaryExpr": {
@@ -136,8 +157,16 @@ export const expressionCheckMethods = {
136
157
  return this.binaryResultType(expr.op, ANY, ANY, expr);
137
158
  return this.binaryResultType(expr.op, lt, rt, expr);
138
159
  }
139
- case "AwaitExpr":
160
+ case "AwaitExpr": {
161
+ if (this.target === "wasm" || this.target === "both") {
162
+ this.recordBlocker("async function");
163
+ this.err(
164
+ "async functions are not supported in wasm packages; keep async code in a js package",
165
+ expr,
166
+ );
167
+ }
140
168
  return this.checkExpr(expr.expr, scope);
169
+ }
141
170
  case "InstantiationExpr":
142
171
  return this._checkInstantiationExpr(expr, scope);
143
172
  default: {
@@ -280,6 +309,16 @@ export const expressionCheckMethods = {
280
309
  },
281
310
 
282
311
  _checkFuncLit(expr, scope) {
312
+ if (
313
+ (this.target === "wasm" || this.target === "both") &&
314
+ (expr.async || expr.isAsync)
315
+ ) {
316
+ this.recordBlocker("async function");
317
+ this.err(
318
+ "async functions are not supported in wasm packages; keep async code in a js package",
319
+ expr,
320
+ );
321
+ }
283
322
  const inner = new Scope(scope);
284
323
  for (const p of expr.params)
285
324
  inner.define(p.name, p.type ? this.resolveTypeNode(p.type, scope) : ANY);