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/README.md +104 -18
- package/package.json +9 -4
- package/src/codegen/expressions.js +351 -6
- package/src/codegen/source-map.js +8 -5
- package/src/codegen.js +329 -4
- package/src/compiler.js +39 -4
- package/src/dev-server.js +1 -0
- package/src/index.js +10 -6
- 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/resolver.js +29 -0
- package/src/templ-lexer.js +737 -0
- package/src/templ-parser.js +235 -0
- package/src/typechecker/stdlib.js +305 -2
- package/src/typechecker/types.js +6 -18
- package/src/typechecker.js +30 -2
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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(
|
|
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
|
|
package/src/parser/statements.js
CHANGED
|
@@ -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)
|
|
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 {
|
|
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() {
|