gofront 1.3.9 → 1.3.11

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
@@ -401,7 +401,23 @@ gofront init [dir] scaffold a new project
401
401
  gofront --version / -v print version
402
402
  gofront --help / -h print this help
403
403
  ```
404
+ ### Project configuration
404
405
 
406
+ `gofront dev`, `build`, `prep`, `check` and `test` read optional settings from a `gofront.json`
407
+ file or a `"gofront": { … }` object in `package.json`:
408
+
409
+ | Key | Default | Description |
410
+ |---|---|---|
411
+ | `src` | `app/src`, `src` or `main.go` | Package directory (or single file) to compile |
412
+ | `serveDir` | `app`, `.` or `public` (first with `index.html`) | Directory served by `dev` and mirrored into the build |
413
+ | `outDir` | `public` | Release output directory for `build` |
414
+ | `output` | `app/app.js` | Dev-mode compiled bundle path |
415
+ | `port` | `3000` | Dev server port |
416
+ | `assetExtensions` | `[]` | Extra file extensions (e.g. `[".bmesh", ".mat"]`) copied from `serveDir` into `outDir` on `build`, in addition to the built-in web asset list (html, css, js, json, images, fonts, audio, video, wasm) |
417
+ | `vendor` | `app/vendor.js` | Vendor bundle written by `prep`/`build` from `package.json` `dependencies`. Either a destination path string or `{ "dest": string \| string[], "packages": string[], "minify": boolean, "globals": { "<pkg>": string \| string[] } }` |
418
+ | `assetCopy` | `[]` | Static files copied by `prep`/`build`: `[{ "source": "node_modules/x/font.woff2", "dest": "app/fonts/font.woff2" }]` |
419
+
420
+ All settings live in this one place — top-level `"vendor"` / `"assetCopy"` keys in `package.json` are not read.
405
421
  ---
406
422
 
407
423
  ## Multi-file packages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.9",
3
+ "version": "1.3.11",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,14 +1,7 @@
1
1
  // Native static asset manager for GoFront projects.
2
2
  // Zero-dependency file and directory copier using node:fs.
3
3
 
4
- import {
5
- copyFileSync,
6
- cpSync,
7
- existsSync,
8
- mkdirSync,
9
- readFileSync,
10
- statSync,
11
- } from "node:fs";
4
+ import { copyFileSync, cpSync, existsSync, mkdirSync, statSync } from "node:fs";
12
5
  import {
13
6
  basename,
14
7
  dirname,
@@ -18,25 +11,12 @@ import {
18
11
  resolve,
19
12
  sep,
20
13
  } from "node:path";
14
+ import { log } from "./colors.js";
15
+ import { loadProjectConfig } from "./project-config.js";
21
16
 
22
17
  export function loadAssetConfig(projectDir) {
23
- const gofrontJsonPath = join(projectDir, "gofront.json");
24
- if (existsSync(gofrontJsonPath)) {
25
- try {
26
- const parsed = JSON.parse(readFileSync(gofrontJsonPath, "utf8"));
27
- if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
28
- } catch {}
29
- }
30
-
31
- const pkgJsonPath = join(projectDir, "package.json");
32
- if (existsSync(pkgJsonPath)) {
33
- try {
34
- const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
35
- if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
36
- } catch {}
37
- }
38
-
39
- return [];
18
+ const entries = loadProjectConfig(projectDir).assetCopy;
19
+ return Array.isArray(entries) ? entries : [];
40
20
  }
41
21
 
42
22
  function copyFileAsset(srcPath, destPath, dest) {
@@ -71,7 +51,7 @@ function copyAssetEntry(entry, projectRoot) {
71
51
 
72
52
  const srcPath = resolve(projectRoot, source);
73
53
  if (!existsSync(srcPath)) {
74
- console.warn(`gofront: asset source '${source}' not found — skipping`);
54
+ log.warn(`asset source '${source}' not found — skipping`);
75
55
  return {
76
56
  status: "skipped",
77
57
  item: { source, dest, status: "skipped", reason: "source not found" },
@@ -94,7 +74,7 @@ function copyAssetEntry(entry, projectRoot) {
94
74
  item: { source, dest: targetFile, type: "file", status: "copied" },
95
75
  };
96
76
  } catch (err) {
97
- console.warn(`gofront: failed to copy asset '${source}': ${err.message}`);
77
+ log.warn(`failed to copy asset '${source}': ${err.message}`);
98
78
  return {
99
79
  status: "failed",
100
80
  item: { source, dest, status: "failed", reason: err.message },
package/src/cli-core.js CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  sep,
24
24
  } from "node:path";
25
25
  import { copyAssets } from "./asset-manager.js";
26
+ import { colors, log } from "./colors.js";
26
27
  import { compileDir, compileSingleFile } from "./compiler.js";
27
28
  import { createDevServer } from "./dev-server.js";
28
29
  import { minify } from "./minifier.js";
30
+ import { loadProjectConfig } from "./project-config.js";
29
31
  import { generatePwa } from "./pwa.js";
30
32
  import { runTests } from "./test-runner.js";
31
33
  import { bundleVendor } from "./vendor.js";
@@ -314,25 +316,6 @@ export function findPackageDirs(root) {
314
316
 
315
317
  // ── Project detection ─────────────────────────────────────────
316
318
 
317
- function loadProjectConfig(projectRoot) {
318
- const gofrontJson = join(projectRoot, "gofront.json");
319
- if (existsSync(gofrontJson)) {
320
- try {
321
- return JSON.parse(readFileSync(gofrontJson, "utf8"));
322
- } catch {}
323
- }
324
- const pkgJson = join(projectRoot, "package.json");
325
- if (existsSync(pkgJson)) {
326
- try {
327
- const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
328
- if (pkg.gofront && typeof pkg.gofront === "object") {
329
- return pkg.gofront;
330
- }
331
- } catch {}
332
- }
333
- return {};
334
- }
335
-
336
319
  function resolveSrcDir(projectRoot, config) {
337
320
  const src = config.src || config.input;
338
321
  if (src) return isAbsolute(src) ? src : resolve(projectRoot, src);
@@ -562,10 +545,23 @@ const STATIC_ASSET_EXTS = new Set([
562
545
 
563
546
  const ROOT_CONFIG_EXTS = new Set([".js", ".mjs", ".json"]);
564
547
 
548
+ // Builds the asset extension whitelist: defaults plus any `assetExtensions` from project config.
549
+ export function resolveAssetExtensions(config = {}) {
550
+ const exts = new Set(STATIC_ASSET_EXTS);
551
+ const extra = config.assetExtensions;
552
+ if (!Array.isArray(extra)) return exts;
553
+ for (const raw of extra) {
554
+ if (typeof raw !== "string" || raw.trim() === "") continue;
555
+ const ext = raw.trim().toLowerCase();
556
+ exts.add(ext.startsWith(".") ? ext : `.${ext}`);
557
+ }
558
+ return exts;
559
+ }
560
+
565
561
  // Whether a file in the served directory is a web asset worth copying to the build output.
566
- function isStaticAsset(name, atProjectRoot) {
562
+ function isStaticAsset(name, atProjectRoot, exts = STATIC_ASSET_EXTS) {
567
563
  const ext = extname(name).toLowerCase();
568
- if (!STATIC_ASSET_EXTS.has(ext)) return false;
564
+ if (!exts.has(ext)) return false;
569
565
  // At the project root, .js/.json files are tooling config, not web assets.
570
566
  if (atProjectRoot && ROOT_CONFIG_EXTS.has(ext))
571
567
  return name === "manifest.json";
@@ -578,6 +574,7 @@ function copyServeDirAssets(project, outDir, srcDir) {
578
574
  const out = resolve(outDir);
579
575
  if (!existsSync(serveDir) || serveDir === out) return 0;
580
576
  const isRoot = serveDir === resolve(project.projectRoot);
577
+ const exts = resolveAssetExtensions(project.config);
581
578
  const skipDirs = [out, resolve(srcDir)];
582
579
  const devOutput = resolve(project.devOutputFile);
583
580
  const skipFiles = new Set([devOutput, `${devOutput}.map`]);
@@ -600,7 +597,7 @@ function copyServeDirAssets(project, outDir, srcDir) {
600
597
  } else if (
601
598
  entry.isFile() &&
602
599
  !skipFiles.has(full) &&
603
- isStaticAsset(entry.name, isRoot)
600
+ isStaticAsset(entry.name, isRoot, exts)
604
601
  ) {
605
602
  copyFile(full);
606
603
  }
@@ -689,7 +686,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
689
686
  dest: join(outDir, "vendor.js"),
690
687
  });
691
688
  } catch (e) {
692
- console.warn(`gofront: vendor bundle warning: ${e.message}`);
689
+ log.warn(`vendor bundle: ${e.message}`);
693
690
  }
694
691
 
695
692
  const assets = copyReleaseAssets(
@@ -759,11 +756,12 @@ function buildDevOnce(
759
756
  writeFileSync(outputFile, `${result.js}\n`);
760
757
  const elapsedMs = (performance.now() - startMs).toFixed(0);
761
758
  const note = changedFile ? ` — ${changedFile} changed` : "";
762
- console.error(`gofront: OK — wrote ${outputFile} (${elapsedMs}ms${note})`);
759
+ log.ok(
760
+ `— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
761
+ );
763
762
  devServer.notify();
