gofront 1.1.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 +80 -0
- package/package.json +5 -2
- package/src/asset-manager.js +6 -1
- package/src/cli-core.js +51 -0
- package/src/codegen/expressions.js +6 -0
- package/src/codegen/index.js +17 -9
- package/src/codegen/runtime.js +118 -3
- package/src/codegen/stdlib/index.js +3 -0
- package/src/codegen/stdlib/testing.js +34 -0
- package/src/compiler.js +17 -4
- package/src/dev-server.js +8 -3
- package/src/index.js +29 -15
- package/src/resolver.js +1 -0
- package/src/test-runner.js +309 -0
- package/src/typechecker/index.js +13 -1
- package/src/typechecker/stdlib/extended.js +58 -0
- package/src/vendor.js +129 -64
package/README.md
CHANGED
|
@@ -395,6 +395,7 @@ gofront <input> --minify minify output (built-in minifier)
|
|
|
395
395
|
gofront <input> --minify --mangle minify and rename local identifiers
|
|
396
396
|
gofront <file.go> --ast dump AST (debug)
|
|
397
397
|
gofront <file.go> --tokens dump tokens (debug)
|
|
398
|
+
gofront test [dir] [--dom] run unit tests (-v verbose, -run <regex>)
|
|
398
399
|
gofront prep [dir] copy static assets and bundle vendor dependencies
|
|
399
400
|
gofront init [dir] scaffold a new project
|
|
400
401
|
gofront --version / -v print version
|
|
@@ -403,6 +404,64 @@ gofront --help / -h print this help
|
|
|
403
404
|
|
|
404
405
|
---
|
|
405
406
|
|
|
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
|
+
`--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
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
406
465
|
## Static assets & vendor bundling
|
|
407
466
|
|
|
408
467
|
GoFront includes native tooling to manage static assets and package external npm dependencies for the browser without requiring external wrappers or complex build tools.
|
|
@@ -432,6 +491,27 @@ gofront prep
|
|
|
432
491
|
|
|
433
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`).
|
|
434
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
|
+
|
|
435
515
|
---
|
|
436
516
|
|
|
437
517
|
## Multi-file packages
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"author": "Luuk van Venrooij",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,11 @@
|
|
|
36
36
|
"build:gom": "node src/index.js example/gom/src -o example/gom/app.js",
|
|
37
37
|
"build:templ": "node src/index.js example/templ/src -o example/templ/app.js",
|
|
38
38
|
"build:all": "npm run build:simple && npm run build:reactive && npm run build:gom && npm run build:templ",
|
|
39
|
-
"test
|
|
39
|
+
"test": "npm run test:unit",
|
|
40
|
+
"test:all": "npm run test:unit && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
|
|
40
41
|
"test:unit": "node test/unit/run.js",
|
|
42
|
+
"test:examples": "node src/index.js test example/simple/src && node src/index.js test example/simple/src/utils && node src/index.js test example/reactive/src && node src/index.js test example/reactive/src/utils && node src/index.js test example/gom/src && node src/index.js test example/gom/src/utils && node src/index.js test example/templ/src && node src/index.js test example/templ/src/utils",
|
|
43
|
+
"test:examples:dom": "node src/index.js test --dom example/simple/src && node src/index.js test --dom example/gom/src && node src/index.js test --dom example/templ/src",
|
|
41
44
|
"test:coverage": "c8 --include='src/**' --reporter=text --reporter=lcov node test/unit/run.js",
|
|
42
45
|
"test:e2e": "npm run build:all && playwright test",
|
|
43
46
|
"format": "biome format --write .",
|
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
|
@@ -6,6 +6,7 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
6
6
|
import { copyAssets } from "./asset-manager.js";
|
|
7
7
|
import { compileDir, compileSingleFile } from "./compiler.js";
|
|
8
8
|
import { minify } from "./minifier.js";
|
|
9
|
+
import { runTests } from "./test-runner.js";
|
|
9
10
|
import { bundleVendor } from "./vendor.js";
|
|
10
11
|
|
|
11
12
|
export function runCompile(inputPath, isDir, options) {
|
|
@@ -72,3 +73,53 @@ export async function handlePrep(targetDir, options = {}) {
|
|
|
72
73
|
const vendor = await bundleVendor(resolvedTarget, options.vendorConfig);
|
|
73
74
|
return { assets, vendor };
|
|
74
75
|
}
|
|
76
|
+
|
|
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);
|
|
125
|
+
}
|
|
@@ -345,6 +345,8 @@ export const expressionGenMethods = {
|
|
|
345
345
|
return this._genRegexpMethodCall(expr.func.field, expr);
|
|
346
346
|
if (recvName === "time.Time")
|
|
347
347
|
return this._genTimeMethodCall(expr.func.field, expr);
|
|
348
|
+
if (recvName === "testing.T")
|
|
349
|
+
return this._genTestingMethodCall(expr.func.field, expr);
|
|
348
350
|
return undefined;
|
|
349
351
|
},
|
|
350
352
|
|
|
@@ -752,6 +754,10 @@ export const expressionGenMethods = {
|
|
|
752
754
|
const cmp = `__equal(${l}, ${r})`;
|
|
753
755
|
return expr.op === "==" ? cmp : `!${cmp}`;
|
|
754
756
|
}
|
|
757
|
+
if (l === "null" || r === "null") {
|
|
758
|
+
const op = expr.op === "==" ? "==" : "!=";
|
|
759
|
+
return `${l} ${op} ${r}`;
|
|
760
|
+
}
|
|
755
761
|
}
|
|
756
762
|
const op = expr.op === "==" ? "===" : expr.op === "!=" ? "!==" : expr.op;
|
|
757
763
|
return `${l} ${op} ${r}`;
|
package/src/codegen/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
HELPER_PATH_CLEAN,
|
|
31
31
|
HELPER_S,
|
|
32
32
|
HELPER_SPRINTF,
|
|
33
|
+
HELPER_TESTING,
|
|
33
34
|
HELPER_TIME_FMT,
|
|
34
35
|
HELPER_TIME_PARSE,
|
|
35
36
|
} from "./runtime.js";
|
|
@@ -71,6 +72,7 @@ export class CodeGen {
|
|
|
71
72
|
this._usesPathClean = false;
|
|
72
73
|
this._usesTimeFmt = false;
|
|
73
74
|
this._usesTimeParse = false;
|
|
75
|
+
this._usesTesting = false;
|
|
74
76
|
// Iterator (range-over-func) context
|
|
75
77
|
this._inIteratorBody = false;
|
|
76
78
|
this._iterDepth = 0;
|
|
@@ -137,20 +139,24 @@ export class CodeGen {
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
140
|
-
_callInitAndMain(initNames, program) {
|
|
142
|
+
_callInitAndMain(initNames, program, isTest = false) {
|
|
141
143
|
for (const name of initNames) this.line(`${name}();`);
|
|
142
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
!isTest &&
|
|
146
|
+
program.decls.some((d) => d.kind === "FuncDecl" && d.name === "main")
|
|
147
|
+
)
|
|
143
148
|
this.line("main();");
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
generate(program) {
|
|
151
|
+
generate(program, options = {}) {
|
|
152
|
+
const isTest = options.isTest ?? false;
|
|
147
153
|
const methods = this._collectDecls(program);
|
|
148
154
|
this._emitJsImports();
|
|
149
155
|
this._emitTypeDecls(program, methods);
|
|
150
156
|
this._emitVarConstDecls(program);
|
|
151
157
|
const initNames = this._emitFuncDecls(program);
|
|
152
|
-
this._callInitAndMain(initNames, program);
|
|
153
|
-
this._prependHelpers();
|
|
158
|
+
this._callInitAndMain(initNames, program, isTest);
|
|
159
|
+
this._prependHelpers(isTest);
|
|
154
160
|
while (this.out[0] === "") this.out.shift();
|
|
155
161
|
return this.out.join("\n");
|
|
156
162
|
}
|
|
@@ -221,7 +227,8 @@ export class CodeGen {
|
|
|
221
227
|
return methods;
|
|
222
228
|
}
|
|
223
229
|
|
|
224
|
-
_prependHelpers() {
|
|
230
|
+
_prependHelpers(isTest = false) {
|
|
231
|
+
const needsTesting = isTest || this._usesTesting;
|
|
225
232
|
const HELPER_MAP = [
|
|
226
233
|
[this._usesLen, HELPER_LEN],
|
|
227
234
|
[this._usesAppend, HELPER_APPEND],
|
|
@@ -229,12 +236,13 @@ export class CodeGen {
|
|
|
229
236
|
[this._usesEqual, HELPER_EQUAL],
|
|
230
237
|
[this._usesCmul, HELPER_CMUL],
|
|
231
238
|
[this._usesCdiv, HELPER_CDIV],
|
|
232
|
-
[this._usesSprintf, HELPER_SPRINTF],
|
|
239
|
+
[this._usesSprintf || needsTesting, HELPER_SPRINTF],
|
|
233
240
|
[this._usesError, HELPER_ERROR],
|
|
234
241
|
[this._usesErrorIs, HELPER_ERROR_IS],
|
|
235
242
|
[this._usesPathClean, HELPER_PATH_CLEAN],
|
|
236
243
|
[this._usesTimeFmt, HELPER_TIME_FMT],
|
|
237
244
|
[this._usesTimeParse, HELPER_TIME_PARSE],
|
|
245
|
+
[needsTesting, HELPER_TESTING],
|
|
238
246
|
];
|
|
239
247
|
const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
|
|
240
248
|
if (helpers.length > 0) this.out.unshift(...helpers, "");
|
|
@@ -242,14 +250,14 @@ export class CodeGen {
|
|
|
242
250
|
|
|
243
251
|
// Generate a single bundle from multiple programs (same-package multi-file).
|
|
244
252
|
// Annotates each decl with its source file index before merging.
|
|
245
|
-
generateAll(programs) {
|
|
253
|
+
generateAll(programs, options = {}) {
|
|
246
254
|
for (let i = 0; i < programs.length; i++) {
|
|
247
255
|
for (const decl of programs[i].decls) {
|
|
248
256
|
decl._srcFileIdx = i;
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
259
|
const merged = { decls: programs.flatMap((p) => p.decls) };
|
|
252
|
-
return this.generate(merged);
|
|
260
|
+
return this.generate(merged, options);
|
|
253
261
|
}
|
|
254
262
|
|
|
255
263
|
// Returns a source map JSON string for the last generate() call.
|
package/src/codegen/runtime.js
CHANGED
|
@@ -56,9 +56,19 @@ export const HELPER_SPRINTF = `var __sprintf = __sprintf || function(f, ...a) {
|
|
|
56
56
|
case "s": return pad(String(v == null ? "<nil>" : v), width, false);
|
|
57
57
|
case "d": return pad(String(Math.trunc(Number(v))), width, zero);
|
|
58
58
|
case "v": {
|
|
59
|
-
if (typeof v === "object" && v !== null
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if (typeof v === "object" && v !== null) {
|
|
60
|
+
if ("re" in v && "im" in v) {
|
|
61
|
+
const sign = v.im >= 0 ? "+" : "";
|
|
62
|
+
return pad("(" + v.re + sign + v.im + "i)", width, false);
|
|
63
|
+
}
|
|
64
|
+
if (typeof v.Error === "function") {
|
|
65
|
+
return pad(String(v.Error()), width, false);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return pad(JSON.stringify(v), width, false);
|
|
69
|
+
} catch {
|
|
70
|
+
return pad(String(v), width, false);
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
return pad(String(v == null ? "<nil>" : v), width, false);
|
|
64
74
|
}
|
|
@@ -149,3 +159,108 @@ export const HELPER_TIME_PARSE = `var __timeParse = __timeParse || function(layo
|
|
|
149
159
|
return [{ _d: new Date(0) }, "parsing time: cannot parse"];
|
|
150
160
|
}
|
|
151
161
|
};`;
|
|
162
|
+
|
|
163
|
+
export const HELPER_TESTING = `var __GoFront_FailNow = __GoFront_FailNow || class extends Error {
|
|
164
|
+
constructor() { super("FailNow"); this.name = "__GoFront_FailNow"; }
|
|
165
|
+
};
|
|
166
|
+
var __GoFront_SkipNow = __GoFront_SkipNow || class extends Error {
|
|
167
|
+
constructor() { super("SkipNow"); this.name = "__GoFront_SkipNow"; }
|
|
168
|
+
};
|
|
169
|
+
var __GoFront_T = __GoFront_T || class {
|
|
170
|
+
constructor(name = "", parent = null) {
|
|
171
|
+
this._name = name;
|
|
172
|
+
this._parent = parent;
|
|
173
|
+
this.failed = false;
|
|
174
|
+
this.skipped = false;
|
|
175
|
+
this._logs = [];
|
|
176
|
+
this._start = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
177
|
+
}
|
|
178
|
+
Name() { return this._name; }
|
|
179
|
+
Fail() {
|
|
180
|
+
this.failed = true;
|
|
181
|
+
if (this._parent) this._parent.Fail();
|
|
182
|
+
}
|
|
183
|
+
Failed() { return this.failed; }
|
|
184
|
+
FailNow() {
|
|
185
|
+
this.Fail();
|
|
186
|
+
throw new __GoFront_FailNow();
|
|
187
|
+
}
|
|
188
|
+
Log(...args) {
|
|
189
|
+
const msg = args.map(a => {
|
|
190
|
+
if (typeof a === "object" && a !== null) {
|
|
191
|
+
try { return JSON.stringify(a); } catch { return String(a); }
|
|
192
|
+
}
|
|
193
|
+
return String(a);
|
|
194
|
+
}).join(" ");
|
|
195
|
+
this._logs.push(msg);
|
|
196
|
+
}
|
|
197
|
+
Logf(format, ...args) {
|
|
198
|
+
const msg = typeof __sprintf === "function" ? __sprintf(format, ...args) : format;
|
|
199
|
+
this._logs.push(msg);
|
|
200
|
+
}
|
|
201
|
+
Error(...args) {
|
|
202
|
+
this.Fail();
|
|
203
|
+
this.Log(...args);
|
|
204
|
+
}
|
|
205
|
+
Errorf(format, ...args) {
|
|
206
|
+
this.Fail();
|
|
207
|
+
this.Logf(format, ...args);
|
|
208
|
+
}
|
|
209
|
+
Fatal(...args) {
|
|
210
|
+
this.Log(...args);
|
|
211
|
+
this.FailNow();
|
|
212
|
+
}
|
|
213
|
+
Fatalf(format, ...args) {
|
|
214
|
+
this.Logf(format, ...args);
|
|
215
|
+
this.FailNow();
|
|
216
|
+
}
|
|
217
|
+
Skip(...args) {
|
|
218
|
+
this.skipped = true;
|
|
219
|
+
if (args.length > 0) this.Log(...args);
|
|
220
|
+
throw new __GoFront_SkipNow();
|
|
221
|
+
}
|
|
222
|
+
Skipf(format, ...args) {
|
|
223
|
+
this.skipped = true;
|
|
224
|
+
this.Logf(format, ...args);
|
|
225
|
+
throw new __GoFront_SkipNow();
|
|
226
|
+
}
|
|
227
|
+
Skipped() { return this.skipped; }
|
|
228
|
+
Helper() {}
|
|
229
|
+
Run(name, fn) {
|
|
230
|
+
const subName = this._name ? \`\${this._name}/\${name}\` : name;
|
|
231
|
+
const subT = new __GoFront_T(subName, this);
|
|
232
|
+
if (typeof globalThis.__onSubtestStart === "function") globalThis.__onSubtestStart(subName);
|
|
233
|
+
try {
|
|
234
|
+
const res = fn(subT);
|
|
235
|
+
if (res && typeof res.then === "function") {
|
|
236
|
+
return res.then(() => {
|
|
237
|
+
if (typeof globalThis.__onSubtestEnd === "function") globalThis.__onSubtestEnd(subT);
|
|
238
|
+
return !subT.failed;
|
|
239
|
+
}).catch(e => {
|
|
240
|
+
if (e instanceof __GoFront_FailNow) {
|
|
241
|
+
// expected
|
|
242
|
+
} else if (e instanceof __GoFront_SkipNow) {
|
|
243
|
+
// expected
|
|
244
|
+
} else {
|
|
245
|
+
subT.Fail();
|
|
246
|
+
subT.Log("panic: " + (e?.message ?? String(e)));
|
|
247
|
+
}
|
|
248
|
+
if (typeof globalThis.__onSubtestEnd === "function") globalThis.__onSubtestEnd(subT);
|
|
249
|
+
return false;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (e instanceof __GoFront_FailNow) {
|
|
254
|
+
// expected on FailNow
|
|
255
|
+
} else if (e instanceof __GoFront_SkipNow) {
|
|
256
|
+
// expected on Skip
|
|
257
|
+
} else {
|
|
258
|
+
subT.Fail();
|
|
259
|
+
subT.Log("panic: " + (e?.message ?? String(e)));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (typeof globalThis.__onSubtestEnd === "function") globalThis.__onSubtestEnd(subT);
|
|
263
|
+
return !subT.failed;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var __testing_T = __GoFront_T;`;
|
|
@@ -18,6 +18,7 @@ import { slicesMethods } from "./slices.js";
|
|
|
18
18
|
import { sortMethods } from "./sort.js";
|
|
19
19
|
import { strconvMethods } from "./strconv.js";
|
|
20
20
|
import { stringsMethods } from "./strings.js";
|
|
21
|
+
import { testingMethods } from "./testing.js";
|
|
21
22
|
import { timeMethods } from "./time.js";
|
|
22
23
|
import { unicodeMethods } from "./unicode.js";
|
|
23
24
|
import { utf8Methods } from "./utf8.js";
|
|
@@ -45,6 +46,7 @@ const STDLIB_METHOD_MAP = {
|
|
|
45
46
|
rand: "_genRand",
|
|
46
47
|
utf8: "_genUtf8",
|
|
47
48
|
path: "_genPath",
|
|
49
|
+
testing: "_genTesting",
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
const dispatchMethods = {
|
|
@@ -94,5 +96,6 @@ export const stdlibGenMethods = {
|
|
|
94
96
|
...timeMethods,
|
|
95
97
|
...unicodeMethods,
|
|
96
98
|
...utf8Methods,
|
|
99
|
+
...testingMethods,
|
|
97
100
|
...dispatchMethods,
|
|
98
101
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// CodeGen for Go `testing` package and `*testing.T` methods.
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('../index.js').CodeGen} CodeGen */
|
|
4
|
+
|
|
5
|
+
/** @type {ThisType<CodeGen>} */
|
|
6
|
+
export const testingMethods = {
|
|
7
|
+
_genTesting(fn, _a) {
|
|
8
|
+
switch (fn) {
|
|
9
|
+
case "Short":
|
|
10
|
+
return "false";
|
|
11
|
+
case "Verbose":
|
|
12
|
+
return "Boolean(globalThis.__gofront_verbose)";
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
_genTestingMethodCall(method, expr) {
|
|
18
|
+
this._usesTesting = true;
|
|
19
|
+
const recv = expr.func.expr;
|
|
20
|
+
const t = this.genExpr(recv);
|
|
21
|
+
const args = expr.args
|
|
22
|
+
.map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)))
|
|
23
|
+
.join(", ");
|
|
24
|
+
if (
|
|
25
|
+
method === "Errorf" ||
|
|
26
|
+
method === "Fatalf" ||
|
|
27
|
+
method === "Logf" ||
|
|
28
|
+
method === "Skipf"
|
|
29
|
+
) {
|
|
30
|
+
this._usesSprintf = true;
|
|
31
|
+
}
|
|
32
|
+
return `${t}.${method}(${args})`;
|
|
33
|
+
},
|
|
34
|
+
};
|
package/src/compiler.js
CHANGED
|
@@ -62,9 +62,13 @@ function parseGoFrontFile(filePath) {
|
|
|
62
62
|
return ast;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function gwFilesIn(dir) {
|
|
65
|
+
export function gwFilesIn(dir, { includeTests = false } = {}) {
|
|
66
66
|
return readdirSync(dir)
|
|
67
|
-
.filter((f) =>
|
|
67
|
+
.filter((f) => {
|
|
68
|
+
if (!f.endsWith(".go") && !f.endsWith(".templ")) return false;
|
|
69
|
+
if (!includeTests && f.endsWith("_test.go")) return false;
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
68
72
|
.sort() // deterministic order
|
|
69
73
|
.map((f) => join(dir, f));
|
|
70
74
|
}
|
|
@@ -206,11 +210,17 @@ export function compileSingleFile(inputPath, options = {}) {
|
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
export function compileDir(dir, options = {}) {
|
|
209
|
-
const files = gwFilesIn(dir);
|
|
213
|
+
const files = gwFilesIn(dir, { includeTests: options.includeTests ?? false });
|
|
210
214
|
if (files.length === 0) throw new Error(`No .go files found in ${dir}`);
|
|
211
215
|
return compileFiles(files, { ...options, fromDir: dir });
|
|
212
216
|
}
|
|
213
217
|
|
|
218
|
+
export function compilePackageTests(dir, options = {}) {
|
|
219
|
+
const files = gwFilesIn(dir, { includeTests: true });
|
|
220
|
+
if (files.length === 0) throw new Error(`No .go files found in ${dir}`);
|
|
221
|
+
return compileFiles(files, { ...options, fromDir: dir, isTest: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
214
224
|
export function compileFiles(files, options = {}) {
|
|
215
225
|
const fromDir = options.fromDir ?? dirname(resolve(files[0]));
|
|
216
226
|
|
|
@@ -262,7 +272,9 @@ export function compileFiles(files, options = {}) {
|
|
|
262
272
|
|
|
263
273
|
// ── 4. Code generation ────────────────────────────────────────
|
|
264
274
|
const codegen = new CodeGen(checker, jsImports, bundledPackages);
|
|
265
|
-
const mainJs = codegen.generateAll(programs
|
|
275
|
+
const mainJs = codegen.generateAll(programs, {
|
|
276
|
+
isTest: options.isTest ?? false,
|
|
277
|
+
});
|
|
266
278
|
|
|
267
279
|
let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
|
|
268
280
|
|
|
@@ -278,6 +290,7 @@ export function compileFiles(files, options = {}) {
|
|
|
278
290
|
return {
|
|
279
291
|
pkgName,
|
|
280
292
|
js,
|
|
293
|
+
programs,
|
|
281
294
|
exportedSymbols: checker.getExportedSymbols(),
|
|
282
295
|
exportedTypes: checker.getExportedTypes(),
|
|
283
296
|
};
|
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
|
@@ -21,7 +21,16 @@ const { version } = _require("../package.json");
|
|
|
21
21
|
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
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
formatPrepSummary,
|
|
26
|
+
handleInit,
|
|
27
|
+
handlePrep,
|
|
28
|
+
handleTest,
|
|
29
|
+
maybeMinify,
|
|
30
|
+
parsePrepArgs,
|
|
31
|
+
parseTestArgs,
|
|
32
|
+
runCompile,
|
|
33
|
+
} from "./cli-core.js";
|
|
25
34
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
26
35
|
|
|
27
36
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
@@ -51,7 +60,8 @@ Usage:
|
|
|
51
60
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
52
61
|
gofront <file.go> --ast Dump AST (debug)
|
|
53
62
|
gofront <file.go> --tokens Dump tokens (debug)
|
|
54
|
-
gofront
|
|
63
|
+
gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
64
|
+
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
55
65
|
gofront init [dir] Scaffold a new GoFront project
|
|
56
66
|
gofront --version Print version
|
|
57
67
|
`.trim(),
|
|
@@ -62,20 +72,11 @@ Usage:
|
|
|
62
72
|
// ── prep / vendor subcommand ──────────────────────────────────
|
|
63
73
|
|
|
64
74
|
if (args[0] === "prep" || args[0] === "vendor") {
|
|
65
|
-
const
|
|
66
|
-
const targetDir = resolve(targetArg);
|
|
75
|
+
const { targetDir, vendorConfig } = parsePrepArgs(args.slice(1));
|
|
67
76
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
console.error(
|
|
71
|
-
`gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (vendor.bundled?.length > 0) {
|
|
75
|
-
console.error(
|
|
76
|
-
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${vendor.dest}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
77
|
+
const result = await handlePrep(resolve(targetDir), { vendorConfig });
|
|
78
|
+
for (const line of formatPrepSummary(result))
|
|
79
|
+
console.error(`gofront: ${line}`);
|
|
79
80
|
} catch (e) {
|
|
80
81
|
console.error(`gofront: ${e.message}`);
|
|
81
82
|
process.exit(1);
|
|
@@ -102,6 +103,19 @@ if (args[0] === "init") {
|
|
|
102
103
|
process.exit(0);
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// ── test subcommand ───────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
if (args[0] === "test") {
|
|
109
|
+
const { targetDir, ...testOptions } = parseTestArgs(args.slice(1));
|
|
110
|
+
try {
|
|
111
|
+
const result = await handleTest(targetDir, testOptions);
|
|
112
|
+
process.exit(result.exitCode);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`gofront: ${e.message}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
const inputArg = args[0];
|
|
106
120
|
const outputFlag = args.indexOf("-o");
|
|
107
121
|
const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
|
package/src/resolver.js
CHANGED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// GoFront native test runner and harness generator.
|
|
2
|
+
// Handles test function discovery, test harness emission, and subprocess execution.
|
|
3
|
+
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { basename, join, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { compilePackageTests, gwFilesIn } from "./compiler.js";
|
|
9
|
+
|
|
10
|
+
export function isTestFunc(decl) {
|
|
11
|
+
if (decl.kind !== "FuncDecl") return false;
|
|
12
|
+
if (!decl.name.startsWith("Test")) return false;
|
|
13
|
+
if (decl.params?.length !== 1) return false;
|
|
14
|
+
const pType = decl.params[0].type;
|
|
15
|
+
if (pType?.kind === "PointerType" && pType.base?.name === "testing.T")
|
|
16
|
+
return true;
|
|
17
|
+
if (pType?.kind === "TypeName" && pType.name === "testing.T") return true;
|
|
18
|
+
return false;
|
|
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.
|
|
23
|
+
export function discoverTests(programs) {
|
|
24
|
+
const testNames = [];
|
|
25
|
+
for (const p of programs) {
|
|
26
|
+
if (p._filename && !p._filename.endsWith("_test.go")) continue;
|
|
27
|
+
for (const d of p.decls) {
|
|
28
|
+
if (isTestFunc(d) && !testNames.includes(d.name)) {
|
|
29
|
+
testNames.push(d.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return testNames;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function generateTestHarness(bundleJs, testNames, options = {}) {
|
|
37
|
+
const runRegex = options.run ? options.run : null;
|
|
38
|
+
const verbose = Boolean(options.verbose);
|
|
39
|
+
const dom = Boolean(options.dom);
|
|
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";
|
|
46
|
+
|
|
47
|
+
return `
|
|
48
|
+
${
|
|
49
|
+
dom
|
|
50
|
+
? `import { JSDOM } from ${JSON.stringify(jsdomSpec)};
|
|
51
|
+
const __dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
52
|
+
url: "http://localhost/",
|
|
53
|
+
pretendToBeVisual: true,
|
|
54
|
+
});
|
|
55
|
+
globalThis.window = __dom.window;
|
|
56
|
+
globalThis.document = __dom.window.document;
|
|
57
|
+
try {
|
|
58
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
59
|
+
value: __dom.window.navigator,
|
|
60
|
+
configurable: true,
|
|
61
|
+
writable: true,
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
try { globalThis.navigator = __dom.window.navigator; } catch {}
|
|
65
|
+
}
|
|
66
|
+
globalThis.location = __dom.window.location;
|
|
67
|
+
globalThis.history = __dom.window.history;
|
|
68
|
+
globalThis.HTMLElement = __dom.window.HTMLElement;
|
|
69
|
+
globalThis.Element = __dom.window.Element;
|
|
70
|
+
globalThis.Node = __dom.window.Node;
|
|
71
|
+
globalThis.customElements = __dom.window.customElements;
|
|
72
|
+
`
|
|
73
|
+
: `globalThis.window = globalThis.window ?? null;
|
|
74
|
+
globalThis.document = globalThis.document ?? null;
|
|
75
|
+
`
|
|
76
|
+
}
|
|
77
|
+
globalThis.__gofront_verbose = ${verbose};
|
|
78
|
+
|
|
79
|
+
// ── Compiled Package Bundle ──
|
|
80
|
+
${bundleJs}
|
|
81
|
+
// ─────────────────────────────
|
|
82
|
+
|
|
83
|
+
async function __runGoFrontSuite() {
|
|
84
|
+
const allTests = ${JSON.stringify(testNames)};
|
|
85
|
+
const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
|
|
86
|
+
const verbose = ${verbose};
|
|
87
|
+
const pkgName = ${JSON.stringify(pkgName)};
|
|
88
|
+
|
|
89
|
+
const testsToRun = runFilter
|
|
90
|
+
? allTests.filter((name) => runFilter.test(name))
|
|
91
|
+
: allTests;
|
|
92
|
+
|
|
93
|
+
if (testsToRun.length === 0) {
|
|
94
|
+
if (allTests.length > 0 && runFilter) {
|
|
95
|
+
console.log("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)});
|
|
96
|
+
}
|
|
97
|
+
console.log("PASS");
|
|
98
|
+
console.log("ok \\t" + pkgName + "\\t0.000s");
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let totalFailures = 0;
|
|
103
|
+
let totalSkipped = 0;
|
|
104
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
105
|
+
const suiteStart = now();
|
|
106
|
+
|
|
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)");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
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)));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
globalThis.__onSubtestStart = (name) => {
|
|
132
|
+
if (verbose) console.log("=== RUN " + name);
|
|
133
|
+
};
|
|
134
|
+
globalThis.__onSubtestEnd = report;
|
|
135
|
+
|
|
136
|
+
const __testRegistry = {
|
|
137
|
+
${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ? ${n} : null`).join(",\n")}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
for (const name of testsToRun) {
|
|
141
|
+
const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
|
|
142
|
+
if (typeof fn !== "function") {
|
|
143
|
+
console.error("testing: cannot find test function " + name);
|
|
144
|
+
totalFailures++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const t = new __GoFront_T(name);
|
|
149
|
+
if (verbose) console.log("=== RUN " + name);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const res = fn(t);
|
|
153
|
+
if (res && typeof res.then === "function") await res;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
recordTestError(t, e);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (t.skipped) totalSkipped++;
|
|
159
|
+
else if (t.failed) totalFailures++;
|
|
160
|
+
report(t);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const suiteElapsed = ((now() - suiteStart) / 1000).toFixed(3);
|
|
164
|
+
|
|
165
|
+
if (totalFailures > 0) {
|
|
166
|
+
console.log("FAIL");
|
|
167
|
+
console.log("FAIL\\t" + pkgName + "\\t" + suiteElapsed + "s");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
} else {
|
|
170
|
+
console.log("PASS");
|
|
171
|
+
console.log("ok \\t" + pkgName + "\\t" + suiteElapsed + "s");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
__runGoFrontSuite();
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
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
|
+
|
|
190
|
+
function validateTestOptions(options, resolvedDir) {
|
|
191
|
+
if (options.run) {
|
|
192
|
+
try {
|
|
193
|
+
new RegExp(options.run);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
throw new Error(`gofront: invalid regex for -run: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let jsdomPath = null;
|
|
200
|
+
if (options.dom) {
|
|
201
|
+
jsdomPath = resolveJsdomPath(resolvedDir);
|
|
202
|
+
if (!jsdomPath) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"gofront: --dom requires 'jsdom' to be installed. Run 'npm install --save-dev jsdom' to enable DOM testing.",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { jsdomPath };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function reportEmptyTests(label, reason, options) {
|
|
212
|
+
const out = `? \t${label}\t[${reason}]\n`;
|
|
213
|
+
if (options.captureOutput) {
|
|
214
|
+
return { exitCode: 0, stdout: out, stderr: "", noTests: true };
|
|
215
|
+
}
|
|
216
|
+
const dest = options.stdout ?? process.stdout;
|
|
217
|
+
dest.write(out);
|
|
218
|
+
return { exitCode: 0, noTests: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
222
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
223
|
+
const child = spawn(process.execPath, ["--input-type=module", "-"], {
|
|
224
|
+
cwd: resolvedDir,
|
|
225
|
+
env: process.env,
|
|
226
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let stdout = "";
|
|
230
|
+
let stderr = "";
|
|
231
|
+
|
|
232
|
+
child.stdout.on("data", (chunk) => {
|
|
233
|
+
const str = chunk.toString();
|
|
234
|
+
if (options.captureOutput) {
|
|
235
|
+
stdout += str;
|
|
236
|
+
} else {
|
|
237
|
+
(options.stdout ?? process.stdout).write(str);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
child.stderr.on("data", (chunk) => {
|
|
242
|
+
const str = chunk.toString();
|
|
243
|
+
if (options.captureOutput) {
|
|
244
|
+
stderr += str;
|
|
245
|
+
} else {
|
|
246
|
+
(options.stderr ?? process.stderr).write(str);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
child.on("error", (err) => {
|
|
251
|
+
rejectPromise(err);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
child.on("close", (exitCode) => {
|
|
255
|
+
resolvePromise({
|
|
256
|
+
exitCode: exitCode ?? 0,
|
|
257
|
+
stdout,
|
|
258
|
+
stderr,
|
|
259
|
+
testCount,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
child.stdin.on("error", () => {});
|
|
264
|
+
child.stdin.end(harnessJs);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function runTests(targetDir, options = {}) {
|
|
269
|
+
const resolvedDir = resolve(targetDir);
|
|
270
|
+
const { jsdomPath } = validateTestOptions(options, resolvedDir);
|
|
271
|
+
|
|
272
|
+
const allFiles = gwFilesIn(resolvedDir, { includeTests: true });
|
|
273
|
+
if (allFiles.length === 0) {
|
|
274
|
+
throw new Error(`No .go files found in ${targetDir}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const testFiles = allFiles.filter((f) => f.endsWith("_test.go"));
|
|
278
|
+
if (testFiles.length === 0) {
|
|
279
|
+
return reportEmptyTests(basename(resolvedDir), "no test files", options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let compiled;
|
|
283
|
+
try {
|
|
284
|
+
compiled = compilePackageTests(resolvedDir, options);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${err?.message ?? String(err)}\n`;
|
|
287
|
+
if (options.captureOutput) {
|
|
288
|
+
return { exitCode: 1, stdout: "", stderr: out };
|
|
289
|
+
}
|
|
290
|
+
const dest = options.stderr ?? process.stderr;
|
|
291
|
+
dest.write(out);
|
|
292
|
+
return { exitCode: 1 };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const { pkgName, js, programs } = compiled;
|
|
296
|
+
const testNames = discoverTests(programs);
|
|
297
|
+
|
|
298
|
+
if (testNames.length === 0) {
|
|
299
|
+
return reportEmptyTests(pkgName, "no tests to run", options);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const harnessJs = generateTestHarness(js, testNames, {
|
|
303
|
+
...options,
|
|
304
|
+
pkgName,
|
|
305
|
+
jsdomPath,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
|
|
309
|
+
}
|
package/src/typechecker/index.js
CHANGED
|
@@ -225,7 +225,19 @@ export class TypeChecker {
|
|
|
225
225
|
_checkTopDeclsPass(programs) {
|
|
226
226
|
for (const p of programs) {
|
|
227
227
|
this._setCurrentFile(p);
|
|
228
|
-
for (const d of p.decls)
|
|
228
|
+
for (const d of p.decls) {
|
|
229
|
+
if (d.kind === "VarDecl" || d.kind === "ConstDecl") {
|
|
230
|
+
this.checkTopDecl(d, this.globals);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const p of programs) {
|
|
235
|
+
this._setCurrentFile(p);
|
|
236
|
+
for (const d of p.decls) {
|
|
237
|
+
if (d.kind === "FuncDecl" || d.kind === "MethodDecl") {
|
|
238
|
+
this.checkTopDecl(d, this.globals);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
243
|
|
|
@@ -308,6 +308,64 @@ export function setupExtendedGlobals(globals, types) {
|
|
|
308
308
|
},
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
+
const TESTING_T = {
|
|
312
|
+
kind: "named",
|
|
313
|
+
name: "testing.T",
|
|
314
|
+
underlying: { kind: "struct", fields: new Map(), methods: new Map() },
|
|
315
|
+
};
|
|
316
|
+
const TESTING_PTR = { kind: "pointer", base: TESTING_T };
|
|
317
|
+
|
|
318
|
+
TESTING_T.underlying.methods = new Map([
|
|
319
|
+
["Error", { kind: "func", params: [ANY], returns: [VOID], variadic: true }],
|
|
320
|
+
[
|
|
321
|
+
"Errorf",
|
|
322
|
+
{ kind: "func", params: [STRING, ANY], returns: [VOID], variadic: true },
|
|
323
|
+
],
|
|
324
|
+
["Fatal", { kind: "func", params: [ANY], returns: [VOID], variadic: true }],
|
|
325
|
+
[
|
|
326
|
+
"Fatalf",
|
|
327
|
+
{ kind: "func", params: [STRING, ANY], returns: [VOID], variadic: true },
|
|
328
|
+
],
|
|
329
|
+
["Fail", { kind: "func", params: [], returns: [VOID] }],
|
|
330
|
+
["Failed", { kind: "func", params: [], returns: [BOOL] }],
|
|
331
|
+
["FailNow", { kind: "func", params: [], returns: [VOID] }],
|
|
332
|
+
["Log", { kind: "func", params: [ANY], returns: [VOID], variadic: true }],
|
|
333
|
+
[
|
|
334
|
+
"Logf",
|
|
335
|
+
{ kind: "func", params: [STRING, ANY], returns: [VOID], variadic: true },
|
|
336
|
+
],
|
|
337
|
+
["Skip", { kind: "func", params: [ANY], returns: [VOID], variadic: true }],
|
|
338
|
+
[
|
|
339
|
+
"Skipf",
|
|
340
|
+
{ kind: "func", params: [STRING, ANY], returns: [VOID], variadic: true },
|
|
341
|
+
],
|
|
342
|
+
["Skipped", { kind: "func", params: [], returns: [BOOL] }],
|
|
343
|
+
["Helper", { kind: "func", params: [], returns: [VOID] }],
|
|
344
|
+
[
|
|
345
|
+
"Run",
|
|
346
|
+
{
|
|
347
|
+
kind: "func",
|
|
348
|
+
params: [
|
|
349
|
+
STRING,
|
|
350
|
+
{ kind: "func", params: [TESTING_PTR], returns: [VOID] },
|
|
351
|
+
],
|
|
352
|
+
returns: [BOOL],
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
["Name", { kind: "func", params: [], returns: [STRING] }],
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
globals.define("testing", {
|
|
359
|
+
kind: "namespace",
|
|
360
|
+
name: "testing",
|
|
361
|
+
members: {
|
|
362
|
+
T: TESTING_T,
|
|
363
|
+
Short: { kind: "func", params: [], returns: [BOOL] },
|
|
364
|
+
Verbose: { kind: "func", params: [], returns: [BOOL] },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
types.set("testing.T", TESTING_T);
|
|
368
|
+
|
|
311
369
|
for (const name of [
|
|
312
370
|
"append",
|
|
313
371
|
"len",
|
package/src/vendor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Dynamically loads a bundler (rolldown or esbuild) from consumer devDependencies.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
copyFileSync,
|
|
5
6
|
existsSync,
|
|
6
7
|
mkdirSync,
|
|
7
8
|
readFileSync,
|
|
@@ -12,28 +13,17 @@ import { createRequire } from "node:module";
|
|
|
12
13
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
13
14
|
import { pathToFileURL } from "node:url";
|
|
14
15
|
|
|
15
|
-
export function getExportNames(pkgName) {
|
|
16
|
+
export function getExportNames(pkgName, globals = {}) {
|
|
16
17
|
const base = pkgName.replace(/^@[^/]+\//, "");
|
|
17
18
|
const clean = base.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
18
19
|
const names = new Set([pkgName, base, clean]);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
names.add("Fuse");
|
|
23
|
-
names.add("fuse");
|
|
24
|
-
}
|
|
25
|
-
if (pkgName === "prismjs") {
|
|
26
|
-
names.add("Prism");
|
|
27
|
-
names.add("prism");
|
|
28
|
-
}
|
|
29
|
-
if (pkgName === "marked") {
|
|
30
|
-
names.add("marked");
|
|
31
|
-
}
|
|
32
|
-
|
|
20
|
+
const extra = globals[pkgName];
|
|
21
|
+
for (const name of Array.isArray(extra) ? extra : extra ? [extra] : [])
|
|
22
|
+
names.add(name);
|
|
33
23
|
return [...names];
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
export function generateVendorEntry(packages) {
|
|
26
|
+
export function generateVendorEntry(packages, globals = {}) {
|
|
37
27
|
const imports = [];
|
|
38
28
|
const assignments = [];
|
|
39
29
|
const exports = [];
|
|
@@ -42,7 +32,7 @@ export function generateVendorEntry(packages) {
|
|
|
42
32
|
const id = `_dep_${idx}`;
|
|
43
33
|
imports.push(`import * as ${id} from ${JSON.stringify(pkg)};`);
|
|
44
34
|
|
|
45
|
-
const names = getExportNames(pkg);
|
|
35
|
+
const names = getExportNames(pkg, globals);
|
|
46
36
|
for (const name of names) {
|
|
47
37
|
assignments.push(
|
|
48
38
|
` window[${JSON.stringify(name)}] = ${id}.default || ${id};`,
|
|
@@ -90,6 +80,27 @@ export function findBundler(projectDir) {
|
|
|
90
80
|
return null;
|
|
91
81
|
}
|
|
92
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
|
+
|
|
93
104
|
export function loadVendorConfig(projectDir) {
|
|
94
105
|
const pkgPath = join(projectDir, "package.json");
|
|
95
106
|
let pkg = {};
|
|
@@ -112,19 +123,87 @@ export function loadVendorConfig(projectDir) {
|
|
|
112
123
|
|
|
113
124
|
let dest = null;
|
|
114
125
|
let packages = dependencies;
|
|
126
|
+
let minify = false;
|
|
127
|
+
let globals = {};
|
|
115
128
|
|
|
116
129
|
if (typeof vendorConfig === "string") {
|
|
117
130
|
dest = vendorConfig;
|
|
118
131
|
} else if (vendorConfig && typeof vendorConfig === "object") {
|
|
119
132
|
if (vendorConfig.dest) dest = vendorConfig.dest;
|
|
120
133
|
if (Array.isArray(vendorConfig.packages)) packages = vendorConfig.packages;
|
|
134
|
+
if (typeof vendorConfig.minify === "boolean") minify = vendorConfig.minify;
|
|
135
|
+
globals = normalizeGlobals(vendorConfig.globals);
|
|
121
136
|
}
|
|
122
137
|
|
|
123
138
|
if (!dest) {
|
|
124
139
|
dest = existsSync(join(projectDir, "app")) ? "app/vendor.js" : "vendor.js";
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
return { dest, packages };
|
|
142
|
+
return { dest, packages, minify, globals };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resolveDestinationPaths(projectRoot, dest) {
|
|
146
|
+
const destList = Array.isArray(dest) ? dest : [dest];
|
|
147
|
+
if (destList.length === 0) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"gofront: at least one vendor destination must be specified",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return destList.map((d) => {
|
|
154
|
+
const fullPath = resolve(projectRoot, d);
|
|
155
|
+
const relDest = relative(projectRoot, fullPath);
|
|
156
|
+
if (relDest.startsWith("..") || isAbsolute(relDest)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`gofront: vendor destination '${d}' is outside project directory`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return fullPath;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function runBundler(
|
|
166
|
+
bundlerInfo,
|
|
167
|
+
{ projectRoot, entryFile, destPath, minify },
|
|
168
|
+
) {
|
|
169
|
+
if (bundlerInfo.name === "rolldown") {
|
|
170
|
+
const rolldownModule =
|
|
171
|
+
bundlerInfo.instance ??
|
|
172
|
+
(await import(pathToFileURL(bundlerInfo.path).href));
|
|
173
|
+
const rolldownFn =
|
|
174
|
+
rolldownModule.rolldown ||
|
|
175
|
+
rolldownModule.default?.rolldown ||
|
|
176
|
+
rolldownModule.default;
|
|
177
|
+
|
|
178
|
+
const bundle = await rolldownFn({
|
|
179
|
+
input: entryFile,
|
|
180
|
+
cwd: projectRoot,
|
|
181
|
+
});
|
|
182
|
+
await bundle.write({
|
|
183
|
+
file: destPath,
|
|
184
|
+
format: "esm",
|
|
185
|
+
minify: Boolean(minify),
|
|
186
|
+
});
|
|
187
|
+
await bundle.close();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (bundlerInfo.name === "esbuild") {
|
|
192
|
+
const esbuildModule =
|
|
193
|
+
bundlerInfo.instance ??
|
|
194
|
+
(await import(pathToFileURL(bundlerInfo.path).href));
|
|
195
|
+
const buildFn =
|
|
196
|
+
esbuildModule.build || esbuildModule.default?.build || esbuildModule;
|
|
197
|
+
|
|
198
|
+
await buildFn({
|
|
199
|
+
entryPoints: [entryFile],
|
|
200
|
+
bundle: true,
|
|
201
|
+
format: "esm",
|
|
202
|
+
outfile: destPath,
|
|
203
|
+
absWorkingDir: projectRoot,
|
|
204
|
+
minify: Boolean(minify),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
128
207
|
}
|
|
129
208
|
|
|
130
209
|
export async function bundleVendor(projectDir = ".", options = {}) {
|
|
@@ -133,24 +212,20 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
133
212
|
|
|
134
213
|
const packages = options.packages ?? config.packages;
|
|
135
214
|
const dest = options.dest ?? config.dest;
|
|
215
|
+
const minify = options.minify ?? config.minify ?? false;
|
|
216
|
+
const globals = { ...config.globals, ...normalizeGlobals(options.globals) };
|
|
136
217
|
|
|
137
218
|
if (!packages || packages.length === 0) {
|
|
138
219
|
return {
|
|
139
220
|
bundled: [],
|
|
140
221
|
skipped: 0,
|
|
141
222
|
dest,
|
|
223
|
+
minify: Boolean(minify),
|
|
142
224
|
message: "no external dependencies found",
|
|
143
225
|
};
|
|
144
226
|
}
|
|
145
227
|
|
|
146
|
-
const
|
|
147
|
-
const relDest = relative(projectRoot, destPath);
|
|
148
|
-
if (relDest.startsWith("..") || isAbsolute(relDest)) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`gofront: vendor destination '${dest}' is outside project directory`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
228
|
+
const destPaths = resolveDestinationPaths(projectRoot, dest);
|
|
154
229
|
const bundlerInfo = options.bundler ?? findBundler(projectRoot);
|
|
155
230
|
if (!bundlerInfo) {
|
|
156
231
|
console.warn(
|
|
@@ -161,23 +236,31 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
161
236
|
skipped: packages.length,
|
|
162
237
|
dest,
|
|
163
238
|
bundler: null,
|
|
239
|
+
minify: Boolean(minify),
|
|
164
240
|
reason: "no bundler installed",
|
|
165
241
|
};
|
|
166
242
|
}
|
|
167
243
|
|
|
168
|
-
|
|
244
|
+
for (const p of destPaths) {
|
|
245
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
246
|
+
}
|
|
169
247
|
|
|
170
|
-
// Allow custom bundler execution function (useful for tests and plugins)
|
|
171
248
|
if (typeof bundlerInfo.bundle === "function") {
|
|
172
249
|
await bundlerInfo.bundle({
|
|
173
250
|
projectRoot,
|
|
174
251
|
packages,
|
|
175
|
-
dest:
|
|
252
|
+
dest: destPaths.length === 1 ? destPaths[0] : destPaths,
|
|
253
|
+
minify: Boolean(minify),
|
|
176
254
|
});
|
|
177
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
bundled: packages,
|
|
257
|
+
bundler: bundlerInfo.name ?? "custom",
|
|
258
|
+
dest,
|
|
259
|
+
minify: Boolean(minify),
|
|
260
|
+
};
|
|
178
261
|
}
|
|
179
262
|
|
|
180
|
-
const entryCode = generateVendorEntry(packages);
|
|
263
|
+
const entryCode = generateVendorEntry(packages, globals);
|
|
181
264
|
const entryFile = join(
|
|
182
265
|
projectRoot,
|
|
183
266
|
`.gofront-vendor-entry-${Date.now()}.mjs`,
|
|
@@ -185,38 +268,15 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
185
268
|
writeFileSync(entryFile, entryCode);
|
|
186
269
|
|
|
187
270
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const bundle = await rolldownFn({
|
|
198
|
-
input: entryFile,
|
|
199
|
-
cwd: projectRoot,
|
|
200
|
-
});
|
|
201
|
-
await bundle.write({
|
|
202
|
-
file: destPath,
|
|
203
|
-
format: "esm",
|
|
204
|
-
});
|
|
205
|
-
await bundle.close();
|
|
206
|
-
} else if (bundlerInfo.name === "esbuild") {
|
|
207
|
-
const esbuildModule =
|
|
208
|
-
bundlerInfo.instance ??
|
|
209
|
-
(await import(pathToFileURL(bundlerInfo.path).href));
|
|
210
|
-
const buildFn =
|
|
211
|
-
esbuildModule.build || esbuildModule.default?.build || esbuildModule;
|
|
212
|
-
|
|
213
|
-
await buildFn({
|
|
214
|
-
entryPoints: [entryFile],
|
|
215
|
-
bundle: true,
|
|
216
|
-
format: "esm",
|
|
217
|
-
outfile: destPath,
|
|
218
|
-
absWorkingDir: projectRoot,
|
|
219
|
-
});
|
|
271
|
+
await runBundler(bundlerInfo, {
|
|
272
|
+
projectRoot,
|
|
273
|
+
entryFile,
|
|
274
|
+
destPath: destPaths[0],
|
|
275
|
+
minify,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
for (let i = 1; i < destPaths.length; i++) {
|
|
279
|
+
copyFileSync(destPaths[0], destPaths[i]);
|
|
220
280
|
}
|
|
221
281
|
} finally {
|
|
222
282
|
if (existsSync(entryFile)) {
|
|
@@ -224,5 +284,10 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
224
284
|
}
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
bundled: packages,
|
|
289
|
+
bundler: bundlerInfo.name,
|
|
290
|
+
dest,
|
|
291
|
+
minify: Boolean(minify),
|
|
292
|
+
};
|
|
228
293
|
}
|