gofront 1.3.11 → 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.11",
3
+ "version": "1.3.12",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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/compiler.js CHANGED
@@ -20,6 +20,7 @@
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";
23
24
  import { log } from "./colors.js";
24
25
  import { parseDts } from "./dts-parser.js";
25
26
  import { Lexer } from "./lexer.js";
@@ -74,6 +75,87 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
74
75
  .map((f) => join(dir, f));
75
76
  }
76
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
+
77
159
  // ── Import resolution ─────────────────────────────────────────
78
160
  //
79
161
  // Shared by compileFiles (multi-file) and the single-file path in index.js.
@@ -87,6 +169,7 @@ export function resolveImports(
87
169
  bundledPackages,
88
170
  preambles,
89
171
  bundledDirs = new Set(),
172
+ options = {},
90
173
  ) {
91
174
  const fromDir = dirname(resolve(fromFile));
92
175
  const allImports = programs.flatMap((p) => p.imports);
@@ -137,8 +220,12 @@ export function resolveImports(
137
220
  const depKey = resolve(depDir);
138
221
  const alreadyBundled = bundledDirs.has(depKey);
139
222
  bundledDirs.add(depKey);
140
- const dep = compileDir(depDir, { bundledDirs });
141
- 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);
142
229
  if (alias === "_") continue;
143
230
  if (alias === ".") {
144
231
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
@@ -181,6 +268,10 @@ export function compileSingleFile(inputPath, options = {}) {
181
268
  ast._source = source;
182
269
  if (dumpAst) return { ast };
183
270
 
271
+ const outputDir =
272
+ options.outputDir ??
273
+ (outputFile ? dirname(resolve(outputFile)) : resolve("."));
274
+
184
275
  const checker = new TypeChecker();
185
276
  const jsImports = new Map();
186
277
  const bundledPackages = new Set();
@@ -193,6 +284,8 @@ export function compileSingleFile(inputPath, options = {}) {
193
284
  jsImports,
194
285
  bundledPackages,
195
286
  preambles,
287
+ options.bundledDirs ?? new Set(),
288
+ { ...options, outputDir },
196
289
  );
197
290
 
198
291
  const errors = checker.check(ast);
@@ -202,18 +295,22 @@ export function compileSingleFile(inputPath, options = {}) {
202
295
  }
203
296
 
204
297
  const cg = new CodeGen(checker, jsImports, bundledPackages);
205
- let js = cg.generate(ast);
206
-
207
- if (sourceMap) {
208
- const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
209
- const srcPath = relative(outputDir, inputPath);
210
- const map = cg.getSourceMap(srcPath, [source]);
211
- const b64 = Buffer.from(map).toString("base64");
212
- 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);
213
311
  }
214
312
 
215
- const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
216
- return { js: output };
313
+ return { js: merged.js };
217
314
  }
218
315
 
219
316
  export function compileDir(dir, options = {}) {
@@ -230,6 +327,9 @@ export function compilePackageTests(dir, options = {}) {
230
327
 
231
328
  export function compileFiles(files, options = {}) {
232
329
  const fromDir = options.fromDir ?? dirname(resolve(files[0]));
330
+ const outputDir =
331
+ options.outputDir ??
332
+ (options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
233
333
 
234
334
  // ── 1. Parse ─────────────────────────────────────────────────
235
335
  const parseErrors = [];
@@ -257,7 +357,7 @@ export function compileFiles(files, options = {}) {
257
357
  const checker = new TypeChecker();
258
358
  const jsImports = new Map(); // npm imports → exported names (for ESM emit)
259
359
  const bundledPackages = new Set(); // package names whose code is inlined
260
- const preambles = []; // JS code from compiled sub-packages
360
+ const preambles = []; // JS code / chunks from compiled sub-packages
261
361
 
262
362
  const dummyFromFile = join(fromDir, "_dummy.go");
263
363
  resolveImports(
@@ -268,6 +368,7 @@ export function compileFiles(files, options = {}) {
268
368
  bundledPackages,
269
369
  preambles,
270
370
  options.bundledDirs ?? new Set(),
371
+ { ...options, outputDir },
271
372
  );
272
373
 
273
374
  // ── 3. Type-check ─────────────────────────────────────────────
@@ -284,22 +385,27 @@ export function compileFiles(files, options = {}) {
284
385
  isTest: options.isTest ?? false,
285
386
  });
286
387
 
287
- 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]);
288
396
 
289
- if (options.sourceMap) {
290
- const outputDir = options.outputDir ?? fromDir;
291
- const sourceFiles = files.map((f) => relative(outputDir, f));
292
- const sourcesContent = files.map((f) => readFileSync(f, "utf8"));
293
- const map = codegen.getSourceMap(sourceFiles, sourcesContent);
294
- const b64 = Buffer.from(map).toString("base64");
295
- js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
397
+ if (options.sourceMap && !options.isDependency) {
398
+ attachSourceMap(merged, outputDir);
296
399
  }
297
400
 
298
401
  return {
299
402
  pkgName,
300
- js,
403
+ js: merged.js,
301
404
  programs,
302
405
  exportedSymbols: checker.getExportedSymbols(),
303
406
  exportedTypes: checker.getExportedTypes(),
407
+ sourceFiles: merged.sourceFiles,
408
+ sourcesContent: merged.sourcesContent,
409
+ mappings: merged.mappings,
304
410
  };
305
411
  }
@@ -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 = "";
@@ -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",