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 +16 -0
- package/package.json +1 -1
- package/src/asset-manager.js +7 -27
- package/src/cli-core.js +24 -26
- 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/minifier.js +46 -2
- package/src/project-config.js +23 -0
- package/src/test-runner.js +39 -19
- package/src/typechecker/assignability.js +12 -1
- package/src/vendor.js +2 -9
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
package/src/asset-manager.js
CHANGED
|
@@ -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
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
759
|
+
log.ok(
|
|
760
|
+
`— wrote ${colors.cyan(outputFile)} ${colors.dim(`(${elapsedMs}ms${note})`)}`,
|
|
761
|
+
);
|
|
763
762
|
devServer.notify();
|
|
764
763
|
} catch (e) {
|
|
765
|
-
|
|
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
|
-
|
|
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/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
|
|
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
|
+
}
|
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
|
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
|
|
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;
|