gofront 1.2.0 → 1.2.2
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 +1 -87
- package/package.json +1 -1
- package/src/asset-manager.js +6 -1
- package/src/cli-core.js +48 -3
- package/src/codegen/index.js +15 -2
- package/src/codegen/statements.js +4 -1
- package/src/dev-server.js +8 -3
- package/src/index.js +9 -50
- package/src/minifier.js +19 -7
- package/src/test-runner.js +60 -86
- package/src/vendor.js +32 -19
package/README.md
CHANGED
|
@@ -387,16 +387,13 @@ gofront <dir> compile all *.go in directory → stdout
|
|
|
387
387
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
388
388
|
gofront <input> --check type-check only
|
|
389
389
|
gofront <input> --watch watch for changes and recompile
|
|
390
|
-
gofront <input> -o out.js --serve watch + serve with live reload
|
|
390
|
+
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
391
391
|
gofront <input> -o out.js --serve --port 8080 use a custom port
|
|
392
|
-
gofront <input> -o out.js --copy-assets compile and synchronize static assets from package.json assetCopy
|
|
393
392
|
gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
|
|
394
393
|
gofront <input> --minify minify output (built-in minifier)
|
|
395
394
|
gofront <input> --minify --mangle minify and rename local identifiers
|
|
396
395
|
gofront <file.go> --ast dump AST (debug)
|
|
397
396
|
gofront <file.go> --tokens dump tokens (debug)
|
|
398
|
-
gofront test [dir] [--dom] run unit tests (-v verbose, -run <regex>)
|
|
399
|
-
gofront prep [dir] copy static assets and bundle vendor dependencies
|
|
400
397
|
gofront init [dir] scaffold a new project
|
|
401
398
|
gofront --version / -v print version
|
|
402
399
|
gofront --help / -h print this help
|
|
@@ -404,89 +401,6 @@ gofront --help / -h print this help
|
|
|
404
401
|
|
|
405
402
|
---
|
|
406
403
|
|
|
407
|
-
## Unit testing (`gofront test`)
|
|
408
|
-
|
|
409
|
-
GoFront provides native, Go-idiomatic unit testing out of the box:
|
|
410
|
-
|
|
411
|
-
- Files ending in `*_test.go` are automatically excluded from production builds (`gofront .`, `gofront -o app.js`) and compiled only when running tests.
|
|
412
|
-
- Built-in `testing` standard library package with `*testing.T`.
|
|
413
|
-
- Discover and execute `func TestXxx(t *testing.T)` functions with standard Go-style output.
|
|
414
|
-
|
|
415
|
-
### Writing a test
|
|
416
|
-
|
|
417
|
-
```go
|
|
418
|
-
package store
|
|
419
|
-
|
|
420
|
-
import "testing"
|
|
421
|
-
|
|
422
|
-
func TestAddTodo(t *testing.T) {
|
|
423
|
-
store := NewTodoStore()
|
|
424
|
-
store.Add("Buy milk")
|
|
425
|
-
|
|
426
|
-
if store.Count() != 1 {
|
|
427
|
-
t.Errorf("expected 1 item, got %d", store.Count())
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Subtests and skips are fully supported:
|
|
433
|
-
|
|
434
|
-
```go
|
|
435
|
-
func TestFilter(t *testing.T) {
|
|
436
|
-
t.Run("active", func(t *testing.T) {
|
|
437
|
-
// subtest logic
|
|
438
|
-
})
|
|
439
|
-
t.Run("completed", func(t *testing.T) {
|
|
440
|
-
if testing.Short() {
|
|
441
|
-
t.Skip("skipping in short mode")
|
|
442
|
-
}
|
|
443
|
-
})
|
|
444
|
-
}
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
### Running tests
|
|
448
|
-
|
|
449
|
-
```sh
|
|
450
|
-
gofront test # run tests in current directory
|
|
451
|
-
gofront test ./src # run tests in specific directory
|
|
452
|
-
gofront test -v # verbose output (RUN, PASS/FAIL, logs)
|
|
453
|
-
gofront test -run "Todo" # run only tests matching regex pattern
|
|
454
|
-
gofront test --dom # initialize JSDOM for DOM/gom/templ tests
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
---
|
|
458
|
-
|
|
459
|
-
## Static assets & vendor bundling
|
|
460
|
-
|
|
461
|
-
GoFront includes native tooling to manage static assets and package external npm dependencies for the browser without requiring external wrappers or complex build tools.
|
|
462
|
-
|
|
463
|
-
### Asset copying (`assetCopy`)
|
|
464
|
-
|
|
465
|
-
Specify static assets (fonts, stylesheets, images) in `package.json` (or `gofront.json`):
|
|
466
|
-
|
|
467
|
-
```json
|
|
468
|
-
{
|
|
469
|
-
"assetCopy": [
|
|
470
|
-
{ "source": "node_modules/@fontsource/raleway/files", "dest": "app/fonts" },
|
|
471
|
-
{ "source": "node_modules/prismjs/themes", "dest": "app/css/prism-themes" }
|
|
472
|
-
]
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
Run `gofront prep` to copy them, or pass `--copy-assets` to automatically synchronize assets on build.
|
|
477
|
-
|
|
478
|
-
### Vendor bundling (`gofront prep`)
|
|
479
|
-
|
|
480
|
-
Package external npm dependencies for the browser:
|
|
481
|
-
|
|
482
|
-
```sh
|
|
483
|
-
gofront prep
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
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
|
-
|
|
488
|
-
---
|
|
489
|
-
|
|
490
404
|
## Multi-file packages
|
|
491
405
|
|
|
492
406
|
All `.go` files in a directory share the same namespace and are compiled as one unit.
|
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/codegen/index.js
CHANGED
|
@@ -333,10 +333,23 @@ export class CodeGen {
|
|
|
333
333
|
if (fields.length === 0) {
|
|
334
334
|
this.line("constructor() {}");
|
|
335
335
|
} else {
|
|
336
|
-
|
|
336
|
+
// A field named like a type used in a default (e.g. `Project Project`)
|
|
337
|
+
// would shadow that class inside the parameter scope, so alias it.
|
|
338
|
+
const binding = (f) => {
|
|
339
|
+
const re = new RegExp(`\\b${f.name}\\b`);
|
|
340
|
+
return fields.some((o) => re.test(o.zero)) ? `${f.name}$` : f.name;
|
|
341
|
+
};
|
|
342
|
+
const params = fields
|
|
343
|
+
.map((f) => {
|
|
344
|
+
const b = binding(f);
|
|
345
|
+
return b === f.name
|
|
346
|
+
? `${f.name} = ${f.zero}`
|
|
347
|
+
: `${f.name}: ${b} = ${f.zero}`;
|
|
348
|
+
})
|
|
349
|
+
.join(", ");
|
|
337
350
|
this.line(`constructor({ ${params} } = {}) {`);
|
|
338
351
|
this.indented(() => {
|
|
339
|
-
for (const f of fields) this.line(`this.${f.name} = ${f
|
|
352
|
+
for (const f of fields) this.line(`this.${f.name} = ${binding(f)};`);
|
|
340
353
|
});
|
|
341
354
|
this.line("}");
|
|
342
355
|
}
|
|
@@ -448,7 +448,10 @@ export const statementGenMethods = {
|
|
|
448
448
|
},
|
|
449
449
|
|
|
450
450
|
_genRangeIterExpr(iterType, iteree, lhs) {
|
|
451
|
-
if (this._isMapRangeType(iterType))
|
|
451
|
+
if (this._isMapRangeType(iterType)) {
|
|
452
|
+
if (lhs.length === 1) return `Object.keys(${iteree})`;
|
|
453
|
+
return `Object.entries(${iteree})`;
|
|
454
|
+
}
|
|
452
455
|
if (this._isStringRangeType(iterType)) {
|
|
453
456
|
if (lhs.length === 1) return `Array.from(${iteree}).keys()`;
|
|
454
457
|
return `Array.from(${iteree}, (__c, __i) => [__i, __c.codePointAt(0)])`;
|
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/minifier.js
CHANGED
|
@@ -545,10 +545,18 @@ function _applyRenaming(tokens, renameMap) {
|
|
|
545
545
|
|
|
546
546
|
function mangle(code) {
|
|
547
547
|
const tokens = tokenize(code);
|
|
548
|
-
const nameGen = shortNameGeneratorFn();
|
|
549
|
-
const renameMap = new Map();
|
|
550
548
|
const { declaredLocals } = _collectLocalDecls(tokens);
|
|
551
549
|
_filterRenameCandidates(declaredLocals);
|
|
550
|
+
|
|
551
|
+
const occupied = new Set(MANGLE_GLOBALS);
|
|
552
|
+
for (const tok of tokens) {
|
|
553
|
+
if (tok.type === "ident" && !declaredLocals.has(tok.value)) {
|
|
554
|
+
occupied.add(tok.value);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const nameGen = shortNameGeneratorFn(occupied);
|
|
559
|
+
const renameMap = new Map();
|
|
552
560
|
for (const name of declaredLocals) renameMap.set(name, nameGen());
|
|
553
561
|
return _applyRenaming(tokens, renameMap);
|
|
554
562
|
}
|
|
@@ -606,23 +614,27 @@ function collectParams(tokens, openIdx, locals) {
|
|
|
606
614
|
}
|
|
607
615
|
}
|
|
608
616
|
|
|
609
|
-
function* shortNameGenerator() {
|
|
617
|
+
function* shortNameGenerator(occupied = new Set()) {
|
|
610
618
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
611
619
|
let index = 0;
|
|
612
620
|
while (true) {
|
|
621
|
+
let name;
|
|
613
622
|
if (index < 26) {
|
|
614
|
-
|
|
623
|
+
name = chars[index++];
|
|
615
624
|
} else {
|
|
616
625
|
const base = index - 26;
|
|
617
|
-
|
|
626
|
+
name = chars[base % 26] + (Math.floor(base / 26) + 1);
|
|
618
627
|
index++;
|
|
619
628
|
}
|
|
629
|
+
if (!occupied.has(name)) {
|
|
630
|
+
yield name;
|
|
631
|
+
}
|
|
620
632
|
}
|
|
621
633
|
}
|
|
622
634
|
|
|
623
635
|
// Make it callable (not an iterator interface)
|
|
624
|
-
function shortNameGeneratorFn() {
|
|
625
|
-
const gen = shortNameGenerator();
|
|
636
|
+
function shortNameGeneratorFn(occupied = new Set()) {
|
|
637
|
+
const gen = shortNameGenerator(occupied);
|
|
626
638
|
return () => gen.next().value;
|
|
627
639
|
}
|
|
628
640
|
|
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`,
|