gofront 0.0.5 → 0.0.6
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 +25 -19
- package/package.json +1 -1
- package/src/codegen/expressions.js +337 -4
- package/src/codegen/source-map.js +13 -5
- package/src/codegen.js +31 -17
- package/src/compiler.js +10 -3
- package/src/dev-server.js +83 -0
- package/src/index.js +35 -5
- package/src/typechecker.js +216 -0
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
## About
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Go for the backend: simple, type-safe, no nonsense. JavaScript for the frontend: runs
|
|
4
|
+
everywhere, no setup. The problem is JavaScript's loose typing — and TypeScript never
|
|
5
|
+
quite felt like home either.
|
|
6
6
|
|
|
7
|
-
So I built GoFront. Go syntax
|
|
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
|
-
|
|
10
|
+
Probably not useful. Definitely fun to build.
|
|
11
11
|
|
|
12
12
|
```go
|
|
13
13
|
package main
|
|
@@ -258,16 +258,18 @@ npm run build:reactive # → example/reactive/app.js
|
|
|
258
258
|
gofront <file.go> compile single file → stdout
|
|
259
259
|
gofront <dir> compile all *.go in directory → stdout
|
|
260
260
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
261
|
-
gofront <input> --check
|
|
262
|
-
gofront <input> --watch
|
|
263
|
-
gofront <input>
|
|
264
|
-
gofront <input> --
|
|
265
|
-
gofront <input> --
|
|
266
|
-
gofront <
|
|
267
|
-
gofront <
|
|
268
|
-
gofront
|
|
269
|
-
gofront --
|
|
270
|
-
gofront
|
|
261
|
+
gofront <input> --check type-check only
|
|
262
|
+
gofront <input> --watch watch for changes and recompile
|
|
263
|
+
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
264
|
+
gofront <input> -o out.js --serve --port 8080 use a custom port
|
|
265
|
+
gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
|
|
266
|
+
gofront <input> --minify minify output (built-in minifier)
|
|
267
|
+
gofront <input> --minify --mangle minify and rename local identifiers
|
|
268
|
+
gofront <file.go> --ast dump AST (debug)
|
|
269
|
+
gofront <file.go> --tokens dump tokens (debug)
|
|
270
|
+
gofront init [dir] scaffold a new project
|
|
271
|
+
gofront --version / -v print version
|
|
272
|
+
gofront --help / -h print this help
|
|
271
273
|
```
|
|
272
274
|
|
|
273
275
|
---
|
|
@@ -403,14 +405,18 @@ signatures into GoFront's internal type representation.
|
|
|
403
405
|
|
|
404
406
|
| Package | Functions |
|
|
405
407
|
|---|---|
|
|
406
|
-
| `fmt` | `Sprintf`, `Printf`, `Println`, `Print`, `Errorf` — format verbs: `%v`, `%d`, `%s`, `%t`, `%x`, `%o`, `%b`, `%q`, `%e`, `%g`, `%w`, width/precision |
|
|
407
|
-
| `strings` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `LastIndex`, `Count`, `Repeat`, `Replace`, `ReplaceAll`, `ToUpper`, `ToLower`, `TrimSpace`, `Trim`, `TrimPrefix`, `TrimSuffix`, `TrimLeft`, `TrimRight`, `Split`, `Join`, `EqualFold` |
|
|
408
|
-
| `bytes` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `Join`, `Split`, `Replace`, `ToUpper`, `ToLower`, `TrimSpace`, `Equal`, `Count`, `Repeat` |
|
|
408
|
+
| `fmt` | `Sprintf`, `Printf`, `Println`, `Print`, `Errorf`, `Fprintf`, `Fprintln`, `Fprint` — format verbs: `%v`, `%d`, `%s`, `%t`, `%x`, `%o`, `%b`, `%q`, `%e`, `%g`, `%w`, width/precision |
|
|
409
|
+
| `strings` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `LastIndex`, `Count`, `Repeat`, `Replace`, `ReplaceAll`, `ToUpper`, `ToLower`, `TrimSpace`, `Trim`, `TrimPrefix`, `TrimSuffix`, `TrimLeft`, `TrimRight`, `Split`, `Join`, `EqualFold`; **`Builder`** type (`WriteString`, `WriteByte`, `WriteRune`, `Write`, `String`, `Len`, `Reset`, `Grow`) |
|
|
410
|
+
| `bytes` | `Contains`, `HasPrefix`, `HasSuffix`, `Index`, `Join`, `Split`, `Replace`, `ToUpper`, `ToLower`, `TrimSpace`, `Equal`, `Count`, `Repeat`; **`Buffer`** type (`WriteString`, `WriteByte`, `Write`, `String`, `Bytes`, `Len`, `Reset`) |
|
|
409
411
|
| `strconv` | `Itoa`, `Atoi`, `FormatBool`, `FormatInt`, `FormatFloat`, `ParseFloat`, `ParseInt`, `ParseBool` |
|
|
410
412
|
| `sort` | `Ints`, `Float64s`, `Strings`, `Slice`, `SliceStable`, `SliceIsSorted` |
|
|
411
413
|
| `math` | `Abs`, `Floor`, `Ceil`, `Round`, `Sqrt`, `Cbrt`, `Pow`, `Log`, `Log2`, `Log10`, trig functions + `Pi`, `E`, `MaxFloat64`, `MaxInt`, `MinInt` |
|
|
412
414
|
| `errors` | `New`, `Is`, `Unwrap` — custom error types via interface satisfaction |
|
|
413
415
|
| `time` | `Now`, `Since`, `Sleep` + `Millisecond`, `Second`, `Minute`, `Hour` constants |
|
|
416
|
+
| `html` | `EscapeString`, `UnescapeString` |
|
|
417
|
+
| `maps` | `Keys`, `Values`, `Clone`, `Copy`, `Equal`, `EqualFunc`, `Delete`, `DeleteFunc` |
|
|
418
|
+
| `slices` | `Contains`, `Index`, `Equal`, `Compare`, `Sort`, `SortFunc`, `SortStableFunc`, `IsSorted`, `IsSortedFunc`, `Reverse`, `Max`, `Min`, `MaxFunc`, `MinFunc`, `Clone`, `Compact`, `CompactFunc`, `Concat`, `Delete`, `DeleteFunc`, `Insert`, `Replace`, `Grow`, `Clip` |
|
|
419
|
+
| `regexp` | `MustCompile`, `Compile`, `MatchString`, `QuoteMeta`; **`*Regexp`** methods: `MatchString`, `FindString`, `FindStringIndex`, `FindAllString`, `FindStringSubmatch`, `FindAllStringSubmatch`, `ReplaceAllString`, `ReplaceAllLiteralString`, `Split`, `String`. Inline flags (`(?i)`, `(?m)`, `(?s)`) are extracted automatically into the JS `RegExp` constructor. |
|
|
414
420
|
| `unicode` | `IsLetter`, `IsDigit`, `IsSpace`, `IsUpper`, `IsLower`, `IsPunct`, `IsControl`, `IsPrint`, `IsGraphic`, `ToUpper`, `ToLower` |
|
|
415
421
|
| `os` | `Exit`, `Args`, `Getenv` |
|
|
416
422
|
|
|
@@ -494,7 +500,7 @@ Design documents for planned features are organised by release under `docs/v*/`
|
|
|
494
500
|
npm test
|
|
495
501
|
```
|
|
496
502
|
|
|
497
|
-
|
|
503
|
+
940 tests covering language features, type errors, edge cases, DOM (jsdom), external
|
|
498
504
|
`.d.ts`, npm resolver, multi-file compilation, embedded structs, string formatting, map
|
|
499
505
|
iteration order, integer overflow semantics, unused variable detection, unused import
|
|
500
506
|
detection, semantic difference verification, stdlib shim packages, generics (type params,
|
package/package.json
CHANGED
|
@@ -443,6 +443,19 @@ export const expressionGenMethods = {
|
|
|
443
443
|
if (result !== undefined) return result;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
// strings.Builder / bytes.Buffer / regexp.Regexp method dispatch
|
|
447
|
+
if (expr.func.kind === "SelectorExpr") {
|
|
448
|
+
const recvType = expr.func.expr._type;
|
|
449
|
+
const recvName = recvType?.name ?? recvType?.base?.name;
|
|
450
|
+
if (recvName === "strings.Builder" || recvName === "bytes.Buffer") {
|
|
451
|
+
const typeName = recvType?.name ?? recvType?.base?.name;
|
|
452
|
+
return this._genBuilderCall(typeName, expr.func.field, expr);
|
|
453
|
+
}
|
|
454
|
+
if (recvName === "regexp.Regexp") {
|
|
455
|
+
return this._genRegexpMethodCall(expr.func.field, expr);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
446
459
|
// Mark the callee so SelectorExpr knows it's being called, not used as a value
|
|
447
460
|
expr.func._callee = true;
|
|
448
461
|
const rawFn = this.genExpr(expr.func);
|
|
@@ -662,6 +675,8 @@ export const expressionGenMethods = {
|
|
|
662
675
|
case "TypeName": {
|
|
663
676
|
const basic = this._zeroForBasicName(typeNode.name);
|
|
664
677
|
if (basic !== null) return basic;
|
|
678
|
+
if (typeNode.name === "strings.Builder") return '{ _buf: "" }';
|
|
679
|
+
if (typeNode.name === "bytes.Buffer") return "{ _buf: [] }";
|
|
665
680
|
if (this.structNames.has(typeNode.name))
|
|
666
681
|
return `new ${typeNode.name}()`;
|
|
667
682
|
return "null";
|
|
@@ -706,6 +721,8 @@ export const expressionGenMethods = {
|
|
|
706
721
|
return `{ ${fields} }`;
|
|
707
722
|
}
|
|
708
723
|
case "named":
|
|
724
|
+
if (t.name === "strings.Builder") return '{ _buf: "" }';
|
|
725
|
+
if (t.name === "bytes.Buffer") return "{ _buf: [] }";
|
|
709
726
|
if (this.structNames.has(t.name)) return `new ${t.name}()`;
|
|
710
727
|
return "null";
|
|
711
728
|
default:
|
|
@@ -776,6 +793,268 @@ export const expressionGenMethods = {
|
|
|
776
793
|
return base?.kind === "struct" || base?.kind === "array";
|
|
777
794
|
},
|
|
778
795
|
|
|
796
|
+
_genBuilderCall(typeName, method, expr) {
|
|
797
|
+
// Resolve the receiver — may be a direct var or a &var (pointer)
|
|
798
|
+
const recv = expr.func.expr;
|
|
799
|
+
const isPtr = recv._type?.kind === "pointer";
|
|
800
|
+
const base = isPtr ? `${this.genExpr(recv)}.value` : this.genExpr(recv);
|
|
801
|
+
const args = expr.args.map((a) => this.genExpr(a));
|
|
802
|
+
|
|
803
|
+
if (typeName === "strings.Builder") {
|
|
804
|
+
switch (method) {
|
|
805
|
+
case "WriteString":
|
|
806
|
+
return `(${base}._buf += ${args[0]}, [${args[0]}.length, null])`;
|
|
807
|
+
case "WriteByte":
|
|
808
|
+
case "WriteRune":
|
|
809
|
+
return `(${base}._buf += String.fromCodePoint(${args[0]}))`;
|
|
810
|
+
case "Write":
|
|
811
|
+
return `(${base}._buf += String.fromCharCode(...${args[0]}))`;
|
|
812
|
+
case "String":
|
|
813
|
+
return `${base}._buf`;
|
|
814
|
+
case "Len":
|
|
815
|
+
return `${base}._buf.length`;
|
|
816
|
+
case "Reset":
|
|
817
|
+
return `(${base}._buf = "")`;
|
|
818
|
+
case "Grow":
|
|
819
|
+
return "undefined";
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (typeName === "bytes.Buffer") {
|
|
824
|
+
switch (method) {
|
|
825
|
+
case "WriteString":
|
|
826
|
+
return `(${base}._buf.push(...new TextEncoder().encode(${args[0]})), [${args[0]}.length, null])`;
|
|
827
|
+
case "WriteByte":
|
|
828
|
+
return `${base}._buf.push(${args[0]})`;
|
|
829
|
+
case "Write":
|
|
830
|
+
return `(${base}._buf.push(...${args[0]}), [${args[0]}.length, null])`;
|
|
831
|
+
case "String":
|
|
832
|
+
return `new TextDecoder().decode(new Uint8Array(${base}._buf))`;
|
|
833
|
+
case "Bytes":
|
|
834
|
+
return `${base}._buf.slice()`;
|
|
835
|
+
case "Len":
|
|
836
|
+
return `${base}._buf.length`;
|
|
837
|
+
case "Reset":
|
|
838
|
+
return `(${base}._buf = [])`;
|
|
839
|
+
case "Grow":
|
|
840
|
+
return "undefined";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return undefined;
|
|
845
|
+
},
|
|
846
|
+
|
|
847
|
+
_genRegexp(fn, a) {
|
|
848
|
+
// Converts a Go regexp pattern string (which may contain inline flags like (?i))
|
|
849
|
+
// into a JS RegExp, extracting the flags into the second constructor argument.
|
|
850
|
+
const makeRegExp = (pat) =>
|
|
851
|
+
`((p) => { const m = /^\\(\\?([gimsuy]+)\\)/.exec(p); return m ? new RegExp(p.slice(m[0].length), m[1]) : new RegExp(p); })(${pat})`;
|
|
852
|
+
switch (fn) {
|
|
853
|
+
case "MustCompile":
|
|
854
|
+
return makeRegExp(a()[0]);
|
|
855
|
+
case "Compile":
|
|
856
|
+
return `[${makeRegExp(a()[0])}, null]`;
|
|
857
|
+
case "MatchString": {
|
|
858
|
+
const [pat, s] = a();
|
|
859
|
+
return `[new RegExp(${pat}).test(${s}), null]`;
|
|
860
|
+
}
|
|
861
|
+
case "QuoteMeta":
|
|
862
|
+
return `${a()[0]}.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&')`;
|
|
863
|
+
}
|
|
864
|
+
return undefined;
|
|
865
|
+
},
|
|
866
|
+
|
|
867
|
+
_genRegexpMethodCall(method, expr) {
|
|
868
|
+
const recv = expr.func.expr;
|
|
869
|
+
// *Regexp is a plain JS RegExp — no .value boxing needed
|
|
870
|
+
const re = this.genExpr(recv);
|
|
871
|
+
const args = expr.args.map((a) => this.genExpr(a));
|
|
872
|
+
|
|
873
|
+
switch (method) {
|
|
874
|
+
case "MatchString":
|
|
875
|
+
return `${re}.test(${args[0]})`;
|
|
876
|
+
case "FindString":
|
|
877
|
+
return `(${re}.exec(${args[0]})?.[0] ?? "")`;
|
|
878
|
+
case "FindStringIndex":
|
|
879
|
+
return `((m => m ? [m.index, m.index + m[0].length] : null)(${re}.exec(${args[0]})))`;
|
|
880
|
+
case "FindAllString":
|
|
881
|
+
return `[...${args[0]}.matchAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"))].slice(0, ${args[1]} < 0 ? undefined : ${args[1]}).map(m => m[0])`;
|
|
882
|
+
case "FindStringSubmatch":
|
|
883
|
+
return `[...(${re}.exec(${args[0]}) ?? [])]`;
|
|
884
|
+
case "FindAllStringSubmatch":
|
|
885
|
+
return `[...${args[0]}.matchAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"))].slice(0, ${args[1]} < 0 ? undefined : ${args[1]}).map(m => [...m])`;
|
|
886
|
+
case "ReplaceAllString":
|
|
887
|
+
return `${args[0]}.replaceAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"), ${args[1]})`;
|
|
888
|
+
case "ReplaceAllLiteralString":
|
|
889
|
+
return `${args[0]}.replaceAll(new RegExp(${re}.source, ${re}.flags.includes("g") ? ${re}.flags : ${re}.flags + "g"), ${args[1]}.replace(/\\$/g, '$$$$'))`;
|
|
890
|
+
case "Split":
|
|
891
|
+
return `${args[0]}.split(${re}).slice(0, ${args[1]} < 0 ? undefined : ${args[1]})`;
|
|
892
|
+
case "String":
|
|
893
|
+
return `${re}.source`;
|
|
894
|
+
}
|
|
895
|
+
return undefined;
|
|
896
|
+
},
|
|
897
|
+
|
|
898
|
+
_genSlices(fn, a) {
|
|
899
|
+
switch (fn) {
|
|
900
|
+
case "Contains": {
|
|
901
|
+
const [s, v] = a();
|
|
902
|
+
return `${s}.includes(${v})`;
|
|
903
|
+
}
|
|
904
|
+
case "Index": {
|
|
905
|
+
const [s, v] = a();
|
|
906
|
+
return `${s}.indexOf(${v})`;
|
|
907
|
+
}
|
|
908
|
+
case "Equal": {
|
|
909
|
+
this._usesEqual = true;
|
|
910
|
+
const [a1, b1] = a();
|
|
911
|
+
return `__equal(${a1}, ${b1})`;
|
|
912
|
+
}
|
|
913
|
+
case "Compare": {
|
|
914
|
+
const [a1, b1] = a();
|
|
915
|
+
return `((a, b) => { for (let i = 0; i < a.length && i < b.length; i++) { if (a[i] < b[i]) return -1; if (a[i] > b[i]) return 1; } return a.length - b.length; })(${a1}, ${b1})`;
|
|
916
|
+
}
|
|
917
|
+
case "Sort": {
|
|
918
|
+
const [s] = a();
|
|
919
|
+
return `${s}.sort((a, b) => a < b ? -1 : a > b ? 1 : 0)`;
|
|
920
|
+
}
|
|
921
|
+
case "SortFunc": {
|
|
922
|
+
const [s, cmp] = a();
|
|
923
|
+
return `${s}.sort(${cmp})`;
|
|
924
|
+
}
|
|
925
|
+
case "SortStableFunc": {
|
|
926
|
+
const [s, cmp] = a();
|
|
927
|
+
return `${s}.sort(${cmp})`;
|
|
928
|
+
}
|
|
929
|
+
case "IsSorted": {
|
|
930
|
+
const [s] = a();
|
|
931
|
+
return `((s) => { for (let i = 1; i < s.length; i++) { if (s[i] < s[i-1]) return false; } return true; })(${s})`;
|
|
932
|
+
}
|
|
933
|
+
case "IsSortedFunc": {
|
|
934
|
+
const [s, cmp] = a();
|
|
935
|
+
return `((s, f) => { for (let i = 1; i < s.length; i++) { if (f(s[i], s[i-1]) < 0) return false; } return true; })(${s}, ${cmp})`;
|
|
936
|
+
}
|
|
937
|
+
case "Reverse": {
|
|
938
|
+
const [s] = a();
|
|
939
|
+
return `${s}.reverse()`;
|
|
940
|
+
}
|
|
941
|
+
case "Max": {
|
|
942
|
+
const [s] = a();
|
|
943
|
+
return `Math.max(...${s})`;
|
|
944
|
+
}
|
|
945
|
+
case "Min": {
|
|
946
|
+
const [s] = a();
|
|
947
|
+
return `Math.min(...${s})`;
|
|
948
|
+
}
|
|
949
|
+
case "MaxFunc": {
|
|
950
|
+
const [s, cmp] = a();
|
|
951
|
+
return `((s, f) => s.reduce((m, x) => f(x, m) > 0 ? x : m))(${s}, ${cmp})`;
|
|
952
|
+
}
|
|
953
|
+
case "MinFunc": {
|
|
954
|
+
const [s, cmp] = a();
|
|
955
|
+
return `((s, f) => s.reduce((m, x) => f(x, m) < 0 ? x : m))(${s}, ${cmp})`;
|
|
956
|
+
}
|
|
957
|
+
case "Clone": {
|
|
958
|
+
const [s] = a();
|
|
959
|
+
return `${s}.slice()`;
|
|
960
|
+
}
|
|
961
|
+
case "Compact": {
|
|
962
|
+
const [s] = a();
|
|
963
|
+
return `((s) => s.filter((v, i) => i === 0 || v !== s[i-1]))(${s})`;
|
|
964
|
+
}
|
|
965
|
+
case "CompactFunc": {
|
|
966
|
+
const [s, eq] = a();
|
|
967
|
+
return `((s, f) => s.filter((v, i) => i === 0 || !f(v, s[i-1])))(${s}, ${eq})`;
|
|
968
|
+
}
|
|
969
|
+
case "Concat": {
|
|
970
|
+
const args = a();
|
|
971
|
+
return `[].concat(${args.join(", ")})`;
|
|
972
|
+
}
|
|
973
|
+
case "Delete": {
|
|
974
|
+
const [s, i, j] = a();
|
|
975
|
+
return `[...${s}.slice(0, ${i}), ...${s}.slice(${j})]`;
|
|
976
|
+
}
|
|
977
|
+
case "DeleteFunc": {
|
|
978
|
+
const [s, fn2] = a();
|
|
979
|
+
return `${s}.filter((v) => !${fn2}(v))`;
|
|
980
|
+
}
|
|
981
|
+
case "Insert": {
|
|
982
|
+
const args = a();
|
|
983
|
+
const s = args[0];
|
|
984
|
+
const i = args[1];
|
|
985
|
+
const vs = args.slice(2);
|
|
986
|
+
return `[...${s}.slice(0, ${i}), ${vs.join(", ")}, ...${s}.slice(${i})]`;
|
|
987
|
+
}
|
|
988
|
+
case "Replace": {
|
|
989
|
+
const args = a();
|
|
990
|
+
const s = args[0];
|
|
991
|
+
const i = args[1];
|
|
992
|
+
const j = args[2];
|
|
993
|
+
const vs = args.slice(3);
|
|
994
|
+
return `[...${s}.slice(0, ${i}), ${vs.join(", ")}, ...${s}.slice(${j})]`;
|
|
995
|
+
}
|
|
996
|
+
case "Grow":
|
|
997
|
+
case "Clip": {
|
|
998
|
+
const [s] = a();
|
|
999
|
+
return `${s}.slice()`;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return undefined;
|
|
1003
|
+
},
|
|
1004
|
+
|
|
1005
|
+
_genHtml(fn, a) {
|
|
1006
|
+
switch (fn) {
|
|
1007
|
+
case "EscapeString": {
|
|
1008
|
+
const [s] = a();
|
|
1009
|
+
return `${s}.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")`;
|
|
1010
|
+
}
|
|
1011
|
+
case "UnescapeString": {
|
|
1012
|
+
const [s] = a();
|
|
1013
|
+
return `${s}.replace(/'/g,"'").replace(/"/g,'"').replace(/>/g,">").replace(/</g,"<").replace(/&/g,"&")`;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return undefined;
|
|
1017
|
+
},
|
|
1018
|
+
|
|
1019
|
+
_genMaps(fn, a) {
|
|
1020
|
+
switch (fn) {
|
|
1021
|
+
case "Keys": {
|
|
1022
|
+
const [m] = a();
|
|
1023
|
+
return `Object.keys(${m})`;
|
|
1024
|
+
}
|
|
1025
|
+
case "Values": {
|
|
1026
|
+
const [m] = a();
|
|
1027
|
+
return `Object.values(${m})`;
|
|
1028
|
+
}
|
|
1029
|
+
case "Clone": {
|
|
1030
|
+
const [m] = a();
|
|
1031
|
+
return `({...${m}})`;
|
|
1032
|
+
}
|
|
1033
|
+
case "Copy": {
|
|
1034
|
+
const [dst, src] = a();
|
|
1035
|
+
return `Object.assign(${dst}, ${src})`;
|
|
1036
|
+
}
|
|
1037
|
+
case "Equal": {
|
|
1038
|
+
this._usesEqual = true;
|
|
1039
|
+
const [a1, b1] = a();
|
|
1040
|
+
return `__equal(${a1}, ${b1})`;
|
|
1041
|
+
}
|
|
1042
|
+
case "EqualFunc": {
|
|
1043
|
+
const [m1, m2, eq] = a();
|
|
1044
|
+
return `((a, b, f) => { const ka = Object.keys(a); if (ka.length !== Object.keys(b).length) return false; return ka.every(k => k in b && f(a[k], b[k])); })(${m1}, ${m2}, ${eq})`;
|
|
1045
|
+
}
|
|
1046
|
+
case "Delete": {
|
|
1047
|
+
const [m, k] = a();
|
|
1048
|
+
return `(delete ${m}[${k}], undefined)`;
|
|
1049
|
+
}
|
|
1050
|
+
case "DeleteFunc": {
|
|
1051
|
+
const [m, fn2] = a();
|
|
1052
|
+
return `Object.keys(${m}).forEach(k => { if (${fn2}(k, ${m}[k])) delete ${m}[k]; })`;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return undefined;
|
|
1056
|
+
},
|
|
1057
|
+
|
|
779
1058
|
typeComment(typeNode) {
|
|
780
1059
|
if (!typeNode) return "unknown";
|
|
781
1060
|
switch (typeNode.kind) {
|
|
@@ -815,6 +1094,14 @@ export const expressionGenMethods = {
|
|
|
815
1094
|
return this._genErrors(fn, a);
|
|
816
1095
|
case "time":
|
|
817
1096
|
return this._genTime(fn, a);
|
|
1097
|
+
case "regexp":
|
|
1098
|
+
return this._genRegexp(fn, a);
|
|
1099
|
+
case "slices":
|
|
1100
|
+
return this._genSlices(fn, a);
|
|
1101
|
+
case "maps":
|
|
1102
|
+
return this._genMaps(fn, a);
|
|
1103
|
+
case "html":
|
|
1104
|
+
return this._genHtml(fn, a);
|
|
818
1105
|
default:
|
|
819
1106
|
return undefined;
|
|
820
1107
|
}
|
|
@@ -843,6 +1130,52 @@ export const expressionGenMethods = {
|
|
|
843
1130
|
case "Println":
|
|
844
1131
|
this._usesSprintf = true;
|
|
845
1132
|
return `console.log(__sprintf(${fmtArgs}))`;
|
|
1133
|
+
case "Fprintf":
|
|
1134
|
+
case "Fprintln":
|
|
1135
|
+
case "Fprint": {
|
|
1136
|
+
this._usesSprintf = true;
|
|
1137
|
+
const writerArg = expr.args[0];
|
|
1138
|
+
const rest = expr.args.slice(1);
|
|
1139
|
+
const restJs = rest.map((e) => this.genExpr(e)).join(", ");
|
|
1140
|
+
// Resolve the writer to its underlying buffer expression
|
|
1141
|
+
const writerType = writerArg._type;
|
|
1142
|
+
const targetTypeName =
|
|
1143
|
+
writerType?.name ??
|
|
1144
|
+
(writerType?.kind === "pointer" ? writerType.base?.name : null);
|
|
1145
|
+
let buf;
|
|
1146
|
+
if (targetTypeName === "strings.Builder") {
|
|
1147
|
+
const w = this.genExpr(writerArg);
|
|
1148
|
+
buf =
|
|
1149
|
+
writerType?.kind === "pointer" ? `${w}.value._buf` : `${w}._buf`;
|
|
1150
|
+
} else if (targetTypeName === "bytes.Buffer") {
|
|
1151
|
+
const w = this.genExpr(writerArg);
|
|
1152
|
+
const base = writerType?.kind === "pointer" ? `${w}.value` : w;
|
|
1153
|
+
buf = null;
|
|
1154
|
+
if (fn === "Fprintf") {
|
|
1155
|
+
return `((__b,__s)=>{ __b._buf.push(...new TextEncoder().encode(__s)); })(${base}, __sprintf(${restJs}))`;
|
|
1156
|
+
}
|
|
1157
|
+
const valJs =
|
|
1158
|
+
fn === "Fprintln"
|
|
1159
|
+
? `__sprintf("%v\\n", ${restJs})`
|
|
1160
|
+
: `__sprintf("%v", ${restJs})`;
|
|
1161
|
+
return `((__b,__s)=>{ __b._buf.push(...new TextEncoder().encode(__s)); })(${base}, ${valJs})`;
|
|
1162
|
+
} else {
|
|
1163
|
+
// Generic io.Writer fallback — call .WriteString if available
|
|
1164
|
+
const w = this.genExpr(writerArg);
|
|
1165
|
+
const formatted =
|
|
1166
|
+
fn === "Fprintf"
|
|
1167
|
+
? `__sprintf(${restJs})`
|
|
1168
|
+
: fn === "Fprintln"
|
|
1169
|
+
? `__sprintf("%v\\n", ${restJs})`
|
|
1170
|
+
: `__sprintf("%v", ${restJs})`;
|
|
1171
|
+
return `${w}.WriteString(${formatted})`;
|
|
1172
|
+
}
|
|
1173
|
+
// strings.Builder path
|
|
1174
|
+
if (fn === "Fprintf") return `(${buf} += __sprintf(${restJs}))`;
|
|
1175
|
+
if (fn === "Fprintln")
|
|
1176
|
+
return `(${buf} += __sprintf("%v\\n", ${restJs}))`;
|
|
1177
|
+
return `(${buf} += __sprintf("%v", ${restJs}))`;
|
|
1178
|
+
}
|
|
846
1179
|
default:
|
|
847
1180
|
return undefined;
|
|
848
1181
|
}
|
|
@@ -862,7 +1195,7 @@ export const expressionGenMethods = {
|
|
|
862
1195
|
case "LastIndex":
|
|
863
1196
|
return `${args[0]}.lastIndexOf(${args[1]})`;
|
|
864
1197
|
case "Count":
|
|
865
|
-
return
|
|
1198
|
+
return `((s, sep) => sep === "" ? s.length + 1 : s.split(sep).length - 1)(${args[0]}, ${args[1]})`;
|
|
866
1199
|
case "Repeat":
|
|
867
1200
|
return `${args[0]}.repeat(${args[1]})`;
|
|
868
1201
|
case "Replace":
|
|
@@ -878,9 +1211,9 @@ export const expressionGenMethods = {
|
|
|
878
1211
|
case "Trim":
|
|
879
1212
|
return `${args[0]}.replace(new RegExp(\`^[\${${args[1]}}]+|[\${${args[1]}}]+$\`, "g"), "")`;
|
|
880
1213
|
case "TrimPrefix":
|
|
881
|
-
return `(
|
|
1214
|
+
return `((s, pre) => s.startsWith(pre) ? s.slice(pre.length) : s)(${args[0]}, ${args[1]})`;
|
|
882
1215
|
case "TrimSuffix":
|
|
883
|
-
return `(
|
|
1216
|
+
return `((s, suf) => !suf.length || !s.endsWith(suf) ? s : s.slice(0, -suf.length))(${args[0]}, ${args[1]})`;
|
|
884
1217
|
case "TrimLeft":
|
|
885
1218
|
return `${args[0]}.replace(new RegExp(\`^[\${${args[1]}}]+\`), "")`;
|
|
886
1219
|
case "TrimRight":
|
|
@@ -910,7 +1243,7 @@ export const expressionGenMethods = {
|
|
|
910
1243
|
case "Index":
|
|
911
1244
|
return `${__bs}(${args[0]}).indexOf(${__bs}(${args[1]}))`;
|
|
912
1245
|
case "Count":
|
|
913
|
-
return
|
|
1246
|
+
return `((b, sep) => sep.length === 0 ? b.length + 1 : ${__bs}(b).split(${__bs}(sep)).length - 1)(${args[0]}, ${args[1]})`;
|
|
914
1247
|
case "Repeat":
|
|
915
1248
|
return `${__sb}(${__bs}(${args[0]}).repeat(${args[1]}))`;
|
|
916
1249
|
case "Replace":
|
|
@@ -16,21 +16,29 @@ function vlqEncode(value) {
|
|
|
16
16
|
return result;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function buildSourceMap(
|
|
20
|
-
//
|
|
19
|
+
export function buildSourceMap(sources, mappings) {
|
|
20
|
+
// sources: string[]
|
|
21
|
+
// mappings: Array<{ genLine: number, srcLine: number, srcFileIdx?: number }> (0-based)
|
|
21
22
|
// Emits one segment per generated line, column 0 → source line (delta-encoded).
|
|
22
23
|
const lines = [];
|
|
23
24
|
let prevSrcLine = 0;
|
|
25
|
+
let prevSrcFile = 0;
|
|
24
26
|
const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
|
|
25
27
|
const byLine = new Map(mappings.map((e) => [e.genLine, e]));
|
|
26
28
|
for (let g = 0; g <= maxGen; g++) {
|
|
27
29
|
const entry = byLine.get(g);
|
|
28
30
|
if (entry) {
|
|
31
|
+
const fileIdx = entry.srcFileIdx ?? 0;
|
|
32
|
+
const fileDelta = fileIdx - prevSrcFile;
|
|
29
33
|
const srcDelta = entry.srcLine - prevSrcLine;
|
|
30
|
-
// Segment: [genCol=0,
|
|
34
|
+
// Segment: [genCol=0, srcFileIdxDelta, srcLineDelta, srcCol=0]
|
|
31
35
|
lines.push(
|
|
32
|
-
vlqEncode(0) +
|
|
36
|
+
vlqEncode(0) +
|
|
37
|
+
vlqEncode(fileDelta) +
|
|
38
|
+
vlqEncode(srcDelta) +
|
|
39
|
+
vlqEncode(0),
|
|
33
40
|
);
|
|
41
|
+
prevSrcFile = fileIdx;
|
|
34
42
|
prevSrcLine = entry.srcLine;
|
|
35
43
|
} else {
|
|
36
44
|
lines.push(""); // no mapping for this generated line
|
|
@@ -38,7 +46,7 @@ export function buildSourceMap(sourceName, mappings) {
|
|
|
38
46
|
}
|
|
39
47
|
return JSON.stringify({
|
|
40
48
|
version: 3,
|
|
41
|
-
sources
|
|
49
|
+
sources,
|
|
42
50
|
names: [],
|
|
43
51
|
mappings: lines.join(";"),
|
|
44
52
|
});
|
package/src/codegen.js
CHANGED
|
@@ -38,7 +38,8 @@ export class CodeGen {
|
|
|
38
38
|
this.jsImports = jsImports;
|
|
39
39
|
this.bundledPackages = bundledPackages;
|
|
40
40
|
this.namedReturnVars = null; // names of current function's named return vars
|
|
41
|
-
this._srcMappings = []; // { genLine, srcLine } for source map
|
|
41
|
+
this._srcMappings = []; // { genLine, srcLine, srcFileIdx } for source map
|
|
42
|
+
this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
|
|
42
43
|
this._boxedVars = new Set(); // address-taken scalar variables that need boxing
|
|
43
44
|
// Runtime helper usage tracking — only emit helpers that are actually used
|
|
44
45
|
this._usesLen = false;
|
|
@@ -67,8 +68,9 @@ export class CodeGen {
|
|
|
67
68
|
if (srcLine != null) {
|
|
68
69
|
this._srcMappings.push({
|
|
69
70
|
genLine: this.out.length,
|
|
70
|
-
srcLine: srcLine - 1,
|
|
71
|
-
|
|
71
|
+
srcLine: srcLine - 1, // 0-based
|
|
72
|
+
srcFileIdx: this._currentSrcFileIdx,
|
|
73
|
+
});
|
|
72
74
|
}
|
|
73
75
|
this.out.push(" ".repeat(this.indent) + s);
|
|
74
76
|
}
|
|
@@ -109,6 +111,7 @@ export class CodeGen {
|
|
|
109
111
|
|
|
110
112
|
for (const d of program.decls) {
|
|
111
113
|
if (d.kind === "TypeDecl") {
|
|
114
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
112
115
|
this.genTypeDeclWithMethods(d, methods.get(d.name) ?? []);
|
|
113
116
|
this.blank();
|
|
114
117
|
}
|
|
@@ -116,10 +119,12 @@ export class CodeGen {
|
|
|
116
119
|
|
|
117
120
|
for (const d of program.decls) {
|
|
118
121
|
if (d.kind === "VarDecl") {
|
|
122
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
119
123
|
this.genVarDecl(d);
|
|
120
124
|
this.blank();
|
|
121
125
|
}
|
|
122
126
|
if (d.kind === "ConstDecl") {
|
|
127
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
123
128
|
this.genConstDecl(d);
|
|
124
129
|
this.blank();
|
|
125
130
|
}
|
|
@@ -130,6 +135,7 @@ export class CodeGen {
|
|
|
130
135
|
const initNames = [];
|
|
131
136
|
for (const d of program.decls) {
|
|
132
137
|
if (d.kind === "FuncDecl") {
|
|
138
|
+
this._currentSrcFileIdx = d._srcFileIdx ?? 0;
|
|
133
139
|
if (d.name === "init") {
|
|
134
140
|
const renamed = initCount === 0 ? "init" : `init$${initCount}`;
|
|
135
141
|
initNames.push(renamed);
|
|
@@ -152,29 +158,31 @@ export class CodeGen {
|
|
|
152
158
|
// Prepend runtime helpers that were actually used
|
|
153
159
|
const helpers = [];
|
|
154
160
|
if (this._usesLen)
|
|
155
|
-
helpers.push(
|
|
161
|
+
helpers.push(
|
|
162
|
+
"var __len = __len || function(a) { if (a && typeof a === 'object' && !Array.isArray(a)) return Object.keys(a).length; return a?.length ?? 0; };",
|
|
163
|
+
);
|
|
156
164
|
if (this._usesAppend)
|
|
157
165
|
helpers.push(
|
|
158
|
-
"
|
|
166
|
+
"var __append = __append || function(a, ...b) { return a ? [...a, ...b] : b; };",
|
|
159
167
|
);
|
|
160
168
|
if (this._usesSliceGuard)
|
|
161
|
-
helpers.push("
|
|
169
|
+
helpers.push("var __s = __s || function(a) { return a || []; };");
|
|
162
170
|
if (this._usesEqual)
|
|
163
171
|
helpers.push(
|
|
164
|
-
'function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;}',
|
|
172
|
+
'var __equal = __equal || function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;};',
|
|
165
173
|
);
|
|
166
174
|
if (this._usesCmul)
|
|
167
175
|
helpers.push(
|
|
168
|
-
"
|
|
176
|
+
"var __cmul = __cmul || function(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; };",
|
|
169
177
|
);
|
|
170
178
|
if (this._usesCdiv)
|
|
171
179
|
helpers.push(
|
|
172
|
-
"
|
|
180
|
+
"var __cdiv = __cdiv || function(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; };",
|
|
173
181
|
);
|
|
174
182
|
if (this._usesSprintf)
|
|
175
183
|
helpers.push(
|
|
176
184
|
[
|
|
177
|
-
"
|
|
185
|
+
"var __sprintf = __sprintf || function(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
|
|
178
186
|
"if(m==='%%')return'%';const fl=m.slice(1,-1),verb=m.slice(-1),v=a[i++];",
|
|
179
187
|
"const pad=(s,w,z)=>{w=parseInt(w)||0;if(!w)return s;const p=(z?'0':' ').repeat(Math.max(0,w-s.length));return fl.includes('-')?s+p:p+s;};",
|
|
180
188
|
"const [,flags,width,prec]=m.match(/^%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)/)||[];",
|
|
@@ -194,16 +202,16 @@ export class CodeGen {
|
|
|
194
202
|
"case'g':case'G':{const n=Number(v);return pad(prec!==''?n.toPrecision(parseInt(prec)):String(n),width,zero);}",
|
|
195
203
|
"case'w':return pad(String(v==null?'<nil>':typeof v==='object'&&v.Error?v.Error():v),width,false);",
|
|
196
204
|
"default:return m;}});",
|
|
197
|
-
"}",
|
|
205
|
+
"};",
|
|
198
206
|
].join(""),
|
|
199
207
|
);
|
|
200
208
|
if (this._usesError)
|
|
201
209
|
helpers.push(
|
|
202
|
-
"
|
|
210
|
+
"var __error = __error || function(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; };",
|
|
203
211
|
);
|
|
204
212
|
if (this._usesErrorIs)
|
|
205
213
|
helpers.push(
|
|
206
|
-
'
|
|
214
|
+
'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; };',
|
|
207
215
|
);
|
|
208
216
|
|
|
209
217
|
if (helpers.length > 0) this.out.unshift(...helpers, "");
|
|
@@ -214,16 +222,22 @@ export class CodeGen {
|
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
// Generate a single bundle from multiple programs (same-package multi-file).
|
|
217
|
-
//
|
|
225
|
+
// Annotates each decl with its source file index before merging.
|
|
218
226
|
generateAll(programs) {
|
|
227
|
+
for (let i = 0; i < programs.length; i++) {
|
|
228
|
+
for (const decl of programs[i].decls) {
|
|
229
|
+
decl._srcFileIdx = i;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
219
232
|
const merged = { decls: programs.flatMap((p) => p.decls) };
|
|
220
233
|
return this.generate(merged);
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
// Returns a source map JSON string for the last generate() call.
|
|
224
|
-
//
|
|
225
|
-
getSourceMap(
|
|
226
|
-
|
|
237
|
+
// sources: string[] of source filenames (relative to the output file).
|
|
238
|
+
getSourceMap(sources) {
|
|
239
|
+
const srcArray = Array.isArray(sources) ? sources : [sources];
|
|
240
|
+
return buildSourceMap(srcArray, this._srcMappings);
|
|
227
241
|
}
|
|
228
242
|
|
|
229
243
|
// ── Type declarations ────────────────────────────────────────
|
package/src/compiler.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// de-qualifies it because the dependency is inlined.
|
|
17
17
|
|
|
18
18
|
import { readdirSync, readFileSync } from "node:fs";
|
|
19
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
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";
|
|
@@ -165,8 +165,15 @@ export function compileFiles(files, options = {}) {
|
|
|
165
165
|
const codegen = new CodeGen(checker, jsImports, bundledPackages);
|
|
166
166
|
const mainJs = codegen.generateAll(programs);
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
|
|
169
|
+
|
|
170
|
+
if (options.sourceMap) {
|
|
171
|
+
const outputDir = options.outputDir ?? fromDir;
|
|
172
|
+
const sourceFiles = files.map((f) => relative(outputDir, f));
|
|
173
|
+
const map = codegen.getSourceMap(sourceFiles);
|
|
174
|
+
const b64 = Buffer.from(map).toString("base64");
|
|
175
|
+
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
176
|
+
}
|
|
170
177
|
|
|
171
178
|
return {
|
|
172
179
|
pkgName,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Minimal dev server for gofront --serve watch mode.
|
|
2
|
+
// Serves static files and pushes a reload event via SSE after each build.
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const MIME = {
|
|
9
|
+
".html": "text/html; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".jpg": "image/jpeg",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
".woff2": "font/woff2",
|
|
18
|
+
".woff": "font/woff",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Injected at the bottom of compiled JS in serve mode.
|
|
22
|
+
export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
|
|
23
|
+
|
|
24
|
+
export function createDevServer(serveDir, port) {
|
|
25
|
+
const clients = new Set();
|
|
26
|
+
|
|
27
|
+
const server = createServer((req, res) => {
|
|
28
|
+
// SSE endpoint — browser connects here to receive reload events
|
|
29
|
+
if (req.url === "/_gofront/events") {
|
|
30
|
+
res.writeHead(200, {
|
|
31
|
+
"Content-Type": "text/event-stream",
|
|
32
|
+
"Cache-Control": "no-cache",
|
|
33
|
+
Connection: "keep-alive",
|
|
34
|
+
});
|
|
35
|
+
res.write(": connected\n\n");
|
|
36
|
+
clients.add(res);
|
|
37
|
+
req.on("close", () => clients.delete(res));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let urlPath = req.url.split("?")[0];
|
|
42
|
+
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
43
|
+
|
|
44
|
+
const filePath = join(serveDir, urlPath);
|
|
45
|
+
if (!existsSync(filePath)) {
|
|
46
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
47
|
+
res.end("Not found");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const data = readFileSync(filePath);
|
|
53
|
+
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
54
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
55
|
+
res.end(data);
|
|
56
|
+
} catch {
|
|
57
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
58
|
+
res.end("Server error");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
server.on("error", (err) => {
|
|
63
|
+
if (err.code === "EADDRINUSE") {
|
|
64
|
+
console.error(
|
|
65
|
+
`gofront: port ${port} already in use — try --port <number>`,
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`gofront: dev server error: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
server.listen(port, () => {
|
|
73
|
+
console.error(`gofront: dev server → http://localhost:${port}`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function notify() {
|
|
77
|
+
for (const client of clients) {
|
|
78
|
+
client.write("event: reload\ndata: {}\n\n");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { notify };
|
|
83
|
+
}
|
package/src/index.js
CHANGED
|
@@ -26,9 +26,10 @@ import { createRequire } from "node:module";
|
|
|
26
26
|
const _require = createRequire(import.meta.url);
|
|
27
27
|
const { version } = _require("../package.json");
|
|
28
28
|
|
|
29
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
29
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
30
30
|
import { CodeGen } from "./codegen.js";
|
|
31
31
|
import { compileDir } from "./compiler.js";
|
|
32
|
+
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
32
33
|
import { parseDts } from "./dts-parser.js";
|
|
33
34
|
import { Lexer } from "./lexer.js";
|
|
34
35
|
import { minify } from "./minifier.js";
|
|
@@ -55,6 +56,8 @@ Usage:
|
|
|
55
56
|
gofront <input> -o out.js Compile and write to file
|
|
56
57
|
gofront <input> --check Type-check only
|
|
57
58
|
gofront <input> --watch Watch for changes and recompile
|
|
59
|
+
gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
|
|
60
|
+
gofront <input> -o out.js --serve --port 8080 Use a custom port
|
|
58
61
|
gofront <input> --source-map Append inline source map to output
|
|
59
62
|
gofront <input> --minify Minify output
|
|
60
63
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
@@ -120,9 +123,12 @@ const checkOnly = args.includes("--check");
|
|
|
120
123
|
const dumpAst = args.includes("--ast");
|
|
121
124
|
const dumpTokens = args.includes("--tokens");
|
|
122
125
|
const sourceMap = args.includes("--source-map");
|
|
123
|
-
const
|
|
126
|
+
const serveMode = args.includes("--serve");
|
|
127
|
+
const watchMode = args.includes("--watch") || serveMode;
|
|
124
128
|
const minifyOutput = args.includes("--minify");
|
|
125
129
|
const mangleOutput = args.includes("--mangle");
|
|
130
|
+
const portFlag = args.indexOf("--port");
|
|
131
|
+
const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
|
|
126
132
|
|
|
127
133
|
// ── Determine input mode ─────────────────────────────────────
|
|
128
134
|
|
|
@@ -139,7 +145,8 @@ try {
|
|
|
139
145
|
|
|
140
146
|
function runCompile() {
|
|
141
147
|
if (isDir) {
|
|
142
|
-
|
|
148
|
+
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
149
|
+
return compileDir(inputPath, { sourceMap, outputDir });
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
// Single-file mode
|
|
@@ -223,7 +230,9 @@ function runCompile() {
|
|
|
223
230
|
let js = cg.generate(ast);
|
|
224
231
|
|
|
225
232
|
if (sourceMap) {
|
|
226
|
-
const
|
|
233
|
+
const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
|
|
234
|
+
const srcPath = relative(outputDir, inputPath);
|
|
235
|
+
const map = cg.getSourceMap(srcPath);
|
|
227
236
|
const b64 = Buffer.from(map).toString("base64");
|
|
228
237
|
js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
|
|
229
238
|
}
|
|
@@ -291,11 +300,31 @@ function timestamp() {
|
|
|
291
300
|
return new Date().toLocaleTimeString();
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
// Start dev server before first build so the browser can connect immediately
|
|
304
|
+
let devServer = null;
|
|
305
|
+
if (serveMode) {
|
|
306
|
+
if (!outputFile) {
|
|
307
|
+
console.error("gofront: --serve requires -o <output file>");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const serveDir = dirname(resolve(outputFile));
|
|
311
|
+
devServer = createDevServer(serveDir, servePort);
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
function buildOnce() {
|
|
295
315
|
try {
|
|
296
316
|
const startMs = performance.now();
|
|
297
317
|
const result = runCompile();
|
|
298
|
-
|
|
318
|
+
let js = maybeMinify(result.js);
|
|
319
|
+
if (serveMode) {
|
|
320
|
+
// Insert live reload client before the source map comment so it stays last
|
|
321
|
+
const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
|
|
322
|
+
if (smIdx !== -1) {
|
|
323
|
+
js = `${js.slice(0, smIdx)}\n${liveReloadClient}${js.slice(smIdx)}`;
|
|
324
|
+
} else {
|
|
325
|
+
js += `\n${liveReloadClient}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
299
328
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
300
329
|
if (outputFile) {
|
|
301
330
|
writeFileSync(outputFile, `${js}\n`);
|
|
@@ -308,6 +337,7 @@ function buildOnce() {
|
|
|
308
337
|
console.log(js);
|
|
309
338
|
console.error(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
|
|
310
339
|
}
|
|
340
|
+
devServer?.notify();
|
|
311
341
|
} catch (e) {
|
|
312
342
|
console.error(`[${timestamp()}] gofront: ERROR`);
|
|
313
343
|
for (const line of e.message.split("\n")) console.error(` ${line}`);
|
package/src/typechecker.js
CHANGED
|
@@ -146,6 +146,24 @@ export class TypeChecker {
|
|
|
146
146
|
Printf: fmtVariadic(VOID),
|
|
147
147
|
Println: fmtVariadic(VOID),
|
|
148
148
|
Print: fmtVariadic(VOID),
|
|
149
|
+
Fprintf: {
|
|
150
|
+
kind: "func",
|
|
151
|
+
params: [ANY, STRING],
|
|
152
|
+
returns: [INT, ERROR],
|
|
153
|
+
variadic: true,
|
|
154
|
+
},
|
|
155
|
+
Fprintln: {
|
|
156
|
+
kind: "func",
|
|
157
|
+
params: [ANY],
|
|
158
|
+
returns: [INT, ERROR],
|
|
159
|
+
variadic: true,
|
|
160
|
+
},
|
|
161
|
+
Fprint: {
|
|
162
|
+
kind: "func",
|
|
163
|
+
params: [ANY],
|
|
164
|
+
returns: [INT, ERROR],
|
|
165
|
+
variadic: true,
|
|
166
|
+
},
|
|
149
167
|
},
|
|
150
168
|
});
|
|
151
169
|
|
|
@@ -431,6 +449,195 @@ export class TypeChecker {
|
|
|
431
449
|
},
|
|
432
450
|
});
|
|
433
451
|
|
|
452
|
+
// slices package (Go 1.21)
|
|
453
|
+
// Functions are generic — use ANY for slice params so any concrete slice type
|
|
454
|
+
// is accepted without an assignability error (same pattern as sort.Slice).
|
|
455
|
+
this.globals.define("slices", {
|
|
456
|
+
kind: "namespace",
|
|
457
|
+
name: "slices",
|
|
458
|
+
members: {
|
|
459
|
+
Contains: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
|
|
460
|
+
Index: { kind: "func", params: [ANY, ANY], returns: [INT] },
|
|
461
|
+
Equal: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
|
|
462
|
+
Compare: { kind: "func", params: [ANY, ANY], returns: [INT] },
|
|
463
|
+
Sort: { kind: "func", params: [ANY], returns: [VOID] },
|
|
464
|
+
SortFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
|
|
465
|
+
SortStableFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
|
|
466
|
+
IsSorted: { kind: "func", params: [ANY], returns: [BOOL] },
|
|
467
|
+
IsSortedFunc: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
|
|
468
|
+
Reverse: { kind: "func", params: [ANY], returns: [VOID] },
|
|
469
|
+
Max: { kind: "func", params: [ANY], returns: [ANY] },
|
|
470
|
+
Min: { kind: "func", params: [ANY], returns: [ANY] },
|
|
471
|
+
MaxFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
|
|
472
|
+
MinFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
|
|
473
|
+
Clone: { kind: "func", params: [ANY], returns: [ANY] },
|
|
474
|
+
Compact: { kind: "func", params: [ANY], returns: [ANY] },
|
|
475
|
+
CompactFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
|
|
476
|
+
Concat: { kind: "func", params: [ANY], returns: [ANY], variadic: true },
|
|
477
|
+
Delete: { kind: "func", params: [ANY, INT, INT], returns: [ANY] },
|
|
478
|
+
DeleteFunc: { kind: "func", params: [ANY, ANY], returns: [ANY] },
|
|
479
|
+
Insert: {
|
|
480
|
+
kind: "func",
|
|
481
|
+
params: [ANY, INT, ANY],
|
|
482
|
+
returns: [ANY],
|
|
483
|
+
variadic: true,
|
|
484
|
+
},
|
|
485
|
+
Replace: {
|
|
486
|
+
kind: "func",
|
|
487
|
+
params: [ANY, INT, INT, ANY],
|
|
488
|
+
returns: [ANY],
|
|
489
|
+
variadic: true,
|
|
490
|
+
},
|
|
491
|
+
Grow: { kind: "func", params: [ANY, INT], returns: [ANY] },
|
|
492
|
+
Clip: { kind: "func", params: [ANY], returns: [ANY] },
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// html package
|
|
497
|
+
this.globals.define("html", {
|
|
498
|
+
kind: "namespace",
|
|
499
|
+
name: "html",
|
|
500
|
+
members: {
|
|
501
|
+
EscapeString: { kind: "func", params: [STRING], returns: [STRING] },
|
|
502
|
+
UnescapeString: { kind: "func", params: [STRING], returns: [STRING] },
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// maps package (Go 1.21)
|
|
507
|
+
this.globals.define("maps", {
|
|
508
|
+
kind: "namespace",
|
|
509
|
+
name: "maps",
|
|
510
|
+
members: {
|
|
511
|
+
Keys: { kind: "func", params: [ANY], returns: [ANY] },
|
|
512
|
+
Values: { kind: "func", params: [ANY], returns: [ANY] },
|
|
513
|
+
Clone: { kind: "func", params: [ANY], returns: [ANY] },
|
|
514
|
+
Copy: { kind: "func", params: [ANY, ANY], returns: [VOID] },
|
|
515
|
+
Equal: { kind: "func", params: [ANY, ANY], returns: [BOOL] },
|
|
516
|
+
EqualFunc: { kind: "func", params: [ANY, ANY, ANY], returns: [BOOL] },
|
|
517
|
+
Delete: { kind: "func", params: [ANY, ANY], returns: [VOID] },
|
|
518
|
+
DeleteFunc: { kind: "func", params: [ANY, ANY], returns: [VOID] },
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// regexp package
|
|
523
|
+
const REGEXP_T = {
|
|
524
|
+
kind: "named",
|
|
525
|
+
name: "regexp.Regexp",
|
|
526
|
+
underlying: { kind: "struct", fields: new Map(), methods: new Map() },
|
|
527
|
+
};
|
|
528
|
+
const REGEXP_PTR = { kind: "pointer", base: REGEXP_T };
|
|
529
|
+
const STR_SLICE = { kind: "slice", elem: STRING };
|
|
530
|
+
const INT_SLICE = { kind: "slice", elem: INT };
|
|
531
|
+
const regexpMethods = new Map([
|
|
532
|
+
["MatchString", { kind: "func", params: [STRING], returns: [BOOL] }],
|
|
533
|
+
["FindString", { kind: "func", params: [STRING], returns: [STRING] }],
|
|
534
|
+
[
|
|
535
|
+
"FindStringIndex",
|
|
536
|
+
{ kind: "func", params: [STRING], returns: [INT_SLICE] },
|
|
537
|
+
],
|
|
538
|
+
[
|
|
539
|
+
"FindAllString",
|
|
540
|
+
{ kind: "func", params: [STRING, INT], returns: [STR_SLICE] },
|
|
541
|
+
],
|
|
542
|
+
[
|
|
543
|
+
"FindStringSubmatch",
|
|
544
|
+
{ kind: "func", params: [STRING], returns: [STR_SLICE] },
|
|
545
|
+
],
|
|
546
|
+
[
|
|
547
|
+
"FindAllStringSubmatch",
|
|
548
|
+
{
|
|
549
|
+
kind: "func",
|
|
550
|
+
params: [STRING, INT],
|
|
551
|
+
returns: [{ kind: "slice", elem: STR_SLICE }],
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
"ReplaceAllString",
|
|
556
|
+
{ kind: "func", params: [STRING, STRING], returns: [STRING] },
|
|
557
|
+
],
|
|
558
|
+
[
|
|
559
|
+
"ReplaceAllLiteralString",
|
|
560
|
+
{ kind: "func", params: [STRING, STRING], returns: [STRING] },
|
|
561
|
+
],
|
|
562
|
+
["Split", { kind: "func", params: [STRING, INT], returns: [STR_SLICE] }],
|
|
563
|
+
["String", { kind: "func", params: [], returns: [STRING] }],
|
|
564
|
+
]);
|
|
565
|
+
REGEXP_T.underlying.methods = regexpMethods;
|
|
566
|
+
this.types.set("regexp.Regexp", REGEXP_T);
|
|
567
|
+
this.globals.define("regexp", {
|
|
568
|
+
kind: "namespace",
|
|
569
|
+
name: "regexp",
|
|
570
|
+
members: {
|
|
571
|
+
MustCompile: { kind: "func", params: [STRING], returns: [REGEXP_PTR] },
|
|
572
|
+
Compile: {
|
|
573
|
+
kind: "func",
|
|
574
|
+
params: [STRING],
|
|
575
|
+
returns: [REGEXP_PTR, ERROR],
|
|
576
|
+
},
|
|
577
|
+
MatchString: {
|
|
578
|
+
kind: "func",
|
|
579
|
+
params: [STRING, STRING],
|
|
580
|
+
returns: [BOOL, ERROR],
|
|
581
|
+
},
|
|
582
|
+
QuoteMeta: { kind: "func", params: [STRING], returns: [STRING] },
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// strings.Builder
|
|
587
|
+
const BYTE_SLICE_T = { kind: "slice", elem: INT };
|
|
588
|
+
const strBuilderMethods = new Map([
|
|
589
|
+
[
|
|
590
|
+
"WriteString",
|
|
591
|
+
{ kind: "func", params: [STRING], returns: [INT, ERROR] },
|
|
592
|
+
],
|
|
593
|
+
["WriteByte", { kind: "func", params: [INT], returns: [ERROR] }],
|
|
594
|
+
["WriteRune", { kind: "func", params: [INT], returns: [INT, ERROR] }],
|
|
595
|
+
[
|
|
596
|
+
"Write",
|
|
597
|
+
{ kind: "func", params: [BYTE_SLICE_T], returns: [INT, ERROR] },
|
|
598
|
+
],
|
|
599
|
+
["String", { kind: "func", params: [], returns: [STRING] }],
|
|
600
|
+
["Len", { kind: "func", params: [], returns: [INT] }],
|
|
601
|
+
["Reset", { kind: "func", params: [], returns: [VOID] }],
|
|
602
|
+
["Grow", { kind: "func", params: [INT], returns: [VOID] }],
|
|
603
|
+
]);
|
|
604
|
+
this.types.set("strings.Builder", {
|
|
605
|
+
kind: "named",
|
|
606
|
+
name: "strings.Builder",
|
|
607
|
+
underlying: {
|
|
608
|
+
kind: "struct",
|
|
609
|
+
fields: new Map(),
|
|
610
|
+
methods: strBuilderMethods,
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// bytes.Buffer
|
|
615
|
+
const bytesBufferMethods = new Map([
|
|
616
|
+
[
|
|
617
|
+
"WriteString",
|
|
618
|
+
{ kind: "func", params: [STRING], returns: [INT, ERROR] },
|
|
619
|
+
],
|
|
620
|
+
["WriteByte", { kind: "func", params: [INT], returns: [ERROR] }],
|
|
621
|
+
[
|
|
622
|
+
"Write",
|
|
623
|
+
{ kind: "func", params: [BYTE_SLICE_T], returns: [INT, ERROR] },
|
|
624
|
+
],
|
|
625
|
+
["String", { kind: "func", params: [], returns: [STRING] }],
|
|
626
|
+
["Bytes", { kind: "func", params: [], returns: [BYTE_SLICE_T] }],
|
|
627
|
+
["Len", { kind: "func", params: [], returns: [INT] }],
|
|
628
|
+
["Reset", { kind: "func", params: [], returns: [VOID] }],
|
|
629
|
+
["Grow", { kind: "func", params: [INT], returns: [VOID] }],
|
|
630
|
+
]);
|
|
631
|
+
this.types.set("bytes.Buffer", {
|
|
632
|
+
kind: "named",
|
|
633
|
+
name: "bytes.Buffer",
|
|
634
|
+
underlying: {
|
|
635
|
+
kind: "struct",
|
|
636
|
+
fields: new Map(),
|
|
637
|
+
methods: bytesBufferMethods,
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
|
|
434
641
|
this.globals.define("append", { kind: "builtin", name: "append" });
|
|
435
642
|
this.globals.define("len", { kind: "builtin", name: "len" });
|
|
436
643
|
this.globals.define("cap", { kind: "builtin", name: "cap" });
|
|
@@ -1360,6 +1567,15 @@ export class TypeChecker {
|
|
|
1360
1567
|
fieldType(baseType, field, node) {
|
|
1361
1568
|
baseType = this.resolveType(baseType);
|
|
1362
1569
|
if (!baseType || isAny(baseType)) return ANY;
|
|
1570
|
+
// Transparent pointer dereference for named-type method calls: (*T).Method works like T.Method
|
|
1571
|
+
// (only unwrap when the pointer's base is a named type that has methods)
|
|
1572
|
+
if (
|
|
1573
|
+
baseType.kind === "pointer" &&
|
|
1574
|
+
field !== "value" &&
|
|
1575
|
+
baseType.base?.kind === "named"
|
|
1576
|
+
) {
|
|
1577
|
+
baseType = this.resolveType(baseType.base);
|
|
1578
|
+
}
|
|
1363
1579
|
let base = baseType.kind === "named" ? baseType.underlying : baseType;
|
|
1364
1580
|
base = this.resolveType(base);
|
|
1365
1581
|
if (base?.kind === "struct") {
|