gofront 0.0.8 → 0.0.9
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 +84 -8
- package/package.json +3 -2
- package/src/codegen.js +278 -1
- package/src/compiler.js +37 -3
- package/src/index.js +9 -5
- package/src/lexer.js +14 -14
- package/src/parser/expressions.js +3 -2
- package/src/parser/statements.js +5 -1
- package/src/parser.js +8 -1
- package/src/templ-lexer.js +737 -0
- package/src/templ-parser.js +235 -0
- package/src/typechecker/types.js +6 -3
- package/src/typechecker.js +30 -2
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ quite felt like home either.
|
|
|
7
7
|
So I built GoFront. Go syntax and type safety, compiling to plain ES modules. One language
|
|
8
8
|
front and back, no runtime, no framework, no tsconfig.json.
|
|
9
9
|
|
|
10
|
+
With built-in support for declarative DOM rendering via the [`gom`](https://www.gomponents.com) standard library, JSX-like [`.templ`](https://templ.guide/) files, and seamless integration with external JavaScript libraries through TypeScript definition files (`.d.ts`), GoFront is designed specifically as a frontend development target. Build complex, reactive user interfaces entirely in Go.
|
|
11
|
+
|
|
10
12
|
Probably not useful. Definitely fun to build.
|
|
11
13
|
|
|
12
14
|
```go
|
|
@@ -61,6 +63,12 @@ source text (.go files)
|
|
|
61
63
|
→ Parser recursive-descent → AST
|
|
62
64
|
→ Type Checker annotate AST with types + collect errors
|
|
63
65
|
→ Code Gen AST → JavaScript string
|
|
66
|
+
|
|
67
|
+
source text (.templ files)
|
|
68
|
+
→ TemplLexer dual-mode: Go mode for declarations, HTML mode inside templ bodies
|
|
69
|
+
→ TemplParser extends Parser; produces TemplDecl AST nodes with TemplNode children
|
|
70
|
+
→ Type Checker registers each templ component as func(...) gom.Node
|
|
71
|
+
→ Code Gen TemplDecl → direct DOM calls (createElement / setAttribute / appendChild)
|
|
64
72
|
```
|
|
65
73
|
|
|
66
74
|
### 1. Lexer (`src/lexer.js`)
|
|
@@ -184,7 +192,7 @@ The type checker enforces correctness; JavaScript doesn't need to know.
|
|
|
184
192
|
|
|
185
193
|
## Examples — Todo App
|
|
186
194
|
|
|
187
|
-
There are
|
|
195
|
+
There are four example apps — all implement the same todo app to show different aspects
|
|
188
196
|
of GoFront.
|
|
189
197
|
|
|
190
198
|
### Simple (vanilla DOM)
|
|
@@ -244,6 +252,60 @@ The reactive example covers the full reactive.js API surface:
|
|
|
244
252
|
| Component instance | `signal`, `effect`, `on` (auto-cleanup event listeners) |
|
|
245
253
|
| Scan attributes | `data-model`, `data-text`, `data-html`, `data-if`, `data-visible`, `data-class-*`, `data-attr-*`, `data-bool-*`, `data-on-*`, `data-ref` |
|
|
246
254
|
|
|
255
|
+
### templ (template files + direct DOM codegen)
|
|
256
|
+
|
|
257
|
+
Same todo app using GoFront's `.templ` file format — a JSX-like syntax embedded directly
|
|
258
|
+
in `.go` packages. `templ` declarations compile to direct `document.createElement` calls
|
|
259
|
+
with no intermediate representation, producing `{Mount(p){}}` objects that are fully
|
|
260
|
+
compatible with the `gom.Node` interface.
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
example/templ/
|
|
264
|
+
src/
|
|
265
|
+
render.templ ← UI as templ declarations: TodoItem, TodoList, Header, InputRow, StatsBar, AppView, …
|
|
266
|
+
render.go ← helper functions (todoItemClass, filterLabel, …) + render() entry point
|
|
267
|
+
types.go ← Todo struct, filter/priority constants
|
|
268
|
+
store.go ← state, mutations, localStorage persistence
|
|
269
|
+
styles.go ← appStyles() CSS string
|
|
270
|
+
main.go ← event setup, entry point
|
|
271
|
+
utils/ ← Filter[T], Plural, HasText
|
|
272
|
+
app.js ← generated output
|
|
273
|
+
index.html ← HTML shell
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`.templ` syntax at a glance:
|
|
277
|
+
|
|
278
|
+
```go
|
|
279
|
+
templ TodoItem(t Todo) {
|
|
280
|
+
<li class={ todoItemClass(t) } draggable="true" data-id={ t.id }>
|
|
281
|
+
<input type="checkbox" checked?={ t.done }/>
|
|
282
|
+
<span class="todo-text">{ t.text }</span>
|
|
283
|
+
if t.isUrgent() {
|
|
284
|
+
<span class="badge">urgent</span>
|
|
285
|
+
}
|
|
286
|
+
</li>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
templ AppView() {
|
|
290
|
+
<div class="card">
|
|
291
|
+
@Header(highCount(), syncMsg, syncCls)
|
|
292
|
+
@TodoList(visibleTodos())
|
|
293
|
+
</div>
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Key syntax features:
|
|
298
|
+
- `{ expr }` — interpolate any Go expression (strings auto-cast, others use `String()`)
|
|
299
|
+
- `attr={ expr }` — dynamic attribute value (expression)
|
|
300
|
+
- `attr?={ expr }` — boolean attribute (present/absent based on truthiness)
|
|
301
|
+
- `@Component(args)` — call another templ component and mount it as a child
|
|
302
|
+
- `if cond { } else if cond { } else { }` — conditional rendering (arbitrary depth)
|
|
303
|
+
- `for _, v := range slice { }` — loop rendering inside template bodies
|
|
304
|
+
- `switch expr { case v: ... default: ... }` — switch rendering inside template bodies
|
|
305
|
+
- `@templ.Raw(htmlStr)` — inject raw/trusted HTML (uses `insertAdjacentHTML`)
|
|
306
|
+
- Components are called like regular functions (`AppView()`, `TodoItem(t)`) and return
|
|
307
|
+
a `gom.Node`-compatible object, so `gom.Mount("#app", AppView())` works directly.
|
|
308
|
+
|
|
247
309
|
### gom (stdlib component library + todo app)
|
|
248
310
|
|
|
249
311
|
Browser-native declarative DOM components inspired by
|
|
@@ -299,12 +361,19 @@ declarative node-tree pattern (no `innerHTML`, no `querySelector`), and full fea
|
|
|
299
361
|
parity with the other examples (priority mode, validation, localStorage persistence,
|
|
300
362
|
sync status, drag-and-drop reordering).
|
|
301
363
|
|
|
364
|
+
The templ example additionally demonstrates: **`.templ` file compilation**, template
|
|
365
|
+
components with parameters, `{ expr }` interpolation, `attr?={}` conditional boolean
|
|
366
|
+
attributes, `@Component()` calls inside templates, `if / else if / else` chains,
|
|
367
|
+
`switch` blocks, `for range` loops, and `@templ.Raw()` for trusted HTML injection —
|
|
368
|
+
all inside template bodies.
|
|
369
|
+
|
|
302
370
|
### Build and run
|
|
303
371
|
|
|
304
372
|
```sh
|
|
305
373
|
npm run build:simple # → example/simple/app.js
|
|
306
374
|
npm run build:reactive # → example/reactive/app.js
|
|
307
375
|
npm run build:gom # → example/gom/app.js
|
|
376
|
+
npm run build:templ # → example/templ/app.js
|
|
308
377
|
# open the respective index.html in a browser
|
|
309
378
|
```
|
|
310
379
|
|
|
@@ -377,7 +446,10 @@ func greet(name string) {
|
|
|
377
446
|
console.log("Hello, " + name)
|
|
378
447
|
}
|
|
379
448
|
|
|
380
|
-
greet(42)
|
|
449
|
+
greet(42)
|
|
450
|
+
// → Type error in src/main.go at line 5:7: cannot use int as string
|
|
451
|
+
// 5 | greet(42)
|
|
452
|
+
// ^
|
|
381
453
|
```
|
|
382
454
|
|
|
383
455
|
External TypeScript type definitions are supported via `js:` imports:
|
|
@@ -496,6 +568,7 @@ signatures into GoFront's internal type representation.
|
|
|
496
568
|
| Unused import detection | ✓ |
|
|
497
569
|
| External `.d.ts` types | ✓ |
|
|
498
570
|
| npm package type resolution | ✓ |
|
|
571
|
+
| `.templ` files in packages | ✓ — mix `.go` and `.templ` files freely; templ components visible across the whole package |
|
|
499
572
|
|
|
500
573
|
---
|
|
501
574
|
|
|
@@ -561,18 +634,21 @@ Design documents for planned features are organised by release under `docs/v*/`
|
|
|
561
634
|
## Tests
|
|
562
635
|
|
|
563
636
|
```sh
|
|
564
|
-
npm run test:unit # unit tests only (~
|
|
637
|
+
npm run test:unit # unit tests only (~1100 tests, no browser required)
|
|
565
638
|
npm run test:e2e # E2E tests (Playwright, headless Chromium)
|
|
566
639
|
npm test # both
|
|
567
640
|
```
|
|
568
641
|
|
|
569
|
-
**Unit tests** (
|
|
642
|
+
**Unit tests** (~1100) cover language features, type errors, edge cases, DOM (jsdom),
|
|
570
643
|
external `.d.ts`, npm resolver, multi-file compilation, embedded structs, string
|
|
571
644
|
formatting, map iteration order, integer overflow semantics, unused variable detection,
|
|
572
|
-
unused import detection, semantic difference verification, stdlib shim packages,
|
|
573
|
-
|
|
645
|
+
unused import detection, semantic difference verification, stdlib shim packages, generics,
|
|
646
|
+
and `.templ` file compilation (element rendering, interpolation, boolean attrs, component
|
|
647
|
+
calls, `if/else/else-if` chains, `for range`, `switch/case/default`, `@templ.Raw()` raw
|
|
648
|
+
HTML injection, mixed `.go`+`.templ` packages).
|
|
574
649
|
|
|
575
|
-
**E2E tests** (
|
|
650
|
+
**E2E tests** (~104, Playwright) run all four example apps in a real browser and verify
|
|
576
651
|
CRUD, filtering, priority mode, persistence (reload), drag-and-drop reordering, and sync
|
|
577
652
|
status. Per-app suites check app-specific behaviour: scoped styles, stats bar, loading
|
|
578
|
-
placeholder,
|
|
653
|
+
placeholder, `gom.If` conditional rendering, and templ-specific features (`if/else`
|
|
654
|
+
priority hint, `for` loop rendering, conditional bool attributes).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"author": "Luuk van Venrooij",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
"build:simple": "node src/index.js example/simple/src -o example/simple/app.js",
|
|
35
35
|
"build:reactive": "node src/index.js example/reactive/src -o example/reactive/app.js",
|
|
36
36
|
"build:gom": "node src/index.js example/gom/src -o example/gom/app.js",
|
|
37
|
+
"build:templ": "node src/index.js example/templ/src -o example/templ/app.js",
|
|
37
38
|
"test": "npm run test:unit && npm run test:e2e",
|
|
38
39
|
"test:unit": "node test/unit/run.js",
|
|
39
40
|
"test:e2e": "playwright test",
|
|
40
41
|
"format": "biome format --write .",
|
|
41
|
-
"check": "biome check . && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check",
|
|
42
|
+
"check": "biome check . && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check && node src/index.js example/templ/src --check",
|
|
42
43
|
"prepare": "husky"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
package/src/codegen.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { expressionGenMethods } from "./codegen/expressions.js";
|
|
21
21
|
import { buildSourceMap } from "./codegen/source-map.js";
|
|
22
22
|
import { statementGenMethods } from "./codegen/statements.js";
|
|
23
|
+
import { T, Token } from "./lexer.js";
|
|
24
|
+
import { Parser } from "./parser.js";
|
|
23
25
|
import { isComplex, isNumeric } from "./typechecker/types.js";
|
|
24
26
|
|
|
25
27
|
export class CodeGen {
|
|
@@ -146,7 +148,7 @@ export class CodeGen {
|
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
// Emit functions
|
|
151
|
+
// Emit functions and templ components
|
|
150
152
|
let initCount = 0;
|
|
151
153
|
const initNames = [];
|
|
152
154
|
for (const d of program.decls) {
|
|
@@ -161,6 +163,10 @@ export class CodeGen {
|
|
|
161
163
|
this.genFuncDecl(d);
|
|
162
164
|
}
|
|
163
165
|
this.blank();
|
|
166
|
+
} else if (d.kind === "TemplDecl") {
|
|
167
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
168
|
+
this.genTemplDecl(d);
|
|
169
|
+
this.blank();
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
|
|
@@ -467,6 +473,277 @@ export class CodeGen {
|
|
|
467
473
|
this.line("}");
|
|
468
474
|
}
|
|
469
475
|
|
|
476
|
+
// ── TemplDecl code generation ────────────────────────────────
|
|
477
|
+
|
|
478
|
+
genTemplDecl(decl) {
|
|
479
|
+
const params = decl.params
|
|
480
|
+
.map((p, i) =>
|
|
481
|
+
p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
|
|
482
|
+
)
|
|
483
|
+
.join(", ");
|
|
484
|
+
this.line(`function ${decl.name}(${params}) {`);
|
|
485
|
+
this.indented(() => {
|
|
486
|
+
this.line(`return {Mount(___p) {`);
|
|
487
|
+
this.indented(() => {
|
|
488
|
+
this._genTemplNodes(decl.body, "___p");
|
|
489
|
+
});
|
|
490
|
+
this.line(`}};`);
|
|
491
|
+
});
|
|
492
|
+
this.line("}");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Emit mount statements for an array of TemplNodes into parent variable `p`.
|
|
496
|
+
_genTemplNodes(nodes, p) {
|
|
497
|
+
if (!nodes || nodes.length === 0) return;
|
|
498
|
+
if (nodes.length === 1) {
|
|
499
|
+
this._genTemplNodeMount(nodes[0], p);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// Wrap multiple root nodes in a fragment div? No — emit each directly.
|
|
503
|
+
for (const node of nodes) {
|
|
504
|
+
this._genTemplNodeMount(node, p);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Emit JS to mount a single TemplNode onto parent `p`.
|
|
509
|
+
_genTemplNodeMount(node, p) {
|
|
510
|
+
switch (node.kind) {
|
|
511
|
+
case "TemplElement":
|
|
512
|
+
return this._genTemplElement(node, p);
|
|
513
|
+
case "TemplText":
|
|
514
|
+
return this.line(
|
|
515
|
+
`${p}.appendChild(document.createTextNode(${JSON.stringify(node.value)}));`,
|
|
516
|
+
);
|
|
517
|
+
case "TemplExpr":
|
|
518
|
+
return this.line(
|
|
519
|
+
`${p}.appendChild(document.createTextNode(String(${this._genTemplTokenExpr(node.tokens)})));`,
|
|
520
|
+
);
|
|
521
|
+
case "TemplComponent": {
|
|
522
|
+
// Special case: @templ.Raw(expr) → insertAdjacentHTML
|
|
523
|
+
const toks = node.tokens;
|
|
524
|
+
if (
|
|
525
|
+
toks.length >= 4 &&
|
|
526
|
+
toks[0]?.value === "templ" &&
|
|
527
|
+
toks[1]?.type === T.DOT &&
|
|
528
|
+
toks[2]?.value === "Raw" &&
|
|
529
|
+
toks[3]?.type === T.LPAREN
|
|
530
|
+
) {
|
|
531
|
+
const argTokens = toks.slice(4, toks.length - 1);
|
|
532
|
+
const argJs = this._genTemplTokenExpr(argTokens);
|
|
533
|
+
return this.line(`${p}.insertAdjacentHTML("beforeend", ${argJs});`);
|
|
534
|
+
}
|
|
535
|
+
return this.line(
|
|
536
|
+
`(${this._genTemplTokenCallExpr(node.tokens)}).Mount(${p});`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
case "TemplChildren":
|
|
540
|
+
// children is a variadic gom.Node param — mount each child
|
|
541
|
+
return this.line(
|
|
542
|
+
`if(typeof children!=="undefined")(Array.isArray(children)?children:[...children]).forEach(___c=>___c?.Mount?.(${p}));`,
|
|
543
|
+
);
|
|
544
|
+
case "TemplIf":
|
|
545
|
+
return this._genTemplIf(node, p);
|
|
546
|
+
case "TemplFor":
|
|
547
|
+
return this._genTemplFor(node, p);
|
|
548
|
+
case "TemplSwitch":
|
|
549
|
+
return this._genTemplSwitch(node, p);
|
|
550
|
+
default:
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
_genTemplElement(node, p) {
|
|
556
|
+
const { tag, attrs, children } = node;
|
|
557
|
+
const elVar = this._freshVar("e");
|
|
558
|
+
this.line(
|
|
559
|
+
`const ${elVar} = document.createElement(${JSON.stringify(tag)});`,
|
|
560
|
+
);
|
|
561
|
+
for (const attr of attrs) {
|
|
562
|
+
this._genTemplAttr(attr, elVar);
|
|
563
|
+
}
|
|
564
|
+
this._genTemplNodes(children, elVar);
|
|
565
|
+
this.line(`${p}.appendChild(${elVar});`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
_genTemplAttr(attr, el) {
|
|
569
|
+
const { name } = attr;
|
|
570
|
+
if (attr.kind === "static") {
|
|
571
|
+
if (name === "class") {
|
|
572
|
+
this.line(`${el}.className = ${JSON.stringify(attr.value)};`);
|
|
573
|
+
} else {
|
|
574
|
+
this.line(
|
|
575
|
+
`${el}.setAttribute(${JSON.stringify(name)}, ${JSON.stringify(attr.value)});`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
} else if (attr.kind === "expr") {
|
|
579
|
+
const exprJs = this._genTemplTokenExpr(attr.tokens);
|
|
580
|
+
if (name === "class") {
|
|
581
|
+
this.line(`${el}.className = ${exprJs};`);
|
|
582
|
+
} else {
|
|
583
|
+
this.line(
|
|
584
|
+
`${el}.setAttribute(${JSON.stringify(name)}, String(${exprJs}));`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
} else if (attr.kind === "bool") {
|
|
588
|
+
this.line(`${el}.setAttribute(${JSON.stringify(name)}, "");`);
|
|
589
|
+
} else if (attr.kind === "cond-bool") {
|
|
590
|
+
const exprJs = this._genTemplTokenExpr(attr.tokens);
|
|
591
|
+
this.line(
|
|
592
|
+
`if(${exprJs})${el}.setAttribute(${JSON.stringify(name)}, "");`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_genTemplIf(node, p) {
|
|
598
|
+
const condJs = this._genTemplCondTokens(node.condTokens);
|
|
599
|
+
this.line(`if (${condJs}) {`);
|
|
600
|
+
this.indented(() => this._genTemplNodes(node.then, p));
|
|
601
|
+
this._genTemplIfElse(node.else_, p);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
_genTemplIfElse(else_, p) {
|
|
605
|
+
if (!else_ || else_.length === 0) {
|
|
606
|
+
this.line(`}`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (else_.length === 1 && else_[0].kind === "TemplIf") {
|
|
610
|
+
const elseIf = else_[0];
|
|
611
|
+
this.line(`} else if (${this._genTemplCondTokens(elseIf.condTokens)}) {`);
|
|
612
|
+
this.indented(() => this._genTemplNodes(elseIf.then, p));
|
|
613
|
+
this._genTemplIfElse(elseIf.else_, p);
|
|
614
|
+
} else {
|
|
615
|
+
this.line(`} else {`);
|
|
616
|
+
this.indented(() => this._genTemplNodes(else_, p));
|
|
617
|
+
this.line(`}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_genTemplSwitch(node, p) {
|
|
622
|
+
const { exprTokens, cases } = node;
|
|
623
|
+
if (cases.length === 0) return;
|
|
624
|
+
const exprJs = this._genTemplTokenExpr(exprTokens);
|
|
625
|
+
this.line(`switch (${exprJs}) {`);
|
|
626
|
+
this.indented(() => {
|
|
627
|
+
for (const c of cases) {
|
|
628
|
+
const label =
|
|
629
|
+
c.caseTokens === null
|
|
630
|
+
? "default"
|
|
631
|
+
: `case ${this._genTemplTokenExpr(c.caseTokens)}`;
|
|
632
|
+
this.line(`${label}: {`);
|
|
633
|
+
this.indented(() => this._genTemplNodes(c.body, p));
|
|
634
|
+
this.line(`break; }`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
this.line(`}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_genTemplFor(node, p) {
|
|
641
|
+
// stmtTokens include the `for` keyword plus the range clause (no body).
|
|
642
|
+
// Strip any trailing semicolons (inserted by the Go lexer on newlines),
|
|
643
|
+
// then add a fake empty body `{ }` so the parser can complete the ForStmt.
|
|
644
|
+
let tokens = node.stmtTokens;
|
|
645
|
+
while (tokens.length > 0 && tokens[tokens.length - 1].type === T.SEMICOLON)
|
|
646
|
+
tokens = tokens.slice(0, -1);
|
|
647
|
+
const eofTok = new Token(T.EOF, "", 1, 1);
|
|
648
|
+
const lbrace = new Token(T.LBRACE, "{", 1, 1);
|
|
649
|
+
const rbrace = new Token(T.RBRACE, "}", 1, 1);
|
|
650
|
+
const semi = new Token(T.SEMICOLON, ";", 1, 1);
|
|
651
|
+
const withFor =
|
|
652
|
+
tokens[0]?.type === T.FOR
|
|
653
|
+
? [...tokens, lbrace, rbrace, semi, eofTok]
|
|
654
|
+
: [
|
|
655
|
+
new Token(T.FOR, "for", 1, 1),
|
|
656
|
+
...tokens,
|
|
657
|
+
lbrace,
|
|
658
|
+
rbrace,
|
|
659
|
+
semi,
|
|
660
|
+
eofTok,
|
|
661
|
+
];
|
|
662
|
+
try {
|
|
663
|
+
const p2 = new Parser(withFor);
|
|
664
|
+
const forStmt = p2.parseFor();
|
|
665
|
+
this._genTemplRangeFor(forStmt, node.body, p);
|
|
666
|
+
} catch {
|
|
667
|
+
// Fallback for non-range loops: emit a comment + skip
|
|
668
|
+
this.line(`/* templ for: ${this._tokensToSource(tokens)} */`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
_genTemplRangeFor(forStmt, templBody, mountParent) {
|
|
673
|
+
const init = forStmt.init;
|
|
674
|
+
if (!init?.rhs?.[0] || init.rhs[0].kind !== "RangeExpr") {
|
|
675
|
+
// Plain for loop (cond-only or three-clause)
|
|
676
|
+
const condJs = forStmt.cond ? this.genExpr(forStmt.cond) : "true";
|
|
677
|
+
this.line(`while (${condJs}) {`);
|
|
678
|
+
this.indented(() => this._genTemplNodes(templBody, mountParent));
|
|
679
|
+
this.line("}");
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Range for: for key, val := range iter
|
|
683
|
+
const range = init.rhs[0];
|
|
684
|
+
const iterJs = this.genExpr(range.expr);
|
|
685
|
+
const lhs = init.lhs.map((e) => e.name ?? "_");
|
|
686
|
+
const keyName = lhs[0] ?? "_";
|
|
687
|
+
const valName = lhs[1] ?? null;
|
|
688
|
+
|
|
689
|
+
let loopHeader;
|
|
690
|
+
if (valName === null || valName === undefined) {
|
|
691
|
+
loopHeader = `for (const ${keyName === "_" ? "_$" : keyName} of ${iterJs}) {`;
|
|
692
|
+
} else if (keyName === "_" && valName === "_") {
|
|
693
|
+
loopHeader = `for (const _$ of ${iterJs}) {`;
|
|
694
|
+
} else if (keyName === "_") {
|
|
695
|
+
loopHeader = `for (const ${valName} of ${iterJs}) {`;
|
|
696
|
+
} else if (valName === "_") {
|
|
697
|
+
loopHeader = `for (const [${keyName}] of Object.entries(${iterJs})) {`;
|
|
698
|
+
} else {
|
|
699
|
+
// Default to .entries() for arrays, Object.entries for objects
|
|
700
|
+
this._usesSliceGuard = true;
|
|
701
|
+
loopHeader = `for (const [${keyName}, ${valName}] of __s(${iterJs}).entries()) {`;
|
|
702
|
+
}
|
|
703
|
+
this.line(loopHeader);
|
|
704
|
+
this.indented(() => this._genTemplNodes(templBody, mountParent));
|
|
705
|
+
this.line("}");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Parse a token array as a Go expression and emit JS.
|
|
709
|
+
_genTemplTokenExpr(tokens) {
|
|
710
|
+
if (!tokens || tokens.length === 0) return '""';
|
|
711
|
+
const eofTok = new Token(T.EOF, "", 1, 1);
|
|
712
|
+
const p = new Parser([...tokens, eofTok]);
|
|
713
|
+
const ast = p.parseExpr();
|
|
714
|
+
return this.genExpr(ast);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Parse a token array as a Go call expression and emit JS.
|
|
718
|
+
_genTemplTokenCallExpr(tokens) {
|
|
719
|
+
if (!tokens || tokens.length === 0) return "null";
|
|
720
|
+
const eofTok = new Token(T.EOF, "", 1, 1);
|
|
721
|
+
const p = new Parser([...tokens, eofTok]);
|
|
722
|
+
const ast = p.parseExpr();
|
|
723
|
+
return this.genExpr(ast);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Extract condition JS from condTokens (which may start with `if`).
|
|
727
|
+
_genTemplCondTokens(tokens) {
|
|
728
|
+
if (!tokens || tokens.length === 0) return "false";
|
|
729
|
+
let toks = tokens;
|
|
730
|
+
if (toks[0]?.type === T.IF) toks = toks.slice(1); // skip `if` keyword
|
|
731
|
+
const eofTok = new Token(T.EOF, "", 1, 1);
|
|
732
|
+
const p = new Parser([...toks, eofTok]);
|
|
733
|
+
const ast = p.parseExpr();
|
|
734
|
+
return this.genExpr(ast);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
_tokensToSource(tokens) {
|
|
738
|
+
return tokens.map((t) => t.value).join(" ");
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Fresh variable name counter
|
|
742
|
+
_freshVar(prefix = "v") {
|
|
743
|
+
this._templVarCount = (this._templVarCount ?? 0) + 1;
|
|
744
|
+
return `___${prefix}${this._templVarCount}`;
|
|
745
|
+
}
|
|
746
|
+
|
|
470
747
|
_withNamedReturns(decl, fn) {
|
|
471
748
|
const named = decl.returnType?._namedReturns;
|
|
472
749
|
const prev = this.namedReturnVars;
|
package/src/compiler.js
CHANGED
|
@@ -15,29 +15,63 @@
|
|
|
15
15
|
// Cross-package access uses the qualified form: `pkg.Foo`. The codegen
|
|
16
16
|
// de-qualifies it because the dependency is inlined.
|
|
17
17
|
|
|
18
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
18
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
19
19
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
20
20
|
import { CodeGen } from "./codegen.js";
|
|
21
21
|
import { parseDts } from "./dts-parser.js";
|
|
22
22
|
import { Lexer } from "./lexer.js";
|
|
23
23
|
import { Parser } from "./parser.js";
|
|
24
24
|
import { isLocalPath, resolveAll, resolveGwDir } from "./resolver.js";
|
|
25
|
+
import { TemplLexer } from "./templ-lexer.js";
|
|
26
|
+
import { TemplParser } from "./templ-parser.js";
|
|
25
27
|
import { TypeChecker } from "./typechecker.js";
|
|
26
28
|
|
|
29
|
+
// ── Parse cache ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
// Map<filePath, { mtime: number, ast: object }>
|
|
32
|
+
const _parseCache = new Map();
|
|
33
|
+
|
|
34
|
+
export function clearParseCache() {
|
|
35
|
+
_parseCache.clear();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseCacheSize() {
|
|
39
|
+
return _parseCache.size;
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
// ── Helpers ──────────────────────────────────────────────────
|
|
28
43
|
|
|
29
44
|
function parseFile(filePath) {
|
|
45
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
46
|
+
const cached = _parseCache.get(filePath);
|
|
47
|
+
if (cached && cached.mtime === mtime) return cached.ast;
|
|
48
|
+
|
|
30
49
|
const source = readFileSync(filePath, "utf8");
|
|
31
50
|
const filename = basename(filePath);
|
|
32
51
|
const tokens = new Lexer(source, filename).tokenize();
|
|
33
52
|
const ast = new Parser(tokens, filename, source).parse();
|
|
34
53
|
ast._source = source;
|
|
54
|
+
_parseCache.set(filePath, { mtime, ast });
|
|
55
|
+
return ast;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseTemplFile(filePath) {
|
|
59
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
60
|
+
const cached = _parseCache.get(filePath);
|
|
61
|
+
if (cached && cached.mtime === mtime) return cached.ast;
|
|
62
|
+
|
|
63
|
+
const source = readFileSync(filePath, "utf8");
|
|
64
|
+
const filename = basename(filePath);
|
|
65
|
+
const tokens = new TemplLexer(source, filename).tokenize();
|
|
66
|
+
const ast = new TemplParser(tokens, filename, source).parse();
|
|
67
|
+
ast._source = source;
|
|
68
|
+
_parseCache.set(filePath, { mtime, ast });
|
|
35
69
|
return ast;
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
function gwFilesIn(dir) {
|
|
39
73
|
return readdirSync(dir)
|
|
40
|
-
.filter((f) => f.endsWith(".go"))
|
|
74
|
+
.filter((f) => f.endsWith(".go") || f.endsWith(".templ"))
|
|
41
75
|
.sort() // deterministic order
|
|
42
76
|
.map((f) => join(dir, f));
|
|
43
77
|
}
|
|
@@ -58,7 +92,7 @@ export function compileFiles(files, options = {}) {
|
|
|
58
92
|
const programs = [];
|
|
59
93
|
for (const f of files) {
|
|
60
94
|
try {
|
|
61
|
-
programs.push(parseFile(f));
|
|
95
|
+
programs.push(f.endsWith(".templ") ? parseTemplFile(f) : parseFile(f));
|
|
62
96
|
} catch (e) {
|
|
63
97
|
parseErrors.push(e.message);
|
|
64
98
|
}
|
package/src/index.js
CHANGED
|
@@ -311,7 +311,7 @@ if (serveMode) {
|
|
|
311
311
|
devServer = createDevServer(serveDir, servePort);
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
function buildOnce() {
|
|
314
|
+
function buildOnce(changedFile = null) {
|
|
315
315
|
try {
|
|
316
316
|
const startMs = performance.now();
|
|
317
317
|
const result = runCompile();
|
|
@@ -326,16 +326,19 @@ function buildOnce() {
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
329
|
+
const changeNote = changedFile ? ` — ${changedFile} changed` : "";
|
|
329
330
|
if (outputFile) {
|
|
330
331
|
writeFileSync(outputFile, `${js}\n`);
|
|
331
332
|
console.error(
|
|
332
|
-
`[${timestamp()}] gofront: OK — wrote ${outputFile} (${elapsedMs}ms)`,
|
|
333
|
+
`[${timestamp()}] gofront: OK — wrote ${outputFile} (${elapsedMs}ms${changeNote})`,
|
|
333
334
|
);
|
|
334
335
|
} else {
|
|
335
336
|
// Clear screen then print
|
|
336
337
|
process.stdout.write("\x1Bc");
|
|
337
338
|
console.log(js);
|
|
338
|
-
console.error(
|
|
339
|
+
console.error(
|
|
340
|
+
`[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
|
|
341
|
+
);
|
|
339
342
|
}
|
|
340
343
|
devServer?.notify();
|
|
341
344
|
} catch (e) {
|
|
@@ -352,9 +355,10 @@ const watchTarget = isDir ? inputPath : dirname(inputPath);
|
|
|
352
355
|
|
|
353
356
|
let debounce = null;
|
|
354
357
|
watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
355
|
-
if (filename && !filename.endsWith(".go"))
|
|
358
|
+
if (filename && !filename.endsWith(".go") && !filename.endsWith(".templ"))
|
|
359
|
+
return;
|
|
356
360
|
clearTimeout(debounce);
|
|
357
|
-
debounce = setTimeout(() => buildOnce(), 80);
|
|
361
|
+
debounce = setTimeout(() => buildOnce(filename), 80);
|
|
358
362
|
});
|
|
359
363
|
|
|
360
364
|
console.error(`[${timestamp()}] gofront: watching ${inputArg} ...`);
|
package/src/lexer.js
CHANGED
|
@@ -262,7 +262,7 @@ export class Lexer {
|
|
|
262
262
|
const l = this.line,
|
|
263
263
|
c = this.col;
|
|
264
264
|
this.advance(); // opening "
|
|
265
|
-
|
|
265
|
+
const parts = [];
|
|
266
266
|
while (this.pos < this.src.length && this.peek() !== '"') {
|
|
267
267
|
if (this.peek() === "\n") this.err("Unterminated string literal");
|
|
268
268
|
if (this.peek() === "\\") {
|
|
@@ -270,43 +270,42 @@ export class Lexer {
|
|
|
270
270
|
const esc = this.advance();
|
|
271
271
|
switch (esc) {
|
|
272
272
|
case "n":
|
|
273
|
-
|
|
273
|
+
parts.push("\n");
|
|
274
274
|
break;
|
|
275
275
|
case "t":
|
|
276
|
-
|
|
276
|
+
parts.push("\t");
|
|
277
277
|
break;
|
|
278
278
|
case "r":
|
|
279
|
-
|
|
279
|
+
parts.push("\r");
|
|
280
280
|
break;
|
|
281
281
|
case '"':
|
|
282
|
-
|
|
282
|
+
parts.push('"');
|
|
283
283
|
break;
|
|
284
284
|
case "\\":
|
|
285
|
-
|
|
285
|
+
parts.push("\\");
|
|
286
286
|
break;
|
|
287
287
|
case "0":
|
|
288
|
-
|
|
288
|
+
parts.push("\0");
|
|
289
289
|
break;
|
|
290
290
|
default:
|
|
291
|
-
|
|
291
|
+
parts.push(`\\${esc}`);
|
|
292
292
|
}
|
|
293
293
|
} else {
|
|
294
|
-
|
|
294
|
+
parts.push(this.advance());
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
if (this.pos >= this.src.length)
|
|
298
298
|
throw new LexError("Unterminated string", l, c, this.filename, this.src);
|
|
299
299
|
this.advance(); // closing "
|
|
300
|
-
return
|
|
300
|
+
return parts.join("");
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
readRawString() {
|
|
304
304
|
const l = this.line,
|
|
305
305
|
c = this.col;
|
|
306
306
|
this.advance(); // opening `
|
|
307
|
-
|
|
308
|
-
while (this.pos < this.src.length && this.peek() !== "`")
|
|
309
|
-
s += this.advance();
|
|
307
|
+
const start = this.pos;
|
|
308
|
+
while (this.pos < this.src.length && this.peek() !== "`") this.advance();
|
|
310
309
|
if (this.pos >= this.src.length)
|
|
311
310
|
throw new LexError(
|
|
312
311
|
"Unterminated raw string",
|
|
@@ -315,8 +314,9 @@ export class Lexer {
|
|
|
315
314
|
this.filename,
|
|
316
315
|
this.src,
|
|
317
316
|
);
|
|
317
|
+
const raw = this.src.slice(start, this.pos);
|
|
318
318
|
this.advance(); // closing `
|
|
319
|
-
return
|
|
319
|
+
return raw;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
readRuneLiteral() {
|
|
@@ -365,9 +365,10 @@ export const expressionParserMethods = {
|
|
|
365
365
|
},
|
|
366
366
|
|
|
367
367
|
parsePrimary() {
|
|
368
|
-
const
|
|
368
|
+
const t = this.peek();
|
|
369
369
|
const expr = this._parsePrimary();
|
|
370
|
-
if (!expr._line) expr._line = line;
|
|
370
|
+
if (!expr._line) expr._line = t.line;
|
|
371
|
+
if (!expr._col) expr._col = t.col;
|
|
371
372
|
return expr;
|
|
372
373
|
},
|
|
373
374
|
|