764
763
  } catch (e) {
765
- console.error("gofront: ERROR");
766
- for (const line of e.message.split("\n")) console.error(` ${line}`);
764
+ log.error(e.message);
767
765
  devServer.notifyError(e);
768
766
  }
769
767
  }
package/src/colors.js ADDED
@@ -0,0 +1,81 @@
1
+ // ANSI color helpers for CLI output. Honours NO_COLOR / FORCE_COLOR and
2
+ // otherwise only colors when the target stream is a TTY.
3
+
4
+ export function colorEnabled(stream = process.stderr) {
5
+ if ("NO_COLOR" in process.env) return false;
6
+ const force = process.env.FORCE_COLOR;
7
+ if (force !== undefined) return force !== "0" && force !== "false";
8
+ return Boolean(stream?.isTTY);
9
+ }
10
+
11
+ const CODES = {
12
+ red: 31,
13
+ green: 32,
14
+ yellow: 33,
15
+ blue: 34,
16
+ magenta: 35,
17
+ cyan: 36,
18
+ gray: 90,
19
+ bold: 1,
20
+ dim: 2,
21
+ };
22
+
23
+ export function createColors(enabled) {
24
+ const c = {};
25
+ for (const [name, code] of Object.entries(CODES)) {
26
+ c[name] = enabled ? (s) => `\x1b[${code}m${s}\x1b[0m` : (s) => `${s}`;
27
+ }
28
+ c.enabled = enabled;
29
+ return c;
30
+ }
31
+
32
+ export const colors = createColors(colorEnabled(process.stderr));
33
+
34
+ const DIAG_LABEL = /^(Lex error|Parse error|Type error)(\b.*?)(:\s)/;
35
+ const GUTTER = /^(\s*\d+ \| )(.*)$/;
36
+ const CARET = /^(\s*)(\^+)$/;
37
+
38
+ // Colorizes a compiler diagnostic: red label, dimmed source gutter, red caret.
39
+ export function formatDiagnostic(message, c = colors) {
40
+ if (!c.enabled) return message;
41
+ return message
42
+ .split("\n")
43
+ .map((line) => {
44
+ const label = line.match(DIAG_LABEL);
45
+ if (label) {
46
+ return `${c.bold(c.red(label[1]))}${c.dim(label[2])}${label[3]}${line.slice(label[0].length)}`;
47
+ }
48
+ const gutter = line.match(GUTTER);
49
+ if (gutter) return `${c.dim(gutter[1])}${gutter[2]}`;
50
+ const caret = line.match(CARET);
51
+ if (caret) return `${caret[1]}${c.bold(c.red(caret[2]))}`;
52
+ if (/^warning:/i.test(line.trim())) return c.yellow(line);
53
+ return line;
54
+ })
55
+ .join("\n");
56
+ }
57
+
58
+ // Standard CLI log helpers writing to stderr with a colored `gofront:` prefix.
59
+ const prefix = () => colors.bold("gofront:");
60
+ const isDiagnostic = (msg) => DIAG_LABEL.test(msg) || msg.includes("\n");
61
+
62
+ export const log = {
63
+ info: (msg) => console.error(`${prefix()} ${msg}`),
64
+ ok: (msg) =>
65
+ console.error(`${prefix()} ${colors.green("OK")}${msg ? ` ${msg}` : ""}`),
66
+ warn: (msg) =>
67
+ console.error(`${prefix()} ${colors.yellow(`warning: ${msg}`)}`),
68
+ error: (msg) => {
69
+ console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
70
+ for (const line of formatDiagnostic(msg).split("\n"))
71
+ console.error(` ${line}`);
72
+ },
73
+ // Compiler diagnostics get the block form; anything else is one red line.
74
+ fail: (msg) =>
75
+ isDiagnostic(msg)
76
+ ? log.error(msg)
77
+ : console.error(`${prefix()} ${colors.red(msg)}`),
78
+ };
79
+
80
+ export const stamp = () => colors.dim(`[${new Date().toLocaleTimeString()}]`);
81
+ export const ms = (n) => colors.dim(`(${n}ms)`);
package/src/compiler.js CHANGED
@@ -20,6 +20,7 @@
20
20
  import { readdirSync, readFileSync, statSync } from "node:fs";
