gofront 1.3.10 → 1.3.12

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.
package/README.md CHANGED
@@ -109,8 +109,9 @@ Walks the typed AST and emits clean, readable JavaScript. No intermediate repres
109
109
  — the codegen writes directly to an output buffer with indentation tracking.
110
110
 
111
111
  Runtime helpers (`__len`, `__append`, `__s`, `__sprintf`, `__equal`, `__cmul`, `__cdiv`,
112
- `__error`, `__errorIs`, `__timeFmt`, `__timeParse`, `__pathClean`) are tree-shaken: only
113
- emitted when actually used. Optional inline source maps are supported via VLQ-encoded mappings.
112
+ `__error`, `__errorIs`, `__timeFmt`, `__timeParse`, `__pathClean`, `__sortSlice`,
113
+ `__sclone`, `__ifv`, `__ifp`) are tree-shaken: only emitted when actually used. Optional
114
+ inline source maps are supported via VLQ-encoded mappings.
114
115
 
115
116
  ---
116
117
 
@@ -191,10 +192,11 @@ The type checker enforces correctness; JavaScript doesn't need to know.
191
192
 
192
193
  ---
193
194
 
194
- ## Examples — Todo App
195
+ ## Examples
195
196
 
196
- There are four example apps — all implement the same todo app to show different aspects
197
- of GoFront.
197
+ There are five example apps. Four implement the same todo app to show different aspects
198
+ of GoFront; the fifth is a WebGL2 3D showcase demonstrating typed graphics APIs and
199
+ zero-allocation rendering.
198
200
 
199
201
  ### Simple (vanilla DOM)
200
202
 
@@ -224,6 +226,10 @@ signals-based reactive framework. Demonstrates how GoFront integrates with exter
224
226
  libraries via `.d.ts` type declarations — the entire reactive API surface is typed via a
225
227
  hand-written `browser.d.ts` shim and exercised from GoFront source.
226
228
 
229
+ > **Note:** Microtastic is now archived. This example is kept as a reference for
230
+ > `.d.ts` interop with third-party signal libraries. For new projects, use the native
231
+ > `.templ` or `gom` approaches shown below.
232
+
227
233
  ```
228
234
  example/reactive/
229
235
  src/
@@ -368,6 +374,21 @@ attributes, `@Component()` calls inside templates, `if / else if / else` chains,
368
374
  `switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
369
375
  all inside template bodies.
370
376
 
377
+ ### WebGL2 (3D showcase)
378
+
379
+ A 3D rotating colored cube rendered with WebGL2 shaders. Demonstrates GoFront's static
380
+ WebGL2 typings, `Float32Array` / `Uint16Array` slice mapping, zero-allocation `for range`
381
+ loops, and continuous `requestAnimationFrame` rendering — all with zero per-frame heap
382
+ allocations.
383
+
384
+ ```
385
+ example/webgl/
386
+ src/
387
+ main.go ← shaders, mat4 math, vertex/index buffers, render loop
388
+ app.js ← generated output
389
+ index.html ← canvas element
390
+ ```
391
+
371
392
  ### Build and run
372
393
 
373
394
  ```sh
@@ -375,6 +396,7 @@ npm run build:simple # → example/simple/app.js
375
396
  npm run build:reactive # → example/reactive/app.js
376
397
  npm run build:gom # → example/gom/app.js
377
398
  npm run build:templ # → example/templ/app.js
399
+ npm run build:webgl # → example/webgl/app.js
378
400
  # open the respective index.html in a browser
379
401
  ```
380
402
 
@@ -383,23 +405,33 @@ npm run build:templ # → example/templ/app.js
383
405
  ## CLI
384
406
 
385
407
  ```
386
- gofront <file.go> compile single file → stdout
387
- gofront <dir> compile all *.go in directory → stdout
388
- gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
389
- gofront <input> --check type-check only
390
- gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
391
- gofront test <dir>/... [--dom] run tests for every package under <dir>
392
- gofront <input> --watch watch for changes and recompile
393
- gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
394
- gofront <input> -o out.js --serve --port 8080 use a custom port
395
- gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
396
- gofront <input> --minify minify output (built-in minifier)
397
- gofront <input> --minify --mangle minify and rename local identifiers
398
- gofront <file.go> --ast dump AST (debug)
399
- gofront <file.go> --tokens dump tokens (debug)
400
- gofront init [dir] scaffold a new project
401
- gofront --version / -v print version
402
- gofront --help / -h print this help
408
+ gofront dev [dir] watch + compile + asset sync + live reload (default port 3000)
409
+ gofront dev [dir] --port 8080 use a custom port
410
+ gofront build [dir] clean + compile + minify + vendor → production output
411
+ gofront build [dir] --pwa also generate offline service worker (sw.js) + precache manifest
412
+ gofront build [dir] --source-map include inline source maps in the release bundle
413
+ gofront build [dir] --no-minify skip minification
414
+ gofront build [dir] --no-mangle minify but keep original identifiers
415
+ gofront prep [dir] [--minify] run asset copying + vendor bundling only (alias: gofront vendor)
416
+ gofront check <dir> type-check a single package
417
+ gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
418
+ gofront test <dir> [--dom] run tests for a single package
419
+ gofront test <dir>/... [--dom] [-v] [-run <regex>] run tests recursively
420
+ gofront <file.go> compile single file → stdout
421
+ gofront <dir> compile all *.go in directory → stdout
422
+ gofront <input> -o out.js write output to file (prints elapsed compile time)
423
+ gofront <input> -o out.js --copy-assets compile + copy static assets
424
+ gofront <input> --check type-check only (single file / directory)
425
+ gofront <input> --watch watch for changes and recompile
426
+ gofront <input> -o out.js --serve watch + serve with live reload (legacy; prefer gofront dev)
427
+ gofront <input> --source-map append inline source map
428
+ gofront <input> --minify minify output (built-in minifier)
429
+ gofront <input> --minify --mangle minify and rename local identifiers
430
+ gofront <file.go> --ast dump AST (debug)
431
+ gofront <file.go> --tokens dump tokens (debug)
432
+ gofront init [dir] scaffold a new project
433
+ gofront --version / -v print version
434
+ gofront --help / -h print this help
403
435
  ```
404
436
  ### Project configuration
405
437
 
@@ -574,6 +606,7 @@ signatures into GoFront's internal type representation.
574
606
  | `os` | `Exit`, `Args`, `Getenv` |
575
607
  | `io` | `Writer`, `Reader`, `ReadWriter`, `Closer` interface types; `ReadAll`, `EOF`, `Discard`, `WriteString` |
576
608
  | `gom` | Browser-native declarative DOM component library. **Types**: `Node` (interface), `NodeFunc`, `Group`. **Core**: `El(tag, children...)`, `Text(s)`, `Mount(sel, node)`, `MountTo(sel, node)`. **Attributes**: `Attr`, `Class`, `Href`, `Type`, `Src`, `Placeholder`, `DataAttr`, `Style`, `For`, `Name`, `Value`, `Target`, `Rel`, `Alt`, `Title`, `Draggable`, `Role`, `AriaLabel`, `StyleAttr`; boolean: `Disabled`, `Checked`, `Selected`, `Readonly`. **Logic**: `If(cond, node)`, `Map(slice, fn)`. **Elements**: full HTML element set (`Div`, `Span`, `Button`, `Input`, `Ul`, `Li`, `Table`, `Form`, `Img`, `A`, `H1`–`H6`, …) |
