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 +83 -38
- package/package.json +1 -1
- package/src/codegen/index.js +58 -9
- package/src/codegen/runtime.js +11 -0
- package/src/codegen/source-map.js +5 -2
- package/src/codegen/statements.js +5 -1
- package/src/codegen/stdlib/gom.js +2 -2
- package/src/codegen/templ.js +35 -3
- package/src/compiler.js +134 -22
- package/src/resolver.js +3 -1
- package/src/templ-lexer.js +114 -5
- package/src/templ-parser.js +29 -0
- package/src/typechecker/index.js +67 -2
- package/src/typechecker/stdlib/extended.js +12 -2
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`
|
|
113
|
-
emitted when actually used. Optional
|
|
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
|
|
195
|
+
## Examples
|
|
195
196
|
|
|
196
|
-
There are
|
|
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=
|
|
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, `{
|
|
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
|
|
387
|
-
gofront
|
|
388
|
-
gofront
|
|
389
|
-
gofront
|
|
390
|
-
gofront
|
|
391
|
-
gofront
|
|
392
|
-
gofront
|
|
393
|
-
gofront
|
|
394
|
-
gofront <
|
|
395
|
-
gofront <
|
|
396
|
-
gofront <
|
|
397
|
-
gofront <
|
|
398
|
-
gofront <file.go>
|
|
399
|
-
gofront <
|
|
400
|
-
gofront
|
|
401
|
-
gofront
|
|
402
|
-
gofront --
|
|
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/
|
|
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
|
|
657
|
-
npm run test:
|
|
658
|
-
npm run test:
|
|
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** (~
|
|
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,
|
|
666
|
-
|
|
667
|
-
|
|
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** (~
|
|
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,
|
|
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
package/src/codegen/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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)
|
|
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);
|
package/src/codegen/runtime.js
CHANGED
|
@@ -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(
|
|
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({
|
|
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
|
},
|
package/src/codegen/templ.js
CHANGED
|
@@ -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 === "
|
|
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, {
|
|
141
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
package/src/templ-lexer.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 = "";
|
package/src/templ-parser.js
CHANGED
|
@@ -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() {
|
package/src/typechecker/index.js
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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: {
|
|
57
|
-
|
|
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",
|