21
21
  import { basename, dirname, join, relative, resolve } from "node:path";
22
22
  import { CodeGen } from "./codegen/index.js";
23
+ import { log } from "./colors.js";
23
24
  import { parseDts } from "./dts-parser.js";
24
25
  import { Lexer } from "./lexer.js";
25
26
  import { Parser } from "./parser/index.js";
@@ -126,8 +127,8 @@ export function resolveImports(
126
127
  seenLocalPaths.add(path);
127
128
  const depDir = resolveGwDir(path, fromFile);
128
129
  if (!depDir) {
129
- console.error(
130
- `gofront: warning: cannot find local package '${path}' relative to ${fromDir}`,
130
+ log.warn(
131
+ `cannot find local package '${path}' relative to ${fromDir}`,
131
132
  );
132
133
  continue;
133
134
  }
package/src/dev-server.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { colors, log } from "./colors.js";
7
8
 
8
9
  export const MIME = {
9
10
  ".html": "text/html; charset=utf-8",
@@ -275,9 +276,9 @@ function createNotify(clients) {
275
276
 
276
277
  function handleServerError(err, port) {
277
278
  if (err.code === "EADDRINUSE") {
278
- console.error(`gofront: port ${port} already in use — try --port <number>`);
279
+ log.fail(`port ${port} already in use — try --port <number>`);
279
280
  } else {
280
- console.error(`gofront: dev server error: ${err.message}`);
281
+ log.fail(`dev server error: ${err.message}`);
281
282
  }
282
283
  process.exit(1);
283
284
  }
@@ -327,7 +328,7 @@ export function createDevServer(serveDir, port = 3000, options = {}) {
327
328
  server.listen(port, host, () => {
328
329
  const actualPort = server.address()?.port ?? port;
329
330
  if (port !== 0 && !options.silent) {
330
- console.error(`gofront: dev server → http://localhost:${actualPort}`);
331
+ log.info(`dev server → ${colors.cyan(`http://localhost:${actualPort}`)}`);
331
332
  }
332
333
  });
333
334
 
package/src/index.js CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  parseTestArgs,
39
39
  runCompile,
40
40
  } from "./cli-core.js";
41
+ import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
41
42
  import { createDevServer } from "./dev-server.js";
42
43
 
43
44
  // ── Parse CLI args ───────────────────────────────────────────
@@ -85,10 +86,9 @@ if (args[0] === "prep" || args[0] === "vendor") {
85
86
  const { targetDir, vendorConfig } = parsePrepArgs(args.slice(1));
86
87
  try {
87
88
  const result = await handlePrep(resolve(targetDir), { vendorConfig });
88
- for (const line of formatPrepSummary(result))
89
- console.error(`gofront: ${line}`);
89
+ for (const line of formatPrepSummary(result)) log.info(line);
90
90
  } catch (e) {
91
- console.error(`gofront: ${e.message}`);
91
+ log.fail(e.message);
92
92
  process.exit(1);
93
93
  }
94
94
  process.exit(0);
@@ -103,12 +103,12 @@ if (args[0] === "init") {
103
103
  try {
104
104
  ({ mainPath } = handleInit(targetDir));
105
105
  } catch (e) {
106
- console.error(`gofront: ${e.message}`);
106
+ log.fail(e.message);
107
107
  process.exit(1);
108
108
  }
109
- console.error(`gofront: created ${mainPath}`);
110
- console.error(
111
- `gofront: run gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`} to compile`,
109
+ log.info(`created ${colors.cyan(mainPath)}`);
110
+ log.info(
111
+ `run ${colors.cyan(`gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`}`)} to compile`,
112
112
  );
113
113
  process.exit(0);
114
114
  }
@@ -121,7 +121,7 @@ if (args[0] === "test") {
121
121
  const result = await handleTest(targetDir, testOptions);
122
122
  process.exit(result.exitCode);
123
123
  } catch (e) {
124
- console.error(`gofront: ${e.message}`);
124
+ log.fail(e.message);
125
125
  process.exit(1);
126
126
  }
127
127
  }
@@ -132,21 +132,21 @@ if (args[0] === "check") {
132
132
  const { targetDir } = parseCheckArgs(args.slice(1));
133
133
  try {
134
134
  const { elapsedMs, packages } = handleCheck(targetDir);
135
+ const okLine = (label, t) =>
136
+ log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
135
137
  if (packages) {
136
138
  for (const pkg of packages) {
137
- console.error(
138
- `gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
139
- );
139
+ okLine(relative(process.cwd(), pkg.dir) || ".", pkg.elapsedMs);
140
140
  }
141
- console.error(
142
- `gofront: ${packages.length} packages — OK (${elapsedMs}ms)`,
141
+ log.info(
142
+ `${colors.bold(`${packages.length} packages`)} — ${colors.bold(colors.green("OK"))} ${ms(elapsedMs)}`,
143
143
  );
144
144
  } else {
145
- console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
145
+ okLine(targetDir, elapsedMs);
146
146
  }
147
147
  process.exit(0);
148
148
  } catch (e) {
149
- console.error(`gofront: ${e.message}`);
149
+ log.fail(e.message);
150
150
  process.exit(1);
151
151
  }
152
152
  }
@@ -158,11 +158,15 @@ if (args[0] === "build") {
158
158
  try {
159
159
  const result = await handleBuild(buildOptions.targetDir, buildOptions);
160
160
  for (const line of formatBuildSummary(result)) {
161
- console.error(`gofront: ${line}`);
161
+ log.info(
162
+ line
163
+ .replace(/^build complete/, colors.green("build complete"))
164
+ .replace(/\(([\d.]+ kB)\)$/, (_, s) => colors.dim(`(${s})`)),
165
+ );
162
166
  }
163
167
  process.exit(0);
164
168
  } catch (e) {
165
- console.error(`gofront: ${e.message}`);
169
+ log.fail(e.message);
166
170
  process.exit(1);
167
171
  }
168
172
  }
@@ -174,14 +178,14 @@ if (args[0] === "dev") {
174
178
  try {
175
179
  const dev = await handleDev(devOptions.targetDir, devOptions);
176
180
  if (dev.initialError) {
177
- console.error("gofront: ERROR");
178
- for (const line of dev.initialError.message.split("\n"))
179
- console.error(` ${line}`);
181
+ log.error(dev.initialError.message);
180
182
  } else {
181
- console.error(`gofront: OK — wrote ${dev.outputFile}`);
183
+ log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
182
184
  }
183
- console.error(`gofront: dev server running → http://localhost:${dev.port}`);
184
- console.error(`gofront: watching ${dev.srcDir} for changes...`);
185
+ log.info(
186
+ `dev server running → ${colors.cyan(`http://localhost:${dev.port}`)}`,
187
+ );
188
+ log.info(`watching ${colors.cyan(dev.srcDir)} for changes...`);
185
189
 
186
190
  const shutdown = async () => {
187
191
  await dev.close();
@@ -191,7 +195,7 @@ if (args[0] === "dev") {
191
195
  process.on("SIGTERM", shutdown);
192
196
  await new Promise(() => {});
193
197
  } catch (e) {
194
- console.error(`gofront: ${e.message}`);
198
+ log.fail(e.message);
195
199
  process.exit(1);
196
200
  }
197
201
  }
@@ -218,7 +222,7 @@ let isDir = false;
218
222
  try {
219
223
  isDir = statSync(inputPath).isDirectory();
220
224
  } catch (e) {
221
- console.error(`gofront: cannot access '${inputArg}': ${e.message}`);
225
+ log.fail(`cannot access '${inputArg}': ${e.message}`);
222
226
  process.exit(1);
223
227
  }
224
228
 
@@ -235,7 +239,7 @@ if (!watchMode) {
235
239
  dumpAst,
236
240
  });
237
241
  } catch (e) {
238
- console.error(`gofront: ${e.message}`);
242
+ log.fail(e.message);
239
243
  process.exit(1);
240
244
  }
241
245
 
@@ -250,7 +254,9 @@ if (!watchMode) {
250
254
 
251
255
  if (checkOnly) {
252
256
  const elapsedMs = (performance.now() - startMs).toFixed(0);
253
- console.error(`gofront: ${inputArg} — OK (${elapsedMs}ms)`);
257
+ log.info(
258
+ `${colors.cyan(inputArg)} — ${colors.green("OK")} ${ms(elapsedMs)}`,
259
+ );
254
260
  process.exit(0);
255
261
  }
256
262
 
@@ -262,7 +268,7 @@ if (!watchMode) {
262
268
  sourceMap,
263
269
  });
264
270
  } catch (e) {
265
- console.error(`gofront: minify failed: ${e.message}`);
271
+ log.fail(`minify failed: ${e.message}`);
266
272
  process.exit(1);
267
273
  }
268
274
 
@@ -272,9 +278,9 @@ if (!watchMode) {
272
278
  try {
273
279
  mkdirSync(dirname(resolve(outputFile)), { recursive: true });
274
280
  writeFileSync(outputFile, `${js}\n`);
275
- console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
281
+ log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
276
282
  } catch (e) {
277
- console.error(`gofront: cannot write '${outputFile}': ${e.message}`);
283
+ log.fail(`cannot write '${outputFile}': ${e.message}`);
278
284
  process.exit(1);
279
285
  }
280
286
  } else {
@@ -286,10 +292,10 @@ if (!watchMode) {
286
292
  const projectDir = resolve(".");
287
293
  const { copied, skipped } = copyAssets(projectDir);
288
294
  if (copied > 0 || skipped > 0) {
289
- console.error(`gofront: copied ${copied} assets (${skipped} skipped)`);
295
+ log.info(`copied ${copied} assets (${skipped} skipped)`);
290
296
  }
291
297
  } catch (e) {
292
- console.error(`gofront: asset copy failed: ${e.message}`);
298
+ log.warn(`asset copy failed: ${e.message}`);
293
299
  }
294
300
  }
295
301
 
@@ -298,15 +304,11 @@ if (!watchMode) {
298
304
 
299
305
  // ── Watch mode ───────────────────────────────────────────────
300
306
 
301
- function timestamp() {
302
- return new Date().toLocaleTimeString();
303
- }
304
-
305
307
  // Start dev server before first build so the browser can connect immediately
306
308
  let devServer = null;
307
309
  if (serveMode) {
308
310
  if (!outputFile) {
309
- console.error("gofront: --serve requires -o <output file>");
311
+ log.fail("--serve requires -o <output file>");
310
312
  process.exit(1);
311
313
  }
312
314
  const serveDir = dirname(resolve(outputFile));
@@ -324,17 +326,18 @@ function buildOnce(changedFile = null) {
324
326
  });
325
327
  const elapsedMs = (performance.now() - startMs).toFixed(0);
326
328
  const changeNote = changedFile ? ` — ${changedFile} changed` : "";
329
+ const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
327
330
  if (outputFile) {
328
331
  writeFileSync(outputFile, `${js}\n`);
329
332
  console.error(
330
- `[${timestamp()}] gofront: OK — wrote ${outputFile} (${elapsedMs}ms${changeNote})`,
333
+ `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${colors.cyan(outputFile)} ${timing}`,
331
334
  );
332
335
  } else {
333
336
  // Clear screen then print
334
337
  process.stdout.write("\x1Bc");
335
338
  console.log(js);
336
339
  console.error(
337
- `[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
340
+ `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
338
341
  );
339
342
  }
340
343
 
@@ -342,14 +345,17 @@ function buildOnce(changedFile = null) {
342
345
  try {
343
346
  copyAssets(resolve("."));
344
347
  } catch (e) {
345
- console.error(`gofront: asset copy failed: ${e.message}`);
348
+ log.warn(`asset copy failed: ${e.message}`);
346
349
  }
347
350
  }
348
351
 
349
352
  devServer?.notify();
350
353
  } catch (e) {
351
- console.error(`[${timestamp()}] gofront: ERROR`);
352
- for (const line of e.message.split("\n")) console.error(` ${line}`);
354
+ console.error(
355
+ `${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
356
+ );
357
+ for (const line of formatDiagnostic(e.message).split("\n"))
358
+ console.error(` ${line}`);
353
359
  devServer?.notifyError?.(e);
354
360
  }
355
361
  }
@@ -365,7 +371,7 @@ function handleCssWatch(filename) {
365
371
  try {
366
372
  copyAssets(resolve("."));
367
373
  } catch (e) {
368
- console.error(`gofront: asset copy failed: ${e.message}`);
374
+ log.warn(`asset copy failed: ${e.message}`);
369
375
  }
370
376
  }
371
377
  devServer?.notifyCss?.(filename);
@@ -385,4 +391,6 @@ watch(watchTarget, { recursive: true }, (_event, filename) => {
385
391
  debounce = setTimeout(() => buildOnce(filename), 80);
386
392
  });
387
393
 
388
- console.error(`[${timestamp()}] gofront: watching ${inputArg} ...`);
394
+ console.error(
395
+ `${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
396
+ );
package/src/minifier.js CHANGED
@@ -462,12 +462,29 @@ function _collectVarLocalDecls(tokens, i, declaredLocals) {
462
462
  collectDestructured(tokens, next.idx, declaredLocals);
463
463
  }
464
464
 
465
+ function _matchingParen(tokens, openIdx) {
466
+ let depth = 1;
467
+ for (let i = openIdx + 1; i < tokens.length; i++) {
468
+ const v = tokens[i].value;
469
+ if (v === "(") depth++;
470
+ else if (v === ")" && --depth === 0) return i;
471
+ }
472
+ return -1;
473
+ }
474
+
475
+ // A `(` opens a parameter list only when its `)` is followed by a body `{` or `=>`;
476
+ // otherwise `name(...)` is a call and its arguments may be free globals.
465
477
  function _collectParamDecls(tokens, i, declaredLocals) {
466
478
  const prev = findPrevNonWsToken(tokens, i);
467
479
  if (
468
- prev &&
469
- (prev.value === "function" || prev.type === "ident" || prev.value === ")")
480
+ !prev ||
481
+ (prev.value !== "function" && prev.type !== "ident" && prev.value !== ")")
470
482
  )
483
+ return;
484
+ const close = _matchingParen(tokens, i);
485
+ if (close < 0) return;
486
+ const after = nextNonWs(tokens, close);
487
+ if (after && (after.tok.value === "{" || after.tok.value === "=>"))
471
488
  collectParams(tokens, i, declaredLocals);
472
489
  }
473
490
 
@@ -519,6 +536,33 @@ const MANGLE_GLOBALS = new Set([
519
536
  "Infinity",
520
537
  "TextEncoder",
521
538
  "TextDecoder",
539
+ "globalThis",
540
+ "Reflect",
541
+ "Proxy",
542
+ "WeakMap",
543
+ "WeakSet",
544
+ "ArrayBuffer",
545
+ "DataView",
546
+ "Int8Array",
547
+ "Uint8Array",
548
+ "Uint8ClampedArray",
549
+ "Int16Array",
550
+ "Uint16Array",
551
+ "Int32Array",
552
+ "Uint32Array",
553
+ "Float32Array",
554
+ "Float64Array",
555
+ "BigInt64Array",
556
+ "BigUint64Array",
557
+ "Image",
558
+ "URL",
559
+ "Event",
560
+ "fetch",
561
+ "navigator",
562
+ "performance",
563
+ "localStorage",
564
+ "requestAnimationFrame",
565
+ "cancelAnimationFrame",
522
566
  "setTimeout",
523
567
  "setInterval",
524
568
  "clearTimeout",
@@ -0,0 +1,23 @@
1
+ // Project configuration loader shared by the CLI, asset manager and vendor bundler.
2
+ // Settings live in `gofront.json` at the project root, or under `"gofront"` in `package.json`.
3
+
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ export function loadProjectConfig(projectRoot) {
8
+ const gofrontJson = join(projectRoot, "gofront.json");
9
+ if (existsSync(gofrontJson)) {
10
+ try {
11
+ const parsed = JSON.parse(readFileSync(gofrontJson, "utf8"));
12
+ if (parsed && typeof parsed === "object") return parsed;
13
+ } catch {}
14
+ }
15
+ const pkgJson = join(projectRoot, "package.json");
16
+ if (existsSync(pkgJson)) {
17
+ try {
18
+ const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
19
+ if (pkg.gofront && typeof pkg.gofront === "object") return pkg.gofront;
20
+ } catch {}
21
+ }
22
+ return {};
23
+ }
@@ -6,6 +6,7 @@ import { statSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join, resolve } from "node:path";
8
8
  import { pathToFileURL } from "node:url";
9
+ import { colorEnabled, createColors, formatDiagnostic } from "./colors.js";
9
10
  import { compilePackageTests, gwFilesIn } from "./compiler.js";
10
11
 
11
12
  export function isTestFunc(decl) {
@@ -38,6 +39,7 @@ export function generateTestHarness(bundleJs, testNames, options = {}) {
38
39
  const runRegex = options.run ? options.run : null;
39
40
  const verbose = Boolean(options.verbose);
40
41
  const dom = Boolean(options.dom);
42
+ const color = Boolean(options.color);
41
43
  const pkgName = options.pkgName ?? "main";
42
44
  // The harness runs from stdin with cwd = project dir, so a bare "jsdom"
43
45
  // import only works if the project has it locally. Use the resolved path.
@@ -86,6 +88,12 @@ async function __runGoFrontSuite() {
86
88
  const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
87
89
  const verbose = ${verbose};
88
90
  const pkgName = ${JSON.stringify(pkgName)};
91
+ const paint = ${color}
92
+ ? (code) => (s) => "\\x1b[" + code + "m" + s + "\\x1b[0m"
93
+ : () => (s) => s;
94
+ const red = paint(31), green = paint(32), yellow = paint(33), dim = paint(2), bold = paint(1);
95
+ const PASS = green("PASS"), FAIL = bold(red("FAIL")), SKIP = yellow("SKIP");
96
+ const okLine = (name, t) => console.log(green("ok ") + "\\t" + name + "\\t" + dim(t + "s"));
89
97
 
90
98
  const testsToRun = runFilter
91
99
  ? allTests.filter((name) => runFilter.test(name))
@@ -93,10 +101,10 @@ async function __runGoFrontSuite() {
93
101
 
94
102
  if (testsToRun.length === 0) {
95
103
  if (allTests.length > 0 && runFilter) {
96
- console.log("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)});
104
+ console.log(yellow("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)}));
97
105
  }
98
- console.log("PASS");
99
- console.log("ok \\t" + pkgName + "\\t0.000s");
106
+ console.log(PASS);
107
+ okLine(pkgName, "0.000");
100
108
  process.exit(0);
101
109
  }
102
110
 
@@ -108,17 +116,17 @@ async function __runGoFrontSuite() {
108
116
  // Go-style streaming report. In non-verbose mode only failures and skips are
109
117
  // shown, so the "=== RUN" header is emitted late for failures.
110
118
  const report = (t) => {
111
- const elapsed = ((now() - t._start) / 1000).toFixed(2);
119
+ const elapsed = dim("(" + ((now() - t._start) / 1000).toFixed(2) + "s)");
112
120
  if (t.skipped) {
113
- console.log("--- SKIP: " + t._name + " (" + elapsed + "s)");
121
+ console.log("--- " + SKIP + ": " + t._name + " " + elapsed);
114
122
  for (const log of t._logs) console.log(" " + log);
115
123
  } else if (t.failed) {
116
- if (!verbose) console.log("=== RUN " + t._name);
117
- for (const log of t._logs) console.log(" " + log);
118
- console.log("--- FAIL: " + t._name + " (" + elapsed + "s)");
124
+ if (!verbose) console.log(dim("=== RUN ") + t._name);
125
+ for (const log of t._logs) console.log(" " + red(log));
126
+ console.log("--- " + FAIL + ": " + t._name + " " + elapsed);
119
127
  } else if (verbose) {
120
128
  for (const log of t._logs) console.log(" " + log);
121
- console.log("--- PASS: " + t._name + " (" + elapsed + "s)");
129
+ console.log("--- " + PASS + ": " + t._name + " " + elapsed);
122
130
  }
123
131
  };
124
132
 
@@ -130,7 +138,7 @@ async function __runGoFrontSuite() {
130
138
  };
131
139
 
132
140
  globalThis.__onSubtestStart = (name) => {
133
- if (verbose) console.log("=== RUN " + name);
141
+ if (verbose) console.log(dim("=== RUN ") + name);
134
142
  };
135
143
  globalThis.__onSubtestEnd = report;
136
144
 
@@ -141,13 +149,13 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
141
149
  for (const name of testsToRun) {
142
150
  const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
143
151
  if (typeof fn !== "function") {
144
- console.error("testing: cannot find test function " + name);
152
+ console.error(red("testing: cannot find test function " + name));
145
153
  totalFailures++;
146
154
  continue;
147
155
  }
148
156
 
149
157
  const t = new __GoFront_T(name);
150
- if (verbose) console.log("=== RUN " + name);
158
+ if (verbose) console.log(dim("=== RUN ") + name);
151
159
 
152
160
  try {
153
161
  const res = fn(t);
@@ -164,12 +172,12 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
164
172
  const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
165
173
 
166
174
  if (totalFailures > 0) {
167
- console.log("FAIL");
168
- console.log("FAIL\\t" + pkgName + "\\t" + suiteElapsed + "s");
175
+ console.log(FAIL);
176
+ console.log(FAIL + "\\t" + pkgName + "\\t" + dim(suiteElapsed + "s"));
169
177
  process.exit(1);
170
178
  } else {
171
- console.log("PASS");
172
- console.log("ok \\t" + pkgName + "\\t" + suiteElapsed + "s");
179
+ console.log(PASS);
180
+ okLine(pkgName, suiteElapsed);
173
181
  process.exit(0);
174
182
  }
175
183
  }
@@ -215,10 +223,17 @@ function reportEmptyTests(label, reason, options) {
215
223
  return { exitCode: 0, stdout: out, stderr: "", noTests: true };
216
224
  }
217
225
  const dest = options.stdout ?? process.stdout;
218
- dest.write(out);
226
+ dest.write(colorsFor(options, dest).dim(out));
219
227
  return { exitCode: 0, noTests: true };
220
228
  }
221
229
 
230
+ // Color is decided here because the harness child writes to a pipe, not a TTY.
231
+ function colorsFor(options, stream) {
232
+ const enabled =
233
+ options.color ?? (!options.captureOutput && colorEnabled(stream));
234
+ return createColors(enabled);
235
+ }
236
+
222
237
  function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
223
238
  return new Promise((resolvePromise, rejectPromise) => {
224
239
  const child = spawn(process.execPath, ["--input-type=module", "-"], {
@@ -289,12 +304,16 @@ export async function runTests(targetDir, options = {}) {
289
304
  try {
290
305
  compiled = compilePackageTests(resolvedDir, options);
291
306
  } catch (err) {
292
- const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${err?.message ?? String(err)}\n`;
307
+ const msg = err?.message ?? String(err);
293
308
  if (options.captureOutput) {
309
+ const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${msg}\n`;
294
310
  return { exitCode: 1, stdout: "", stderr: out };
295
311
  }
296
312
  const dest = options.stderr ?? process.stderr;
297
- dest.write(out);
313
+ const c = colorsFor(options, dest);
314
+ dest.write(
315
+ `${c.bold(c.red("FAIL"))}\t${basename(resolvedDir)} ${c.red("[build failed]")}\n${formatDiagnostic(msg, c)}\n`,
316
+ );
298
317
  return { exitCode: 1 };
299
318
  }
300
319
 
@@ -309,6 +328,7 @@ export async function runTests(targetDir, options = {}) {
309
328
  ...options,
310
329
  pkgName,
311
330
  jsdomPath,
331
+ color: colorsFor(options, options.stdout ?? process.stdout).enabled,
312
332
  });
313
333
 
314
334
  return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
@@ -227,7 +227,10 @@ export const assignabilityMethods = {
227
227
 
228
228
  _isUntypedAssignable(target, source) {
229
229
  if (isUntyped(target)) return true;
230
- if (target.kind !== "basic") return false;
230
+ // Named types with a basic underlying type (`type Mode string`) accept
231
+ // untyped constants of that kind, as in Go.
232
+ if (target.kind === "named") target = this.resolveType(target.underlying);
233
+ if (target?.kind !== "basic") return false;
231
234
  return (
232
235
  (UNTYPED_COMPAT[source.base]?.has(target.name) ?? false) ||
233
236
  (isComplex(target) &&
@@ -347,6 +350,14 @@ export const assignabilityMethods = {
347
350
  if (srcType?.kind === "pointer") srcType = this.resolveType(srcType.base);
348
351
  let base = srcType.kind === "named" ? srcType.underlying : srcType;
349
352
  base = this.resolveType(base);
353
+ // Interface → interface: the source's method set must cover the target's.
354
+ if (base?.kind === "interface") {
355
+ for (const [name, required] of iface.methods) {
356
+ if (!this._implementsMethod(required, base.methods.get(name)))
357
+ return false;
358
+ }
359
+ return true;
360
+ }
350
361
  const methodMap =
351
362
  base?.kind === "struct"
352
363
  ? base.methods
package/src/vendor.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { createRequire } from "node:module";
13
13
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
14
14
  import { pathToFileURL } from "node:url";
15
+ import { loadProjectConfig } from "./project-config.js";
15
16
 
16
17
  export function getExportNames(pkgName, globals = {}) {
17
18
  const base = pkgName.replace(/^@[^/]+\//, "");
@@ -111,15 +112,7 @@ export function loadVendorConfig(projectDir) {
111
112
  } catch {}
112
113
  }
113
114
 
114
- const gfjPath = join(projectDir, "gofront.json");
115
- let gfj = {};
116
- if (existsSync(gfjPath)) {
117
- try {
118
- gfj = JSON.parse(readFileSync(gfjPath, "utf8"));
119
- } catch {}
120
- }
121
-
122
- const vendorConfig = gfj.vendor ?? pkg.vendor ?? null;
115
+ const vendorConfig = loadProjectConfig(projectDir).vendor ?? null;
123
116
  const dependencies = Object.keys(pkg.dependencies ?? {});
124
117
 
125
118
  let dest = null;