gofront 1.3.11 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/
@@ -277,6 +283,12 @@ example/templ/
277
283
  `.templ` syntax at a glance:
278
284
 
279
285
  ```go
286
+ css cardStyle() {
287
+ background: #ffffff;
288
+ border-radius: 8px;
289
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
290
+ }
291
+
280
292
  templ TodoItem(t Todo) {
281
293
  <li class={ todoItemClass(t) } draggable="true" data-id={ t.id }>
282
294
  <input type="checkbox" checked?={ t.done }/>
@@ -288,7 +300,7 @@ templ TodoItem(t Todo) {
288
300
  }
289
301
 
290
302
  templ AppView() {
291
- <div class="card">
303
+ <div class={ cardStyle() }>
292
304
  @Header(highCount(), syncMsg, syncCls)
293
305
  @TodoList(visibleTodos())
294
306
  </div>
@@ -296,6 +308,7 @@ templ AppView() {
296
308
  ```
297
309
 
298
310
  Key syntax features:
311
+ - `css Name() { ... }` — scoped CSS declarations (templ.guide spec); compiles to a deterministic scoped class name (`gfc_<name>_<hash>`) and injects styles automatically into `<head>`
299
312
  - `{ expr }` — interpolate any Go expression (strings auto-cast, others use `String()`)
300
313
  - `attr={ expr }` — dynamic attribute value (expression)
301
314
  - `attr?={ expr }` — boolean attribute (present/absent based on truthiness)
@@ -363,11 +376,26 @@ parity with the other examples (priority mode, validation, localStorage persiste
363
376
  sync status, drag-and-drop reordering).
364
377
 
365
378
  The templ example additionally demonstrates: **`.templ` file compilation**, template
366
- components with parameters, `{ expr }` interpolation, `attr?={}` conditional boolean
367
- attributes, `@Component()` calls inside templates, `if / else if / else` chains,
379
+ components with parameters, scoped `css` declarations (`css Name() { ... }`), `{ expr }` interpolation,
380
+ `attr?={}` conditional boolean attributes, `@Component()` calls inside templates, `if / else if / else` chains,
368
381
  `switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
369
382
  all inside template bodies.
370
383
 
384
+ ### WebGL2 (3D showcase)
385
+
386
+ A 3D rotating colored cube rendered with WebGL2 shaders. Demonstrates GoFront's static
387
+ WebGL2 typings, `Float32Array` / `Uint16Array` slice mapping, zero-allocation `for range`
388
+ loops, and continuous `requestAnimationFrame` rendering — all with zero per-frame heap
389
+ allocations.
390
+
391
+ ```
392
+ example/webgl/
393
+ src/
394
+ main.go ← shaders, mat4 math, vertex/index buffers, render loop
395
+ app.js ← generated output
396
+ index.html ← canvas element
397
+ ```
398
+
371
399
  ### Build and run
372
400
 
373
401
  ```sh
@@ -375,6 +403,7 @@ npm run build:simple # → example/simple/app.js
375
403
  npm run build:reactive # → example/reactive/app.js
376
404
  npm run build:gom # → example/gom/app.js
377
405
  npm run build:templ # → example/templ/app.js
406
+ npm run build:webgl # → example/webgl/app.js
378
407
  # open the respective index.html in a browser
