gofront 1.0.1 → 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 +88 -2
- package/package.json +11 -8
- package/src/asset-manager.js +125 -0
- package/src/cli-core.js +15 -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 +96 -33
- package/src/index.js +108 -2
- 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 +280 -0
package/README.md
CHANGED
|
@@ -387,13 +387,16 @@ gofront <dir> compile all *.go in directory → stdout
|
|
|
387
387
|
gofront <input> -o out.js write output to file (prints elapsed compile time e.g. "15ms")
|
|
388
388
|
gofront <input> --check type-check only
|
|
389
389
|
gofront <input> --watch watch for changes and recompile
|
|
390
|
-
gofront <input> -o out.js --serve watch + serve with live reload (default port 3000)
|
|
390
|
+
gofront <input> -o out.js --serve watch + serve with live reload & SPA fallback (default port 3000)
|
|
391
391
|
gofront <input> -o out.js --serve --port 8080 use a custom port
|
|
392
|
+
gofront <input> -o out.js --copy-assets compile and synchronize static assets from package.json assetCopy
|
|
392
393
|
gofront <input> --source-map append inline source map (single file or directory; multi-file packages emit per-file mappings)
|
|
393
394
|
gofront <input> --minify minify output (built-in minifier)
|
|
394
395
|
gofront <input> --minify --mangle minify and rename local identifiers
|
|
395
396
|
gofront <file.go> --ast dump AST (debug)
|
|
396
397
|
gofront <file.go> --tokens dump tokens (debug)
|
|
398
|
+
gofront test [dir] [--dom] run unit tests (-v verbose, -run <regex>)
|
|
399
|
+
gofront prep [dir] copy static assets and bundle vendor dependencies
|
|
397
400
|
gofront init [dir] scaffold a new project
|
|
398
401
|
gofront --version / -v print version
|
|
399
402
|
gofront --help / -h print this help
|
|
@@ -401,6 +404,89 @@ gofront --help / -h print this help
|
|
|
401
404
|
|
|
402
405
|
---
|
|
403
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
|
+
|
|
459
|
+
## Static assets & vendor bundling
|
|
460
|
+
|
|
461
|
+
GoFront includes native tooling to manage static assets and package external npm dependencies for the browser without requiring external wrappers or complex build tools.
|
|
462
|
+
|
|
463
|
+
### Asset copying (`assetCopy`)
|
|
464
|
+
|
|
465
|
+
Specify static assets (fonts, stylesheets, images) in `package.json` (or `gofront.json`):
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"assetCopy": [
|
|
470
|
+
{ "source": "node_modules/@fontsource/raleway/files", "dest": "app/fonts" },
|
|
471
|
+
{ "source": "node_modules/prismjs/themes", "dest": "app/css/prism-themes" }
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Run `gofront prep` to copy them, or pass `--copy-assets` to automatically synchronize assets on build.
|
|
477
|
+
|
|
478
|
+
### Vendor bundling (`gofront prep`)
|
|
479
|
+
|
|
480
|
+
Package external npm dependencies for the browser:
|
|
481
|
+
|
|
482
|
+
```sh
|
|
483
|
+
gofront prep
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
GoFront inspects your `dependencies`, dynamically uses `rolldown` or `esbuild` from your `devDependencies`, and bundles them into an ES module (defaulting to `app/vendor.js` or `vendor.js`).
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
404
490
|
## Multi-file packages
|
|
405
491
|
|
|
406
492
|
All `.go` files in a directory share the same namespace and are compiled as one unit.
|
|
@@ -625,7 +711,7 @@ you know exactly what to expect.
|
|
|
625
711
|
|
|
626
712
|
## Roadmap
|
|
627
713
|
|
|
628
|
-
See [`docs/
|
|
714
|
+
See [`docs/roadmap.md`](docs/roadmap.md) for the full roadmap and release history.
|
|
629
715
|
Design documents for planned features are organised by release under `docs/v*/`
|
|
630
716
|
(e.g. [`docs/v0.0.8/`](docs/v0.0.8/), [`docs/v0.0.9/`](docs/v0.0.9/)).
|
|
631
717
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gofront",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"author": "Luuk van Venrooij",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,20 +36,23 @@
|
|
|
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 .",
|
|
44
47
|
"check": "biome check . && sentrux check && sentrux gate && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check && node src/index.js example/templ/src --check",
|
|
45
|
-
"prepare": "
|
|
48
|
+
"prepare": "lefthook install"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "^2.
|
|
49
|
-
"@playwright/test": "^1.
|
|
50
|
-
"c8": "^
|
|
51
|
-
"
|
|
52
|
-
"
|
|
51
|
+
"@biomejs/biome": "^2.5.14",
|
|
52
|
+
"@playwright/test": "^1.63.0",
|
|
53
|
+
"c8": "^12.0.0",
|
|
54
|
+
"jsdom": "^30.1.0",
|
|
55
|
+
"lefthook": "^2.1.14",
|
|
53
56
|
"serve": "^14.2.6"
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Native static asset manager for GoFront projects.
|
|
2
|
+
// Zero-dependency file and directory copier using node:fs.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
copyFileSync,
|
|
6
|
+
cpSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
statSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import {
|
|
13
|
+
basename,
|
|
14
|
+
dirname,
|
|
15
|
+
isAbsolute,
|
|
16
|
+
join,
|
|
17
|
+
relative,
|
|
18
|
+
resolve,
|
|
19
|
+
} from "node:path";
|
|
20
|
+
|
|
21
|
+
export function loadAssetConfig(projectDir) {
|
|
22
|
+
const gofrontJsonPath = join(projectDir, "gofront.json");
|
|
23
|
+
if (existsSync(gofrontJsonPath)) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(gofrontJsonPath, "utf8"));
|
|
26
|
+
if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pkgJsonPath = join(projectDir, "package.json");
|
|
31
|
+
if (existsSync(pkgJsonPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
34
|
+
if (Array.isArray(parsed.assetCopy)) return parsed.assetCopy;
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copyFileAsset(srcPath, destPath, dest) {
|
|
42
|
+
let targetFile = destPath;
|
|
43
|
+
if (
|
|
44
|
+
dest.endsWith("/") ||
|
|
45
|
+
(existsSync(destPath) && statSync(destPath).isDirectory())
|
|
46
|
+
) {
|
|
47
|
+
targetFile = join(destPath, basename(srcPath));
|
|
48
|
+
}
|
|
49
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
50
|
+
copyFileSync(srcPath, targetFile);
|
|
51
|
+
return targetFile;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function copyAssetEntry(entry, projectRoot) {
|
|
55
|
+
if (!entry || typeof entry !== "object") return null;
|
|
56
|
+
const { source, dest } = entry;
|
|
57
|
+
if (!source || !dest) return null;
|
|
58
|
+
|
|
59
|
+
const destPath = resolve(projectRoot, dest);
|
|
60
|
+
const relDest = relative(projectRoot, destPath);
|
|
61
|
+
if (relDest.startsWith("..") || isAbsolute(relDest)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`gofront: asset destination '${dest}' is outside project directory`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const srcPath = resolve(projectRoot, source);
|
|
68
|
+
if (!existsSync(srcPath)) {
|
|
69
|
+
console.warn(`gofront: asset source '${source}' not found — skipping`);
|
|
70
|
+
return {
|
|
71
|
+
status: "skipped",
|
|
72
|
+
item: { source, dest, status: "skipped", reason: "source not found" },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const stat = statSync(srcPath);
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
mkdirSync(destPath, { recursive: true });
|
|
80
|
+
cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
81
|
+
return {
|
|
82
|
+
status: "copied",
|
|
83
|
+
item: { source, dest, type: "directory", status: "copied" },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const targetFile = copyFileAsset(srcPath, destPath, dest);
|
|
87
|
+
return {
|
|
88
|
+
status: "copied",
|
|
89
|
+
item: { source, dest: targetFile, type: "file", status: "copied" },
|
|
90
|
+
};
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.warn(`gofront: failed to copy asset '${source}': ${err.message}`);
|
|
93
|
+
return {
|
|
94
|
+
status: "failed",
|
|
95
|
+
item: { source, dest, status: "failed", reason: err.message },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function copyAssets(projectDir = ".", config = null) {
|
|
101
|
+
const projectRoot = resolve(projectDir);
|
|
102
|
+
let entries;
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(config)) {
|
|
105
|
+
entries = config;
|
|
106
|
+
} else if (config && Array.isArray(config.assetCopy)) {
|
|
107
|
+
entries = config.assetCopy;
|
|
108
|
+
} else {
|
|
109
|
+
entries = loadAssetConfig(projectRoot);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const items = [];
|
|
113
|
+
let copied = 0;
|
|
114
|
+
let skipped = 0;
|
|
115
|
+
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const result = copyAssetEntry(entry, projectRoot);
|
|
118
|
+
if (!result) continue;
|
|
119
|
+
if (result.status === "copied") copied++;
|
|
120
|
+
else skipped++;
|
|
121
|
+
items.push(result.item);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { copied, skipped, items };
|
|
125
|
+
}
|
package/src/cli-core.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { copyAssets } from "./asset-manager.js";
|
|
6
7
|
import { compileDir, compileSingleFile } from "./compiler.js";
|
|
7
8
|
import { minify } from "./minifier.js";
|
|
9
|
+
import { runTests } from "./test-runner.js";
|
|
10
|
+
import { bundleVendor } from "./vendor.js";
|
|
8
11
|
|
|
9
12
|
export function runCompile(inputPath, isDir, options) {
|
|
10
13
|
const {
|
|
@@ -63,3 +66,15 @@ export function handleInit(targetDir) {
|
|
|
63
66
|
|
|
64
67
|
return { mainPath };
|
|
65
68
|
}
|
|
69
|
+
|
|
70
|
+
export async function handlePrep(targetDir, options = {}) {
|
|
71
|
+
const resolvedTarget = resolve(targetDir);
|
|
72
|
+
const assets = copyAssets(resolvedTarget, options.assetConfig);
|
|
73
|
+
const vendor = await bundleVendor(resolvedTarget, options.vendorConfig);
|
|
74
|
+
return { assets, vendor };
|
|
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
|
};
|