gofront 1.3.7 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -387,6 +387,8 @@ gofront <file.go> compile single file → stdout
387
387
  gofront <dir> compile all *.go in directory → stdout
388
388
  gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
389
389
  gofront <input> --check type-check only
390
+ gofront check <dir>/... type-check every package under <dir> (Go-style `./...`)
391
+ gofront test <dir>/... [--dom] run tests for every package under <dir>
390
392
  gofront <input> --watch watch for changes and recompile
391
393
  gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
392
394
  gofront <input> -o out.js --serve --port 8080 use a custom port
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli-core.js CHANGED
@@ -229,6 +229,9 @@ export function parseTestArgs(argv) {
229
229
  }
230
230
 
231
231
  export function handleTest(targetDir = ".", options = {}) {
232
+ const pattern = parsePackagePattern(targetDir);
233
+ if (pattern) return runTestsRecursive(pattern, options);
234
+
232
235
  let testTarget = targetDir;
233
236
  try {
234
237
  const project = detectProject(targetDir);
@@ -246,6 +249,69 @@ export function handleTest(targetDir = ".", options = {}) {
246
249
  return runTests(testTarget, options);
247
250
  }
248
251
 
252
+ async function runTestsRecursive(rootDir, options) {
253
+ const dirs = findPackageDirs(rootDir);
254
+ if (dirs.length === 0) {
255
+ throw new Error(`No .go files found under ${rootDir}`);
256
+ }
257
+ let exitCode = 0;
258
+ let stdout = "";
259
+ let stderr = "";
260
+ for (const dir of dirs) {
261
+ const result = await runTests(dir, options);
262
+ if (result.exitCode !== 0) exitCode = result.exitCode;
263
+ if (options.captureOutput) {
264
+ stdout += result.stdout ?? "";
265
+ stderr += result.stderr ?? "";
266
+ }
267
+ }
268
+ return options.captureOutput
269
+ ? { exitCode, stdout, stderr, packages: dirs }
270
+ : { exitCode, packages: dirs };
271
+ }
272
+
273
+ // ── Package patterns (`dir/...`) ──────────────────────────────
274
+
275
+ // Returns the root directory of a Go-style recursive pattern (`./...`,
276
+ // `app/src/...`), or null when the argument is a plain path.
277
+ export function parsePackagePattern(arg) {
278
+ if (arg === "...") return resolve(".");
279
+ if (!arg.endsWith("/...")) return null;
280
+ return resolve(arg.slice(0, -4) || ".");
281
+ }
282
+
283
+ const SKIPPED_DIRS = new Set(["node_modules", "dist", "public"]);
284
+
285
+ // Lists every directory under root (inclusive) that contains .go or .templ
286
+ // files, in sorted order, skipping hidden directories and build output.
287
+ export function findPackageDirs(root) {
288
+ const out = [];
289
+ const walk = (dir) => {
290
+ let entries;
291
+ try {
292
+ entries = readdirSync(dir, { withFileTypes: true });
293
+ } catch {
294
+ return;
295
+ }
296
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
297
+ if (
298
+ entries.some(
299
+ (e) =>
300
+ e.isFile() && (e.name.endsWith(".go") || e.name.endsWith(".templ")),
301
+ )
302
+ ) {
303
+ out.push(dir);
304
+ }
305
+ for (const e of entries) {
306
+ if (!e.isDirectory()) continue;
307
+ if (e.name.startsWith(".") || SKIPPED_DIRS.has(e.name)) continue;
308
+ walk(join(dir, e.name));
309
+ }
310
+ };
311
+ walk(resolve(root));
312
+ return out;
313
+ }
314
+
249
315
  // ── Project detection ─────────────────────────────────────────
250
316
 
251
317
  function loadProjectConfig(projectRoot) {
@@ -357,6 +423,26 @@ export function parseCheckArgs(argv) {
357
423
  }
358
424
 
359
425
  export function handleCheck(targetDir = ".", options = {}) {
426
+ const pattern = parsePackagePattern(targetDir);
427
+ if (pattern) {
428
+ const dirs = findPackageDirs(pattern);
429
+ if (dirs.length === 0) {
430
+ throw new Error(`No .go files found under ${targetDir}`);
431
+ }
432
+ const startMs = performance.now();
433
+ const packages = [];
434
+ for (const dir of dirs) {
435
+ const pkgStart = performance.now();
436
+ runCompile(dir, true, { ...options });
437
+ packages.push({
438
+ dir,
439
+ elapsedMs: (performance.now() - pkgStart).toFixed(0),
440
+ });
441
+ }
442
+ const elapsedMs = (performance.now() - startMs).toFixed(0);
443
+ return { target: targetDir, compileTarget: pattern, elapsedMs, packages };
444
+ }
445
+
360
446
  const project = detectProject(targetDir);
361
447
  const targetPath = resolve(targetDir);
362
448
  let isDir = false;
@@ -610,16 +610,23 @@ export const expressionGenMethods = {
610
610
  },
611
611
 
612
612
  _genTypeAssertExpr(expr) {
613
- const val = this.genExpr(expr.expr);
613
+ const src = this.genExpr(expr.expr);
614
+ // The operand appears in both the check and the result; hoist calls so
615
+ // they run once.
616
+ const hoist = this._hasCallExpr(expr.expr);
617
+ const val = hoist ? "__v" : src;
614
618
  const check = this._typeCheckExpr(expr.type, val);
619
+ const wrap = (body) => (hoist ? `((__v) => ${body})(${src})` : body);
615
620
  if (!expr._commaOk) {
616
621
  // plain assertion: panic if check fails (matches Go behavior)
617
- if (check === "true") return val; // can't check at runtime — pass through
618
- return `(${check} ? ${val} : (() => { throw new Error("interface conversion: type assertion failed"); })())`;
622
+ if (check === "true") return src; // can't check at runtime — pass through
623
+ return wrap(
624
+ `(${check} ? ${val} : (() => { throw new Error("interface conversion: type assertion failed"); })())`,
625
+ );
619
626
  }
620
627
  // comma-ok: emit [value-or-zero, runtimeTypeCheck]
621
628
  const zero = this.zeroValueForTypeNode(expr.type);
622
- return `(${check} ? [${val}, true] : [${zero}, false])`;
629
+ return wrap(`(${check} ? [${val}, true] : [${zero}, false])`);
623
630
  },
624
631
 
625
632
  _genTypeConversionCall(expr) {
@@ -1028,16 +1035,28 @@ export const expressionGenMethods = {
1028
1035
 
1029
1036
  getTypeName(typeNode) {
1030
1037
  if (!typeNode) return null;
1031
- if (typeNode.kind === "TypeName") return typeNode.name;
1038
+ if (typeNode.kind === "TypeName") return this._dequalify(typeNode.name);
1032
1039
  if (typeNode.kind === "GenericTypeName") return typeNode.name;
1033
1040
  if (typeNode.kind === "Ident") return typeNode.name;
1034
1041
  if (typeNode.kind === "InstantiationExpr")
1035
1042
  return this.getTypeName(typeNode.expr);
1036
- if (typeNode.kind === "SelectorExpr")
1037
- return `${this.getTypeName(typeNode.expr)}.${typeNode.field}`;
1043
+ if (typeNode.kind === "SelectorExpr") {
1044
+ const base = this.getTypeName(typeNode.expr);
1045
+ if (this.bundledPackages.has(base)) return typeNode.field;
1046
+ return `${base}.${typeNode.field}`;
1047
+ }
1038
1048
  return null;
1039
1049
  },
1040
1050
 
1051
+ // Bundled GoFront packages are inlined, so `pkg.T` names the class `T`.
1052
+ _dequalify(name) {
1053
+ const dot = name.indexOf(".");
1054
+ if (dot < 0) return name;
1055
+ return this.bundledPackages.has(name.slice(0, dot))
1056
+ ? name.slice(dot + 1)
1057
+ : name;
1058
+ },
1059
+
1041
1060
  isIntType(t) {
1042
1061
  if (!t) return false;
1043
1062
  if (t.kind === "untyped") return t.base === "int";
@@ -1063,6 +1082,7 @@ export const expressionGenMethods = {
1063
1082
  _zeroForNamedType(name) {
1064
1083
  if (name === "strings.Builder") return '{ _buf: "" }';
1065
1084
  if (name === "bytes.Buffer") return "{ _buf: [] }";
1085
+ name = this._dequalify(name);
1066
1086
  if (this.structNames.has(name)) return `new ${name}()`;
1067
1087
  return "null";
1068
1088
  },
@@ -31,6 +31,7 @@ import {
31
31
  HELPER_PATH_CLEAN,
32
32
  HELPER_S,
33
33
  HELPER_SCLONE,
34
+ HELPER_SORT_SLICE,
34
35
  HELPER_SPRINTF,
35
36
  HELPER_TESTING,
36
37
  HELPER_TIME_FMT,
@@ -113,6 +114,7 @@ export class CodeGen {
113
114
  this._usesError = false;
114
115
  this._usesErrorIs = false;
115
116
  this._usesPathClean = false;
117
+ this._usesSortSlice = false;
116
118
  this._usesTimeFmt = false;
117
119
  this._usesTimeParse = false;
118
120
  this._usesTesting = false;
@@ -377,6 +379,7 @@ export class CodeGen {
377
379
  [this._usesError, HELPER_ERROR],
378
380
  [this._usesErrorIs, HELPER_ERROR_IS],
379
381
  [this._usesPathClean, HELPER_PATH_CLEAN],
382
+ [this._usesSortSlice, HELPER_SORT_SLICE],
380
383
  [this._usesTimeFmt, HELPER_TIME_FMT],
381
384
  [this._usesTimeParse, HELPER_TIME_PARSE],
382
385
  [needsTesting, HELPER_TESTING],
@@ -19,6 +19,21 @@ export const HELPER_APPEND = `var __append = __append || function(a, ...b) {
19
19
 
20
20
  export const HELPER_S = "var __s = __s || function(a) { return a || []; };";
21
21
 
22
+ // In-place stable sort driven by Go's less(i, j) index comparator. The
23
+ // comparator reads the live slice, so elements are moved as it runs
24
+ // (binary insertion: O(n log n) compares, O(n^2) moves, fine for typed arrays).
25
+ export const HELPER_SORT_SLICE = `var __sortSlice = __sortSlice || function(s, less) {
26
+ if (!s) return;
27
+ for (let i = 1; i < s.length; i++) {
28
+ let lo = 0, hi = i;
29
+ while (lo < hi) { const mid = (lo + hi) >>> 1; if (less(i, mid)) hi = mid; else lo = mid + 1; }
30
+ if (lo === i) continue;
31
+ const v = s[i];
32
+ for (let k = i; k > lo; k--) s[k] = s[k - 1];
33
+ s[lo] = v;
34
+ }
35
+ };`;
36
+
22
37
  // Copy of a struct value whose class is not statically known.
23
38
  export const HELPER_SCLONE = `var __sclone = __sclone || function(x) {
24
39
  if (x === null || typeof x !== "object") return x;
@@ -253,8 +253,9 @@ export const statementGenMethods = {
253
253
  _genCommaOkTypeAssert(stmt) {
254
254
  const val = this.genExpr(stmt.rhs[0]);
255
255
  const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
256
- const tmp = "__ta";
257
- this.line(`let ${tmp} = ${val};`);
256
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
257
+ const tmp = `__ta${this._tmpCounter}`;
258
+ this.line(`const ${tmp} = ${val};`);
258
259
  if (vName !== "_") this.line(`${vName} = ${tmp}[0];`);
259
260
  if (okName !== "_") this.line(`${okName} = ${tmp}[1];`);
260
261
  },
@@ -273,8 +274,9 @@ export const statementGenMethods = {
273
274
  },
274
275
 
275
276
  _genMultiAssignWithBlanks(stmt, lhs, rhs, rhsStr) {
276
- const tmp = "__t";
277
- this.line(`let ${tmp} = ${rhsStr};`);
277
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
278
+ const tmp = `__t${this._tmpCounter}`;
279
+ this.line(`const ${tmp} = ${rhsStr};`);
278
280
  for (let i = 0; i < lhs.length; i++) {
279
281
  if (lhs[i] !== "_") {
280
282
  const src = rhs.length === 1 ? `${tmp}[${i}]` : rhs[i];
@@ -6,7 +6,6 @@ const MATH1 = {
6
6
  Abs: "abs",
7
7
  Floor: "floor",
8
8
  Ceil: "ceil",
9
- Round: "round",
10
9
  Sqrt: "sqrt",
11
10
  Cbrt: "cbrt",
12
11
  Log: "log",
@@ -29,6 +28,8 @@ const MATH2 = {
29
28
  Hypot: "hypot",
30
29
  };
31
30
  const MATH_EXTRA = {
31
+ // Go rounds half away from zero; Math.round rounds half toward +Infinity.
32
+ Round: ([x]) => `(Math.sign(${x}) * Math.round(Math.abs(${x})))`,
32
33
  Mod: ([x, y]) => `${x} % ${y}`,
33
34
  Inf: ([x]) => `(${x} >= 0 ? Infinity : -Infinity)`,
34
35
  IsNaN: ([x]) => `Number.isNaN(${x})`,
@@ -6,12 +6,10 @@ const SORT_DISPATCH = {
6
6
  Ints: (a) => `${a[0]}.sort((a, b) => a - b)`,
7
7
  Float64s: (a) => `${a[0]}.sort((a, b) => a - b)`,
8
8
  Strings: (a) => `${a[0]}.sort()`,
9
- Slice: (a) =>
10
- `${a[0]}.sort((a, b) => ${a[1]}(a, b) ? -1 : ${a[1]}(b, a) ? 1 : 0)`,
11
- SliceStable: (a) =>
12
- `${a[0]}.sort((a, b) => ${a[1]}(a, b) ? -1 : ${a[1]}(b, a) ? 1 : 0)`,
9
+ Slice: (a) => `__sortSlice(${a[0]}, ${a[1]})`,
10
+ SliceStable: (a) => `__sortSlice(${a[0]}, ${a[1]})`,
13
11
  SliceIsSorted: (a) =>
14
- `${a[0]}.every((v, i, a) => i === 0 || ${a[1]}(a[i - 1], v))`,
12
+ `((s, less) => { for (let i = (s?.length ?? 0) - 1; i > 0; i--) if (less(i, i - 1)) return false; return true; })(${a[0]}, ${a[1]})`,
15
13
  Search: (a) =>
16
14
  `((n, f) => { let lo = 0, hi = n; while (lo < hi) { const mid = (lo + hi) >>> 1; if (f(mid)) hi = mid; else lo = mid + 1; } return lo; })(${a[0]}, ${a[1]})`,
17
15
  IntsAreSorted: (a) =>
@@ -25,6 +23,7 @@ const SORT_DISPATCH = {
25
23
  /** @type {ThisType<CodeGen>} */
26
24
  export const sortMethods = {
27
25
  _genSort(fn, a) {
26
+ if (fn === "Slice" || fn === "SliceStable") this._usesSortSlice = true;
28
27
  const gen = SORT_DISPATCH[fn];
29
28
  return gen ? gen(a()) : undefined;
30
29
  },
package/src/compiler.js CHANGED
@@ -85,6 +85,7 @@ export function resolveImports(
85
85
  jsImports,
86
86
  bundledPackages,
87
87
  preambles,
88
+ bundledDirs = new Set(),
88
89
  ) {
89
90
  const fromDir = dirname(resolve(fromFile));
90
91
  const allImports = programs.flatMap((p) => p.imports);
@@ -130,8 +131,13 @@ export function resolveImports(
130
131
  );
131
132
  continue;
132
133
  }
133
- const dep = compileDir(depDir);
134
- preambles.push(dep.js);
134
+ // Diamond imports: a package reached through several paths is
135
+ // compiled for its symbols each time but emitted only once.
136
+ const depKey = resolve(depDir);
137
+ const alreadyBundled = bundledDirs.has(depKey);
138
+ bundledDirs.add(depKey);
139
+ const dep = compileDir(depDir, { bundledDirs });
140
+ if (!alreadyBundled) preambles.push(dep.js);
135
141
  if (alias === "_") continue;
136
142
  if (alias === ".") {
137
143
  checker.addDefinitions(dep.exportedTypes, dep.exportedSymbols);
@@ -260,6 +266,7 @@ export function compileFiles(files, options = {}) {
260
266
  jsImports,
261
267
  bundledPackages,
262
268
  preambles,
269
+ options.bundledDirs ?? new Set(),
263
270
  );
264
271
 
265
272
  // ── 3. Type-check ─────────────────────────────────────────────
package/src/index.js CHANGED
@@ -19,7 +19,7 @@ const _require = createRequire(import.meta.url);
19
19
  const { version } = _require("../package.json");
20
20
 
21
21
  import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
22
- import { dirname, resolve } from "node:path";
22
+ import { dirname, relative, resolve } from "node:path";
23
23
  import { copyAssets } from "./asset-manager.js";
24
24
  import {
25
25
  formatBuildSummary,
@@ -56,8 +56,8 @@ GoFront — a Go-inspired language that compiles to JavaScript
56
56
  Usage:
57
57
  gofront dev [dir] [options] Start dev server with live reload (default port 3000)
58
58
  gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify)
59
- gofront check [dir] Type-check only
60
- gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
59
+ gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
60
+ gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
61
61
  gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
62
62
  gofront init [dir] Scaffold a new GoFront project
63
63
  gofront <file.go> Compile single file and print to stdout
@@ -131,8 +131,19 @@ if (args[0] === "test") {
131
131
  if (args[0] === "check") {
132
132
  const { targetDir } = parseCheckArgs(args.slice(1));
133
133
  try {
134
- const { elapsedMs } = handleCheck(targetDir);
135
- console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
134
+ const { elapsedMs, packages } = handleCheck(targetDir);
135
+ if (packages) {
136
+ for (const pkg of packages) {
137
+ console.error(
138
+ `gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
139
+ );
140
+ }
141
+ console.error(
142
+ `gofront: ${packages.length} packages — OK (${elapsedMs}ms)`,
143
+ );
144
+ } else {
145
+ console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
146
+ }
136
147
  process.exit(0);
137
148
  } catch (e) {
138
149
  console.error(`gofront: ${e.message}`);
@@ -352,6 +352,7 @@ export const expressionParserMethods = {
352
352
  if (
353
353
  tt === T.MAP ||
354
354
  tt === T.IDENT ||
355
+ tt === T.DOT || // qualified types: []pkg.Type{...}
355
356
  tt === T.STAR ||
356
357
  tt === T.INT ||
357
358
  tt === T.FLOAT ||
@@ -343,6 +343,8 @@ export const assignabilityMethods = {
343
343
  },
344
344
 
345
345
  implements(srcType, iface, _node) {
346
+ // *T has the full method set of T (value + pointer receivers).
347
+ if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
346
348
  let base = srcType.kind === "named" ? srcType.underlying : srcType;
347
349
  base = this.resolveType(base);
348
350
  const methodMap =
@@ -430,9 +430,39 @@ export const expressionCheckMethods = {
430
430
  }
431
431
  return this.instantiateGenericFunc(baseType, typeArgs);
432
432
  }
433
+ // The parser's type-arg heuristic mistakes `xs[d.Field]` (uppercase field
434
+ // selector as index) for a generic instantiation. When the base is an
435
+ // indexable value and the sole "type arg" is a plain/qualified name,
436
+ // rewrite the node in place into an IndexExpr and check that instead.
437
+ const indexExpr = this._instantiationAsIndex(expr, baseType);
438
+ if (indexExpr) return this._checkIndexExpr(indexExpr, scope);
433
439
  return baseType;
434
440
  },
435
441
 
442
+ _instantiationAsIndex(expr, baseType) {
443
+ if (!baseType || expr.typeArgs.length !== 1) return null;
444
+ const ta = expr.typeArgs[0];
445
+ if (ta.kind !== "TypeName") return null;
446
+ const btu =
447
+ baseType.kind === "named"
448
+ ? (this.resolveType(baseType.underlying) ?? baseType.underlying)
449
+ : baseType;
450
+ const indexable =
451
+ btu?.kind === "slice" ||
452
+ btu?.kind === "array" ||
453
+ btu?.kind === "map" ||
454
+ isString(baseType);
455
+ if (!indexable) return null;
456
+ const parts = ta.name.split(".");
457
+ let index = { kind: "Ident", name: parts[0], _line: expr._line };
458
+ for (let i = 1; i < parts.length; i++)
459
+ index = { kind: "SelectorExpr", expr: index, field: parts[i] };
460
+ expr.kind = "IndexExpr";
461
+ expr.index = index;
462
+ delete expr.typeArgs;
463
+ return expr;
464
+ },
465
+
436
466
  _constIntValue(expr) {
437
467
  if (expr.kind === "BasicLit" && expr.litKind === "INT")
438
468
  return Number(expr.value);
@@ -88,7 +88,12 @@ export const resolveMethods = {
88
88
  if (fromScope?.kind === "typeParam") return fromScope;
89
89
  }
90
90
  const named = this.types.get(node.name);
91
- if (named) return named;
91
+ if (named) {
92
+ // `pkg.T` in a type position counts as using the import.
93
+ const dot = node.name.indexOf(".");
94
+ if (dot > 0) this.globals.lookup(node.name.slice(0, dot));
95
+ return named;
96
+ }
92
97
  return this.err(`Unknown type '${node.name}'`, node);
93
98
  }
94
99
  case "GenericTypeName": {
@@ -132,6 +137,18 @@ export const resolveMethods = {
132
137
  { kind: "TypeName", name: node.name },
133
138
  scope,
134
139
  );
140
+ case "SelectorExpr":
141
+ // `pkg.T{}` composite literal — the parser keeps the type as a
142
+ // SelectorExpr; resolve it like a qualified TypeName.
143
+ if (node.expr?.kind === "Ident" && typeof node.field === "string") {
144
+ const qualified = `${node.expr.name}.${node.field}`;
145
+ if (this.types.has(qualified))
146
+ return this.resolveTypeNode(
147
+ { kind: "TypeName", name: qualified },
148
+ scope,
149
+ );
150
+ }
151
+ return ANY;
135
152
  case "ArrayType":
136
153
  return this._resolveArrayTypeNode(node, scope);
137
154
  case "FuncType":
@@ -5,6 +5,7 @@ import {
5
5
  ANY,
6
6
  BOOL,
7
7
  ERROR,
8
+ FLOAT32,
8
9
  FLOAT64,
9
10
  INT,
10
11
  STRING,
@@ -256,7 +257,7 @@ export function setupExtendedGlobals(globals, types) {
256
257
  members: {
257
258
  Intn: { kind: "func", params: [INT], returns: [INT] },
258
259
  Float64: { kind: "func", params: [], returns: [FLOAT64] },
259
- Float32: { kind: "func", params: [], returns: [FLOAT64] },
260
+ Float32: { kind: "func", params: [], returns: [FLOAT32] },
260
261
  Int: { kind: "func", params: [], returns: [INT] },
261
262
  Int63: { kind: "func", params: [], returns: [INT] },
262
263
  Int63n: { kind: "func", params: [INT], returns: [INT] },