gofront 1.3.10 → 1.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -34
- package/package.json +1 -1
- package/src/asset-manager.js +3 -2
- package/src/cli-core.js +6 -4
- 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/colors.js +81 -0
- package/src/compiler.js +131 -24
- package/src/dev-server.js +4 -3
- package/src/index.js +52 -44
- package/src/templ-lexer.js +7 -2
- package/src/test-runner.js +39 -19
- package/src/typechecker/assignability.js +12 -1
- 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/asset-manager.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolve,
|
|
12
12
|
sep,
|
|
13
13
|
} from "node:path";
|
|
14
|
+
import { log } from "./colors.js";
|
|
14
15
|
import { loadProjectConfig } from "./project-config.js";
|
|
15
16
|
|
|
16
17
|
export function loadAssetConfig(projectDir) {
|
|
@@ -50,7 +51,7 @@ function copyAssetEntry(entry, projectRoot) {
|
|
|
50
51
|
|
|
51
52
|
const srcPath = resolve(projectRoot, source);
|
|
52
53
|
if (!existsSync(srcPath)) {
|
|
53
|
-
|
|
54
|
+
log.warn(`asset source '${source}' not found — skipping`);
|
|
54
55
|
return {
|
|
55
56
|
status: "skipped",
|
|
56
57
|
item: { source, dest, status: "skipped", reason: "source not found" },
|
|
@@ -73,7 +74,7 @@ function copyAssetEntry(entry, projectRoot) {
|
|
|
73
74
|
item: { source, dest: targetFile, type: "file", status: "copied" },
|
|
74
75
|
};
|
|
75
76
|
} catch (err) {
|
|
76
|
-
|
|
77
|
+
log.warn(`failed to copy asset '${source}': ${err.message}`);
|
|
77
78
|
return {
|
|
78
79
|
status: "failed",
|
|
79
80
|
item: { source, dest, status: "failed", reason: err.message },
|
package/src/cli-core.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
sep,
|
|
24
24
|
} from "node:path";
|
|
25
25
|
import { copyAssets } from "./asset-manager.js";
|
|
26
|
+
import { colors, log } from "./colors.js";
|
|
26
27
|
import { compileDir, compileSingleFile } from "./compiler.js";
|
|
27
28
|
import { createDevServer } from "./dev-server.js";
|
|
28
29
|
import { minify } from "./minifier.js";
|
|
@@ -685,7 +686,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
|
|
|
685
686
|
dest: join(outDir, "vendor.js"),
|
|
686
687
|
});
|
|
687
688
|
} catch (e) {
|
|
688
|
-
|
|
689
|
+
log.warn(`vendor bundle: ${e.message}`);
|
|
689
690
|
}
|
|
690
691
|
|
|
691
692
|
const assets = copyReleaseAssets(
|
|
@@ -755,11 +756,12 @@ function buildDevOnce(
|
|
|
755
756
|
writeFileSync(outputFile, `${result.js}\n`);
|
|
756
757
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
757
758
|
const note = changedFile ? ` — ${changedFile} changed` : "";
|
|
758
|
-
|
|
759
|
+
log.ok(
|
|
760
|
+
`— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
|
|
761
|
+
);
|
|
759
762
|
devServer.notify();
|
|
760
763
|
} catch (e) {
|
|
761
|
-
|
|
762
|
-
for (const line of e.message.split("\n")) console.error(` ${line}`);
|
|
764
|
+
log.error(e.message);
|
|
763
765
|
devServer.notifyError(e);
|
|
764
766
|
}
|
|
765
767
|
}
|
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/colors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ANSI color helpers for CLI output. Honours NO_COLOR / FORCE_COLOR and
|
|
2
|
+
// otherwise only colors when the target stream is a TTY.
|
|
3
|
+
|
|
4
|
+
export function colorEnabled(stream = process.stderr) {
|
|
5
|
+
if ("NO_COLOR" in process.env) return false;
|
|
6
|
+
const force = process.env.FORCE_COLOR;
|
|
7
|
+
if (force !== undefined) return force !== "0" && force !== "false";
|
|
8
|
+
return Boolean(stream?.isTTY);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CODES = {
|
|
12
|
+
red: 31,
|
|
13
|
+
green: 32,
|
|
14
|
+
yellow: 33,
|
|
15
|
+
blue: 34,
|
|
16
|
+
magenta: 35,
|
|
17
|
+
cyan: 36,
|
|
18
|
+
gray: 90,
|
|
19
|
+
bold: 1,
|
|
20
|
+
dim: 2,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createColors(enabled) {
|
|
24
|
+
const c = {};
|
|
25
|
+
for (const [name, code] of Object.entries(CODES)) {
|
|
26
|
+
c[name] = enabled ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => `${s}`;
|
|
27
|
+
}
|
|
28
|
+
c.enabled = enabled;
|
|
29
|
+
return c;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const colors = createColors(colorEnabled(process.stderr));
|
|
33
|
+
|
|
34
|
+
const DIAG_LABEL = /^(Lex error|Parse error|Type error)(\b.*?)(:\s)/;
|
|
35
|
+
const GUTTER = /^(\s*\d+ \| )(.*)$/;
|
|
36
|
+
const CARET = /^(\s*)(\^+)$/;
|
|
37
|
+
|
|
38
|
+
// Colorizes a compiler diagnostic: red label, dimmed source gutter, red caret.
|
|
39
|
+
export function formatDiagnostic(message, c = colors) {
|
|
40
|
+
if (!c.enabled) return message;
|
|
41
|
+
return message
|
|
42
|
+
.split("\n")
|
|
43
|
+
.map((line) => {
|
|
44
|
+
const label = line.match(DIAG_LABEL);
|
|
45
|
+
if (label) {
|
|
46
|
+
return `${c.bold(c.red(label[1]))}${c.dim(label[2])}${label[3]}${line.slice(label[0].length)}`;
|
|
47
|
+
}
|
|
48
|
+
const gutter = line.match(GUTTER);
|
|
49
|
+
if (gutter) return `${c.dim(gutter[1])}${gutter[2]}`;
|
|
50
|
+
const caret = line.match(CARET);
|
|
51
|
+
if (caret) return `${caret[1]}${c.bold(c.red(caret[2]))}`;
|
|
52
|
+
if (/^warning:/i.test(line.trim())) return c.yellow(line);
|
|
53
|
+
return line;
|
|
54
|
+
})
|
|
55
|
+
.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Standard CLI log helpers writing to stderr with a colored `gofront:` prefix.
|
|
59
|
+
const prefix = () => colors.bold("gofront:");
|
|
60
|
+
const isDiagnostic = (msg) => DIAG_LABEL.test(msg) || msg.includes("\n");
|
|
61
|
+
|
|
62
|
+
export const log = {
|
|
63
|
+
info: (msg) => console.error(`${prefix()} ${msg}`),
|
|
64
|
+
ok: (msg) =>
|
|
65
|
+
console.error(`${prefix()} ${colors.green("OK")}${msg ? ` ${msg}` : ""}`),
|
|
66
|
+
warn: (msg) =>
|
|
67
|
+
console.error(`${prefix()} ${colors.yellow(`warning: ${msg}`)}`),
|
|
68
|
+
error: (msg) => {
|
|
69
|
+
console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
|
|
70
|
+
for (const line of formatDiagnostic(msg).split("\n"))
|
|
71
|
+
console.error(` ${line}`);
|
|
72
|
+
},
|
|
73
|
+
// Compiler diagnostics get the block form; anything else is one red line.
|
|
74
|
+
fail: (msg) =>
|
|
75
|
+
isDiagnostic(msg)
|
|
76
|
+
? log.error(msg)
|
|
77
|
+
: console.error(`${prefix()} ${colors.red(msg)}`),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const stamp = () => colors.dim(`[${new Date().toLocaleTimeString()}]`);
|
|
81
|
+
export const ms = (n) => colors.dim(`(${n}ms)`);
|
package/src/compiler.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
21
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
22
22
|
import { CodeGen } from "./codegen/index.js";
|
|
23
|
+
import { buildSourceMap } from "./codegen/source-map.js";
|
|
24
|
+
import { log } from "./colors.js";
|
|
23
25
|
import { parseDts } from "./dts-parser.js";
|
|
24
26
|
import { Lexer } from "./lexer.js";
|
|
25
27
|
import { Parser } from "./parser/index.js";
|
|
@@ -73,6 +75,87 @@ export function gwFilesIn(dir, { includeTests = false } = {}) {
|
|
|
73
75
|
.map((f) => join(dir, f));
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function normalizeChunk(c) {
|
|
79
|
+
if (typeof c === "string") {
|
|
80
|
+
return { js: c, sourceFiles: [], sourcesContent: [], mappings: [] };
|
|
81
|
+
}
|
|
82
|
+
return c;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendChunkMappings(dest, mappings, localToUnifiedIdx, lineOffset) {
|
|
86
|
+
if (!mappings) return;
|
|
87
|
+
for (const m of mappings) {
|
|
88
|
+
dest.push({
|
|
89
|
+
genLine: m.genLine + lineOffset,
|
|
90
|
+
srcLine: m.srcLine,
|
|
91
|
+
srcFileIdx: localToUnifiedIdx[m.srcFileIdx ?? 0] ?? 0,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mergeCompilationChunks(chunks) {
|
|
97
|
+
const activeChunks = chunks
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map(normalizeChunk)
|
|
100
|
+
.filter((c) => c.js.length > 0);
|
|
101
|
+
|
|
102
|
+
if (activeChunks.length === 0) {
|
|
103
|
+
return { js: "", sourceFiles: [], sourcesContent: [], mappings: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const unifiedSources = [];
|
|
107
|
+
const unifiedSourcesContent = [];
|
|
108
|
+
const fileIdxMap = new Map();
|
|
109
|
+
|
|
110
|
+
function getOrAddFile(filePath, content) {
|
|
111
|
+
const key = resolve(filePath);
|
|
112
|
+
let idx = fileIdxMap.get(key);
|
|
113
|
+
if (idx === undefined) {
|
|
114
|
+
idx = unifiedSources.length;
|
|
115
|
+
fileIdxMap.set(key, idx);
|
|
116
|
+
unifiedSources.push(key);
|
|
117
|
+
unifiedSourcesContent.push(content);
|
|
118
|
+
}
|
|
119
|
+
return idx;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const combinedJsParts = [];
|
|
123
|
+
const mergedMappings = [];
|
|
124
|
+
let currentLineOffset = 0;
|
|
125
|
+
|
|
126
|
+
for (const chunk of activeChunks) {
|
|
127
|
+
combinedJsParts.push(chunk.js);
|
|
128
|
+
const localToUnifiedIdx = (chunk.sourceFiles ?? []).map((file, i) =>
|
|
129
|
+
getOrAddFile(file, chunk.sourcesContent?.[i] ?? ""),
|
|
130
|
+
);
|
|
131
|
+
appendChunkMappings(
|
|
132
|
+
mergedMappings,
|
|
133
|
+
chunk.mappings,
|
|
134
|
+
localToUnifiedIdx,
|
|
135
|
+
currentLineOffset,
|
|
136
|
+
);
|
|
137
|
+
currentLineOffset += chunk.js.split("\n").length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
js: combinedJsParts.join("\n"),
|
|
142
|
+
sourceFiles: unifiedSources,
|
|
143
|
+
sourcesContent: unifiedSourcesContent,
|
|
144
|
+
mappings: mergedMappings,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function attachSourceMap(merged, outputDir) {
|
|
149
|
+
const relativeSources = merged.sourceFiles.map((f) => relative(outputDir, f));
|
|
150
|
+
const mapJson = buildSourceMap(
|
|
151
|
+
relativeSources,
|
|
152
|
+
merged.mappings,
|
|
153
|
+
merged.sourcesContent,
|
|
154
|
+
);
|
|
155
|
+
const b64 = Buffer.from(mapJson).toString("base64");
|
|
156
|
+
merged.js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
76
159
|
// ── Import resolution ─────────────────────────────────────────
|
|
77
160
|
//
|
|
78
161
|
// Shared by compileFiles (multi-file) and the single-file path in index.js.
|
|
@@ -86,6 +169,7 @@ export function resolveImports(
|
|
|
86
169
|
bundledPackages,
|
|
87
170
|
preambles,
|
|
88
171
|
bundledDirs = new Set(),
|
|
172
|
+
options = {},
|
|
89
173
|
) {
|
|
90
174
|
const fromDir = dirname(resolve(fromFile));
|
|
91
175
|
const allImports = programs.flatMap((p) => p.imports);
|
|
@@ -126,8 +210,8 @@ export function resolveImports(
|
|
|
126
210
|
seenLocalPaths.add(path);
|
|
127
211
|
const depDir = resolveGwDir(path, fromFile);
|
|
128
212
|
if (!depDir) {
|
|
129
|
-
|
|
130
|
-
`
|
|
213
|
+
log.warn(
|
|
214
|
+
`cannot find local package '${path}' relative to ${fromDir}`,
|
|
131
215
|
);
|
|
132
216
|
continue;
|
|
133
217
|
}
|
|
@@ -136,8 +220,12 @@ export function resolveImports(
|
|
|
136
220
|
const depKey = resolve(depDir);
|
|
137
221
|
const alreadyBundled = bundledDirs.has(depKey);
|
|
138
222
|
bundledDirs.add(depKey);
|
|
139
|
-
const dep = compileDir(depDir, {
|
|
140
|
-
|
|
223
|
+
const dep = compileDir(depDir, {
|
|
224
|
+
...options,
|
|
225
|
+
bundledDirs,
|
|
226
|
+
isDependency: true,
|
|
227
|
+
});
|
|
228
|
+
if (!alreadyBundled) preambles.push(dep);
|
|
141
229
|
if (alias === "_") continue;
|
|
142
230
|
if (alias === ".") {
|
|
143
231
|
checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
|
|
@@ -180,6 +268,10 @@ export function compileSingleFile(inputPath, options = {}) {
|
|
|
180
268
|
ast._source = source;
|
|
181
269
|
if (dumpAst) return { ast };
|
|
182
270
|
|
|
271
|
+
const outputDir =
|
|
272
|
+
options.outputDir ??
|
|
273
|
+
(outputFile ? dirname(resolve(outputFile)) : resolve("."));
|
|
274
|
+
|
|
183
275
|
const checker = new TypeChecker();
|
|
184
276
|
const jsImports = new Map();
|
|
185
277
|
const bundledPackages = new Set();
|
|
@@ -192,6 +284,8 @@ export function compileSingleFile(inputPath, options = {}) {
|
|
|
192
284
|
jsImports,
|
|
193
285
|
bundledPackages,
|
|
194
286
|
preambles,
|
|
287
|
+
options.bundledDirs ?? new Set(),
|
|
288
|
+
{ ...options, outputDir },
|
|
195
289
|
);
|
|
196
290
|
|
|
197
291
|
const errors = checker.check(ast);
|
|
@@ -201,18 +295,22 @@ export function compileSingleFile(inputPath, options = {}) {
|
|
|
201
295
|
}
|
|
202
296
|
|
|
203
297
|
const cg = new CodeGen(checker, jsImports, bundledPackages);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
298
|
+
const mainJs = cg.generate(ast);
|
|
299
|
+
|
|
300
|
+
const mainChunk = {
|
|
301
|
+
js: mainJs,
|
|
302
|
+
sourceFiles: [resolve(inputPath)],
|
|
303
|
+
sourcesContent: [source],
|
|
304
|
+
mappings: cg.getMappings(),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const merged = mergeCompilationChunks([...preambles, mainChunk]);
|
|
308
|
+
|
|
309
|
+
if (sourceMap && !options.isDependency) {
|
|
310
|
+
attachSourceMap(merged, outputDir);
|
|
212
311
|
}
|
|
213
312
|
|
|
214
|
-
|
|
215
|
-
return { js: output };
|
|
313
|
+
return { js: merged.js };
|
|
216
314
|
}
|
|
217
315
|
|
|
218
316
|
export function compileDir(dir, options = {}) {
|
|
@@ -229,6 +327,9 @@ export function compilePackageTests(dir, options = {}) {
|
|
|
229
327
|
|
|
230
328
|
export function compileFiles(files, options = {}) {
|
|
231
329
|
const fromDir = options.fromDir ?? dirname(resolve(files[0]));
|
|
330
|
+
const outputDir =
|
|
331
|
+
options.outputDir ??
|
|
332
|
+
(options.outputFile ? dirname(resolve(options.outputFile)) : fromDir);
|
|
232
333
|
|
|
233
334
|
// ── 1. Parse ─────────────────────────────────────────────────
|
|
234
335
|
const parseErrors = [];
|
|
@@ -256,7 +357,7 @@ export function compileFiles(files, options = {}) {
|
|
|
256
357
|
const checker = new TypeChecker();
|
|
257
358
|
const jsImports = new Map(); // npm imports → exported names (for ESM emit)
|
|
258
359
|
const bundledPackages = new Set(); // package names whose code is inlined
|
|
259
|
-
const preambles = []; // JS code from compiled sub-packages
|
|
360
|
+
const preambles = []; // JS code / chunks from compiled sub-packages
|
|
260
361
|
|
|
261
362
|
const dummyFromFile = join(fromDir, "_dummy.go");
|
|
262
363
|
resolveImports(
|
|
@@ -267,6 +368,7 @@ export function compileFiles(files, options = {}) {
|
|
|
267
368
|
bundledPackages,
|
|
268
369
|
preambles,
|
|
269
370
|
options.bundledDirs ?? new Set(),
|
|
371
|
+
{ ...options, outputDir },
|
|
270
372
|
);
|
|
271
373
|
|
|
272
374
|
// ── 3. Type-check ─────────────────────────────────────────────
|
|
@@ -283,22 +385,27 @@ export function compileFiles(files, options = {}) {
|
|
|
283
385
|
isTest: options.isTest ?? false,
|
|
284
386
|
});
|
|
285
387
|
|
|
286
|
-
|
|
388
|
+
const mainChunk = {
|
|
389
|
+
js: mainJs,
|
|
390
|
+
sourceFiles: files.map((f) => resolve(f)),
|
|
391
|
+
sourcesContent: files.map((f) => readFileSync(f, "utf8")),
|
|
392
|
+
mappings: codegen.getMappings(),
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const merged = mergeCompilationChunks([...preambles, mainChunk]);
|
|
287
396
|
|
|
288
|
-
if (options.sourceMap) {
|
|
289
|
-
|
|
290
|
-
const sourceFiles = files.map((f) => relative(outputDir, f));
|
|
291
|
-
const sourcesContent = files.map((f) => readFileSync(f, "utf8"));
|
|
292
|
-
const map = codegen.getSourceMap(sourceFiles, sourcesContent);
|
|
293
|
-
const b64 = Buffer.from(map).toString("base64");
|
|
294
|
-
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
397
|
+
if (options.sourceMap && !options.isDependency) {
|
|
398
|
+
attachSourceMap(merged, outputDir);
|
|
295
399
|
}
|
|
296
400
|
|
|
297
401
|
return {
|
|
298
402
|
pkgName,
|
|
299
|
-
js,
|
|
403
|
+
js: merged.js,
|
|
300
404
|
programs,
|
|
301
405
|
exportedSymbols: checker.getExportedSymbols(),
|
|
302
406
|
exportedTypes: checker.getExportedTypes(),
|
|
407
|
+
sourceFiles: merged.sourceFiles,
|
|
408
|
+
sourcesContent: merged.sourcesContent,
|
|
409
|
+
mappings: merged.mappings,
|
|
303
410
|
};
|
|
304
411
|
}
|
package/src/dev-server.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { colors, log } from "./colors.js";
|
|
7
8
|
|
|
8
9
|
export const MIME = {
|
|
9
10
|
".html": "text/html; charset=utf-8",
|
|
@@ -275,9 +276,9 @@ function createNotify(clients) {
|
|
|
275
276
|
|
|
276
277
|
function handleServerError(err, port) {
|
|
277
278
|
if (err.code === "EADDRINUSE") {
|
|
278
|
-
|
|
279
|
+
log.fail(`port ${port} already in use — try --port <number>`);
|
|
279
280
|
} else {
|
|
280
|
-
|
|
281
|
+
log.fail(`dev server error: ${err.message}`);
|
|
281
282
|
}
|
|
282
283
|
process.exit(1);
|
|
283
284
|
}
|
|
@@ -327,7 +328,7 @@ export function createDevServer(serveDir, port = 3000, options = {}) {
|
|
|
327
328
|
server.listen(port, host, () => {
|
|
328
329
|
const actualPort = server.address()?.port ?? port;
|
|
329
330
|
if (port !== 0 && !options.silent) {
|
|
330
|
-
|
|
331
|
+
log.info(`dev server → ${colors.cyan(`http://localhost:${actualPort}`)}`);
|
|
331
332
|
}
|
|
332
333
|
});
|
|
333
334
|
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
parseTestArgs,
|
|
39
39
|
runCompile,
|
|
40
40
|
} from "./cli-core.js";
|
|
41
|
+
import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
|
|
41
42
|
import { createDevServer } from "./dev-server.js";
|
|
42
43
|
|
|
43
44
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
@@ -85,10 +86,9 @@ if (args[0] === "prep" || args[0] === "vendor") {
|
|
|
85
86
|
const { targetDir, vendorConfig } = parsePrepArgs(args.slice(1));
|
|
86
87
|
try {
|
|
87
88
|
const result = await handlePrep(resolve(targetDir), { vendorConfig });
|
|
88
|
-
for (const line of formatPrepSummary(result))
|
|
89
|
-
console.error(`gofront: ${line}`);
|
|
89
|
+
for (const line of formatPrepSummary(result)) log.info(line);
|
|
90
90
|
} catch (e) {
|
|
91
|
-
|
|
91
|
+
log.fail(e.message);
|
|
92
92
|
process.exit(1);
|
|
93
93
|
}
|
|
94
94
|
process.exit(0);
|
|
@@ -103,12 +103,12 @@ if (args[0] === "init") {
|
|
|
103
103
|
try {
|
|
104
104
|
({ mainPath } = handleInit(targetDir));
|
|
105
105
|
} catch (e) {
|
|
106
|
-
|
|
106
|
+
log.fail(e.message);
|
|
107
107
|
process.exit(1);
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`
|
|
109
|
+
log.info(`created ${colors.cyan(mainPath)}`);
|
|
110
|
+
log.info(
|
|
111
|
+
`run ${colors.cyan(`gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`}`)} to compile`,
|
|
112
112
|
);
|
|
113
113
|
process.exit(0);
|
|
114
114
|
}
|
|
@@ -121,7 +121,7 @@ if (args[0] === "test") {
|
|
|
121
121
|
const result = await handleTest(targetDir, testOptions);
|
|
122
122
|
process.exit(result.exitCode);
|
|
123
123
|
} catch (e) {
|
|
124
|
-
|
|
124
|
+
log.fail(e.message);
|
|
125
125
|
process.exit(1);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -132,21 +132,21 @@ if (args[0] === "check") {
|
|
|
132
132
|
const { targetDir } = parseCheckArgs(args.slice(1));
|
|
133
133
|
try {
|
|
134
134
|
const { elapsedMs, packages } = handleCheck(targetDir);
|
|
135
|
+
const okLine = (label, t) =>
|
|
136
|
+
log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
|
|
135
137
|
if (packages) {
|
|
136
138
|
for (const pkg of packages) {
|
|
137
|
-
|
|
138
|
-
`gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
|
|
139
|
-
);
|
|
139
|
+
okLine(relative(process.cwd(), pkg.dir) || ".", pkg.elapsedMs);
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
log.info(
|
|
142
|
+
`${colors.bold(`${packages.length} packages`)} — ${colors.bold(colors.green("OK"))} ${ms(elapsedMs)}`,
|
|
143
143
|
);
|
|
144
144
|
} else {
|
|
145
|
-
|
|
145
|
+
okLine(targetDir, elapsedMs);
|
|
146
146
|
}
|
|
147
147
|
process.exit(0);
|
|
148
148
|
} catch (e) {
|
|
149
|
-
|
|
149
|
+
log.fail(e.message);
|
|
150
150
|
process.exit(1);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -158,11 +158,15 @@ if (args[0] === "build") {
|
|
|
158
158
|
try {
|
|
159
159
|
const result = await handleBuild(buildOptions.targetDir, buildOptions);
|
|
160
160
|
for (const line of formatBuildSummary(result)) {
|
|
161
|
-
|
|
161
|
+
log.info(
|
|
162
|
+
line
|
|
163
|
+
.replace(/^build complete/, colors.green("build complete"))
|
|
164
|
+
.replace(/\(([\d.]+ kB)\)$/, (_, s) => colors.dim(`(${s})`)),
|
|
165
|
+
);
|
|
162
166
|
}
|
|
163
167
|
process.exit(0);
|
|
164
168
|
} catch (e) {
|
|
165
|
-
|
|
169
|
+
log.fail(e.message);
|
|
166
170
|
process.exit(1);
|
|
167
171
|
}
|
|
168
172
|
}
|
|
@@ -174,14 +178,14 @@ if (args[0] === "dev") {
|
|
|
174
178
|
try {
|
|
175
179
|
const dev = await handleDev(devOptions.targetDir, devOptions);
|
|
176
180
|
if (dev.initialError) {
|
|
177
|
-
|
|
178
|
-
for (const line of dev.initialError.message.split("\n"))
|
|
179
|
-
console.error(` ${line}`);
|
|
181
|
+
log.error(dev.initialError.message);
|
|
180
182
|
} else {
|
|
181
|
-
|
|
183
|
+
log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
|
|
182
184
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
log.info(
|
|
186
|
+
`dev server running → ${colors.cyan(`http://localhost:${dev.port}`)}`,
|
|
187
|
+
);
|
|
188
|
+
log.info(`watching ${colors.cyan(dev.srcDir)} for changes...`);
|
|
185
189
|
|
|
186
190
|
const shutdown = async () => {
|
|
187
191
|
await dev.close();
|
|
@@ -191,7 +195,7 @@ if (args[0] === "dev") {
|
|
|
191
195
|
process.on("SIGTERM", shutdown);
|
|
192
196
|
await new Promise(() => {});
|
|
193
197
|
} catch (e) {
|
|
194
|
-
|
|
198
|
+
log.fail(e.message);
|
|
195
199
|
process.exit(1);
|
|
196
200
|
}
|
|
197
201
|
}
|
|
@@ -218,7 +222,7 @@ let isDir = false;
|
|
|
218
222
|
try {
|
|
219
223
|
isDir = statSync(inputPath).isDirectory();
|
|
220
224
|
} catch (e) {
|
|
221
|
-
|
|
225
|
+
log.fail(`cannot access '${inputArg}': ${e.message}`);
|
|
222
226
|
process.exit(1);
|
|
223
227
|
}
|
|
224
228
|
|
|
@@ -235,7 +239,7 @@ if (!watchMode) {
|
|
|
235
239
|
dumpAst,
|
|
236
240
|
});
|
|
237
241
|
} catch (e) {
|
|
238
|
-
|
|
242
|
+
log.fail(e.message);
|
|
239
243
|
process.exit(1);
|
|
240
244
|
}
|
|
241
245
|
|
|
@@ -250,7 +254,9 @@ if (!watchMode) {
|
|
|
250
254
|
|
|
251
255
|
if (checkOnly) {
|
|
252
256
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
253
|
-
|
|
257
|
+
log.info(
|
|
258
|
+
`${colors.cyan(inputArg)} — ${colors.green("OK")} ${ms(elapsedMs)}`,
|
|
259
|
+
);
|
|
254
260
|
process.exit(0);
|
|
255
261
|
}
|
|
256
262
|
|
|
@@ -262,7 +268,7 @@ if (!watchMode) {
|
|
|
262
268
|
sourceMap,
|
|
263
269
|
});
|
|
264
270
|
} catch (e) {
|
|
265
|
-
|
|
271
|
+
log.fail(`minify failed: ${e.message}`);
|
|
266
272
|
process.exit(1);
|
|
267
273
|
}
|
|
268
274
|
|
|
@@ -272,9 +278,9 @@ if (!watchMode) {
|
|
|
272
278
|
try {
|
|
273
279
|
mkdirSync(dirname(resolve(outputFile)), { recursive: true });
|
|
274
280
|
writeFileSync(outputFile, `${js}\n`);
|
|
275
|
-
|
|
281
|
+
log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
|
|
276
282
|
} catch (e) {
|
|
277
|
-
|
|
283
|
+
log.fail(`cannot write '${outputFile}': ${e.message}`);
|
|
278
284
|
process.exit(1);
|
|
279
285
|
}
|
|
280
286
|
} else {
|
|
@@ -286,10 +292,10 @@ if (!watchMode) {
|
|
|
286
292
|
const projectDir = resolve(".");
|
|
287
293
|
const { copied, skipped } = copyAssets(projectDir);
|
|
288
294
|
if (copied > 0 || skipped > 0) {
|
|
289
|
-
|
|
295
|
+
log.info(`copied ${copied} assets (${skipped} skipped)`);
|
|
290
296
|
}
|
|
291
297
|
} catch (e) {
|
|
292
|
-
|
|
298
|
+
log.warn(`asset copy failed: ${e.message}`);
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
@@ -298,15 +304,11 @@ if (!watchMode) {
|
|
|
298
304
|
|
|
299
305
|
// ── Watch mode ───────────────────────────────────────────────
|
|
300
306
|
|
|
301
|
-
function timestamp() {
|
|
302
|
-
return new Date().toLocaleTimeString();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
307
|
// Start dev server before first build so the browser can connect immediately
|
|
306
308
|
let devServer = null;
|
|
307
309
|
if (serveMode) {
|
|
308
310
|
if (!outputFile) {
|
|
309
|
-
|
|
311
|
+
log.fail("--serve requires -o <output file>");
|
|
310
312
|
process.exit(1);
|
|
311
313
|
}
|
|
312
314
|
const serveDir = dirname(resolve(outputFile));
|
|
@@ -324,17 +326,18 @@ function buildOnce(changedFile = null) {
|
|
|
324
326
|
});
|
|
325
327
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
326
328
|
const changeNote = changedFile ? ` — ${changedFile} changed` : "";
|
|
329
|
+
const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
|
|
327
330
|
if (outputFile) {
|
|
328
331
|
writeFileSync(outputFile, `${js}\n`);
|
|
329
332
|
console.error(
|
|
330
|
-
|
|
333
|
+
`${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${colors.cyan(outputFile)} ${timing}`,
|
|
331
334
|
);
|
|
332
335
|
} else {
|
|
333
336
|
// Clear screen then print
|
|
334
337
|
process.stdout.write("\x1Bc");
|
|
335
338
|
console.log(js);
|
|
336
339
|
console.error(
|
|
337
|
-
|
|
340
|
+
`${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
|
|
338
341
|
);
|
|
339
342
|
}
|
|
340
343
|
|
|
@@ -342,14 +345,17 @@ function buildOnce(changedFile = null) {
|
|
|
342
345
|
try {
|
|
343
346
|
copyAssets(resolve("."));
|
|
344
347
|
} catch (e) {
|
|
345
|
-
|
|
348
|
+
log.warn(`asset copy failed: ${e.message}`);
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
devServer?.notify();
|
|
350
353
|
} catch (e) {
|
|
351
|
-
console.error(
|
|
352
|
-
|
|
354
|
+
console.error(
|
|
355
|
+
`${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
|
|
356
|
+
);
|
|
357
|
+
for (const line of formatDiagnostic(e.message).split("\n"))
|
|
358
|
+
console.error(` ${line}`);
|
|
353
359
|
devServer?.notifyError?.(e);
|
|
354
360
|
}
|
|
355
361
|
}
|
|
@@ -365,7 +371,7 @@ function handleCssWatch(filename) {
|
|
|
365
371
|
try {
|
|
366
372
|
copyAssets(resolve("."));
|
|
367
373
|
} catch (e) {
|
|
368
|
-
|
|
374
|
+
log.warn(`asset copy failed: ${e.message}`);
|
|
369
375
|
}
|
|
370
376
|
}
|
|
371
377
|
devServer?.notifyCss?.(filename);
|
|
@@ -385,4 +391,6 @@ watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
|
385
391
|
debounce = setTimeout(() => buildOnce(filename), 80);
|
|
386
392
|
});
|
|
387
393
|
|
|
388
|
-
console.error(
|
|
394
|
+
console.error(
|
|
395
|
+
`${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
|
|
396
|
+
);
|
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 = "";
|
package/src/test-runner.js
CHANGED
|
@@ -6,6 +6,7 @@ import { statSync } from "node:fs";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { basename, dirname, join, resolve } from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
|
|
9
10
|
import { compilePackageTests, gwFilesIn } from "./compiler.js";
|
|
10
11
|
|
|
11
12
|
export function isTestFunc(decl) {
|
|
@@ -38,6 +39,7 @@ export function generateTestHarness(bundleJs, testNames, options = {}) {
|
|
|
38
39
|
const runRegex = options.run ? options.run : null;
|
|
39
40
|
const verbose = Boolean(options.verbose);
|
|
40
41
|
const dom = Boolean(options.dom);
|
|
42
|
+
const color = Boolean(options.color);
|
|
41
43
|
const pkgName = options.pkgName ?? "main";
|
|
42
44
|
// The harness runs from stdin with cwd = project dir, so a bare "jsdom"
|
|
43
45
|
// import only works if the project has it locally. Use the resolved path.
|
|
@@ -86,6 +88,12 @@ async function __runGoFrontSuite() {
|
|
|
86
88
|
const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
|
|
87
89
|
const verbose = ${verbose};
|
|
88
90
|
const pkgName = ${JSON.stringify(pkgName)};
|
|
91
|
+
const paint = ${color}
|
|
92
|
+
? (code) => (s) => "\\x1b[" + code + "m" + s + "\\x1b[0m"
|
|
93
|
+
: () => (s) => s;
|
|
94
|
+
const red = paint(31), green = paint(32), yellow = paint(33), dim = paint(2), bold = paint(1);
|
|
95
|
+
const PASS = green("PASS"), FAIL = bold(red("FAIL")), SKIP = yellow("SKIP");
|
|
96
|
+
const okLine = (name, t) => console.log(green("ok ") + "\\t" + name + "\\t" + dim(t + "s"));
|
|
89
97
|
|
|
90
98
|
const testsToRun = runFilter
|
|
91
99
|
? allTests.filter((name) => runFilter.test(name))
|
|
@@ -93,10 +101,10 @@ async function __runGoFrontSuite() {
|
|
|
93
101
|
|
|
94
102
|
if (testsToRun.length === 0) {
|
|
95
103
|
if (allTests.length > 0 && runFilter) {
|
|
96
|
-
console.log("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)});
|
|
104
|
+
console.log(yellow("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)}));
|
|
97
105
|
}
|
|
98
|
-
console.log(
|
|
99
|
-
|
|
106
|
+
console.log(PASS);
|
|
107
|
+
okLine(pkgName, "0.000");
|
|
100
108
|
process.exit(0);
|
|
101
109
|
}
|
|
102
110
|
|
|
@@ -108,17 +116,17 @@ async function __runGoFrontSuite() {
|
|
|
108
116
|
// Go-style streaming report. In non-verbose mode only failures and skips are
|
|
109
117
|
// shown, so the "=== RUN" header is emitted late for failures.
|
|
110
118
|
const report = (t) => {
|
|
111
|
-
const elapsed = ((now() - t._start) / 1000).toFixed(2);
|
|
119
|
+
const elapsed = dim("(" + ((now() - t._start) / 1000).toFixed(2) + "s)");
|
|
112
120
|
if (t.skipped) {
|
|
113
|
-
console.log("--- SKIP: " + t._name + "
|
|
121
|
+
console.log("--- " + SKIP + ": " + t._name + " " + elapsed);
|
|
114
122
|
for (const log of t._logs) console.log(" " + log);
|
|
115
123
|
} else if (t.failed) {
|
|
116
|
-
if (!verbose) console.log("=== RUN " + t._name);
|
|
117
|
-
for (const log of t._logs) console.log(" " + log);
|
|
118
|
-
console.log("--- FAIL: " + t._name + "
|
|
124
|
+
if (!verbose) console.log(dim("=== RUN ") + t._name);
|
|
125
|
+
for (const log of t._logs) console.log(" " + red(log));
|
|
126
|
+
console.log("--- " + FAIL + ": " + t._name + " " + elapsed);
|
|
119
127
|
} else if (verbose) {
|
|
120
128
|
for (const log of t._logs) console.log(" " + log);
|
|
121
|
-
console.log("--- PASS: " + t._name + "
|
|
129
|
+
console.log("--- " + PASS + ": " + t._name + " " + elapsed);
|
|
122
130
|
}
|
|
123
131
|
};
|
|
124
132
|
|
|
@@ -130,7 +138,7 @@ async function __runGoFrontSuite() {
|
|
|
130
138
|
};
|
|
131
139
|
|
|
132
140
|
globalThis.__onSubtestStart = (name) => {
|
|
133
|
-
if (verbose) console.log("=== RUN " + name);
|
|
141
|
+
if (verbose) console.log(dim("=== RUN ") + name);
|
|
134
142
|
};
|
|
135
143
|
globalThis.__onSubtestEnd = report;
|
|
136
144
|
|
|
@@ -141,13 +149,13 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
|
|
|
141
149
|
for (const name of testsToRun) {
|
|
142
150
|
const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
|
|
143
151
|
if (typeof fn !== "function") {
|
|
144
|
-
console.error("testing: cannot find test function " + name);
|
|
152
|
+
console.error(red("testing: cannot find test function " + name));
|
|
145
153
|
totalFailures++;
|
|
146
154
|
continue;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
const t = new __GoFront_T(name);
|
|
150
|
-
if (verbose) console.log("=== RUN " + name);
|
|
158
|
+
if (verbose) console.log(dim("=== RUN ") + name);
|
|
151
159
|
|
|
152
160
|
try {
|
|
153
161
|
const res = fn(t);
|
|
@@ -164,12 +172,12 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
|
|
|
164
172
|
const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
|
|
165
173
|
|
|
166
174
|
if (totalFailures > 0) {
|
|
167
|
-
console.log(
|
|
168
|
-
console.log("
|
|
175
|
+
console.log(FAIL);
|
|
176
|
+
console.log(FAIL + "\\t" + pkgName + "\\t" + dim(suiteElapsed + "s"));
|
|
169
177
|
process.exit(1);
|
|
170
178
|
} else {
|
|
171
|
-
console.log(
|
|
172
|
-
|
|
179
|
+
console.log(PASS);
|
|
180
|
+
okLine(pkgName, suiteElapsed);
|
|
173
181
|
process.exit(0);
|
|
174
182
|
}
|
|
175
183
|
}
|
|
@@ -215,10 +223,17 @@ function reportEmptyTests(label, reason, options) {
|
|
|
215
223
|
return { exitCode: 0, stdout: out, stderr: "", noTests: true };
|
|
216
224
|
}
|
|
217
225
|
const dest = options.stdout ?? process.stdout;
|
|
218
|
-
dest.write(out);
|
|
226
|
+
dest.write(colorsFor(options, dest).dim(out));
|
|
219
227
|
return { exitCode: 0, noTests: true };
|
|
220
228
|
}
|
|
221
229
|
|
|
230
|
+
// Color is decided here because the harness child writes to a pipe, not a TTY.
|
|
231
|
+
function colorsFor(options, stream) {
|
|
232
|
+
const enabled =
|
|
233
|
+
options.color ?? (!options.captureOutput && colorEnabled(stream));
|
|
234
|
+
return createColors(enabled);
|
|
235
|
+
}
|
|
236
|
+
|
|
222
237
|
function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
223
238
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
224
239
|
const child = spawn(process.execPath, ["--input-type=module", "-"], {
|
|
@@ -289,12 +304,16 @@ export async function runTests(targetDir, options = {}) {
|
|
|
289
304
|
try {
|
|
290
305
|
compiled = compilePackageTests(resolvedDir, options);
|
|
291
306
|
} catch (err) {
|
|
292
|
-
const
|
|
307
|
+
const msg = err?.message ?? String(err);
|
|
293
308
|
if (options.captureOutput) {
|
|
309
|
+
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
|
|
294
310
|
return { exitCode: 1, stdout: "", stderr: out };
|
|
295
311
|
}
|
|
296
312
|
const dest = options.stderr ?? process.stderr;
|
|
297
|
-
dest
|
|
313
|
+
const c = colorsFor(options, dest);
|
|
314
|
+
dest.write(
|
|
315
|
+
`${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
|
|
316
|
+
);
|
|
298
317
|
return { exitCode: 1 };
|
|
299
318
|
}
|
|
300
319
|
|
|
@@ -309,6 +328,7 @@ export async function runTests(targetDir, options = {}) {
|
|
|
309
328
|
...options,
|
|
310
329
|
pkgName,
|
|
311
330
|
jsdomPath,
|
|
331
|
+
color: colorsFor(options, options.stdout ?? process.stdout).enabled,
|
|
312
332
|
});
|
|
313
333
|
|
|
314
334
|
return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
|
|
@@ -227,7 +227,10 @@ export const assignabilityMethods = {
|
|
|
227
227
|
|
|
228
228
|
_isUntypedAssignable(target, source) {
|
|
229
229
|
if (isUntyped(target)) return true;
|
|
230
|
-
|
|
230
|
+
// Named types with a basic underlying type (`type Mode string`) accept
|
|
231
|
+
// untyped constants of that kind, as in Go.
|
|
232
|
+
if (target.kind === "named") target = this.resolveType(target.underlying);
|
|
233
|
+
if (target?.kind !== "basic") return false;
|
|
231
234
|
return (
|
|
232
235
|
(UNTYPED_COMPAT[source.base]?.has(target.name) ?? false) ||
|
|
233
236
|
(isComplex(target) &&
|
|
@@ -347,6 +350,14 @@ export const assignabilityMethods = {
|
|
|
347
350
|
if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
|
|
348
351
|
let base = srcType.kind === "named" ? srcType.underlying : srcType;
|
|
349
352
|
base = this.resolveType(base);
|
|
353
|
+
// Interface → interface: the source's method set must cover the target's.
|
|
354
|
+
if (base?.kind === "interface") {
|
|
355
|
+
for (const [name, required] of iface.methods) {
|
|
356
|
+
if (!this._implementsMethod(required, base.methods.get(name)))
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
350
361
|
const methodMap =
|
|
351
362
|
base?.kind === "struct"
|
|
352
363
|
? base.methods
|
|
@@ -53,8 +53,18 @@ export function setupExtendedGlobals(globals, types) {
|
|
|
53
53
|
DataAttr: { kind: "func", params: [STRING, STRING], returns: [GOM_NODE_T] },
|
|
54
54
|
If: { kind: "func", params: [BOOL, GOM_NODE_T], returns: [GOM_NODE_T] },
|
|
55
55
|
Map: { kind: "func", params: [ANY, ANY], returns: [GOM_NODE_T] },
|
|
56
|
-
Mount: {
|
|
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",
|