gofront 1.3.10 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@ import {
11
11
  resolve,
12
12
  sep,
13
13
  } from "node:path";
14
+ import { log } from "./colors.js";
14
15
  import { loadProjectConfig } from "./project-config.js";
15
16
 
16
17
  export function loadAssetConfig(projectDir) {
@@ -50,7 +51,7 @@ function copyAssetEntry(entry, projectRoot) {
50
51
 
51
52
  const srcPath = resolve(projectRoot, source);
52
53
  if (!existsSync(srcPath)) {
53
- console.warn(`gofront: asset source '${source}' not found — skipping`);
54
+ log.warn(`asset source '${source}' not found — skipping`);
54
55
  return {
55
56
  status: "skipped",
56
57
  item: { source, dest, status: "skipped", reason: "source not found" },
@@ -73,7 +74,7 @@ function copyAssetEntry(entry, projectRoot) {
73
74
  item: { source, dest: targetFile, type: "file", status: "copied" },
74
75
  };
75
76
  } catch (err) {
76
- console.warn(`gofront: failed to copy asset '${source}': ${err.message}`);
77
+ log.warn(`failed to copy asset '${source}': ${err.message}`);
77
78
  return {
78
79
  status: "failed",
79
80
  item: { source, dest, status: "failed", reason: err.message },
package/src/cli-core.js CHANGED
@@ -23,6 +23,7 @@ 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";
@@ -685,7 +686,7 @@ export async function handleBuild(targetDir = ".", options = {}) {
685
686
  dest: join(outDir, "vendor.js"),
686
687
  });
687
688
  } catch (e) {
688
- console.warn(`gofront: vendor bundle warning: ${e.message}`);
689
+ log.warn(`vendor bundle: ${e.message}`);
689
690
  }
690
691
 
691
692
  const assets = copyReleaseAssets(
@@ -755,11 +756,12 @@ function buildDevOnce(
755
756
  writeFileSync(outputFile, `${result.js}\n`);
756
757
  const elapsedMs = (performance.now() - startMs).toFixed(0);
757
758
  const note = changedFile ? ` — ${changedFile} changed` : "";
758
- console.error(`gofront: OK — wrote ${outputFile} (${elapsedMs}ms${note})`);
759
+ log.ok(
760
+ `— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
761
+ );
759
762
  devServer.notify();
760
763
  } catch (e) {
761
- console.error("gofront: ERROR");
762
- for (const line of e.message.split("\n")) console.error(` ${line}`);
764
+ log.error(e.message);
763
765
  devServer.notifyError(e);
764
766
  }
765
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
+ );
@@ -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