gofront 0.0.7 → 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/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 {
@@ -52,6 +54,9 @@ export class CodeGen {
52
54
  this._usesCdiv = false;
53
55
  this._usesError = false;
54
56
  this._usesErrorIs = false;
57
+ this._usesPathClean = false;
58
+ this._usesTimeFmt = false;
59
+ this._usesTimeParse = false;
55
60
  // Iterator (range-over-func) context
56
61
  this._inIteratorBody = false;
57
62
  this._iterDepth = 0;
@@ -143,7 +148,7 @@ export class CodeGen {
143
148
  }
144
149
  }
145
150
 
146
- // Emit functions — rename duplicate init() to init$0, init$1, etc.
151
+ // Emit functions and templ components
147
152
  let initCount = 0;
148
153
  const initNames = [];
149
154
  for (const d of program.decls) {
@@ -158,6 +163,10 @@ export class CodeGen {
158
163
  this.genFuncDecl(d);
159
164
  }
160
165
  this.blank();
166
+ } else if (d.kind === "TemplDecl") {
167
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
168
+ this.genTemplDecl(d);
169
+ this.blank();
161
170
  }
162
171
  }
163
172
 
@@ -226,6 +235,47 @@ export class CodeGen {
226
235
  helpers.push(
227
236
  'var __errorIs = __errorIs || function(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; };',
228
237
  );
238
+ if (this._usesPathClean)
239
+ helpers.push(
240
+ 'var __pathClean = __pathClean || function(p) { if (!p) return "."; const abs = p.startsWith("/"); const parts = p.split("/").reduce((acc, s) => { if (s === "" || s === ".") return acc; if (s === "..") { if (acc.length && acc[acc.length-1] !== "..") acc.pop(); else if (!abs) acc.push(".."); } else acc.push(s); return acc; }, []); return (abs ? "/" : "") + (parts.join("/") || "."); };',
241
+ );
242
+ if (this._usesTimeFmt)
243
+ helpers.push(
244
+ [
245
+ "var __timeFmt = __timeFmt || function(d, layout) {",
246
+ 'const pad = (n, w=2) => String(n).padStart(w, "0");',
247
+ 'const DAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];',
248
+ 'const MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];',
249
+ "return layout.replace(/2006|06|Monday|Mon|January|Jan|01|1|02|2|15|PM|3|04|4|05|5|Z07:00|\\.000|\\.999/g, t => {",
250
+ "switch(t){",
251
+ 'case "2006": return d.getFullYear();',
252
+ 'case "06": return String(d.getFullYear()).slice(-2);',
253
+ 'case "Monday": return DAYS[d.getDay()];',
254
+ 'case "Mon": return DAYS[d.getDay()].slice(0,3);',
255
+ 'case "January": return MONTHS[d.getMonth()];',
256
+ 'case "Jan": return MONTHS[d.getMonth()].slice(0,3);',
257
+ 'case "01": return pad(d.getMonth()+1);',
258
+ 'case "1": return String(d.getMonth()+1);',
259
+ 'case "02": return pad(d.getDate());',
260
+ 'case "2": return String(d.getDate());',
261
+ 'case "15": return pad(d.getHours());',
262
+ 'case "PM": return d.getHours()<12?"AM":"PM";',
263
+ 'case "3": return String(d.getHours()%12||12);',
264
+ 'case "04": return pad(d.getMinutes());',
265
+ 'case "4": return String(d.getMinutes());',
266
+ 'case "05": return pad(d.getSeconds());',
267
+ 'case "5": return String(d.getSeconds());',
268
+ 'case "Z07:00": { const o=-d.getTimezoneOffset(); return o===0?"Z":(o>0?"+":"-")+pad(Math.floor(Math.abs(o)/60))+":"+pad(Math.abs(o)%60); }',
269
+ 'case ".000": return "."+pad(d.getMilliseconds(),3);',
270
+ 'case ".999": return d.getMilliseconds()>0?"."+String(d.getMilliseconds()).replace(/0+$/,""):"";',
271
+ "default: return t;}});",
272
+ "};",
273
+ ].join(""),
274
+ );
275
+ if (this._usesTimeParse)
276
+ helpers.push(
277
+ 'var __timeParse = __timeParse || function(layout, value) { try { const d = new Date(value); if (isNaN(d.getTime())) throw 0; return [{_d:d}, null]; } catch { return [{_d:new Date(0)}, "parsing time: cannot parse"]; } };',
278
+ );
229
279
 
230
280
  if (helpers.length > 0) this.out.unshift(...helpers, "");
231
281
 
@@ -248,9 +298,10 @@ export class CodeGen {
248
298
 
249
299
  // Returns a source map JSON string for the last generate() call.
250
300
  // sources: string[] of source filenames (relative to the output file).
251
- getSourceMap(sources) {
301
+ // sourcesContent: string[] of original file contents (embedded for DevTools breakpoints).
302
+ getSourceMap(sources, sourcesContent) {
252
303
  const srcArray = Array.isArray(sources) ? sources : [sources];
253
- return buildSourceMap(srcArray, this._srcMappings);
304
+ return buildSourceMap(srcArray, this._srcMappings, sourcesContent);
254
305
  }
255
306
 
256
307
  // ── Type declarations ────────────────────────────────────────
@@ -422,6 +473,277 @@ export class CodeGen {
422
473
  this.line("}");
423
474
  }
424
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
+
425
747
  _withNamedReturns(decl, fn) {
426
748
  const named = decl.returnType?._namedReturns;
427
749
  const prev = this.namedReturnVars;
@@ -534,7 +856,10 @@ export class CodeGen {
534
856
  }
535
857
  } else {
536
858
  const zero = spec.type ? this.zeroValueForTypeNode(spec.type) : "null";
537
- for (const name of spec.names) this.line(`let ${name} = ${zero};`);
859
+ for (const name of spec.names) {
860
+ const val = this._boxedVars.has(name) ? `{ value: ${zero} }` : zero;
861
+ this.line(`let ${name} = ${val};`);
862
+ }
538
863
  }
539
864
  }
540
865
  }
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
  }
