gofront 1.1.0 → 1.2.0
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 +53 -0
- package/package.json +5 -2
- package/src/cli-core.js +6 -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/index.js +60 -5
- package/src/resolver.js +1 -0
- package/src/test-runner.js +335 -0
- package/src/typechecker/index.js +13 -1
- package/src/typechecker/stdlib/extended.js +58 -0
- package/src/vendor.js +98 -46
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,58 @@ 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
|
+
---
|
|
458
|
+
|
|
406
459
|
## Static assets & vendor bundling
|
|
407
460
|
|
|
408
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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/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,8 @@ 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 async function handleTest(targetDir, options = {}) {
|
|
78
|
+
const resolvedTarget = resolve(targetDir);
|
|
79
|
+
return runTests(resolvedTarget, options);
|
|
80
|
+
}
|
|
@@ -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/index.js
CHANGED
|
@@ -21,7 +21,13 @@ 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
|
+
handleInit,
|
|
26
|
+
handlePrep,
|
|
27
|
+
handleTest,
|
|
28
|
+
maybeMinify,
|
|
29
|
+
runCompile,
|
|
30
|
+
} from "./cli-core.js";
|
|
25
31
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
26
32
|
|
|
27
33
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
@@ -51,7 +57,8 @@ Usage:
|
|
|
51
57
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
52
58
|
gofront <file.go> --ast Dump AST (debug)
|
|
53
59
|
gofront <file.go> --tokens Dump tokens (debug)
|
|
54
|
-
gofront
|
|
60
|
+
gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
61
|
+
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
55
62
|
gofront init [dir] Scaffold a new GoFront project
|
|
56
63
|
gofront --version Print version
|
|
57
64
|
`.trim(),
|
|
@@ -62,18 +69,27 @@ Usage:
|
|
|
62
69
|
// ── prep / vendor subcommand ──────────────────────────────────
|
|
63
70
|
|
|
64
71
|
if (args[0] === "prep" || args[0] === "vendor") {
|
|
65
|
-
const
|
|
72
|
+
const hasMinify = args.includes("--minify");
|
|
73
|
+
const filteredArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
74
|
+
const targetArg = filteredArgs[0] ?? ".";
|
|
66
75
|
const targetDir = resolve(targetArg);
|
|
67
76
|
try {
|
|
68
|
-
const {
|
|
77
|
+
const vendorConfig = hasMinify ? { minify: true } : {};
|
|
78
|
+
const { assets, vendor } = await handlePrep(targetDir, {
|
|
79
|
+
vendorConfig,
|
|
80
|
+
});
|
|
69
81
|
if (assets.copied > 0 || assets.skipped > 0) {
|
|
70
82
|
console.error(
|
|
71
83
|
`gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
|
|
72
84
|
);
|
|
73
85
|
}
|
|
74
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)" : "";
|
|
75
91
|
console.error(
|
|
76
|
-
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${
|
|
92
|
+
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${destStr}${minStr}`,
|
|
77
93
|
);
|
|
78
94
|
}
|
|
79
95
|
} catch (e) {
|
|
@@ -102,6 +118,45 @@ if (args[0] === "init") {
|
|
|
102
118
|
process.exit(0);
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
// ── test subcommand ───────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
if (args[0] === "test") {
|
|
124
|
+
const flags = args.slice(1);
|
|
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
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = await handleTest(targetDir, {
|
|
149
|
+
verbose,
|
|
150
|
+
dom,
|
|
151
|
+
run: runFilter,
|
|
152
|
+
});
|
|
153
|
+
process.exit(result.exitCode);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(`gofront: ${e.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
105
160
|
const inputArg = args[0];
|
|
106
161
|
const outputFlag = args.indexOf("-o");
|
|
107
162
|
const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
|
package/src/resolver.js
CHANGED
|
@@ -0,0 +1,335 @@
|
|
|
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 { compilePackageTests, gwFilesIn } from "./compiler.js";
|
|
8
|
+
|
|
9
|
+
export function isTestFunc(decl) {
|
|
10
|
+
if (decl.kind !== "FuncDecl") return false;
|
|
11
|
+
if (!decl.name.startsWith("Test")) return false;
|
|
12
|
+
if (decl.params?.length !== 1) return false;
|
|
13
|
+
const pType = decl.params[0].type;
|
|
14
|
+
if (pType?.kind === "PointerType" && pType.base?.name === "testing.T")
|
|
15
|
+
return true;
|
|
16
|
+
if (pType?.kind === "TypeName" && pType.name === "testing.T") return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function discoverTests(programs) {
|
|
21
|
+
const testNames = [];
|
|
22
|
+
for (const p of programs) {
|
|
23
|
+
for (const d of p.decls) {
|
|
24
|
+
if (isTestFunc(d) && !testNames.includes(d.name)) {
|
|
25
|
+
testNames.push(d.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return testNames;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateTestHarness(bundleJs, testNames, options = {}) {
|
|
33
|
+
const runRegex = options.run ? options.run : null;
|
|
34
|
+
const verbose = Boolean(options.verbose);
|
|
35
|
+
const dom = Boolean(options.dom);
|
|
36
|
+
const pkgName = options.pkgName ?? "main";
|
|
37
|
+
|
|
38
|
+
return `
|
|
39
|
+
${
|
|
40
|
+
dom
|
|
41
|
+
? `import { JSDOM } from "jsdom";
|
|
42
|
+
const __dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
43
|
+
url: "http://localhost/",
|
|
44
|
+
pretendToBeVisual: true,
|
|
45
|
+
});
|
|
46
|
+
globalThis.window = __dom.window;
|
|
47
|
+
globalThis.document = __dom.window.document;
|
|
48
|
+
try {
|
|
49
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
50
|
+
value: __dom.window.navigator,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
try { globalThis.navigator = __dom.window.navigator; } catch {}
|
|
56
|
+
}
|
|
57
|
+
globalThis.location = __dom.window.location;
|
|
58
|
+
globalThis.history = __dom.window.history;
|
|
59
|
+
globalThis.HTMLElement = __dom.window.HTMLElement;
|
|
60
|
+
globalThis.Element = __dom.window.Element;
|
|
61
|
+
globalThis.Node = __dom.window.Node;
|
|
62
|
+
globalThis.customElements = __dom.window.customElements;
|
|
63
|
+
`
|
|
64
|
+
: `globalThis.window = globalThis.window ?? null;
|
|
65
|
+
globalThis.document = globalThis.document ?? null;
|
|
66
|
+
`
|
|
67
|
+
}
|
|
68
|
+
globalThis.__gofront_verbose = ${verbose};
|
|
69
|
+
|
|
70
|
+
// ── Compiled Package Bundle ──
|
|
71
|
+
${bundleJs}
|
|
72
|
+
// ─────────────────────────────
|
|
73
|
+
|
|
74
|
+
async function __runGoFrontSuite() {
|
|
75
|
+
const allTests = ${JSON.stringify(testNames)};
|
|
76
|
+
const runFilter = ${runRegex ? `new RegExp(${JSON.stringify(runRegex)})` : "null"};
|
|
77
|
+
const verbose = ${verbose};
|
|
78
|
+
const pkgName = ${JSON.stringify(pkgName)};
|
|
79
|
+
|
|
80
|
+
const testsToRun = runFilter
|
|
81
|
+
? allTests.filter((name) => runFilter.test(name))
|
|
82
|
+
: allTests;
|
|
83
|
+
|
|
84
|
+
if (testsToRun.length === 0) {
|
|
85
|
+
if (allTests.length > 0 && runFilter) {
|
|
86
|
+
console.log("testing: warning: no tests to run matching " + ${JSON.stringify(runRegex)});
|
|
87
|
+
}
|
|
88
|
+
console.log("PASS");
|
|
89
|
+
console.log("ok \\t" + pkgName + "\\t0.000s");
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let totalFailures = 0;
|
|
94
|
+
let totalSkipped = 0;
|
|
95
|
+
const suiteStart = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
96
|
+
|
|
97
|
+
globalThis.__onSubtestStart = (name) => {
|
|
98
|
+
if (verbose) {
|
|
99
|
+
console.log("=== RUN " + name);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
globalThis.__onSubtestEnd = (subT) => {
|
|
104
|
+
const elapsed = (((typeof performance !== "undefined" ? performance.now() : Date.now()) - subT._start) / 1000).toFixed(2);
|
|
105
|
+
if (subT.skipped) {
|
|
106
|
+
console.log("--- SKIP: " + subT._name + " (" + elapsed + "s)");
|
|
107
|
+
for (const log of subT._logs) {
|
|
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
|
+
};
|
|
125
|
+
|
|
126
|
+
const __testRegistry = {
|
|
127
|
+
${testNames.map((n) => ` ${JSON.stringify(n)}: typeof ${n} !== "undefined" ? ${n} : null`).join(",\n")}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const name of testsToRun) {
|
|
131
|
+
const fn = __testRegistry[name] ?? (typeof globalThis[name] === "function" ? globalThis[name] : null);
|
|
132
|
+
if (typeof fn !== "function") {
|
|
133
|
+
console.error("testing: cannot find test function " + name);
|
|
134
|
+
totalFailures++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const t = new __GoFront_T(name);
|
|
139
|
+
if (verbose) {
|
|
140
|
+
console.log("=== RUN " + name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const res = fn(t);
|
|
145
|
+
if (res && typeof res.then === "function") {
|
|
146
|
+
await res;
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e instanceof __GoFront_FailNow) {
|
|
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
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const elapsed = (((typeof performance !== "undefined" ? performance.now() : Date.now()) - t._start) / 1000).toFixed(2);
|
|
160
|
+
|
|
161
|
+
if (t.skipped) {
|
|
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
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const suiteElapsed = (((typeof performance !== "undefined" ? performance.now() : Date.now()) - suiteStart) / 1000).toFixed(3);
|
|
187
|
+
|
|
188
|
+
if (totalFailures > 0) {
|
|
189
|
+
console.log("FAIL");
|
|
190
|
+
console.log("FAIL\\t" + pkgName + "\\t" + suiteElapsed + "s");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
} else {
|
|
193
|
+
console.log("PASS");
|
|
194
|
+
console.log("ok \\t" + pkgName + "\\t" + suiteElapsed + "s");
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
__runGoFrontSuite();
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateTestOptions(options, resolvedDir) {
|
|
204
|
+
if (options.run) {
|
|
205
|
+
try {
|
|
206
|
+
new RegExp(options.run);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
throw new Error(`gofront: invalid regex for -run: ${e.message}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (options.dom) {
|
|
213
|
+
let jsdomFound = false;
|
|
214
|
+
try {
|
|
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) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"gofront: --dom requires 'jsdom' to be installed. Run 'npm install --save-dev jsdom' to enable DOM testing.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reportEmptyTests(label, reason, options) {
|
|
235
|
+
const out = `? \t${label}\t[${reason}]\n`;
|
|
236
|
+
if (options.captureOutput) {
|
|
237
|
+
return { exitCode: 0, stdout: out, stderr: "", noTests: true };
|
|
238
|
+
}
|
|
239
|
+
const dest = options.stdout ?? process.stdout;
|
|
240
|
+
dest.write(out);
|
|
241
|
+
return { exitCode: 0, noTests: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function spawnTestRunner(resolvedDir, harnessJs, testCount, options) {
|
|
245
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
246
|
+
const child = spawn(process.execPath, ["--input-type=module", "-"], {
|
|
247
|
+
cwd: resolvedDir,
|
|
248
|
+
env: process.env,
|
|
249
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
let stdout = "";
|
|
253
|
+
let stderr = "";
|
|
254
|
+
|
|
255
|
+
child.stdout.on("data", (chunk) => {
|
|
256
|
+
const str = chunk.toString();
|
|
257
|
+
if (options.captureOutput) {
|
|
258
|
+
stdout += str;
|
|
259
|
+
} else {
|
|
260
|
+
(options.stdout ?? process.stdout).write(str);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
child.stderr.on("data", (chunk) => {
|
|
265
|
+
const str = chunk.toString();
|
|
266
|
+
if (options.captureOutput) {
|
|
267
|
+
stderr += str;
|
|
268
|
+
} else {
|
|
269
|
+
(options.stderr ?? process.stderr).write(str);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
child.on("error", (err) => {
|
|
274
|
+
rejectPromise(err);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
child.on("close", (exitCode) => {
|
|
278
|
+
resolvePromise({
|
|
279
|
+
exitCode: exitCode ?? 0,
|
|
280
|
+
stdout,
|
|
281
|
+
stderr,
|
|
282
|
+
testCount,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
child.stdin.on("error", () => {});
|
|
287
|
+
child.stdin.end(harnessJs);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function runTests(targetDir, options = {}) {
|
|
292
|
+
const resolvedDir = resolve(targetDir);
|
|
293
|
+
validateTestOptions(options, resolvedDir);
|
|
294
|
+
|
|
295
|
+
const allFiles = gwFilesIn(resolvedDir, { includeTests: true });
|
|
296
|
+
if (allFiles.length === 0) {
|
|
297
|
+
throw new Error(`No .go files found in ${targetDir}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const testFiles = allFiles.filter((f) => f.endsWith("_test.go"));
|
|
301
|
+
if (testFiles.length === 0) {
|
|
302
|
+
return reportEmptyTests(basename(resolvedDir), "no test files", options);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let compiled;
|
|
306
|
+
try {
|
|
307
|
+
compiled = compilePackageTests(resolvedDir, options);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
const out = `FAIL\t${basename(resolvedDir)} [build failed]\n${err?.message ?? String(err)}\n`;
|
|
310
|
+
if (options.captureOutput) {
|
|
311
|
+
return { exitCode: 1, stdout: "", stderr: out };
|
|
312
|
+
}
|
|
313
|
+
const dest = options.stderr ?? process.stderr;
|
|
314
|
+
dest.write(out);
|
|
315
|
+
return { exitCode: 1 };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const { pkgName, js, programs } = compiled;
|
|
319
|
+
const testNames = discoverTests(programs);
|
|
320
|
+
|
|
321
|
+
if (testNames.length === 0) {
|
|
322
|
+
return reportEmptyTests(pkgName, "no tests to run", options);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const harnessJs = generateTestHarness(bundleJsClean(js), testNames, {
|
|
326
|
+
...options,
|
|
327
|
+
pkgName,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return spawnTestRunner(resolvedDir, harnessJs, testNames.length, options);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function bundleJsClean(js) {
|
|
334
|
+
return js;
|
|
335
|
+
}
|
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,
|
|
@@ -112,19 +113,85 @@ export function loadVendorConfig(projectDir) {
|
|
|
112
113
|
|
|
113
114
|
let dest = null;
|
|
114
115
|
let packages = dependencies;
|
|
116
|
+
let minify = false;
|
|
115
117
|
|
|
116
118
|
if (typeof vendorConfig === "string") {
|
|
117
119
|
dest = vendorConfig;
|
|
118
120
|
} else if (vendorConfig && typeof vendorConfig === "object") {
|
|
119
121
|
if (vendorConfig.dest) dest = vendorConfig.dest;
|
|
120
122
|
if (Array.isArray(vendorConfig.packages)) packages = vendorConfig.packages;
|
|
123
|
+
if (typeof vendorConfig.minify === "boolean") minify = vendorConfig.minify;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
if (!dest) {
|
|
124
127
|
dest = existsSync(join(projectDir, "app")) ? "app/vendor.js" : "vendor.js";
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
return { dest, packages };
|
|
130
|
+
return { dest, packages, minify };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function resolveDestinationPaths(projectRoot, dest) {
|
|
134
|
+
const destList = Array.isArray(dest) ? dest : [dest];
|
|
135
|
+
if (destList.length === 0) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"gofront: at least one vendor destination must be specified",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return destList.map((d) => {
|
|
142
|
+
const fullPath = resolve(projectRoot, d);
|
|
143
|
+
const relDest = relative(projectRoot, fullPath);
|
|
144
|
+
if (relDest.startsWith("..") || isAbsolute(relDest)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`gofront: vendor destination '${d}' is outside project directory`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return fullPath;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runBundler(
|
|
154
|
+
bundlerInfo,
|
|
155
|
+
{ projectRoot, entryFile, destPath, minify },
|
|
156
|
+
) {
|
|
157
|
+
if (bundlerInfo.name === "rolldown") {
|
|
158
|
+
const rolldownModule =
|
|
159
|
+
bundlerInfo.instance ??
|
|
160
|
+
(await import(pathToFileURL(bundlerInfo.path).href));
|
|
161
|
+
const rolldownFn =
|
|
162
|
+
rolldownModule.rolldown ||
|
|
163
|
+
rolldownModule.default?.rolldown ||
|
|
164
|
+
rolldownModule.default;
|
|
165
|
+
|
|
166
|
+
const bundle = await rolldownFn({
|
|
167
|
+
input: entryFile,
|
|
168
|
+
cwd: projectRoot,
|
|
169
|
+
});
|
|
170
|
+
await bundle.write({
|
|
171
|
+
file: destPath,
|
|
172
|
+
format: "esm",
|
|
173
|
+
minify: Boolean(minify),
|
|
174
|
+
});
|
|
175
|
+
await bundle.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (bundlerInfo.name === "esbuild") {
|
|
180
|
+
const esbuildModule =
|
|
181
|
+
bundlerInfo.instance ??
|
|
182
|
+
(await import(pathToFileURL(bundlerInfo.path).href));
|
|
183
|
+
const buildFn =
|
|
184
|
+
esbuildModule.build || esbuildModule.default?.build || esbuildModule;
|
|
185
|
+
|
|
186
|
+
await buildFn({
|
|
187
|
+
entryPoints: [entryFile],
|
|
188
|
+
bundle: true,
|
|
189
|
+
format: "esm",
|
|
190
|
+
outfile: destPath,
|
|
191
|
+
absWorkingDir: projectRoot,
|
|
192
|
+
minify: Boolean(minify),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
128
195
|
}
|
|
129
196
|
|
|
130
197
|
export async function bundleVendor(projectDir = ".", options = {}) {
|
|
@@ -133,24 +200,19 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
133
200
|
|
|
134
201
|
const packages = options.packages ?? config.packages;
|
|
135
202
|
const dest = options.dest ?? config.dest;
|
|
203
|
+
const minify = options.minify ?? config.minify ?? false;
|
|
136
204
|
|
|
137
205
|
if (!packages || packages.length === 0) {
|
|
138
206
|
return {
|
|
139
207
|
bundled: [],
|
|
140
208
|
skipped: 0,
|
|
141
209
|
dest,
|
|
210
|
+
minify: Boolean(minify),
|
|
142
211
|
message: "no external dependencies found",
|
|
143
212
|
};
|
|
144
213
|
}
|
|
145
214
|
|
|
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
|
-
|
|
215
|
+
const destPaths = resolveDestinationPaths(projectRoot, dest);
|
|
154
216
|
const bundlerInfo = options.bundler ?? findBundler(projectRoot);
|
|
155
217
|
if (!bundlerInfo) {
|
|
156
218
|
console.warn(
|
|
@@ -161,20 +223,28 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
161
223
|
skipped: packages.length,
|
|
162
224
|
dest,
|
|
163
225
|
bundler: null,
|
|
226
|
+
minify: Boolean(minify),
|
|
164
227
|
reason: "no bundler installed",
|
|
165
228
|
};
|
|
166
229
|
}
|
|
167
230
|
|
|
168
|
-
|
|
231
|
+
for (const p of destPaths) {
|
|
232
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
233
|
+
}
|
|
169
234
|
|
|
170
|
-
// Allow custom bundler execution function (useful for tests and plugins)
|
|
171
235
|
if (typeof bundlerInfo.bundle === "function") {
|
|
172
236
|
await bundlerInfo.bundle({
|
|
173
237
|
projectRoot,
|
|
174
238
|
packages,
|
|
175
|
-
dest:
|
|
239
|
+
dest: destPaths.length === 1 ? destPaths[0] : destPaths,
|
|
240
|
+
minify: Boolean(minify),
|
|
176
241
|
});
|
|
177
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
bundled: packages,
|
|
244
|
+
bundler: bundlerInfo.name ?? "custom",
|
|
245
|
+
dest,
|
|
246
|
+
minify: Boolean(minify),
|
|
247
|
+
};
|
|
178
248
|
}
|
|
179
249
|
|
|
180
250
|
const entryCode = generateVendorEntry(packages);
|
|
@@ -185,38 +255,15 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
185
255
|
writeFileSync(entryFile, entryCode);
|
|
186
256
|
|
|
187
257
|
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
|
-
});
|
|
258
|
+
await runBundler(bundlerInfo, {
|
|
259
|
+
projectRoot,
|
|
260
|
+
entryFile,
|
|
261
|
+
destPath: destPaths[0],
|
|
262
|
+
minify,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
for (let i = 1; i < destPaths.length; i++) {
|
|
266
|
+
copyFileSync(destPaths[0], destPaths[i]);
|
|
220
267
|
}
|
|
221
268
|
} finally {
|
|
222
269
|
if (existsSync(entryFile)) {
|
|
@@ -224,5 +271,10 @@ export async function bundleVendor(projectDir = ".", options = {}) {
|
|
|
224
271
|
}
|
|
225
272
|
}
|
|
226
273
|
|
|
227
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
bundled: packages,
|
|
276
|
+
bundler: bundlerInfo.name,
|
|
277
|
+
dest,
|
|
278
|
+
minify: Boolean(minify),
|
|
279
|
+
};
|
|
228
280
|
}
|