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 +1 -1
- package/src/asset-manager.js +3 -2
- package/src/cli-core.js +6 -4
- package/src/colors.js +81 -0
- package/src/compiler.js +3 -2
- package/src/dev-server.js +4 -3
- package/src/index.js +52 -44
- package/src/test-runner.js +39 -19
- package/src/typechecker/assignability.js +12 -1
package/package.json
CHANGED
package/src/asset-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
759
|
+
log.ok(
|
|
760
|
+
`— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
|
|
761
|
+
);
|
|
759
762
|
devServer.notify();
|
|
760
763
|
} catch (e) {
|
|
761
|
-
|
|
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
|
-
|
|
130
|
-
`
|
|
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
|
-
|
|
279
|
+
log.fail(`port ${port} already in use — try --port <number>`);
|
|
279
280
|
} else {
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
log.fail(e.message);
|
|
107
107
|
process.exit(1);
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
`gofront: ${relative(process.cwd(), pkg.dir) || "."} — OK (${pkg.elapsedMs}ms)`,
|
|
139
|
-
);
|
|
139
|
+
okLine(relative(process.cwd(), pkg.dir) || ".", pkg.elapsedMs);
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
log.info(
|
|
142
|
+
`${colors.bold(`${packages.length} packages`)} — ${colors.bold(colors.green("OK"))} ${ms(elapsedMs)}`,
|
|
143
143
|
);
|
|
144
144
|
} else {
|
|
145
|
-
|
|
145
|
+
okLine(targetDir, elapsedMs);
|
|
146
146
|
}
|
|
147
147
|
process.exit(0);
|
|
148
148
|
} catch (e) {
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
for (const line of dev.initialError.message.split("\n"))
|
|
179
|
-
console.error(` ${line}`);
|
|
181
|
+
log.error(dev.initialError.message);
|
|
180
182
|
} else {
|
|
181
|
-
|
|
183
|
+
log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
|
|
182
184
|
}
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
|
|
276
282
|
} catch (e) {
|
|
277
|
-
|
|
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
|
-
|
|
295
|
+
log.info(`copied ${copied} assets (${skipped} skipped)`);
|
|
290
296
|
}
|
|
291
297
|
} catch (e) {
|
|
292
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
352
|
-
|
|
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
|
-
|
|
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(
|
|
394
|
+
console.error(
|
|
395
|
+
`${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
|
|
396
|
+
);
|
package/src/test-runner.js
CHANGED
|
@@ -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(
|
|
99
|
-
|
|
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 + "
|
|
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 + "
|
|
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 + "
|
|
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(
|
|
168
|
-
console.log("
|
|
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(
|
|
172
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|