@@ -170,7 +204,8 @@ export function compileFiles(files, options = {}) {
170
204
  if (options.sourceMap) {
171
205
  const outputDir = options.outputDir ?? fromDir;
172
206
  const sourceFiles = files.map((f) => relative(outputDir, f));
173
- const map = codegen.getSourceMap(sourceFiles);
207
+ const sourcesContent = files.map((f) => readFileSync(f, "utf8"));
208
+ const map = codegen.getSourceMap(sourceFiles, sourcesContent);
174
209
  const b64 = Buffer.from(map).toString("base64");
175
210
  js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
176
211
  }
package/src/dev-server.js CHANGED
@@ -10,6 +10,7 @@ const MIME = {
10
10
  ".js": "application/javascript; charset=utf-8",
11
11
  ".css": "text/css; charset=utf-8",
12
12
  ".json": "application/json; charset=utf-8",
13
+ ".go": "text/plain; charset=utf-8",
13
14
  ".svg": "image/svg+xml",
14
15
  ".png": "image/png",
15
16
  ".jpg": "image/jpeg",
package/src/index.js CHANGED
@@ -232,7 +232,7 @@ function runCompile() {
232
232
  if (sourceMap) {
233
233
  const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
234
234
  const srcPath = relative(outputDir, inputPath);
235
- const map = cg.getSourceMap(srcPath);
235
+ const map = cg.getSourceMap(srcPath, [source]);
236
236
  const b64 = Buffer.from(map).toString("base64");
237
237
  js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
238
238
  }
@@ -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
 
@@ -20,10 +20,14 @@ export const statementParserMethods = {
20
20
  const t = this.peek();
21
21
  const stmt = this._parseStmt(t);
22
22
  if (Array.isArray(stmt)) {
23
- for (const s of stmt) s._line = t.line;
23
+ for (const s of stmt) {
24
+ s._line = t.line;
25
+ s._col = t.col;
26
+ }
24
27
  return stmt;
25
28
  }
26
29
  stmt._line = t.line;
30
+ stmt._col = t.col;
27
31
  return stmt;
28
32
  },
29
33
 
package/src/parser.js CHANGED
@@ -107,7 +107,14 @@ export class Parser {
107
107
  if (Array.isArray(result)) decls.push(...result);
108
108
  else decls.push(result);
109
109
  }
110
- return { kind: "Program", pkg, imports, decls, _filename: this.filename };
110
+ return {
111
+ kind: "Program",
112
+ pkg,
113
+ imports,
114
+ decls,
115
+ _filename: this.filename,
116
+ _source: this.source,
117
+ };
111
118
  }
112
119
 
113
120
  parsePackage() {