gofront 1.3.11 → 1.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -34
- package/package.json +1 -1
- package/src/codegen/index.js +32 -8
- 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 +6 -3
- package/src/compiler.js +128 -22
- package/src/templ-lexer.js +7 -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/
|
|
@@ -368,6 +374,21 @@ attributes, `@Component()` calls inside templates, `if / else if / else` chains,
|
|
|
368
374
|
`switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
|
|
369
375
|
all inside template bodies.
|
|
370
376
|
|
|
377
|
+
### WebGL2 (3D showcase)
|
|
378
|
+
|
|
379
|
+
A 3D rotating colored cube rendered with WebGL2 shaders. Demonstrates GoFront's static
|
|
380
|
+
WebGL2 typings, `Float32Array` / `Uint16Array` slice mapping, zero-allocation `for range`
|
|
381
|
+
loops, and continuous `requestAnimationFrame` rendering — all with zero per-frame heap
|
|
382
|
+
allocations.
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
example/webgl/
|
|
386
|
+
src/
|
|
387
|
+
main.go ← shaders, mat4 math, vertex/index buffers, render loop
|
|
388
|
+
app.js ← generated output
|
|
389
|
+
index.html ← canvas element
|
|
390
|
+
```
|
|
391
|
+
|
|
371
392
|
### Build and run
|
|
372
393
|
|
|
373
394
|
```sh
|
|
@@ -375,6 +396,7 @@ npm run build:simple # → example/simple/app.js
|
|
|
375
396
|
npm run build:reactive # → example/reactive/app.js
|
|
376
397
|
npm run build:gom # → example/gom/app.js
|
|
377
398
|
npm run build:templ # → example/templ/app.js
|
|
399
|
+
npm run build:webgl # → example/webgl/app.js
|
|
378
400
|
# open the respective index.html in a browser
|
|
379
401
|
```
|
|
380
402
|
|
|
@@ -383,23 +405,33 @@ npm run build:templ # → example/templ/app.js
|
|
|
383
405
|
## CLI
|
|
384
406
|
|
|
385
407
|
```
|
|
386
|
-
gofront
|
|
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 --
|
|
408
|
+
gofront dev [dir] watch + compile + asset sync + live reload (default port 3000)
|
|
409
|
+
gofront dev [dir] --port 8080 use a custom port
|
|
410
|
+
gofront build [dir] clean + compile + minify + vendor → production output
|
|
411
|
+
gofront build [dir] --pwa also generate offline service worker (sw.js) + precache manifest
|
|
412
|
+
gofront build [dir] --source-map include inline source maps in the release bundle
|
|
413
|
+
gofront build [dir] --no-minify skip minification
|
|
414
|
+
gofront build [dir] --no-mangle minify but keep original identifiers
|
|
415
|
+
gofront prep [dir] [--minify] run asset copying + vendor bundling only (alias: gofront vendor)
|
|
416
|
+
gofront check <dir> type-check a single package
|
|
417
|
+
gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
|
|
418
|
+
gofront test <dir> [--dom] run tests for a single package
|
|
419
|
+
gofront test <dir>/... [--dom] [-v] [-run <regex>] run tests recursively
|
|
420
|
+
gofront <file.go> compile single file → stdout
|
|
421
|
+
gofront <dir> compile all *.go in directory → stdout
|
|
422
|
+
gofront <input> -o out.js write output to file (prints elapsed compile time)
|
|
423
|
+
gofront <input> -o out.js --copy-assets compile + copy static assets
|
|
424
|
+
gofront <input> --check type-check only (single file / directory)
|
|
425
|
+
gofront <input> --watch watch for changes and recompile
|
|
426
|
+
gofront <input> -o out.js --serve watch + serve with live reload (legacy; prefer gofront dev)
|
|
427
|
+
gofront <input> --source-map append inline source map
|
|
428
|
+
gofront <input> --minify minify output (built-in minifier)
|
|
429
|
+
gofront <input> --minify --mangle minify and rename local identifiers
|
|
430
|
+
gofront <file.go> --ast dump AST (debug)
|
|
431
|
+
gofront <file.go> --tokens dump tokens (debug)
|
|
432
|
+
gofront init [dir] scaffold a new project
|
|
433
|
+
gofront --version / -v print version
|
|
434
|
+
gofront --help / -h print this help
|
|
403
435
|
```
|
|
404
436
|
### Project configuration
|
|
405
437
|
|
|
@@ -574,6 +606,7 @@ signatures into GoFront's internal type representation.
|
|
|
574
606
|
| `os` | `Exit`, `Args`, `Getenv` |
|
|
575
607
|
| `io` | `Writer`, `Reader`, `ReadWriter`, `Closer` interface types; `ReadAll`, `EOF`, `Discard`, `WriteString` |
|
|
576
608
|
| `gom` | Browser-native declarative DOM component library. **Types**: `Node` (interface), `NodeFunc`, `Group`. **Core**: `El(tag, children...)`, `Text(s)`, `Mount(sel, node)`, `MountTo(sel, node)`. **Attributes**: `Attr`, `Class`, `Href`, `Type`, `Src`, `Placeholder`, `DataAttr`, `Style`, `For`, `Name`, `Value`, `Target`, `Rel`, `Alt`, `Title`, `Draggable`, `Role`, `AriaLabel`, `StyleAttr`; boolean: `Disabled`, `Checked`, `Selected`, `Readonly`. **Logic**: `If(cond, node)`, `Map(slice, fn)`. **Elements**: full HTML element set (`Div`, `Span`, `Button`, `Input`, `Ul`, `Li`, `Table`, `Form`, `Img`, `A`, `H1`–`H6`, …) |
|
|
609
|
+
| `testing` | **`testing.T`**: `Error`, `Errorf`, `Fatal`, `Fatalf`, `Fail`, `Failed`, `FailNow`, `Log`, `Logf`, `Skip`, `Skipf`, `Skipped`, `Helper`, `Run`, `Name`. **Package functions**: `Short()`, `Verbose()`. Tests are `func TestXxx(t *testing.T)` in `*_test.go` files, excluded from normal compilation and run via `gofront test`. |
|
|
577
610
|
|
|
578
611
|
### Packages & imports
|
|
579
612
|
|
|
@@ -604,7 +637,7 @@ These features are intentional additions for the JavaScript platform:
|
|
|
604
637
|
| Feature | Purpose |
|
|
605
638
|
|---|---|
|
|
606
639
|
| `async func` / `await` | First-class async syntax for frontend work. |
|
|
607
|
-
| Browser globals (`document`, `console`, etc.) | Predeclared as `any` for practical DOM access. |
|
|
640
|
+
| Browser globals (`document`, `console`, etc.) | Predeclared as `any` for practical DOM access. `WebGL2RenderingContext`, `WebGLRenderingContext`, `GPUDevice`, `GPUAdapter`, `GPUQueue`, `ArrayBuffer`, `DataView`, and TypedArrays have full static typings with method-level checking. |
|
|
608
641
|
| `.d.ts` type imports (`import "js:./types.d.ts"`) | Type-safe interop with JavaScript libraries. |
|
|
609
642
|
| npm package resolution | Import types from `node_modules/` and `@types/` automatically. |
|
|
610
643
|
|
|
@@ -646,28 +679,33 @@ you know exactly what to expect.
|
|
|
646
679
|
|
|
647
680
|
See [`docs/roadmap.md`](docs/roadmap.md) for the full roadmap and release history.
|
|
648
681
|
Design documents for planned features are organised by release under `docs/v*/`
|
|
649
|
-
(e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/
|
|
682
|
+
(e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v1.2.0/`](docs/v1.2.0/), [`docs/v1.3.0/`](docs/v1.3.0/)).
|
|
650
683
|
|
|
651
684
|
---
|
|
652
685
|
|
|
653
686
|
## Tests
|
|
654
687
|
|
|
655
688
|
```sh
|
|
656
|
-
npm run test:unit
|
|
657
|
-
npm run test:
|
|
658
|
-
npm run test:
|
|
689
|
+
npm run test:unit # unit tests only (~1400 tests, no browser required)
|
|
690
|
+
npm run test:perf # zero-allocation benchmark (100k ray-triangle intersections, 0 bytes/frame)
|
|
691
|
+
npm run test:examples # GoFront-native unit tests across all example apps
|
|
692
|
+
npm run test:examples:dom # same, with JSDOM for DOM/gom/templ component testing
|
|
693
|
+
npm run test:e2e # E2E tests (Playwright, headless Chromium)
|
|
694
|
+
npm run test:all # all of the above
|
|
659
695
|
```
|
|
660
696
|
|
|
661
|
-
**Unit tests** (~
|
|
697
|
+
**Unit tests** (~1,396) cover language features, type errors, edge cases, DOM (jsdom),
|
|
662
698
|
external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
|
|
663
699
|
formatting, map iteration order, integer overflow semantics, unused variable detection,
|
|
664
700
|
unused import detection, semantic difference verification, stdlib shim packages, generics,
|
|
665
|
-
and `.templ` file compilation (element rendering,
|
|
666
|
-
calls, `if/else/else-if` chains, `for range`,
|
|
667
|
-
HTML injection,
|
|
701
|
+
the `testing` framework itself, and `.templ` file compilation (element rendering,
|
|
702
|
+
interpolation, boolean attrs, component calls, `if/else/else-if` chains, `for range`,
|
|
703
|
+
`switch/case/default`, `@templ.Raw()` raw HTML injection, SVG namespace handling, mixed
|
|
704
|
+
`.go`+`.templ` packages).
|
|
668
705
|
|
|
669
|
-
**E2E tests** (~104, Playwright) run all
|
|
706
|
+
**E2E tests** (~104, Playwright) run all five example apps in a real browser and verify
|
|
670
707
|
CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
|
|
671
708
|
status. Per-app suites check app-specific behaviour: scoped styles, stats bar, loading
|
|
672
|
-
placeholder, `gom.If` conditional rendering,
|
|
673
|
-
priority hint, `for` loop rendering, conditional bool attributes)
|
|
709
|
+
placeholder, `gom.If` conditional rendering, templ-specific features (`if/else`
|
|
710
|
+
priority hint, `for` loop rendering, conditional bool attributes), and the WebGL2 cube
|
|
711
|
+
rendering.
|
package/package.json
CHANGED
package/src/codegen/index.js
CHANGED
|
@@ -208,8 +208,22 @@ export class CodeGen {
|
|
|
208
208
|
this._emitVarConstDecls(program);
|
|
209
209
|
const initNames = this._emitFuncDecls(program);
|
|
210
210
|
this._callInitAndMain(initNames, program, isTest);
|
|
211
|
-
this._prependHelpers(isTest);
|
|
212
|
-
|
|
211
|
+
const prependedLines = this._prependHelpers(isTest);
|
|
212
|
+
return this._finalizeOutput(prependedLines);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_finalizeOutput(prependedLines) {
|
|
216
|
+
let shiftedLines = 0;
|
|
217
|
+
while (this.out[0] === "") {
|
|
218
|
+
this.out.shift();
|
|
219
|
+
shiftedLines++;
|
|
220
|
+
}
|
|
221
|
+
const netShift = prependedLines - shiftedLines;
|
|
222
|
+
if (netShift !== 0) {
|
|
223
|
+
for (const m of this._srcMappings) {
|
|
224
|
+
m.genLine = Math.max(0, m.genLine + netShift);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
213
227
|
return this.out.join("\n");
|
|
214
228
|
}
|
|
215
229
|
|
|
@@ -385,7 +399,13 @@ export class CodeGen {
|
|
|
385
399
|
[needsTesting, HELPER_TESTING],
|
|
386
400
|
];
|
|
387
401
|
const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
|
|
388
|
-
if (helpers.length > 0)
|
|
402
|
+
if (helpers.length > 0) {
|
|
403
|
+
const helperLines = helpers.flatMap((h) => h.split("\n"));
|
|
404
|
+
helperLines.push("");
|
|
405
|
+
this.out.unshift(...helperLines);
|
|
406
|
+
return helperLines.length;
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
389
409
|
}
|
|
390
410
|
|
|
391
411
|
// Generate a single bundle from multiple programs (same-package multi-file).
|
|
@@ -400,6 +420,10 @@ export class CodeGen {
|
|
|
400
420
|
return this.generate(merged, options);
|
|
401
421
|
}
|
|
402
422
|
|
|
423
|
+
getMappings() {
|
|
424
|
+
return this._srcMappings;
|
|
425
|
+
}
|
|
426
|
+
|
|
403
427
|
// Returns a source map JSON string for the last generate() call.
|
|
404
428
|
// sources: string[] of source filenames (relative to the output file).
|
|
405
429
|
// sourcesContent: string[] of original file contents (embedded for DevTools breakpoints).
|
|
@@ -581,9 +605,11 @@ export class CodeGen {
|
|
|
581
605
|
}
|
|
582
606
|
|
|
583
607
|
genMethod(decl, recvField = null) {
|
|
608
|
+
const prevFileIdx = this._currentSrcFileIdx;
|
|
609
|
+
if (decl._srcFileIdx != null) this._currentSrcFileIdx = decl._srcFileIdx;
|
|
584
610
|
const params = decl.params.map((p) => p.name).join(", ");
|
|
585
611
|
const asyncPrefix = decl.async ? "async " : "";
|
|
586
|
-
this.line(`${asyncPrefix}${decl.name}(${params}) {
|
|
612
|
+
this.line(`${asyncPrefix}${decl.name}(${params}) {`, decl._line ?? null);
|
|
587
613
|
const prevBoxed = this._boxedVars;
|
|
588
614
|
this._boxedVars = new Set();
|
|
589
615
|
this._scanAddressTaken(decl.body);
|
|
@@ -605,6 +631,7 @@ export class CodeGen {
|
|
|
605
631
|
this._unwrappedRecv = prevUnwrapped;
|
|
606
632
|
this._boxedVars = prevBoxed;
|
|
607
633
|
this.line("}");
|
|
634
|
+
this._currentSrcFileIdx = prevFileIdx;
|
|
608
635
|
}
|
|
609
636
|
|
|
610
637
|
// Value receivers are copies in Go; only materialize the copy when the body mutates it.
|
|
@@ -648,10 +675,7 @@ export class CodeGen {
|
|
|
648
675
|
.join(", ");
|
|
649
676
|
const asyncPrefix = decl.async ? "async " : "";
|
|
650
677
|
const srcLine = decl._line ?? null;
|
|
651
|
-
this.line(
|
|
652
|
-
`${asyncPrefix}function ${name}(${params}) {`,
|
|
653
|
-
srcLine ? srcLine - 1 : null,
|
|
654
|
-
);
|
|
678
|
+
this.line(`${asyncPrefix}function ${name}(${params}) {`, srcLine);
|
|
655
679
|
const prevBoxed = this._boxedVars;
|
|
656
680
|
this._boxedVars = new Set();
|
|
657
681
|
this._scanAddressTaken(decl.body);
|
|
@@ -25,7 +25,10 @@ export function buildSourceMap(sources, mappings, sourcesContent) {
|
|
|
25
25
|
let prevSrcLine = 0;
|
|
26
26
|
let prevSrcFile = 0;
|
|
27
27
|
const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
|
|
28
|
-
const byLine = new Map(
|
|
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
|
});
|
|
@@ -85,7 +85,7 @@ export const templGenMethods = {
|
|
|
85
85
|
return this.line(`${p}.insertAdjacentHTML("beforeend", ${argJs});`);
|
|
86
86
|
}
|
|
87
87
|
return this.line(
|
|
88
|
-
`(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p});`,
|
|
88
|
+
`(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p}, ___refs);`,
|
|
89
89
|
);
|
|
90
90
|
},
|
|
91
91
|
|
|
@@ -119,7 +119,10 @@ export const templGenMethods = {
|
|
|
119
119
|
isSvg
|
|
120
120
|
? `${el}.setAttribute("class", ${js});`
|
|
121
121
|
: `${el}.className = ${js};`;
|
|
122
|
-
if (attr.kind === "
|
|
122
|
+
if (attr.kind === "ref") {
|
|
123
|
+
// ref="key" — capture element into the refs map at mount time
|
|
124
|
+
this.line(`if(___refs)___refs[${JSON.stringify(attr.value)}]=${el};`);
|
|
125
|
+
} else if (attr.kind === "static") {
|
|
123
126
|
if (name === "class") {
|
|
124
127
|
this.line(setClass(JSON.stringify(attr.value)));
|
|
125
128
|
} else {
|
package/src/compiler.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
21
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
22
22
|
import { CodeGen } from "./codegen/index.js";
|
|
23
|
+
import { buildSourceMap } from "./codegen/source-map.js";
|
|
23
24
|
import { log } from "./colors.js";
|
|
24
25
|
import { parseDts } from "./dts-parser.js";
|
|
25
26
|
import { Lexer } from "./lexer.js";
|
|
@@ -74,6 +75,87 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
|
|
|
74
75
|
.map((f) => join(dir, f));
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
function normalizeChunk(c) {
|
|
79
|
+
if (typeof c === "string") {
|
|
80
|
+
return { js: c, sourceFiles: [], sourcesContent: [], mappings: [] };
|
|
81
|
+
}
|
|
82
|
+
return c;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendChunkMappings(dest, mappings, localToUnifiedIdx, lineOffset) {
|
|
86
|
+
if (!mappings) return;
|
|
87
|
+
for (const m of mappings) {
|
|
88
|
+
dest.push({
|
|
89
|
+
genLine: m.genLine + lineOffset,
|
|
90
|
+
srcLine: m.srcLine,
|
|
91
|
+
srcFileIdx: localToUnifiedIdx[m.srcFileIdx ?? 0] ?? 0,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeCompilationChunks(chunks) {
|
|
97
|
+
const activeChunks = chunks
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map(normalizeChunk)
|
|
100
|
+
.filter((c) => c.js.length > 0);
|
|
101
|
+
|
|
102
|
+
if (activeChunks.length === 0) {
|
|
103
|
+
return { js: "", sourceFiles: [], sourcesContent: [], mappings: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const unifiedSources = [];
|
|
107
|
+
const unifiedSourcesContent = [];
|
|
108
|
+
const fileIdxMap = new Map();
|
|
109
|
+
|
|
110
|
+
function getOrAddFile(filePath, content) {
|
|
111
|
+
const key = resolve(filePath);
|
|
112
|
+
let idx = fileIdxMap.get(key);
|
|
113
|
+
if (idx === undefined) {
|
|
114
|
+
idx = unifiedSources.length;
|
|
115
|
+
fileIdxMap.set(key, idx);
|
|
116
|
+
unifiedSources.push(key);
|
|
117
|
+
unifiedSourcesContent.push(content);
|
|
118
|
+
}
|
|
119
|
+
return idx;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const combinedJsParts = [];
|
|
123
|
+
const mergedMappings = [];
|
|
124
|
+
let currentLineOffset = 0;
|
|
125
|
+
|
|
126
|
+
for (const chunk of activeChunks) {
|
|
127
|
+
combinedJsParts.push(chunk.js);
|
|
128
|
+
const localToUnifiedIdx = (chunk.sourceFiles ?? []).map((file, i) =>
|
|
129
|
+
getOrAddFile(file, chunk.sourcesContent?.[i] ?? ""),
|
|
130
|
+
);
|
|
131
|
+
appendChunkMappings(
|
|
132
|
+
mergedMappings,
|
|
133
|
+
chunk.mappings,
|
|
134
|
+
localToUnifiedIdx,
|
|
135
|
+
currentLineOffset,
|
|
136
|
+
);
|
|
137
|
+
currentLineOffset += chunk.js.split("\n").length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
js: combinedJsParts.join("\n"),
|
|
142
|
+
sourceFiles: unifiedSources,
|
|
143
|
+
sourcesContent: unifiedSourcesContent,
|
|
144
|
+
mappings: mergedMappings,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function attachSourceMap(merged, outputDir) {
|
|
149
|
+
const relativeSources = merged.sourceFiles.map((f) => relative(outputDir, f));
|
|
150
|
+
const mapJson = buildSourceMap(
|
|
151
|
+
relativeSources,
|
|
152
|
+
merged.mappings,
|
|
153
|
+
merged.sourcesContent,
|
|
154
|
+
);
|
|
155
|
+
const b64 = Buffer.from(mapJson).toString("base64");
|
|
156
|
+
merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
// ── Import resolution ─────────────────────────────────────────
|
|
78
160
|
//
|
|
79
161
|
// Shared by compileFiles (multi-file) and the single-file path in index.js.
|
|
@@ -87,6 +169,7 @@ export function resolveImports(
|
|
|
87
169
|
bundledPackages,
|
|
88
170
|
preambles,
|
|
89
171
|
bundledDirs = new Set(),
|
|
172
|
+
options = {},
|
|
90
173
|
) {
|
|
91
174
|
const fromDir = dirname(resolve(fromFile));
|
|
92
175
|
const allImports = programs.flatMap((p) => p.imports);
|
|
@@ -137,8 +220,12 @@ export function resolveImports(
|
|
|
137
220
|
const depKey = resolve(depDir);
|
|
138
221
|
const alreadyBundled = bundledDirs.has(depKey);
|
|
139
222
|
bundledDirs.add(depKey);
|
|
140
|
-
const dep = compileDir(depDir, {
|
|
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 };
|
|
217
314
|
}
|
|
218
315
|
|
|
219
316
|
export function compileDir(dir, options = {}) {
|
|
@@ -230,6 +327,9 @@ export function compilePackageTests(dir, options = {}) {
|
|
|
230
327
|
|
|
231
328
|
export function compileFiles(files, options = {}) {
|
|
232
329
|
const fromDir = options.fromDir ?? dirname(resolve(files[0]));
|
|
330
|
+
const outputDir =
|
|
331
|
+
options.outputDir ??
|
|
332
|
+
(options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
|
|
233
333
|
|
|
234
334
|
// ── 1. Parse ─────────────────────────────────────────────────
|
|
235
335
|
const parseErrors = [];
|
|
@@ -257,7 +357,7 @@ export function compileFiles(files, options = {}) {
|
|
|
257
357
|
const checker = new TypeChecker();
|
|
258
358
|
const jsImports = new Map(); // npm imports → exported names (for ESM emit)
|
|
259
359
|
const bundledPackages = new Set(); // package names whose code is inlined
|
|
260
|
-
const preambles = []; // JS code from compiled sub-packages
|
|
360
|
+
const preambles = []; // JS code / chunks from compiled sub-packages
|
|
261
361
|
|
|
262
362
|
const dummyFromFile = join(fromDir, "_dummy.go");
|
|
263
363
|
resolveImports(
|
|
@@ -268,6 +368,7 @@ export function compileFiles(files, options = {}) {
|
|
|
268
368
|
bundledPackages,
|
|
269
369
|
preambles,
|
|
270
370
|
options.bundledDirs ?? new Set(),
|
|
371
|
+
{ ...options, outputDir },
|
|
271
372
|
);
|
|
272
373
|
|
|
273
374
|
// ── 3. Type-check ─────────────────────────────────────────────
|
|
@@ -284,22 +385,27 @@ export function compileFiles(files, options = {}) {
|
|
|
284
385
|
isTest: options.isTest ?? false,
|
|
285
386
|
});
|
|
286
387
|
|
|
287
|
-
|
|
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
|
|
|
298
401
|
return {
|
|
299
402
|
pkgName,
|
|
300
|
-
js,
|
|
403
|
+
js: merged.js,
|
|
301
404
|
programs,
|
|
302
405
|
exportedSymbols: checker.getExportedSymbols(),
|
|
303
406
|
exportedTypes: checker.getExportedTypes(),
|
|
407
|
+
sourceFiles: merged.sourceFiles,
|
|
408
|
+
sourcesContent: merged.sourcesContent,
|
|
409
|
+
mappings: merged.mappings,
|
|
304
410
|
};
|
|
305
411
|
}
|
package/src/templ-lexer.js
CHANGED
|
@@ -368,9 +368,14 @@ export class TemplLexer {
|
|
|
368
368
|
const exprTokens = this._readGoExprInBraces();
|
|
369
369
|
attrs.push({ kind: "expr", name: attrName, tokens: exprTokens });
|
|
370
370
|
} else if (this.peek() === '"') {
|
|
371
|
-
// Static string attribute
|
|
371
|
+
// Static string attribute (or ref capture)
|
|
372
372
|
const value = this._readQuotedString();
|
|
373
|
-
|
|
373
|
+
if (attrName === "ref") {
|
|
374
|
+
// ref="fieldName" — element reference capture, not a DOM attribute
|
|
375
|
+
attrs.push({ kind: "ref", value });
|
|
376
|
+
} else {
|
|
377
|
+
attrs.push({ kind: "static", name: attrName, value });
|
|
378
|
+
}
|
|
374
379
|
} else {
|
|
375
380
|
// Unquoted attribute value
|
|
376
381
|
let val = "";
|
|
@@ -53,8 +53,18 @@ export function setupExtendedGlobals(globals, types) {
|
|
|
53
53
|
DataAttr: { kind: "func", params: [STRING, STRING], returns: [GOM_NODE_T] },
|
|
54
54
|
If: { kind: "func", params: [BOOL, GOM_NODE_T], returns: [GOM_NODE_T] },
|
|
55
55
|
Map: { kind: "func", params: [ANY, ANY], returns: [GOM_NODE_T] },
|
|
56
|
-
Mount: {
|
|
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",
|