379
408
  ```
380
409
 
@@ -383,23 +412,33 @@ npm run build:templ # → example/templ/app.js
383
412
  ## CLI
384
413
 
385
414
  ```
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
415
+ gofront dev [dir] watch + compile + asset sync + live reload (default port 3000)
416
+ gofront dev [dir] --port 8080 use a custom port
417
+ gofront build [dir] clean + compile + minify + vendor → production output
418
+ gofront build [dir] --pwa also generate offline service worker (sw.js) + precache manifest
419
+ gofront build [dir] --source-map include inline source maps in the release bundle
420
+ gofront build [dir] --no-minify skip minification
421
+ gofront build [dir] --no-mangle minify but keep original identifiers
422
+ gofront prep [dir] [--minify] run asset copying + vendor bundling only (alias: gofront vendor)
423
+ gofront check <dir> type-check a single package
424
+ gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
425
+ gofront test <dir> [--dom] run tests for a single package
426
+ gofront test <dir>/... [--dom] [-v] [-run <regex>] run tests recursively
427
+ gofront <file.go> compile single file → stdout
428
+ gofront <dir> compile all *.go in directory → stdout
429
+ gofront <input> -o out.js write output to file (prints elapsed compile time)
430
+ gofront <input> -o out.js --copy-assets compile + copy static assets
431
+ gofront <input> --check type-check only (single file / directory)
432
+ gofront <input> --watch watch for changes and recompile
433
+ gofront <input> -o out.js --serve watch + serve with live reload (legacy; prefer gofront dev)
434
+ gofront <input> --source-map append inline source map
435
+ gofront <input> --minify minify output (built-in minifier)
436
+ gofront <input> --minify --mangle minify and rename local identifiers
437
+ gofront <file.go> --ast dump AST (debug)
438
+ gofront <file.go> --tokens dump tokens (debug)
439
+ gofront init [dir] scaffold a new project
440
+ gofront --version / -v print version
441
+ gofront --help / -h print this help
403
442
  ```
404
443
  ### Project configuration
405
444
 
@@ -574,6 +613,7 @@ signatures into GoFront's internal type representation.
574
613
  | `os` | `Exit`, `Args`, `Getenv` |
575
614
  | `io` | `Writer`, `Reader`, `ReadWriter`, `Closer` interface types; `ReadAll`, `EOF`, `Discard`, `WriteString` |
576
615
  | `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`, …) |
616
+ | `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
617
 
578
618
  ### Packages & imports
579
619
 
@@ -587,7 +627,7 @@ signatures into GoFront's internal type representation.
587
627
  | Unused import detection | ✓ |
588
628
  | External `.d.ts` types | ✓ |
589
629
  | npm package type resolution | ✓ |
590
- | `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components visible across the whole package |
630
+ | `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components and `css` declarations visible across the whole package |
591
631
 
592
632
  ---
593
633
 
@@ -604,7 +644,7 @@ These features are intentional additions for the JavaScript platform:
604
644
  | Feature | Purpose |
605
645
  |---|---|
606
646
  | `async func` / `await` | First-class async syntax for frontend work. |
607
- | Browser globals (`document`, `console`, etc.) | Predeclared as `any` for practical DOM access. |
647
+ | 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
648
  | `.d.ts` type imports (`import "js:./types.d.ts"`) | Type-safe interop with JavaScript libraries. |
609
649
  | npm package resolution | Import types from `node_modules/` and `@types/` automatically. |
610
650
 
@@ -646,28 +686,33 @@ you know exactly what to expect.
646
686
 
647
687
  See [`docs/roadmap.md`](docs/roadmap.md) for the full roadmap and release history.
648
688
  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/)).
689
+ (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
690
 
651
691
  ---
652
692
 
653
693
  ## Tests
654
694
 
655
695
  ```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
696
+ npm run test:unit # unit tests only (~1400 tests, no browser required)
697
+ npm run test:perf # zero-allocation benchmark (100k ray-triangle intersections, 0 bytes/frame)
698
+ npm run test:examples # GoFront-native unit tests across all example apps
699
+ npm run test:examples:dom # same, with JSDOM for DOM/gom/templ component testing
700
+ npm run test:e2e # E2E tests (Playwright, headless Chromium)
701
+ npm run test:all # all of the above
659
702
  ```
660
703
 
661
- **Unit tests** (~1118) cover language features, type errors, edge cases, DOM (jsdom),
704
+ **Unit tests** (~1,400+) cover language features, type errors, edge cases, DOM (jsdom),
662
705
  external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
663
706
  formatting, map iteration order, integer overflow semantics, unused variable detection,
664
707
  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).
708
+ the `testing` framework itself, and `.templ` file compilation (element rendering,
709
+ interpolation, boolean attrs, component calls, scoped `css` declarations with class hashing,
710
+ `if/else/else-if` chains, `for range`, `switch/case/default`, `@templ.Raw()` raw HTML injection,
711
+ SVG namespace handling, mixed `.go`+`.templ` packages).
668
712
 
669
- **E2E tests** (~104, Playwright) run all four example apps in a real browser and verify
713
+ **E2E tests** (~105, Playwright) run all five example apps in a real browser and verify
670
714
  CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
671
715
  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).
