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 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 three example apps — all implement the same todo app to show different aspects
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) // → Type error in src/main.go at line 5: cannot use int as string
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 (~1076 tests, no browser required)
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** (1076) cover language features, type errors, edge cases, DOM (jsdom),
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, and
573
- generics.
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** (74, Playwright) run all three example apps in a real browser and verify
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, and `gom.If` conditional rendering.
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.8",
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 — rename duplicate init() to init$0, init$1, etc.
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(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
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")) return;
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
- let s = "";
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
- s += "\n";
273
+ parts.push("\n");
274
274
  break;
275
275
  case "t":
276
- s += "\t";
276
+ parts.push("\t");
277
277
  break;
278
278
  case "r":
279
- s += "\r";
279
+ parts.push("\r");
280
280
  break;
281
281
  case '"':
282
- s += '"';
282
+ parts.push('"');
283
283
  break;
284
284
  case "\\":
285
- s += "\\";
285
+ parts.push("\\");
286
286
  break;
287
287
  case "0":
288
- s += "\0";
288
+ parts.push("\0");
289
289
  break;
290
290
  default:
291
- s += `\\${esc}`;
291
+ parts.push(`\\${esc}`);
292
292
  }
293
293
  } else {
294
- s += this.advance();
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 s;
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
- let s = "";
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 s;
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 line = this.peek().line;
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