gofront 1.2.0 → 1.2.1
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 +27 -0
- package/package.json +1 -1
- package/src/asset-manager.js +6 -1
- package/src/cli-core.js +48 -3
- package/src/dev-server.js +8 -3
- package/src/index.js +9 -50
- package/src/test-runner.js +60 -86
- package/src/vendor.js +32 -19
package/README.md
CHANGED
|
@@ -454,6 +454,12 @@ gofront test -run "Todo" # run only tests matching regex pattern
|
|
|
454
454
|
gofront test --dom # initialize JSDOM for DOM/gom/templ tests
|
|
455
455
|
```
|
|
456
456
|
|
|
457
|
+
`--dom` requires `jsdom`. It is resolved from the project directory first and
|
|
458
|
+
then from GoFront's own installation, so a globally installed `gofront` works
|
|
459
|
+
without a project-local copy.
|
|
460
|
+
|
|
461
|
+
Only `TestXxx` functions declared in `*_test.go` files are discovered, matching Go.
|
|
462
|
+
|
|
457
463
|
---
|
|
458
464
|
|
|
459
465
|
## Static assets & vendor bundling
|
|
@@ -485,6 +491,27 @@ gofront prep
|
|
|
485
491
|
|
|
486
492
|
GoFront inspects your `dependencies`, dynamically uses `rolldown` or `esbuild` from your `devDependencies`, and bundles them into an ES module (defaulting to `app/vendor.js` or `vendor.js`).
|
|
487
493
|
|
|
494
|
+
Each bundled package is exposed on `window` under its full name, its unscoped name,
|
|
495
|
+
and a sanitised identifier (e.g. `fuse.js` → `window["fuse.js"]`, `window.fuse_js`).
|
|
496
|
+
To expose additional global names, add a `vendor.globals` mapping:
|
|
497
|
+
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"vendor": {
|
|
501
|
+
"dest": ["app/vendor.js", "public/vendor.js"],
|
|
502
|
+
"minify": true,
|
|
503
|
+
"globals": {
|
|
504
|
+
"fuse.js": ["Fuse"],
|
|
505
|
+
"prismjs": ["Prism"],
|
|
506
|
+
"@emailjs/browser": ["emailjs"]
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`dest` accepts a string or an array (the bundle is written once and copied to each
|
|
513
|
+
extra destination). `gofront prep --minify` overrides `minify` from the command line.
|
|
514
|
+
|
|
488
515
|
---
|
|
489
516
|
|
|
490
517
|
## Multi-file packages
|
package/package.json
CHANGED
package/src/asset-manager.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
join,
|
|
17
17
|
relative,
|
|
18
18
|
resolve,
|
|
19
|
+
sep,
|
|
19
20
|
} from "node:path";
|
|
20
21
|
|
|
21
22
|
export function loadAssetConfig(projectDir) {
|
|
@@ -58,7 +59,11 @@ function copyAssetEntry(entry, projectRoot) {
|
|
|
58
59
|
|
|
59
60
|
const destPath = resolve(projectRoot, dest);
|
|
60
61
|
const relDest = relative(projectRoot, destPath);
|
|
61
|
-
if (
|
|
62
|
+
if (
|
|
63
|
+
relDest === ".." ||
|
|
64
|
+
relDest.startsWith(`..${sep}`) ||
|
|
65
|
+
isAbsolute(relDest)
|
|
66
|
+
) {
|
|
62
67
|
throw new Error(
|
|
63
68
|
`gofront: asset destination '${dest}' is outside project directory`,
|
|
64
69
|
);
|
package/src/cli-core.js
CHANGED
|
@@ -74,7 +74,52 @@ export async function handlePrep(targetDir, options = {}) {
|
|
|
74
74
|
return { assets, vendor };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export
|
|
78
|
-
const
|
|
79
|
-
return
|
|
77
|
+
export function parsePrepArgs(argv) {
|
|
78
|
+
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
79
|
+
return {
|
|
80
|
+
targetDir: positional[0] ?? ".",
|
|
81
|
+
vendorConfig: argv.includes("--minify") ? { minify: true } : {},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatPrepSummary({ assets, vendor }) {
|
|
86
|
+
const lines = [];
|
|
87
|
+
if (assets.copied > 0 || assets.skipped > 0) {
|
|
88
|
+
lines.push(`copied ${assets.copied} assets (${assets.skipped} skipped)`);
|
|
89
|
+
}
|
|
90
|
+
if (vendor.bundled?.length > 0) {
|
|
91
|
+
const dest = Array.isArray(vendor.dest)
|
|
92
|
+
? vendor.dest.join(", ")
|
|
93
|
+
: vendor.dest;
|
|
94
|
+
const min = vendor.minify ? " (minified)" : "";
|
|
95
|
+
lines.push(
|
|
96
|
+
`bundled ${vendor.bundled.length} vendor dependencies → ${dest}${min}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseTestArgs(argv) {
|
|
103
|
+
let run = null;
|
|
104
|
+
const positional = [];
|
|
105
|
+
for (let i = 0; i < argv.length; i++) {
|
|
106
|
+
const arg = argv[i];
|
|
107
|
+
if (arg === "-run" || arg === "--run") {
|
|
108
|
+
run = argv[++i] ?? null;
|
|
109
|
+
} else if (arg.startsWith("-run=") || arg.startsWith("--run=")) {
|
|
110
|
+
run = arg.slice(arg.indexOf("=") + 1);
|
|
111
|
+
} else if (!arg.startsWith("-")) {
|
|
112
|
+
positional.push(arg);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
targetDir: positional[0] ?? ".",
|
|
117
|
+
verbose: argv.includes("-v"),
|
|
118
|
+
dom: argv.includes("--dom"),
|
|
119
|
+
run,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function handleTest(targetDir, options = {}) {
|
|
124
|
+
return runTests(targetDir, options);
|
|
80
125
|
}
|
package/src/dev-server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { extname, join, resolve } from "node:path";
|
|
6
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
|
|
8
8
|
export const MIME = {
|
|
9
9
|
".html": "text/html; charset=utf-8",
|
|
@@ -22,6 +22,12 @@ export const MIME = {
|
|
|
22
22
|
// Injected at the bottom of compiled JS in serve mode.
|
|
23
23
|
export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
|
|
24
24
|
|
|
25
|
+
// True when `target` is `root` itself or a path inside it (no `..` escape).
|
|
26
|
+
function isInsideDir(root, target) {
|
|
27
|
+
const rel = relative(root, target);
|
|
28
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export function handleDevRequest(req, res, serveDir, clients = new Set()) {
|
|
26
32
|
// SSE endpoint — browser connects here to receive reload events
|
|
27
33
|
if (req.url === "/_gofront/events") {
|
|
@@ -45,9 +51,8 @@ export function handleDevRequest(req, res, serveDir, clients = new Set()) {
|
|
|
45
51
|
|
|
46
52
|
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
47
53
|
|
|
48
|
-
const resolvedServe = resolve(serveDir);
|
|
49
54
|
const filePath = resolve(serveDir, `.${urlPath}`);
|
|
50
|
-
if (!
|
|
55
|
+
if (!isInsideDir(resolve(serveDir), filePath)) {
|
|
51
56
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
52
57
|
res.end("Forbidden");
|
|
53
58
|
return;
|
package/src/index.js
CHANGED
|
@@ -22,10 +22,13 @@ import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
|
22
22
|
import { dirname, resolve } from "node:path";
|
|
23
23
|
import { copyAssets } from "./asset-manager.js";
|
|
24
24
|
import {
|
|
25
|
+
formatPrepSummary,
|
|
25
26
|
handleInit,
|
|
26
27
|
handlePrep,
|
|
27
28
|
handleTest,
|
|
28
29
|
maybeMinify,
|
|
30
|
+
parsePrepArgs,
|
|
31
|
+
parseTestArgs,
|
|
29
32
|
runCompile,
|
|
30
33
|
} from "./cli-core.js";
|
|
31
34
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
@@ -69,29 +72,11 @@ Usage:
|
|
|
69
72
|
// ── prep / vendor subcommand ──────────────────────────────────
|
|
70
73
|
|
|
71
74
|
if (args[0] === "prep" || args[0] === "vendor") {
|
|
72
|
-
const
|
|
73
|
-
const filteredArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
74
|
-
const targetArg = filteredArgs[0] ?? ".";
|
|
75
|
-
const targetDir = resolve(targetArg);
|
|
75
|
+
const { targetDir, vendorConfig } = parsePrepArgs(args.slice(1));
|
|
76
76
|
try {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
if (assets.copied > 0 || assets.skipped > 0) {
|
|
82
|
-
console.error(
|
|
83
|
-
`gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
if (vendor.bundled?.length > 0) {
|
|
87
|
-
const destStr = Array.isArray(vendor.dest)
|
|
88
|
-
? vendor.dest.join(", ")
|
|
89
|
-
: vendor.dest;
|
|
90
|
-
const minStr = vendor.minify ? " (minified)" : "";
|
|
91
|
-
console.error(
|
|
92
|
-
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${destStr}${minStr}`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
77
|
+
const result = await handlePrep(resolve(targetDir), { vendorConfig });
|
|
78
|
+
for (const line of formatPrepSummary(result))
|
|
79
|
+
console.error(`gofront: ${line}`);
|
|
95
80
|
} catch (e) {
|
|
96
81
|
console.error(`gofront: ${e.message}`);
|
|
97
82
|
process.exit(1);
|
|
@@ -121,35 +106,9 @@ if (args[0] === "init") {
|
|
|
121
106
|
// ── test subcommand ───────────────────────────────────────────
|
|
122
107
|
|
|
123
108
|
if (args[0] === "test") {
|
|
124
|
-
const
|
|
125
|
-
let targetDir = ".";
|
|
126
|
-
let runFilter = null;
|
|
127
|
-
const nonFlagArgs = [];
|
|
128
|
-
for (let i = 0; i < flags.length; i++) {
|
|
129
|
-
const arg = flags[i];
|
|
130
|
-
if (arg === "-run" || arg === "--run") {
|
|
131
|
-
runFilter = flags[++i] ?? null;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (arg.startsWith("-run=") || arg.startsWith("--run=")) {
|
|
135
|
-
runFilter = arg.slice(arg.indexOf("=") + 1);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (arg.startsWith("-")) continue;
|
|
139
|
-
nonFlagArgs.push(arg);
|
|
140
|
-
}
|
|
141
|
-
if (nonFlagArgs.length > 0) {
|
|
142
|
-
targetDir = nonFlagArgs[0];
|
|
143
|
-
}
|
|
144
|
-
const verbose = flags.includes("-v");
|
|
145
|
-
const dom = flags.includes("--dom");
|
|
146
|
-
|
|
109
|
+
const { targetDir, ...testOptions } = parseTestArgs(args.slice(1));
|
|
147
110
|
try {
|
|
148
|
-
const result = await handleTest(targetDir,
|
|
149
|
-
verbose,
|
|
150
|
-
dom,
|
|
151
|
-
run: runFilter,
|
|
152
|
-
});
|
|
111
|
+
const result = await handleTest(targetDir, testOptions);
|
|
153
112
|
process.exit(result.exitCode);
|
|
154
113
|
} catch (e) {
|
|
155
114
|
console.error(`gofront: ${e.message}`);
|
package/src/test-runner.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { basename, join, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { compilePackageTests, gwFilesIn } from "./compiler.js";
|
|
8
9
|
|
|
9
10
|
export function isTestFunc(decl) {
|
|
@@ -17,9 +18,12 @@ export function isTestFunc(decl) {
|
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Only programs from *_test.go files are scanned, matching Go. Programs
|
|
22
|
+
// without a `_filename` (constructed by hand) are treated as test files.
|
|
20
23
|
export function discoverTests(programs) {
|
|
21
24
|
const testNames = [];
|
|
22
25
|
for (const p of programs) {
|
|
26
|
+
if (p._filename && !p._filename.endsWith("_test.go")) continue;
|
|
23
27
|
for (const d of p.decls) {
|
|
24
28
|
if (isTestFunc(d) && !testNames.includes(d.name)) {
|
|
25
29
|
testNames.push(d.name);
|
|
@@ -34,11 +38,16 @@ export function generateTestHarness(bundleJs, testNames, options = {}) {
|
|
|
34
38
|
const verbose = Boolean(options.verbose);
|
|
35
39
|
const dom = Boolean(options.dom);
|
|
36
40
|
const pkgName = options.pkgName ?? "main";
|
|
41
|
+
// The harness runs from stdin with cwd = project dir, so a bare "jsdom"
|
|
42
|
+
// import only works if the project has it locally. Use the resolved path.
|
|
43
|
+
const jsdomSpec = options.jsdomPath
|
|
44
|
+
? pathToFileURL(options.jsdomPath).href
|
|
45
|
+
: "jsdom";
|
|
37
46
|
|
|
38
47
|
return `
|
|
39
48
|
${
|
|
40
49
|
dom
|
|
41
|
-
? `import { JSDOM } from
|
|
50
|
+
? `import { JSDOM } from ${JSON.stringify(jsdomSpec)};
|
|
42
51
|
const __dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
43
52
|
url: "http://localhost/",
|
|
44
53
|
pretendToBeVisual: true,
|
|
@@ -92,37 +101,38 @@ async function __runGoFrontSuite() {
|
|
|
92
101
|
|
|
93
102
|
let totalFailures = 0;
|
|
94
103
|
let totalSkipped = 0;
|
|
95
|
-
const
|
|
104
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
105
|
+
const suiteStart = now();
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
// Go-style streaming report. In non-verbose mode only failures and skips are
|
|
108
|
+
// shown, so the "=== RUN" header is emitted late for failures.
|
|
109
|
+
const report = (t) => {
|
|
110
|
+
const elapsed = ((now() - t._start) / 1000).toFixed(2);
|
|
111
|
+
if (t.skipped) {
|
|
112
|
+
console.log("--- SKIP: " + t._name + " (" + elapsed + "s)");
|
|
113
|
+
for (const log of t._logs) console.log(" " + log);
|
|
114
|
+
} else if (t.failed) {
|
|
115
|
+
if (!verbose) console.log("=== RUN " + t._name);
|
|
116
|
+
for (const log of t._logs) console.log(" " + log);
|
|
117
|
+
console.log("--- FAIL: " + t._name + " (" + elapsed + "s)");
|
|
118
|
+
} else if (verbose) {
|
|
119
|
+
for (const log of t._logs) console.log(" " + log);
|
|
120
|
+
console.log("--- PASS: " + t._name + " (" + elapsed + "s)");
|
|
100
121
|
}
|
|
101
122
|
};
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.log(" " + log);
|
|
109
|
-
}
|
|
110
|
-
} else if (subT.failed) {
|
|
111
|
-
if (!verbose) {
|
|
112
|
-
console.log("=== RUN " + subT._name);
|
|
113
|
-
}
|
|
114
|
-
for (const log of subT._logs) {
|
|
115
|
-
console.log(" " + log);
|
|
116
|
-
}
|
|
117
|
-
console.log("--- FAIL: " + subT._name + " (" + elapsed + "s)");
|
|
118
|
-
} else if (verbose) {
|
|
119
|
-
for (const log of subT._logs) {
|
|
120
|
-
console.log(" " + log);
|
|
121
|
-
}
|
|
122
|
-
console.log("--- PASS: " + subT._name + " (" + elapsed + "s)");
|
|
123
|
-
}
|
|
124
|
+
// FailNow/SkipNow are control flow; anything else is a panic → failure.
|
|
125
|
+
const recordTestError = (t, e) => {
|
|
126
|
+
if (e instanceof __GoFront_FailNow || e instanceof __GoFront_SkipNow) return;
|
|
127
|
+
t.Fail();
|
|
128
|
+
t.Log("panic: " + (e?.message ?? String(e)));
|
|
124
129
|
};
|
|
125
130
|
|
|
131
|
+
globalThis.__onSubtestStart = (name) => {
|
|
132
|
+
if (verbose) console.log("=== RUN " + name);
|
|
133
|
+
};
|
|
134
|
+
globalThis.__onSubtestEnd = report;
|
|
135
|
+
|
|
126
136
|
const __testRegistry = {
|
|
127
137
|
${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ? ${n} : null`).join(",\n")}
|
|
128
138
|
};
|
|
@@ -136,54 +146,21 @@ ${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ?
|
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
const t = new __GoFront_T(name);
|
|
139
|
-
if (verbose)
|
|
140
|
-
console.log("=== RUN " + name);
|
|
141
|
-
}
|
|
149
|
+
if (verbose) console.log("=== RUN " + name);
|
|
142
150
|
|
|
143
151
|
try {
|
|
144
152
|
const res = fn(t);
|
|
145
|
-
if (res && typeof res.then === "function")
|
|
146
|
-
await res;
|
|
147
|
-
}
|
|
153
|
+
if (res && typeof res.then === "function") await res;
|
|
148
154
|
} catch (e) {
|
|
149
|
-
|
|
150
|
-
// expected on FailNow
|
|
151
|
-
} else if (e instanceof __GoFront_SkipNow) {
|
|
152
|
-
// expected on SkipNow
|
|
153
|
-
} else {
|
|
154
|
-
t.Fail();
|
|
155
|
-
t.Log("panic: " + (e?.message ?? String(e)));
|
|
156
|
-
}
|
|
155
|
+
recordTestError(t, e);
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
totalSkipped++;
|
|
163
|
-
console.log("--- SKIP: " + name + " (" + elapsed + "s)");
|
|
164
|
-
for (const log of t._logs) {
|
|
165
|
-
console.log(" " + log);
|
|
166
|
-
}
|
|
167
|
-
} else if (t.failed) {
|
|
168
|
-
totalFailures++;
|
|
169
|
-
if (!verbose) {
|
|
170
|
-
console.log("=== RUN " + name);
|
|
171
|
-
}
|
|
172
|
-
for (const log of t._logs) {
|
|
173
|
-
console.log(" " + log);
|
|
174
|
-
}
|
|
175
|
-
console.log("--- FAIL: " + name + " (" + elapsed + "s)");
|
|
176
|
-
} else {
|
|
177
|
-
if (verbose) {
|
|
178
|
-
for (const log of t._logs) {
|
|
179
|
-
console.log(" " + log);
|
|
180
|
-
}
|
|
181
|
-
console.log("--- PASS: " + name + " (" + elapsed + "s)");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
158
|
+
if (t.skipped) totalSkipped++;
|
|
159
|
+
else if (t.failed) totalFailures++;
|
|
160
|
+
report(t);
|
|
184
161
|
}
|
|
185
162
|
|
|
186
|
-
const suiteElapsed = ((
|
|
163
|
+
const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
|
|
187
164
|
|
|
188
165
|
if (totalFailures > 0) {
|
|
189
166
|
console.log("FAIL");
|
|
@@ -200,6 +177,16 @@ __runGoFrontSuite();
|
|
|
200
177
|
`;
|
|
201
178
|
}
|
|
202
179
|
|
|
180
|
+
// Project-local jsdom wins; fall back to the one next to GoFront (global/npx installs).
|
|
181
|
+
export function resolveJsdomPath(projectDir) {
|
|
182
|
+
for (const from of [join(projectDir, "dummy.js"), import.meta.url]) {
|
|
183
|
+
try {
|
|
184
|
+
return createRequire(from).resolve("jsdom");
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
203
190
|
function validateTestOptions(options, resolvedDir) {
|
|
204
191
|
if (options.run) {
|
|
205
192
|
try {
|
|
@@ -209,26 +196,16 @@ function validateTestOptions(options, resolvedDir) {
|
|
|
209
196
|
}
|
|
210
197
|
}
|
|
211
198
|
|
|
199
|
+
let jsdomPath = null;
|
|
212
200
|
if (options.dom) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const req = createRequire(join(resolvedDir, "dummy.js"));
|
|
216
|
-
req.resolve("jsdom");
|
|
217
|
-
jsdomFound = true;
|
|
218
|
-
} catch {}
|
|
219
|
-
if (!jsdomFound) {
|
|
220
|
-
try {
|
|
221
|
-
const req = createRequire(import.meta.url);
|
|
222
|
-
req.resolve("jsdom");
|
|
223
|
-
jsdomFound = true;
|
|
224
|
-
} catch {}
|
|
225
|
-
}
|
|
226
|
-
if (!jsdomFound) {
|
|
201
|
+
jsdomPath = resolveJsdomPath(resolvedDir);
|
|
202
|
+
if (!jsdomPath) {
|
|
227
203
|
throw new Error(
|
|
228
204
|
"gofront: --dom requires 'jsdom' to be installed. Run 'npm install --save-dev jsdom' to enable DOM testing.",
|
|
229
205
|
);
|
|
230
206
|
}
|
|
231
207
|
}
|
|
208
|
+
return { jsdomPath };
|
|
232
209
|
}
|
|
233
210
|
|
|
234
211
|
function reportEmptyTests(label, reason, options) {
|
|
@@ -290,7 +267,7 @@ function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
|
290
267
|
|
|
291
268
|
export async function runTests(targetDir, options = {}) {
|
|
292
269
|
const resolvedDir = resolve(targetDir);
|
|
293
|
-
validateTestOptions(options, resolvedDir);
|
|
270
|
+
const { jsdomPath } = validateTestOptions(options, resolvedDir);
|
|
294
271
|
|
|
295
272
|
const allFiles = gwFilesIn(resolvedDir, { includeTests: true });
|
|
296
273
|
if (allFiles.length === 0) {
|
|
@@ -322,14 +299,11 @@ export async function runTests(targetDir, options = {}) {
|
|
|
322
299
|
return reportEmptyTests(pkgName, "no tests to run", options);
|
|
323
300
|
}
|
|
324
301
|
|
|
325
|
-
const harnessJs = generateTestHarness(
|
|
302
|
+
const harnessJs = generateTestHarness(js, testNames, {
|
|
326
303
|
...options,
|
|
327
304
|
pkgName,
|
|
305
|
+
jsdomPath,
|
|
328
306
|
});
|
|
329
307
|
|
|
330
308
|
return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
|
|
331
309
|
}
|
|
332
|
-
|
|
333
|
-
function bundleJsClean(js) {
|
|
334
|
-
return js;
|
|
335
|
-
}
|
package/src/vendor.js
CHANGED
|
@@ -13,28 +13,17 @@ import { createRequire } from "node:module";
|
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
14
14
|
import { pathToFileURL } from "node:url";
|
|
15
15
|
|
|
16
|
-
export function getExportNames(pkgName) {
|
|
16
|
+
export function getExportNames(pkgName, globals = {}) {
|
|
17
17
|
const base = pkgName.replace(/^@[^/]+\//, "");
|
|
18
18
|
const clean = base.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
19
19
|
const names = new Set([pkgName, base, clean]);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
names.add("Fuse");
|
|
24
|
-
names.add("fuse");
|
|
25
|
-
}
|
|
26
|
-
if (pkgName === "prismjs") {
|
|
27
|
-
names.add("Prism");
|
|
28
|
-
names.add("prism");
|
|
29
|
-
}
|
|
30
|
-
if (pkgName === "marked") {
|
|
31
|
-
names.add("marked");
|
|
32
|
-
}
|
|
33
|
-
|
|
20
|
+
const extra = globals[pkgName];
|
|
21
|
+
for (const name of Array.isArray(extra) ? extra : extra ? [extra] : [])
|
|
22
|
+
names.add(name);
|
|
34
23
|
return [...names];
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
export function generateVendorEntry(packages) {
|
|
26
|
+
export function generateVendorEntry(packages, globals = {}) {
|
|
38
27
|
const imports = [];
|
|
39
28
|
const assignments = [];
|
|
40
29
|
const exports = [];
|
|
@@ -43,7 +32,7 @@ export function generateVendorEntry(packages) {
|
|
|
43
32
|
const id = `_dep_${idx}`;
|
|
44
33
|
imports.push(`import * as ${id} from ${JSON.stringify(pkg)};`);
|
|
45
34
|
|
|
46
|
-
const names = getExportNames(pkg);
|
|
35
|
+
const names = getExportNames(pkg, globals);
|
|
47
36
|
for (const name of names) {
|
|
48
37
|
assignments.push(
|
|
49
38
|
` window[${JSON.stringify(name)}] = ${id}.default || ${id};`,
|
|
@@ -91,6 +80,27 @@ export function findBundler(projectDir) {
|
|
|
91
80
|
return null;
|
|
92
81
|
}
|
|
93
82
|
|
|
83
|
+
// Normalises `vendor.globals` ({ pkg: string | string[] }) to { pkg: string[] }.
|
|
84
|
+
function normalizeGlobals(raw) {
|
|
85
|
+
if (raw === undefined || raw === null) return {};
|
|
86
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"gofront: vendor.globals must be an object mapping package names to global names",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [pkg, value] of Object.entries(raw)) {
|
|
93
|
+
const list = Array.isArray(value) ? value : [value];
|
|
94
|
+
if (list.some((v) => typeof v !== "string")) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`gofront: vendor.globals[${JSON.stringify(pkg)}] must be a string or array of strings`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
out[pkg] = list;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
export function loadVendorConfig(projectDir) {
|
|
95
105
|
const pkgPath = join(projectDir, "package.json");
|
|
96
106
|
let pkg = {};
|
|
@@ -114,6 +124,7 @@ export function loadVendorConfig(projectDir) {
|
|
|
114
124
|
let dest = null;
|
|
115
125
|
let packages = dependencies;
|
|
116
126
|
let minify = false;
|
|
127
|
+
let globals = {};
|
|
117
128
|
|
|
118
129
|
if (typeof vendorConfig === "string") {
|
|
119
130
|
dest = vendorConfig;
|
|
@@ -121,13 +132,14 @@ export function loadVendorConfig(projectDir) {
|
|
|
121
132
|
if (vendorConfig.dest) dest = vendorConfig.dest;
|
|
122
133
|
if (Array.isArray(vendorConfig.packages)) packages = vendorConfig.packages;
|
|
123
134
|
if (typeof vendorConfig.minify === "boolean") minify = vendorConfig.minify;
|
|
135
|
+
globals = normalizeGlobals(vendorConfig.globals);
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
if (!dest) {
|
|
127
139
|
dest = existsSync(join(projectDir, "app")) ? "app/vendor.js" : "vendor.js";
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
return { dest, packages, minify };
|
|
142
|
+
return { dest, packages, minify, globals };
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
export function resolveDestinationPaths(projectRoot, dest) {
|
|
@@ -201,6 +213,7 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
201
213
|
const packages = options.packages ?? config.packages;
|
|
202
214
|
const dest = options.dest ?? config.dest;
|
|
203
215
|
const minify = options.minify ?? config.minify ?? false;
|
|
216
|
+
const globals = { ...config.globals, ...normalizeGlobals(options.globals) };
|
|
204
217
|
|
|
205
218
|
if (!packages || packages.length === 0) {
|
|
206
219
|
return {
|
|
@@ -247,7 +260,7 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
247
260
|
};
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
const entryCode = generateVendorEntry(packages);
|
|
263
|
+
const entryCode = generateVendorEntry(packages, globals);
|
|
251
264
|
const entryFile = join(
|
|
252
265
|
projectRoot,
|
|
253
266
|
`.gofront-vendor-entry-${Date.now()}.mjs`,
|