716
+ placeholder, `gom.If` conditional rendering, templ-specific features (scoped `css` injection,
717
+ `if/else` priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
718
+ rendering.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.11",
3
+ "version": "1.4.0",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,7 @@ import {
27
27
  HELPER_ERROR,
28
28
  HELPER_ERROR_IS,
29
29
  HELPER_IFACE_BOX,
30
+ HELPER_INJECT_STYLES,
30
31
  HELPER_LEN,
31
32
  HELPER_PATH_CLEAN,
32
33
  HELPER_S,
@@ -97,6 +98,7 @@ export class CodeGen {
97
98
  this.structNames = new Set();
98
99
  this.structFields = new Map();
99
100
  this.namedWrapperNames = new Set();
101
+ this.collectedCss = [];
100
102
  this.jsImports = jsImports;
101
103
  this.bundledPackages = bundledPackages;
102
104
  this.namedReturnVars = null; // names of current function's named return vars
@@ -191,6 +193,10 @@ export class CodeGen {
191
193
  }
192
194
 
193
195
  _callInitAndMain(initNames, program, isTest = false) {
196
+ if (this.collectedCss.length > 0) {
197
+ const uniqueCss = [...new Set(this.collectedCss)];
198
+ this.line(`__injectStyles(${JSON.stringify(uniqueCss.join("\n\n"))});`);
199
+ }
194
200
  for (const name of initNames) this.line(`${name}();`);
195
201
  if (
196
202
  !isTest &&
@@ -201,6 +207,14 @@ export class CodeGen {
201
207
 
202
208
  generate(program, options = {}) {
203
209
  const isTest = options.isTest ?? false;
210
+ // Ensure _pkgName is set on all decls (generateAll sets it for
211
+ // multi-file; for single-file compiles we derive it from the AST).
212
+ const pkgName = program.pkg?.name;
213
+ if (pkgName) {
214
+ for (const d of program.decls) {
215
+ if (!d._pkgName) d._pkgName = pkgName;
216
+ }
217
+ }
204
218
  this._renameReservedIdents(program.decls);
205
219
  const methods = this._collectDecls(program);
206
220
  this._emitJsImports();
@@ -208,12 +222,26 @@ export class CodeGen {
208
222
  this._emitVarConstDecls(program);
209
223
  const initNames = this._emitFuncDecls(program);
210
224
  this._callInitAndMain(initNames, program, isTest);
211
- this._prependHelpers(isTest);
212
- while (this.out[0] === "") this.out.shift();
225
+ const prependedLines = this._prependHelpers(isTest);
226
+ return this._finalizeOutput(prependedLines);
227
+ }
228
+
229
+ _finalizeOutput(prependedLines) {
230
+ let shiftedLines = 0;
231
+ while (this.out[0] === "") {
232
+ this.out.shift();
233
+ shiftedLines++;
234
+ }
235
+ const netShift = prependedLines - shiftedLines;
236
+ if (netShift !== 0) {
237
+ for (const m of this._srcMappings) {
238
+ m.genLine = Math.max(0, m.genLine + netShift);
239
+ }
240
+ }
213
241
  return this.out.join("\n");
214
242
  }
215
243
 
216
- // Emits FuncDecl and TemplDecl nodes; returns renamed init function names.
244
+ // Emits FuncDecl, TemplDecl, and CssDecl nodes; returns renamed init function names.
217
245
  _emitFuncDecls(program) {
218
246
  let initCount = 0;
219
247
  const initNames = [];
@@ -233,6 +261,10 @@ export class CodeGen {
233
261
  this._currentSrcFileIdx = d._srcFileIdx ?? 0;
234
262
  this.genTemplDecl(d);
235
263
  this.blank();
264
+ } else if (d.kind === "CssDecl") {
265
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
266
+ this.genCssDecl(d);
267
+ this.blank();
236
268
  }
237
269
  }
238
270
  return initNames;
@@ -292,6 +324,7 @@ export class CodeGen {
292
324
  MethodDecl: "_renameMethodDeclNode",
293
325
  FuncLit: "_renameSignature",
294
326
  TemplDecl: "_renameSignature",
327
+ CssDecl: "_renameSignature",
295
328
  VarDecl: "_renameDeclSpecs",
296
329
  ConstDecl: "_renameDeclSpecs",
297
330
  TypeSwitchStmt: "_renameTypeSwitchNode",
@@ -367,6 +400,7 @@ export class CodeGen {
367
400
  _prependHelpers(isTest = false) {
368
401
  const needsTesting = isTest || this._usesTesting;
369
402
  const HELPER_MAP = [
403
+ [this.collectedCss.length > 0, HELPER_INJECT_STYLES],
370
404
  [this._usesLen, HELPER_LEN],
371
405
  [this._usesAppend, HELPER_APPEND],
372
406
  [this._usesSliceGuard, HELPER_S],
@@ -385,7 +419,13 @@ export class CodeGen {
385
419
  [needsTesting, HELPER_TESTING],
386
420
  ];
387
421
  const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
388
- if (helpers.length > 0) this.out.unshift(...helpers, "");
422
+ if (helpers.length > 0) {
423
+ const helperLines = helpers.flatMap((h) => h.split("\n"));
424
+ helperLines.push("");
425
+ this.out.unshift(...helperLines);
426
+ return helperLines.length;
427
+ }
428
+ return 0;
389
429
  }
390
430
 
391
431
  // Generate a single bundle from multiple programs (same-package multi-file).
@@ -394,12 +434,21 @@ export class CodeGen {
394
434
  for (let i = 0; i < programs.length; i++) {
395
435
  for (const decl of programs[i].decls) {
396
436
  decl._srcFileIdx = i;
437
+ decl._pkgName = programs[i].pkg?.name;
397
438
  }
398
439
  }
399
440
  const merged = { decls: programs.flatMap((p) => p.decls) };
400
441
  return this.generate(merged, options);
401
442
  }
402
443
 
444
+ getCss() {
445
+ return [...new Set(this.collectedCss)].join("\n\n");
446
+ }
447
+
448
+ getMappings() {
449
+ return this._srcMappings;
450
+ }
451
+
403
452
  // Returns a source map JSON string for the last generate() call.
404
453
  // sources: string[] of source filenames (relative to the output file).
405
454
  // sourcesContent: string[] of original file contents (embedded for DevTools breakpoints).
@@ -581,9 +630,11 @@ export class CodeGen {
581
630
  }
582
631
 
583
632
  genMethod(decl, recvField = null) {
633
+ const prevFileIdx = this._currentSrcFileIdx;
634
+ if (decl._srcFileIdx != null) this._currentSrcFileIdx = decl._srcFileIdx;
584
635
  const params = decl.params.map((p) => p.name).join(", ");
585
636
  const asyncPrefix = decl.async ? "async " : "";
586
- this.line(`${asyncPrefix}${decl.name}(${params}) {`);
637
+ this.line(`${asyncPrefix}${decl.name}(${params}) {`, decl._line ?? null);
587
638
  const prevBoxed = this._boxedVars;
588
639
  this._boxedVars = new Set();
589
640
  this._scanAddressTaken(decl.body);
@@ -605,6 +656,7 @@ export class CodeGen {
605
656
  this._unwrappedRecv = prevUnwrapped;
606
657
  this._boxedVars = prevBoxed;
607
658
  this.line("}");
659
+ this._currentSrcFileIdx = prevFileIdx;
608
660
  }
609
661
 
610
662
  // Value receivers are copies in Go; only materialize the copy when the body mutates it.
@@ -648,10 +700,7 @@ export class CodeGen {
648
700
  .join(", ");
649
701
  const asyncPrefix = decl.async ? "async " : "";
650
702
  const srcLine = decl._line ?? null;
651
- this.line(
652
- `${asyncPrefix}function ${name}(${params}) {`,
653
- srcLine ? srcLine - 1 : null,
654
- );
703
+ this.line(`${asyncPrefix}function ${name}(${params}) {`, srcLine);
655
704
  const prevBoxed = this._boxedVars;
656
705
  this._boxedVars = new Set();
657
706
  this._scanAddressTaken(decl.body);
@@ -312,3 +312,14 @@ var __GoFront_T = __GoFront_T || class {
312
312
  }
313
313
  };
314
314
  var __testing_T = __GoFront_T;`;
315
+
316
+ export const HELPER_INJECT_STYLES = `var __injectStyles = __injectStyles || function(css) {
317
+ if (typeof document === "undefined" || !document || !document.head || !css) return;
318
+ let s = document.getElementById("gofront-styles");
319
+ if (!s) {
320
+ s = document.createElement("style");
321
+ s.id = "gofront-styles";
322
+ document.head.appendChild(s);
323
+ }
324
+ s.textContent += "\\n" + css;
325
+ };`;
@@ -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
  });
@@ -26,6 +26,35 @@ export const templGenMethods = {
26
26
  this.line("}");
27
27
  },
28
28
 
29
+ genCssDecl(decl) {
30
+ if (decl.params && decl.params.length > 0) {
31
+ throw new Error(
32
+ `css ${decl.name}: parameterized css declarations are not yet supported`,
33
+ );
34
+ }
35
+ const className = this.getCssClassName(decl);
36
+ const scopedCss = `.${className} {\n${decl.cssText.trim()}\n}`;
37
+ this.collectedCss.push(scopedCss);
38
+
39
+ this.line(`function ${decl.name}() {`);
40
+ this.indented(() => {
41
+ this.line(`return ${JSON.stringify(className)};`);
42
+ });
43
+ this.line("}");
44
+ },
45
+
46
+ getCssClassName(decl) {
47
+ if (decl._className) return decl._className;
48
+ let h = 2166136261 >>> 0;
49
+ const str = `${decl._pkgName || ""}_${decl.name}_${decl.cssText}`;
50
+ for (let i = 0; i < str.length; i++) {
51
+ h = Math.imul(h ^ str.charCodeAt(i), 16777619) >>> 0;
52
+ }
53
+ const hash = (h >>> 0).toString(36);
54
+ decl._className = `gfc_${decl.name}_${hash}`;
55
+ return decl._className;
56
+ },
57
+
29
58
  // Emit mount statements for an array of TemplNodes into parent variable `p`.
30
59
  _genTemplNodes(nodes, p) {
31
60
  if (!nodes || nodes.length === 0) return;
@@ -85,7 +114,7 @@ export const templGenMethods = {
85
114
  return this.line(`${p}.insertAdjacentHTML("beforeend", ${argJs});`);
86
115
  }
87
116
  return this.line(
88
- `(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p});`,
117
+ `(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p}, ___refs);`,
89
118
  );
90
119
  },
91
120
 
@@ -119,7 +148,10 @@ export const templGenMethods = {
119
148
  isSvg
120
149
  ? `${el}.setAttribute("class", ${js});`
121
150
  : `${el}.className = ${js};`;
122
- if (attr.kind === "static") {
151
+ if (attr.kind === "ref") {
152
+ // ref="key" — capture element into the refs map at mount time
153
+ this.line(`if(___refs)___refs[${JSON.stringify(attr.value)}]=${el};`);
154
+ } else if (attr.kind === "static") {
123
155
  if (name === "class") {
124
156
  this.line(setClass(JSON.stringify(attr.value)));
125
157
  } 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, css: cg.getCss() };
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,33 @@ 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
 
401
+ const mainCss = codegen.getCss();
402
+ const allCss = [...preambles.map((p) => p.css).filter(Boolean), mainCss]
403
+ .filter(Boolean)
404
+ .join("\n\n");
405
+
298
406
  return {
299
407
  pkgName,
300
- js,
408
+ js: merged.js,
409
+ css: allCss,
301
410
  programs,
302
411
  exportedSymbols: checker.getExportedSymbols(),
303
412
  exportedTypes: checker.getExportedTypes(),
413
+ sourceFiles: merged.sourceFiles,
414
+ sourcesContent: merged.sourcesContent,
415
+ mappings: merged.mappings,
304
416
  };
305
417
  }
package/src/resolver.js CHANGED
@@ -65,7 +65,9 @@ export function resolveGwDir(importPath, fromFile) {
65
65
  const fromDir = dirname(resolve(fromFile));
66
66
  const dir = resolve(fromDir, importPath);
67
67
  if (!existsSync(dir)) return null;
68
- const gwFiles = readdirSync(dir).filter((f) => f.endsWith(".go"));
68
+ const gwFiles = readdirSync(dir).filter(
69
+ (f) => f.endsWith(".go") || f.endsWith(".templ"),
70
+ );
69
71
  return gwFiles.length > 0 ? dir : null;
70
72
  }
71
73
 
@@ -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) {
@@ -368,9 +472,14 @@ export class TemplLexer {
368
472
  const exprTokens = this._readGoExprInBraces();
369
473
  attrs.push({ kind: "expr", name: attrName, tokens: exprTokens });
370
474
  } else if (this.peek() === '"') {
371
- // Static string attribute
475
+ // Static string attribute (or ref capture)
372
476
  const value = this._readQuotedString();
373
- attrs.push({ kind: "static", name: attrName, value });
477
+ if (attrName === "ref") {
478
+ // ref="fieldName" — element reference capture, not a DOM attribute
479
+ attrs.push({ kind: "ref", value });
480
+ } else {
481
+ attrs.push({ kind: "static", name: attrName, value });
482
+ }
374
483
  } else {
375
484
  // Unquoted attribute value
376
485
  let val = "";
@@ -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() {
@@ -241,6 +241,7 @@ export class TypeChecker {
241
241
  if (d.kind === "FuncDecl" || d.kind === "MethodDecl")
242
242
  this.collectFunc(d);
243
243
  else if (d.kind === "TemplDecl") this._collectTemplDecl(d);
244
+ else if (d.kind === "CssDecl") this._collectCssDecl(d);
244
245
  }
245
246
  }
246
247
  }
@@ -267,7 +268,11 @@ export class TypeChecker {
267
268
  for (const p of programs) {
268
269
  this._setCurrentFile(p);
269
270
  for (const d of p.decls) {
270
- if (d.kind === "FuncDecl" || d.kind === "MethodDecl") {
271
+ if (
272
+ d.kind === "FuncDecl" ||
273
+ d.kind === "MethodDecl" ||
274
+ d.kind === "TemplDecl"
275
+ ) {
271
276
  this.checkTopDecl(d, this.globals);
272
277
  }
273
278
  }
@@ -457,11 +462,71 @@ export class TypeChecker {
457
462
  this.checkConstDecl(decl, scope);
458
463
  break;
459
464
  case "TypeDecl":
465
+ case "CssDecl":
466
+ break;
460
467
  case "TemplDecl":
461
- break; // already collected / no body to check
468
+ this._markTemplNodesUsed(decl.body);
469
+ break;
470
+ }
471
+ }
472
+
473
+ _markTokensUsed(tokens) {
474
+ if (!tokens) return;
475
+ for (const t of tokens) {
476
+ if (t.type === "IDENT" || t.type?.name === "IDENT") {
477
+ this.globals.lookup(t.value);
478
+ }
479
+ }
480
+ }
481
+
482
+ _markTemplNodesUsed(nodes) {
483
+ if (!nodes) return;
484
+ for (const n of nodes) {
485
+ if (n.kind === "TemplElement") {
486
+ for (const a of n.attrs || []) {
487
+ if (a.tokens) this._markTokensUsed(a.tokens);
488
+ }
489
+ this._markTemplNodesUsed(n.children);
490
+ } else if (n.kind === "TemplExpr" || n.kind === "TemplComponent") {
491
+ this._markTokensUsed(n.tokens);
492
+ } else if (n.kind === "TemplIf") {
493
+ this._markTokensUsed(n.condTokens);
494
+ this._markTemplNodesUsed(n.then);
495
+ this._markTemplNodesUsed(n.else_);
496
+ } else if (n.kind === "TemplFor") {
497
+ this._markTokensUsed(n.stmtTokens);
498
+ this._markTemplNodesUsed(n.body);
499
+ } else if (n.kind === "TemplSwitch") {
500
+ this._markTokensUsed(n.exprTokens);
501
+ for (const c of n.cases || []) {
502
+ this._markTokensUsed(c.caseTokens);
503
+ this._markTemplNodesUsed(c.body);
504
+ }
505
+ }
462
506
  }
463
507
  }
464
508
 
509
+ _collectCssDecl(decl) {
510
+ const paramTypes = (decl.params || []).map((p) =>
511
+ this.resolveTypeNode(p.type, this.globals),
512
+ );
513
+ const stringType = this.types.get("string") ?? {
514
+ kind: "basic",
515
+ name: "string",
516
+ };
517
+ const funcType = {
518
+ kind: "func",
519
+ params: paramTypes,
520
+ returns: [stringType],
521
+ variadic: false,
522
+ async: false,
523
+ };
524
+ if (this.globals.symbols.has(decl.name) && decl.name !== "init") {
525
+ this.err(`${decl.name} redeclared in this block`, decl);
526
+ }
527
+ this.globals.define(decl.name, funcType);
528
+ }
529
+
465
530
  _collectTemplDecl(decl) {
466
531
  const paramTypes = decl.params.map((p) =>
467
532
  this.resolveTypeNode(p.type, this.globals),
@@ -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",