609
+ | `testing` | **`testing.T`**: `Error`, `Errorf`, `Fatal`, `Fatalf`, `Fail`, `Failed`, `FailNow`, `Log`, `Logf`, `Skip`, `Skipf`, `Skipped`, `Helper`, `Run`, `Name`. **Package functions**: `Short()`, `Verbose()`. Tests are `func TestXxx(t *testing.T)` in `*_test.go` files, excluded from normal compilation and run via `gofront test`. |
577
610
 
578
611
  ### Packages & imports
579
612
 
@@ -604,7 +637,7 @@ These features are intentional additions for the JavaScript platform:
604
637
  | Feature | Purpose |
605
638
  |---|---|
606
639
  | `async func` / `await` | First-class async syntax for frontend work. |
607
- | Browser globals (`document`, `console`, etc.) | Predeclared as `any` for practical DOM access. |
640
+ | Browser globals (`document`, `console`, etc.) | Predeclared as `any` for practical DOM access. `WebGL2RenderingContext`, `WebGLRenderingContext`, `GPUDevice`, `GPUAdapter`, `GPUQueue`, `ArrayBuffer`, `DataView`, and TypedArrays have full static typings with method-level checking. |
608
641
  | `.d.ts` type imports (`import "js:./types.d.ts"`) | Type-safe interop with JavaScript libraries. |
609
642
  | npm package resolution | Import types from `node_modules/` and `@types/` automatically. |
610
643
 
@@ -646,28 +679,33 @@ you know exactly what to expect.
646
679
 
647
680
  See [`docs/roadmap.md`](docs/roadmap.md) for the full roadmap and release history.
648
681
  Design documents for planned features are organised by release under `docs/v*/`
649
- (e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v0.0.9/`](docs/v0.0.9/)).
682
+ (e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v1.2.0/`](docs/v1.2.0/), [`docs/v1.3.0/`](docs/v1.3.0/)).
650
683
 
651
684
  ---
652
685
 
653
686
  ## Tests
654
687
 
655
688
  ```sh
656
- npm run test:unit # unit tests only (~1100 tests, no browser required)
657
- npm run test:e2e # E2E tests (Playwright, headless Chromium)
658
- npm run test:all # both
689
+ npm run test:unit # unit tests only (~1400 tests, no browser required)
690
+ npm run test:perf # zero-allocation benchmark (100k ray-triangle intersections, 0 bytes/frame)
691
+ npm run test:examples # GoFront-native unit tests across all example apps
692
+ npm run test:examples:dom # same, with JSDOM for DOM/gom/templ component testing
693
+ npm run test:e2e # E2E tests (Playwright, headless Chromium)
694
+ npm run test:all # all of the above
659
695
  ```
660
696
 
661
- **Unit tests** (~1118) cover language features, type errors, edge cases, DOM (jsdom),
697
+ **Unit tests** (~1,396) cover language features, type errors, edge cases, DOM (jsdom),
662
698
  external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
663
699
  formatting, map iteration order, integer overflow semantics, unused variable detection,
664
700
  unused import detection, semantic difference verification, stdlib shim packages, generics,
665
- and `.templ` file compilation (element rendering, interpolation, boolean attrs, component
666
- calls, `if/else/else-if` chains, `for range`, `switch/case/default`, `@templ.Raw()` raw
667
- HTML injection, mixed `.go`+`.templ` packages).
701
+ the `testing` framework itself, and `.templ` file compilation (element rendering,
702
+ interpolation, boolean attrs, component calls, `if/else/else-if` chains, `for range`,
703
+ `switch/case/default`, `@templ.Raw()` raw HTML injection, SVG namespace handling, mixed
704
+ `.go`+`.templ` packages).
668
705
 
669
- **E2E tests** (~104, Playwright) run all four example apps in a real browser and verify
706
+ **E2E tests** (~104, Playwright) run all five example apps in a real browser and verify
670
707
  CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
671
708
  status. Per-app suites check app-specific behaviour: scoped styles, stats bar, loading
672
- placeholder, `gom.If` conditional rendering, and templ-specific features (`if/else`
673
- priority hint, `for` loop rendering, conditional bool attributes).
709
+ placeholder, `gom.If` conditional rendering, templ-specific features (`if/else`
710
+ priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
711
+ rendering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@ import {
11
11
  resolve,
12
12
  sep,
13
13
  } from "node:path";
14
+ import { log } from "./colors.js";
14
15
  import { loadProjectConfig } from "./project-config.js";
15
16
 
16
17
  export function loadAssetConfig(projectDir) {
@@ -50,7 +51,7 @@ function copyAssetEntry(entry, projectRoot) {
50
51
 
51
52
  const srcPath = resolve(projectRoot, source);
52
53
  if (!existsSync(srcPath)) {
53
- console.warn(`gofront: asset source '${source}' not found — skipping`);
54
+ log.warn(`asset source '${source}' not found — skipping`);
54
55
  return {
55
56
  status: "skipped",
56
57
  item: { source, dest, status: "skipped", reason: "source not found" },
@@ -73,7 +74,7 @@ function copyAssetEntry(entry, projectRoot) {
73
74
  item: { source, dest: targetFile, type: "file", status: "copied" },
74
75
  };
75
76
  } catch (err) {
76
- console.warn(`gofront: failed to copy asset '${source}': ${err.message}`);
77
+ log.warn(`failed to copy asset '${source}': ${err.message}`);
77
78
  return {
78
79
  status: "failed",
79
80
  item: { source, dest, status: "failed", reason: err.message },
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 { colors, log } from "./colors.js";
26
27
  import { compileDir, compileSingleFile } from "./compiler.js";
27
28
  import { createDevServer } from "./dev-server.js";
28
29
  import { minify } from "./minifier.js";
@@ -685,7 +686,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
685
686
  dest: join(outDir, "vendor.js"),
686
687
  });
687
688
  } catch (e) {
688
- console.warn(`gofront: vendor bundle warning: ${e.message}`);
689
+ log.warn(`vendor bundle: ${e.message}`);
689
690
  }
690
691
 
691
692
  const assets = copyReleaseAssets(
@@ -755,11 +756,12 @@ function buildDevOnce(
755
756
  writeFileSync(outputFile, `${result.js}\n`);
756
757
  const elapsedMs = (performance.now() - startMs).toFixed(0);
757
758
  const note = changedFile ? ` — ${changedFile} changed` : "";
758
- console.error(`gofront: OK — wrote ${outputFile} (${elapsedMs}ms${note})`);
759
+ log.ok(
760
+ `— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
761
+ );
759
762
  devServer.notify();
760
763
  } catch (e) {
761
- console.error("gofront: ERROR");
762
- for (const line of e.message.split("\n")) console.error(` ${line}`);
764
+ log.error(e.message);
763
765
  devServer.notifyError(e);
764
766
  }
765
767
  }
@@ -208,8 +208,22 @@ export class CodeGen {
208
208
  this._emitVarConstDecls(program);
209
209
  const initNames = this._emitFuncDecls(program);
210
210
  this._callInitAndMain(initNames, program, isTest);
211
- this._prependHelpers(isTest);
212
- while (this.out[0] === "") this.out.shift();
211
+ const prependedLines = this._prependHelpers(isTest);
212
+ return this._finalizeOutput(prependedLines);
213
+ }
214
+
215
+ _finalizeOutput(prependedLines) {
216
+ let shiftedLines = 0;
217
+ while (this.out[0] === "") {
218
+ this.out.shift();
219
+ shiftedLines++;
220
+ }
221
+ const netShift = prependedLines - shiftedLines;
222
+ if (netShift !== 0) {
223
+ for (const m of this._srcMappings) {
224
+ m.genLine = Math.max(0, m.genLine + netShift);
225
+ }
226
+ }
213
227
  return this.out.join("\n");
214
228
  }
215
229
 
@@ -385,7 +399,13 @@ export class CodeGen {
385
399
  [needsTesting, HELPER_TESTING],
386
400
  ];
387
401
  const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
388
- if (helpers.length > 0) this.out.unshift(...helpers, "");
402
+ if (helpers.length > 0) {
403
+ const helperLines = helpers.flatMap((h) => h.split("\n"));
404
+ helperLines.push("");
405
+ this.out.unshift(...helperLines);
406
+ return helperLines.length;
407
+ }
408
+ return 0;
389
409
  }
390
410
 
391
411
  // Generate a single bundle from multiple programs (same-package multi-file).
@@ -400,6 +420,10 @@ export class CodeGen {
400
420
  return this.generate(merged, options);
401
421
  }
402
422
 
423
+ getMappings() {
424
+ return this._srcMappings;
425
+ }
426
+
403
427
  // Returns a source map JSON string for the last generate() call.
404
428
  // sources: string[] of source filenames (relative to the output file).
405
429
  // sourcesContent: string[] of original file contents (embedded for DevTools breakpoints).
@@ -581,9 +605,11 @@ export class CodeGen {
581
605
  }
582
606
 
583
607
  genMethod(decl, recvField = null) {
608
+ const prevFileIdx = this._currentSrcFileIdx;
609
+ if (decl._srcFileIdx != null) this._currentSrcFileIdx = decl._srcFileIdx;
584
610
  const params = decl.params.map((p) => p.name).join(", ");
585
611
  const asyncPrefix = decl.async ? "async " : "";
586
- this.line(`${asyncPrefix}${decl.name}(${params}) {`);
612
+ this.line(`${asyncPrefix}${decl.name}(${params}) {`, decl._line ?? null);
587
613
  const prevBoxed = this._boxedVars;
588
614
  this._boxedVars = new Set();
589
615
  this._scanAddressTaken(decl.body);
@@ -605,6 +631,7 @@ export class CodeGen {
605
631
  this._unwrappedRecv = prevUnwrapped;
606
632
  this._boxedVars = prevBoxed;
607
633
  this.line("}");
634
+ this._currentSrcFileIdx = prevFileIdx;
608
635
  }
609
636
 
610
637
  // Value receivers are copies in Go; only materialize the copy when the body mutates it.
@@ -648,10 +675,7 @@ export class CodeGen {
648
675
  .join(", ");
649
676
  const asyncPrefix = decl.async ? "async " : "";
650
677
  const srcLine = decl._line ?? null;
651
- this.line(
652
- `${asyncPrefix}function ${name}(${params}) {`,
653
- srcLine ? srcLine - 1 : null,
654
- );
678
+ this.line(`${asyncPrefix}function ${name}(${params}) {`, srcLine);
655
679
  const prevBoxed = this._boxedVars;
656
680
  this._boxedVars = new Set();
657
681
  this._scanAddressTaken(decl.body);
@@ -25,7 +25,10 @@ export function buildSourceMap(sources, mappings, sourcesContent) {
25
25
  let prevSrcLine = 0;
26
26
  let prevSrcFile = 0;
27
27
  const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
28
- const byLine = new Map(mappings.map((e) => [e.genLine, e]));
28
+ const byLine = new Map();
29
+ for (const m of mappings) {
30
+ if (!byLine.has(m.genLine)) byLine.set(m.genLine, m);
31
+ }
29
32
  for (let g = 0; g <= maxGen; g++) {
30
33
  const entry = byLine.get(g);
31
34
  if (entry) {
@@ -47,7 +50,7 @@ export function buildSourceMap(sources, mappings, sourcesContent) {
47
50
  }
48
51
  const map = {
49
52
  version: 3,
50
- sources,
53
+ sources: sources.map((s) => s.replace(/\\/g, "/")),
51
54
  names: [],
52
55
  mappings: lines.join(";"),
53
56
  };
@@ -96,7 +96,11 @@ export const statementGenMethods = {
96
96
  }
97
97
  // Record source mapping: first output line this statement produced
98
98
  if (srcLine != null && this.out.length > _line0) {
99
- this._srcMappings.push({ genLine: _line0, srcLine: srcLine - 1 });
99
+ this._srcMappings.push({
100
+ genLine: _line0,
101
+ srcLine: srcLine - 1,
102
+ srcFileIdx: this._currentSrcFileIdx,
103
+ });
100
104
  }
101
105
  },
102
106
 
@@ -146,9 +146,9 @@ export const gomMethods = {
146
146
  case "Style":
147
147
  return `((s)=>({Mount(p){const e=document.createElement("style");e.textContent=s;p.appendChild(e);}})) (${args[0]})`;
148
148
  case "Mount":
149
- return `((sel,n)=>{const e=document.querySelector(sel);e.innerHTML="";n.Mount(e)})(${args[0]},${args[1]})`;
149
+ return `((sel,n,r)=>{const e=document.querySelector(sel);e.innerHTML="";n.Mount(e,r)})(${args[0]},${args[1]},${args[2] ?? "undefined"})`;
150
150
  case "MountTo":
151
- return `((sel,n)=>{const e=document.querySelector(sel);n.Mount(e)})(${args[0]},${args[1]})`;
151
+ return `((sel,n,r)=>{const e=document.querySelector(sel);n.Mount(e,r)})(${args[0]},${args[1]},${args[2] ?? "undefined"})`;
152
152
  }
153
153
  return undefined;
154
154
  },
@@ -17,7 +17,7 @@ export const templGenMethods = {
17
17
  .join(", ");
18
18
  this.line(`function ${decl.name}(${params}) {`);
19
19
  this.indented(() => {
20
- this.line(`return {Mount(___p) {`);
20
+ this.line(`return {Mount(___p, ___refs) {`);
21
21
  this.indented(() => {
22
22
  this._genTemplNodes(decl.body, "___p");
23
23
  });
@@ -85,7 +85,7 @@ export const templGenMethods = {
85
85
  return this.line(`${p}.insertAdjacentHTML("beforeend", ${argJs});`);
86
86
  }
87
87
  return this.line(
88
- `(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p});`,
88
+ `(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p}, ___refs);`,
89
89
  );
90
90
  },
91
91
 
@@ -119,7 +119,10 @@ export const templGenMethods = {
119
119
  isSvg
120
120
  ? `${el}.setAttribute("class", ${js});`
121
121
  : `${el}.className = ${js};`;
122
- if (attr.kind === "static") {
122
+ if (attr.kind === "ref") {
123
+ // ref="key" — capture element into the refs map at mount time
124
+ this.line(`if(___refs)___refs[${JSON.stringify(attr.value)}]=${el};`);
125
+ } else if (attr.kind === "static") {
123
126
  if (name === "class") {
124
127
  this.line(setClass(JSON.stringify(attr.value)));
125
128
  } else {
package/src/colors.js ADDED
@@ -0,0 +1,81 @@
1
+ // ANSI color helpers for CLI output. Honours NO_COLOR / FORCE_COLOR and
2
+ // otherwise only colors when the target stream is a TTY.
3
+
4
+ export function colorEnabled(stream = process.stderr) {
5
+ if ("NO_COLOR" in process.env) return false;
6
+ const force = process.env.FORCE_COLOR;
7
+ if (force !== undefined) return force !== "0" && force !== "false";
8
+ return Boolean(stream?.isTTY);
9
+ }
10
+
11
+ const CODES = {
12
+ red: 31,
13
+ green: 32,
14
+ yellow: 33,
15
+ blue: 34,
16
+ magenta: 35,
17
+ cyan: 36,
18
+ gray: 90,
19
+ bold: 1,
20
+ dim: 2,
21
+ };
22
+
23
+ export function createColors(enabled) {
24
+ const c = {};
25
+ for (const [name, code] of Object.entries(CODES)) {
26
+ c[name] = enabled ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => `${s}`;
27
+ }
28
+ c.enabled = enabled;
29
+ return c;
30
+ }
31
+
32
+ export const colors = createColors(colorEnabled(process.stderr));
33
+
34
+ const DIAG_LABEL = /^(Lex error|Parse error|Type error)(\b.*?)(:\s)/;
35
+ const GUTTER = /^(\s*\d+ \| )(.*)$/;
36
+ const CARET = /^(\s*)(\^+)$/;
37
+
38
+ // Colorizes a compiler diagnostic: red label, dimmed source gutter, red caret.
39
+ export function formatDiagnostic(message, c = colors) {
40
+ if (!c.enabled) return message;
41
+ return message
42
+ .split("\n")
43
+ .map((line) => {
44
+ const label = line.match(DIAG_LABEL);
45
+ if (label) {
46
+ return `${c.bold(c.red(label[1]))}${c.dim(label[2])}${label[3]}${line.slice(label[0].length)}`;
47
+ }
48
+ const gutter = line.match(GUTTER);
49
+ if (gutter) return `${c.dim(gutter[1])}${gutter[2]}`;
50
+ const caret = line.match(CARET);
51
+ if (caret) return `${caret[1]}${c.bold(c.red(caret[2]))}`;
52
+ if (/^warning:/i.test(line.trim())) return c.yellow(line);
53
+ return line;
54
+ })
55
+ .join("\n");
56
+ }
57
+
58
+ // Standard CLI log helpers writing to stderr with a colored `gofront:` prefix.
59
+ const prefix = () => colors.bold("gofront:");
60
+ const isDiagnostic = (msg) => DIAG_LABEL.test(msg) || msg.includes("\n");
61
+
62
+ export const log = {
63
+ info: (msg) => console.error(`${prefix()} ${msg}`),
64
+ ok: (msg) =>
65
+ console.error(`${prefix()} ${colors.green("OK")}${msg ? ` ${msg}` : ""}`),
66
+ warn: (msg) =>
67
+ console.error(`${prefix()} ${colors.yellow(`warning: ${msg}`)}`),
68
+ error: (msg) => {
69
+ console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
70
+ for (const line of formatDiagnostic(msg).split("\n"))
71
+ console.error(` ${line}`);
72
+ },
73
+ // Compiler diagnostics get the block form; anything else is one red line.
74
+ fail: (msg) =>
75
+ isDiagnostic(msg)
76
+ ? log.error(msg)
77
+ : console.error(`${prefix()} ${colors.red(msg)}`),
78
+ };
79
+
80
+ export const stamp = () => colors.dim(`[${new Date().toLocaleTimeString()}]`);
81
+ export const ms = (n) => colors.dim(`(${n}ms)`);
package/src/compiler.js CHANGED
@@ -20,6 +20,8 @@
20
20
  import { readdirSync, readFileSync, statSync } from "node:fs";
21
21
  import { basename, dirname, join, relative, resolve } from "node:path";
22
22
  import { CodeGen } from "./codegen/index.js";
23
+ import { buildSourceMap } from "./codegen/source-map.js";
24
+ import { log } from "./colors.js";
23
25
  import { parseDts } from "./dts-parser.js";
24
26
  import { Lexer } from "./lexer.js";
25
27
  import { Parser } from "./parser/index.js";
@@ -73,6 +75,87 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
73
75
  .map((f) => join(dir, f));
74
76
  }
75
77
 
78
+ function normalizeChunk(c) {
79
+ if (typeof c === "string") {
80
+ return { js: c, sourceFiles: [], sourcesContent: [], mappings: [] };
81
+ }
82
+ return c;
83
+ }
84
+
85
+ function appendChunkMappings(dest, mappings, localToUnifiedIdx, lineOffset) {
86
+ if (!mappings) return;
87
+ for (const m of mappings) {
88
+ dest.push({
89
+ genLine: m.genLine + lineOffset,
90
+ srcLine: m.srcLine,
91
+ srcFileIdx: localToUnifiedIdx[m.srcFileIdx ?? 0] ?? 0,
92
+ });
93
+ }
94
+ }
95
+
96
+ function mergeCompilationChunks(chunks) {
97
+ const activeChunks = chunks
98
+ .filter(Boolean)
99
+ .map(normalizeChunk)
100
+ .filter((c) => c.js.length > 0);
101
+
102
+ if (activeChunks.length === 0) {
103
+ return { js: "", sourceFiles: [], sourcesContent: [], mappings: [] };
104
+ }
105
+
106
+ const unifiedSources = [];
107
+ const unifiedSourcesContent = [];
108
+ const fileIdxMap = new Map();
109
+
110
+ function getOrAddFile(filePath, content) {
111
+ const key = resolve(filePath);
112
+ let idx = fileIdxMap.get(key);
113
+ if (idx === undefined) {
114
+ idx = unifiedSources.length;
115
+ fileIdxMap.set(key, idx);
116
+ unifiedSources.push(key);
117
+ unifiedSourcesContent.push(content);
118
+ }
119
+ return idx;
120
+ }
121
+
122
+ const combinedJsParts = [];
123
+ const mergedMappings = [];
124
+ let currentLineOffset = 0;
125
+
126
+ for (const chunk of activeChunks) {
127
+ combinedJsParts.push(chunk.js);
128
+ const localToUnifiedIdx = (chunk.sourceFiles ?? []).map((file, i) =>
129
+ getOrAddFile(file, chunk.sourcesContent?.[i] ?? ""),
130
+ );
131
+ appendChunkMappings(
132
+ mergedMappings,
133
+ chunk.mappings,
134
+ localToUnifiedIdx,
135
+ currentLineOffset,
136
+ );
137
+ currentLineOffset += chunk.js.split("\n").length;
138
+ }
139
+
140
+ return {
141
+ js: combinedJsParts.join("\n"),
142
+ sourceFiles: unifiedSources,
143
+ sourcesContent: unifiedSourcesContent,
144
+ mappings: mergedMappings,
145
+ };
146
+ }
147
+
148
+ function attachSourceMap(merged, outputDir) {
149
+ const relativeSources = merged.sourceFiles.map((f) => relative(outputDir, f));
150
+ const mapJson = buildSourceMap(
151
+ relativeSources,
152
+ merged.mappings,
153
+ merged.sourcesContent,
154
+ );
155
+ const b64 = Buffer.from(mapJson).toString("base64");
156
+ merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
157
+ }
158
+
76
159
  // ── Import resolution ─────────────────────────────────────────
77
160
  //
78
161
  // Shared by compileFiles (multi-file) and the single-file path in index.js.
@@ -86,6 +169,7 @@ export function resolveImports(
86
169
  bundledPackages,
87
170
  preambles,
88
171
  bundledDirs = new Set(),
172
+ options = {},
89
173
  ) {
90
174
  const fromDir = dirname(resolve(fromFile));
91
175
  const allImports = programs.flatMap((p) => p.imports);
@@ -126,8 +210,8 @@ export function resolveImports(
126
210
  seenLocalPaths.add(path);
127
211
  const depDir = resolveGwDir(path, fromFile);
128
212
  if (!depDir) {
129
- console.error(
130
- `gofront: warning: cannot find local package '${path}' relative to ${fromDir}`,
213
+ log.warn(
214
+ `cannot find local package '${path}' relative to ${fromDir}`,
131
215
  );
132
216
  continue;
133
217
  }
@@ -136,8 +220,12 @@ export function resolveImports(
136
220
  const depKey = resolve(depDir);
137
221
  const alreadyBundled = bundledDirs.has(depKey);
138
222
  bundledDirs.add(depKey);
139
- const dep = compileDir(depDir, { bundledDirs });
140
- if (!alreadyBundled) preambles.push(dep.js);
223
+ const dep = compileDir(depDir, {
224
+ ...options,
225
+ bundledDirs,
226
+ isDependency: true,
227
+ });
228
+ if (!alreadyBundled) preambles.push(dep);
141
229
  if (alias === "_") continue;
142
230
  if (alias === ".") {
143
231
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
@@ -180,6 +268,10 @@ export function compileSingleFile(inputPath, options = {}) {
180
268
  ast._source = source;
181
269
  if (dumpAst) return { ast };
182
270
 
271
+ const outputDir =
272
+ options.outputDir ??
273
+ (outputFile ? dirname(resolve(outputFile)) : resolve("."));
274
+
183
275
  const checker = new TypeChecker();
184
276
  const jsImports = new Map();
185
277
  const bundledPackages = new Set();
@@ -192,6 +284,8 @@ export function compileSingleFile(inputPath, options = {}) {
192
284
  jsImports,
193
285
  bundledPackages,
194
286
  preambles,
287
+ options.bundledDirs ?? new Set(),
288
+ { ...options, outputDir },
195
289
  );
196
290
 
197
291
  const errors = checker.check(ast);
@@ -201,18 +295,22 @@ export function compileSingleFile(inputPath, options = {}) {
201
295
  }
202
296
 
203
297
  const cg = new CodeGen(checker, jsImports, bundledPackages);
204
- let js = cg.generate(ast);
205
-
206
- if (sourceMap) {
207
- const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
208
- const srcPath = relative(outputDir, inputPath);
209
- const map = cg.getSourceMap(srcPath, [source]);
210
- const b64 = Buffer.from(map).toString("base64");
211
- js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
298
+ const mainJs = cg.generate(ast);
299
+
300
+ const mainChunk = {
301
+ js: mainJs,
302
+ sourceFiles: [resolve(inputPath)],
303
+ sourcesContent: [source],
304
+ mappings: cg.getMappings(),
305
+ };
306
+
307
+ const merged = mergeCompilationChunks([...preambles, mainChunk]);
308
+
309
+ if (sourceMap && !options.isDependency) {
310
+ attachSourceMap(merged, outputDir);
212
311
  }
213
312
 
214
- const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
215
- return { js: output };
313
+ return { js: merged.js };
216
314
  }
217
315
 
218
316
  export function compileDir(dir, options = {}) {
@@ -229,6 +327,9 @@ export function compilePackageTests(dir, options = {}) {
229
327
 
230
328
  export function compileFiles(files, options = {}) {
231
329
  const fromDir = options.fromDir ?? dirname(resolve(files[0]));
330
+ const outputDir =
331
+ options.outputDir ??
332
+ (options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
232
333
 
233
334
  // ── 1. Parse ─────────────────────────────────────────────────
234
335
  const parseErrors = [];
@@ -256,7 +357,7 @@ export function compileFiles(files, options = {}) {
256
357
  const checker = new TypeChecker();
257
358
  const jsImports = new Map(); // npm imports → exported names (for ESM emit)
258
359
  const bundledPackages = new Set(); // package names whose code is inlined
259
- const preambles = []; // JS code from compiled sub-packages
360
+ const preambles = []; // JS code / chunks from compiled sub-packages
260
361
 
261
362
  const dummyFromFile = join(fromDir, "_dummy.go");
262
363
  resolveImports(
@@ -267,6 +368,7 @@ export function compileFiles(files, options = {}) {
267
368
  bundledPackages,
268
369
  preambles,
269
370
  options.bundledDirs ?? new Set(),
371
+ { ...options, outputDir },
270
372
  );
271
373
 
272
374
  // ── 3. Type-check ─────────────────────────────────────────────
@@ -283,22 +385,27 @@ export function compileFiles(files, options = {}) {
283
385
  isTest: options.isTest ?? false,
284
386
  });
285
387
 
286
- let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
388
+ const mainChunk = {
389
+ js: mainJs,
390
+ sourceFiles: files.map((f) => resolve(f)),
391
+ sourcesContent: files.map((f) => readFileSync(f, "utf8")),
392
+ mappings: codegen.getMappings(),
393
+ };
394
+
395
+ const merged = mergeCompilationChunks([...preambles, mainChunk]);
287
396
 
288
- if (options.sourceMap) {
289
- const outputDir = options.outputDir ?? fromDir;
290
- const sourceFiles = files.map((f) => relative(outputDir, f));
291
- const sourcesContent = files.map((f) => readFileSync(f, "utf8"));
292
- const map = codegen.getSourceMap(sourceFiles, sourcesContent);
293
- const b64 = Buffer.from(map).toString("base64");
294
- js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
397
+ if (options.sourceMap && !options.isDependency) {
398
+ attachSourceMap(merged, outputDir);
295
399
  }
296
400
 
297
401
  return {
298
402
  pkgName,
299
- js,
403
+ js: merged.js,
300
404
  programs,
301
405
  exportedSymbols: checker.getExportedSymbols(),
302
406
  exportedTypes: checker.getExportedTypes(),
407
+ sourceFiles: merged.sourceFiles,
408
+ sourcesContent: merged.sourcesContent,
409
+ mappings: merged.mappings,
303
410
  };
304
411
  }
package/src/dev-server.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { colors, log } from "./colors.js";
7
8
 
8
9
  export const MIME = {
9
10
  ".html": "text/html; charset=utf-8",
@@ -275,9 +276,9 @@ function createNotify(clients) {
275
276
 
276
277
  function handleServerError(err, port) {
277
278
  if (err.code === "EADDRINUSE") {
278
- console.error(`gofront: port ${port} already in use — try --port <number>`);
279
+ log.fail(`port ${port} already in use — try --port <number>`);
279
280
  } else {
280
- console.error(`gofront: dev server error: ${err.message}`);
281
+ log.fail(`dev server error: ${err.message}`);
281
282
  }
282
283
  process.exit(1);
283
284
  }
@@ -327,7 +328,7 @@ export function createDevServer(serveDir, port = 3000, options = {}) {
327
328
  server.listen(port, host, () => {
328
329
  const actualPort = server.address()?.port ?? port;
329
330
  if (port !== 0 && !options.silent) {
330
- console.error(`gofront: dev server → http://localhost:${actualPort}`);
331
+ log.info(`dev server → ${colors.cyan(`http://localhost:${actualPort}`)}`);
331
332
  }
332
333
  });
333
334
 
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  parseTestArgs,
39
39
  runCompile,
40
40
  } from "./cli-core.js";
41
+ import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
41
42
  import { createDevServer } from "./dev-server.js";
42
43
 
43
44
  // ── Parse CLI args ───────────────────────────────────────────
@@ -85,10 +86,9 @@ if (args[0] === "prep" || args[0] === "vendor") {
85
86
  const { targetDir, vendorConfig } = parsePrepArgs(args.slice(1));
86
87
  try {
87
88
  const result = await handlePrep(resolve(targetDir), { vendorConfig });
88
- for (const line of formatPrepSummary(result))
89
- console.error(`gofront: ${line}`);
89
+ for (const line of formatPrepSummary(result)) log.info(line);
90
90
  } catch (e) {
91
- console.error(`gofront: ${e.message}`);
91
+ log.fail(e.message);
92
92
  process.exit(1);
93
93
  }
94
94
  process.exit(0);
@@ -103,12 +103,12 @@ if (args[0] === "init") {
103
103
  try {
104
104
  ({ mainPath } = handleInit(targetDir));
105
105
  } catch (e) {
106
- console.error(`gofront: ${e.message}`);
106
+ log.fail(e.message);
107
107
  process.exit(1);
108
108
  }
109
- console.error(`gofront: created ${mainPath}`);
110
- console.error(
111
- `gofront: run gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`} to compile`,
109
+ log.info(`created ${colors.cyan(mainPath)}`);
110
+ log.info(
111
+ `run ${colors.cyan(`gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`}`)} to compile`,
112
112
  );
113
113
  process.exit(0);
114
114
  }
@@ -121,7 +121,7 @@ if (args[0] === "test") {
121
121
  const result = await handleTest(targetDir, testOptions);
122
122
  process.exit(result.exitCode);
123
123
  } catch (e) {
124
- console.error(`gofront: ${e.message}`);
124
+ log.fail(e.message);
125
125
  process.exit(1);
126
126
  }
127
127
  }
@@ -132,21 +132,21 @@ if (args[0] === "check") {
132
132
  const { targetDir } = parseCheckArgs(args.slice(1));
133
133
  try {
134
134
  const { elapsedMs, packages } = handleCheck(targetDir);
135
+ const okLine = (label, t) =>
136
+ log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
135
137
  if (packages) {
136
138
  for (const pkg of packages) {
137
- console.error(
138
- `gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
139
- );
139
+ okLine(relative(process.cwd(), pkg.dir) || ".", pkg.elapsedMs);
140
140
  }
141
- console.error(
142
- `gofront: ${packages.length} packages — OK (${elapsedMs}ms)`,
141
+ log.info(
142
+ `${colors.bold(`${packages.length} packages`)} — ${colors.bold(colors.green("OK"))} ${ms(elapsedMs)}`,
143
143
  );
144
144
  } else {
145
- console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
145
+ okLine(targetDir, elapsedMs);
146
146
  }
147
147
  process.exit(0);
148
148
  } catch (e) {
149
- console.error(`gofront: ${e.message}`);
149
+ log.fail(e.message);
150
150
  process.exit(1);
151
151
  }
152
152
  }
@@ -158,11 +158,15 @@ if (args[0] === "build") {
158
158
  try {
159
159
  const result = await handleBuild(buildOptions.targetDir, buildOptions);
160
160
  for (const line of formatBuildSummary(result)) {
161
- console.error(`gofront: ${line}`);
161
+ log.info(
162
+ line
163
+ .replace(/^build complete/, colors.green("build complete"))
164
+ .replace(/\(([\d.]+ kB)\)$/, (_, s) => colors.dim(`(${s})`)),
165
+ );
162
166
  }
163
167
  process.exit(0);
164
168
  } catch (e) {
165
- console.error(`gofront: ${e.message}`);
169
+ log.fail(e.message);
166
170
  process.exit(1);
167
171
  }
168
172
  }
@@ -174,14 +178,14 @@ if (args[0] === "dev") {
174
178
  try {
175
179
  const dev = await handleDev(devOptions.targetDir, devOptions);
176
180
  if (dev.initialError) {
177
- console.error("gofront: ERROR");
178
- for (const line of dev.initialError.message.split("\n"))
179
- console.error(` ${line}`);
181
+ log.error(dev.initialError.message);
180
182
  } else {
181
- console.error(`gofront: OK — wrote ${dev.outputFile}`);
183
+ log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
182
184
  }
183
- console.error(`gofront: dev server running → http://localhost:${dev.port}`);
184
- console.error(`gofront: watching ${dev.srcDir} for changes...`);
185
+ log.info(
186
+ `dev server running → ${colors.cyan(`http://localhost:${dev.port}`)}`,
187
+ );
188
+ log.info(`watching ${colors.cyan(dev.srcDir)} for changes...`);
185
189
 
186
190
  const shutdown = async () => {
187
191
  await dev.close();
@@ -191,7 +195,7 @@ if (args[0] === "dev") {
191
195
  process.on("SIGTERM", shutdown);
192
196
  await new Promise(() => {});
193
197
  } catch (e) {
194
- console.error(`gofront: ${e.message}`);
198
+ log.fail(e.message);
195
199
  process.exit(1);
196
200
  }
197
201
  }
@@ -218,7 +222,7 @@ let isDir = false;
218
222
  try {
219
223
  isDir = statSync(inputPath).isDirectory();
220
224
  } catch (e) {
221
- console.error(`gofront: cannot access '${inputArg}': ${e.message}`);
225
+ log.fail(`cannot access '${inputArg}': ${e.message}`);
222
226
  process.exit(1);
223
227
  }
224
228
 
@@ -235,7 +239,7 @@ if (!watchMode) {
235
239
  dumpAst,
236
240
  });
237
241
  } catch (e) {
238
- console.error(`gofront: ${e.message}`);
242
+ log.fail(e.message);
239
243
  process.exit(1);
240
244
  }
241
245
 
@@ -250,7 +254,9 @@ if (!watchMode) {
250
254
 
251
255
  if (checkOnly) {
252
256
  const elapsedMs = (performance.now() - startMs).toFixed(0);
253
- console.error(`gofront: ${inputArg} — OK (${elapsedMs}ms)`);
257
+ log.info(
258
+ `${colors.cyan(inputArg)} — ${colors.green("OK")} ${ms(elapsedMs)}`,
259
+ );
254
260
  process.exit(0);
255
261
  }
256
262
 
@@ -262,7 +268,7 @@ if (!watchMode) {
262
268
  sourceMap,
263
269
  });
264
270
  } catch (e) {
265
- console.error(`gofront: minify failed: ${e.message}`);
271
+ log.fail(`minify failed: ${e.message}`);
266
272
  process.exit(1);
267
273
  }
268
274
 
@@ -272,9 +278,9 @@ if (!watchMode) {
272
278
  try {
273
279
  mkdirSync(dirname(resolve(outputFile)), { recursive: true });
274
280
  writeFileSync(outputFile, `${js}\n`);
275
- console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
281
+ log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
276
282
  } catch (e) {
277
- console.error(`gofront: cannot write '${outputFile}': ${e.message}`);
283
+ log.fail(`cannot write '${outputFile}': ${e.message}`);
278
284
  process.exit(1);
279
285
  }
280
286
  } else {
@@ -286,10 +292,10 @@ if (!watchMode) {
286
292
  const projectDir = resolve(".");
287
293
  const { copied, skipped } = copyAssets(projectDir);
288
294
  if (copied > 0 || skipped > 0) {
289
- console.error(`gofront: copied ${copied} assets (${skipped} skipped)`);
295
+ log.info(`copied ${copied} assets (${skipped} skipped)`);
290
296
  }
291
297
  } catch (e) {
292
- console.error(`gofront: asset copy failed: ${e.message}`);
298
+ log.warn(`asset copy failed: ${e.message}`);
293
299
  }
294
300
  }
295
301
 
@@ -298,15 +304,11 @@ if (!watchMode) {
298
304
 
299
305
  // ── Watch mode ───────────────────────────────────────────────
300
306
 
301
- function timestamp() {
302
- return new Date().toLocaleTimeString();
303
- }
304
-
305
307
  // Start dev server before first build so the browser can connect immediately
306
308
  let devServer = null;
307
309
  if (serveMode) {
308
310
  if (!outputFile) {
309
- console.error("gofront: --serve requires -o <output file>");
311
+ log.fail("--serve requires -o <output file>");
310
312
  process.exit(1);
311
313
  }
312
314
  const serveDir = dirname(resolve(outputFile));
@@ -324,17 +326,18 @@ function buildOnce(changedFile = null) {
324
326
  });
325
327
  const elapsedMs = (performance.now() - startMs).toFixed(0);
326
328
  const changeNote = changedFile ? ` — ${changedFile} changed` : "";
329
+ const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
327
330
  if (outputFile) {
328
331
  writeFileSync(outputFile, `${js}\n`);
329
332
  console.error(
330
- `[${timestamp()}] gofront: OK — wrote ${outputFile} (${elapsedMs}ms${changeNote})`,
333
+ `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${colors.cyan(outputFile)} ${timing}`,
331
334
  );
332
335
  } else {
333
336
  // Clear screen then print
334
337
  process.stdout.write("\x1Bc");
335
338
  console.log(js);
336
339
  console.error(
337
- `[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
340
+ `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
338
341
  );
339
342
  }
340
343
 
@@ -342,14 +345,17 @@ function buildOnce(changedFile = null) {
342
345
  try {
343
346
  copyAssets(resolve("."));
344
347
  } catch (e) {
345
- console.error(`gofront: asset copy failed: ${e.message}`);
348
+ log.warn(`asset copy failed: ${e.message}`);
346
349
  }
347
350
  }
348
351
 
349
352
  devServer?.notify();
350
353
  } catch (e) {
351
- console.error(`[${timestamp()}] gofront: ERROR`);
352
- for (const line of e.message.split("\n")) console.error(` ${line}`);
354
+ console.error(
355
+ `${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
356
+ );
357
+ for (const line of formatDiagnostic(e.message).split("\n"))
358
+ console.error(` ${line}`);
353
359
  devServer?.notifyError?.(e);
354
360
  }
355
361
  }
@@ -365,7 +371,7 @@ function handleCssWatch(filename) {
365
371
  try {
366
372
  copyAssets(resolve("."));
367
373
  } catch (e) {
368
- console.error(`gofront: asset copy failed: ${e.message}`);
374
+ log.warn(`asset copy failed: ${e.message}`);
369
375
  }
370
376
  }
371
377
  devServer?.notifyCss?.(filename);
@@ -385,4 +391,6 @@ watch(watchTarget, { recursive: true }, (_event, filename) => {
385
391
  debounce = setTimeout(() => buildOnce(filename), 80);
386
392
  });
387
393
 
388
- console.error(`[${timestamp()}] gofront: watching ${inputArg} ...`);
394
+ console.error(
395
+ `${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
396
+ );
@@ -368,9 +368,14 @@ export class TemplLexer {
368
368
  const exprTokens = this._readGoExprInBraces();
369
369
  attrs.push({ kind: "expr", name: attrName, tokens: exprTokens });
370
370
  } else if (this.peek() === '"') {
371
- // Static string attribute
371
+ // Static string attribute (or ref capture)
372
372
  const value = this._readQuotedString();
373
- attrs.push({ kind: "static", name: attrName, value });
373
+ if (attrName === "ref") {
374
+ // ref="fieldName" — element reference capture, not a DOM attribute
375
+ attrs.push({ kind: "ref", value });
376
+ } else {
377
+ attrs.push({ kind: "static", name: attrName, value });
378
+ }
374
379
  } else {
375
380
  // Unquoted attribute value
376
381
  let val = "";
@@ -6,6 +6,7 @@ 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 { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
9
10
  import { compilePackageTests, gwFilesIn } from "./compiler.js";
10
11
 
11
12
  export function isTestFunc(decl) {
@@ -38,6 +39,7 @@ export function generateTestHarness(bundleJs, testNames, options = {}) {
38
39
  const runRegex = options.run ? options.run : null;
39
40
  const verbose = Boolean(options.verbose);
40
41
  const dom = Boolean(options.dom);
42
+ const color = Boolean(options.color);
41
43
  const pkgName = options.pkgName ?? "main";
42
44
  // The harness runs from stdin with cwd = project dir, so a bare "jsdom"
43
45
  // import only works if the project has it locally. Use the resolved path.
@@ -86,6 +88,12 @@ async function __runGoFrontSuite() {
86
88
  const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
87
89
  const verbose = ${verbose};
88
90
  const pkgName = ${JSON.stringify(pkgName)};
91
+ const paint = ${color}
92
+ ? (code) => (s) => "\\x1b[" + code + "m" + s + "\\x1b[0m"
93
+ : () => (s) => s;
94
+ const red = paint(31), green = paint(32), yellow = paint(33), dim = paint(2), bold = paint(1);
95
+ const PASS = green("PASS"), FAIL = bold(red("FAIL")), SKIP = yellow("SKIP");
96
+ const okLine = (name, t) => console.log(green("ok ") + "\\t" + name + "\\t" + dim(t + "s"));
89
97
 
90
98
  const testsToRun = runFilter
91
99
  ? allTests.filter((name) => runFilter.test(name))
@@ -93,10 +101,10 @@ async function __runGoFrontSuite() {
93
101
 
94
102
  if (testsToRun.length === 0) {
95
103
  if (allTests.length > 0 && runFilter) {
96
- console.log("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)});
104
+ console.log(yellow("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)}));
97
105
  }
98
- console.log("PASS");
99
- console.log("ok \\t" + pkgName + "\\t0.000s");
106
+ console.log(PASS);
107
+ okLine(pkgName, "0.000");
100
108
  process.exit(0);
101
109
  }
102
110
 
@@ -108,17 +116,17 @@ async function __runGoFrontSuite() {
108
116
  // Go-style streaming report. In non-verbose mode only failures and skips are
109
117
  // shown, so the "=== RUN" header is emitted late for failures.
110
118
  const report = (t) => {
111
- const elapsed = ((now() - t._start) / 1000).toFixed(2);
119
+ const elapsed = dim("(" + ((now() - t._start) / 1000).toFixed(2) + "s)");
112
120
  if (t.skipped) {
113
- console.log("--- SKIP: " + t._name + " (" + elapsed + "s)");
121
+ console.log("--- " + SKIP + ": " + t._name + " " + elapsed);
114
122
  for (const log of t._logs) console.log(" " + log);
115
123
  } else if (t.failed) {
116
- if (!verbose) console.log("=== RUN " + t._name);
117
- for (const log of t._logs) console.log(" " + log);
118
- console.log("--- FAIL: " + t._name + " (" + elapsed + "s)");
124
+ if (!verbose) console.log(dim("=== RUN ") + t._name);
125
+ for (const log of t._logs) console.log(" " + red(log));
126
+ console.log("--- " + FAIL + ": " + t._name + " " + elapsed);
119
127
  } else if (verbose) {
120
128
  for (const log of t._logs) console.log(" " + log);
121
- console.log("--- PASS: " + t._name + " (" + elapsed + "s)");
129
+ console.log("--- " + PASS + ": " + t._name + " " + elapsed);
122
130
  }
123
131
  };
124
132
 
@@ -130,7 +138,7 @@ async function __runGoFrontSuite() {
130
138
  };
131
139
 
132
140
  globalThis.__onSubtestStart = (name) => {
133
- if (verbose) console.log("=== RUN " + name);
141
+ if (verbose) console.log(dim("=== RUN ") + name);
134
142
  };
135
143
  globalThis.__onSubtestEnd = report;
136
144
 
@@ -141,13 +149,13 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
141
149
  for (const name of testsToRun) {
142
150
  const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
143
151
  if (typeof fn !== "function") {
144
- console.error("testing: cannot find test function " + name);
152
+ console.error(red("testing: cannot find test function " + name));
145
153
  totalFailures++;
146
154
  continue;
147
155
  }
148
156
 
149
157
  const t = new __GoFront_T(name);
150
- if (verbose) console.log("=== RUN " + name);
158
+ if (verbose) console.log(dim("=== RUN ") + name);
151
159
 
152
160
  try {
153
161
  const res = fn(t);
@@ -164,12 +172,12 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
164
172
  const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
165
173
 
166
174
  if (totalFailures > 0) {
167
- console.log("FAIL");
168
- console.log("FAIL\\t" + pkgName + "\\t" + suiteElapsed + "s");
175
+ console.log(FAIL);
176
+ console.log(FAIL + "\\t" + pkgName + "\\t" + dim(suiteElapsed + "s"));
169
177
  process.exit(1);
170
178
  } else {
171
- console.log("PASS");
172
- console.log("ok \\t" + pkgName + "\\t" + suiteElapsed + "s");
179
+ console.log(PASS);
180
+ okLine(pkgName, suiteElapsed);
173
181
  process.exit(0);
174
182
  }
175
183
  }
@@ -215,10 +223,17 @@ function reportEmptyTests(label, reason, options) {
215
223
  return { exitCode: 0, stdout: out, stderr: "", noTests: true };
216
224
  }
217
225
  const dest = options.stdout ?? process.stdout;
218
- dest.write(out);
226
+ dest.write(colorsFor(options, dest).dim(out));
219
227
  return { exitCode: 0, noTests: true };
220
228
  }
221
229
 
230
+ // Color is decided here because the harness child writes to a pipe, not a TTY.
231
+ function colorsFor(options, stream) {
232
+ const enabled =
233
+ options.color ?? (!options.captureOutput && colorEnabled(stream));
234
+ return createColors(enabled);
235
+ }
236
+
222
237
  function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
223
238
  return new Promise((resolvePromise, rejectPromise) => {
224
239
  const child = spawn(process.execPath, ["--input-type=module", "-"], {
@@ -289,12 +304,16 @@ export async function runTests(targetDir, options = {}) {
289
304
  try {
290
305
  compiled = compilePackageTests(resolvedDir, options);
291
306
  } catch (err) {
292
- const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${err?.message ?? String(err)}\n`;
307
+ const msg = err?.message ?? String(err);
293
308
  if (options.captureOutput) {
309
+ const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
294
310
  return { exitCode: 1, stdout: "", stderr: out };
295
311
  }
296
312
  const dest = options.stderr ?? process.stderr;
297
- dest.write(out);
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
+ );
298
317
  return { exitCode: 1 };
299
318
  }
300
319
 
@@ -309,6 +328,7 @@ export async function runTests(targetDir, options = {}) {
309
328
  ...options,
310
329
  pkgName,
311
330
  jsdomPath,
331
+ color: colorsFor(options, options.stdout ?? process.stdout).enabled,
312
332
  });
313
333
 
314
334
  return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
@@ -227,7 +227,10 @@ export const assignabilityMethods = {
227
227
 
228
228
  _isUntypedAssignable(target, source) {
229
229
  if (isUntyped(target)) return true;
230
- if (target.kind !== "basic") return false;
230
+ // Named types with a basic underlying type (`type Mode string`) accept
231
+ // untyped constants of that kind, as in Go.
232
+ if (target.kind === "named") target = this.resolveType(target.underlying);
233
+ if (target?.kind !== "basic") return false;
231
234
  return (
232
235
  (UNTYPED_COMPAT[source.base]?.has(target.name) ?? false) ||
233
236
  (isComplex(target) &&
@@ -347,6 +350,14 @@ export const assignabilityMethods = {
347
350
  if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
348
351
  let base = srcType.kind === "named" ? srcType.underlying : srcType;
349
352
  base = this.resolveType(base);
353
+ // Interface → interface: the source's method set must cover the target's.
354
+ if (base?.kind === "interface") {
355
+ for (const [name, required] of iface.methods) {
356
+ if (!this._implementsMethod(required, base.methods.get(name)))
357
+ return false;
358
+ }
359
+ return true;
360
+ }
350
361
  const methodMap =
351
362
  base?.kind === "struct"
352
363
  ? base.methods
@@ -53,8 +53,18 @@ export function setupExtendedGlobals(globals, types) {
53
53
  DataAttr: { kind: "func", params: [STRING, STRING], returns: [GOM_NODE_T] },
54
54
  If: { kind: "func", params: [BOOL, GOM_NODE_T], returns: [GOM_NODE_T] },
55
55
  Map: { kind: "func", params: [ANY, ANY], returns: [GOM_NODE_T] },
56
- Mount: { kind: "func", params: [STRING, GOM_NODE_T], returns: [VOID] },
57
- MountTo: { kind: "func", params: [STRING, GOM_NODE_T], returns: [VOID] },
56
+ Mount: {
57
+ kind: "func",
58
+ params: [STRING, GOM_NODE_T, ANY],
59
+ variadic: true,
60
+ returns: [VOID],
61
+ },
62
+ MountTo: {
63
+ kind: "func",
64
+ params: [STRING, GOM_NODE_T, ANY],
65
+ variadic: true,
66
+ returns: [VOID],
67
+ },
58
68
  };
59
69
  for (const name of [
60
70
  "Div",