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/src/dev-server.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Minimal dev server for gofront --serve watch mode.
|
|
2
2
|
// Serves static files and pushes a reload event via SSE after each build.
|
|
3
3
|
|
|
4
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
-
import { extname, join } from "node:path";
|
|
6
|
+
import { extname, join, resolve } from "node:path";
|
|
7
7
|
|
|
8
|
-
const MIME = {
|
|
8
|
+
export const MIME = {
|
|
9
9
|
".html": "text/html; charset=utf-8",
|
|
10
10
|
".js": "application/javascript; charset=utf-8",
|
|
11
11
|
".css": "text/css; charset=utf-8",
|
|
@@ -22,42 +22,97 @@ 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
|
-
export function
|
|
26
|
-
|
|
25
|
+
export function handleDevRequest(req, res, serveDir, clients = new Set()) {
|
|
26
|
+
// SSE endpoint — browser connects here to receive reload events
|
|
27
|
+
if (req.url === "/_gofront/events") {
|
|
28
|
+
res.writeHead(200, {
|
|
29
|
+
"Content-Type": "text/event-stream",
|
|
30
|
+
"Cache-Control": "no-cache",
|
|
31
|
+
Connection: "keep-alive",
|
|
32
|
+
});
|
|
33
|
+
res.write(": connected\n\n");
|
|
34
|
+
clients.add(res);
|
|
35
|
+
req.on?.("close", () => clients.delete(res));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
res.write(": connected\n\n");
|
|
37
|
-
clients.add(res);
|
|
38
|
-
req.on("close", () => clients.delete(res));
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
39
|
+
let urlPath = req.url.split("?")[0].split("#")[0];
|
|
40
|
+
try {
|
|
41
|
+
urlPath = decodeURIComponent(urlPath);
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore malformed URL encoding and use raw path
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
const resolvedServe = resolve(serveDir);
|
|
49
|
+
const filePath = resolve(serveDir, `.${urlPath}`);
|
|
50
|
+
if (!filePath.startsWith(resolvedServe)) {
|
|
51
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
52
|
+
res.end("Forbidden");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(filePath)) {
|
|
57
|
+
// SPA fallback: clean paths with no file extension fall back to index.html
|
|
58
|
+
const ext = extname(urlPath);
|
|
59
|
+
if (!ext) {
|
|
60
|
+
const indexPath = join(serveDir, "index.html");
|
|
61
|
+
if (existsSync(indexPath)) {
|
|
62
|
+
try {
|
|
63
|
+
const data = readFileSync(indexPath);
|
|
64
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
65
|
+
res.end(data);
|
|
66
|
+
return;
|
|
67
|
+
} catch {
|
|
68
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
69
|
+
res.end("Server error");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
75
|
+
res.end("Not found");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
try {
|
|
80
|
+
const stat = statSync(filePath);
|
|
81
|
+
if (stat.isDirectory()) {
|
|
82
|
+
const dirIndex = join(filePath, "index.html");
|
|
83
|
+
if (existsSync(dirIndex)) {
|
|
84
|
+
const data = readFileSync(dirIndex);
|
|
85
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
86
|
+
res.end(data);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const rootIndex = join(serveDir, "index.html");
|
|
90
|
+
if (existsSync(rootIndex)) {
|
|
91
|
+
const data = readFileSync(rootIndex);
|
|
92
|
+
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
93
|
+
res.end(data);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
47
96
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
48
97
|
res.end("Not found");
|
|
49
98
|
return;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
const data = readFileSync(filePath);
|
|
102
|
+
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
103
|
+
res.writeHead(200, { "Content-Type": mime });
|
|
104
|
+
res.end(data);
|
|
105
|
+
} catch {
|
|
106
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
107
|
+
res.end("Server error");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createDevServer(serveDir, port = 3000) {
|
|
112
|
+
const clients = new Set();
|
|
113
|
+
|
|
114
|
+
const server = createServer((req, res) => {
|
|
115
|
+
handleDevRequest(req, res, serveDir, clients);
|
|
61
116
|
});
|
|
62
117
|
|
|
63
118
|
server.on("error", (err) => {
|
|
@@ -70,8 +125,12 @@ export function createDevServer(serveDir, port) {
|
|
|
70
125
|
}
|
|
71
126
|
process.exit(1);
|
|
72
127
|
});
|
|
128
|
+
|
|
73
129
|
server.listen(port, () => {
|
|
74
|
-
|
|
130
|
+
const actualPort = server.address()?.port ?? port;
|
|
131
|
+
if (port !== 0) {
|
|
132
|
+
console.error(`gofront: dev server → http://localhost:${actualPort}`);
|
|
133
|
+
}
|
|
75
134
|
});
|
|
76
135
|
|
|
77
136
|
function notify() {
|
|
@@ -80,5 +139,9 @@ export function createDevServer(serveDir, port) {
|
|
|
80
139
|
}
|
|
81
140
|
}
|
|
82
141
|
|
|
83
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
notify,
|
|
144
|
+
server,
|
|
145
|
+
close: () => new Promise((res) => server.close(res)),
|
|
146
|
+
};
|
|
84
147
|
}
|
package/src/index.js
CHANGED
|
@@ -18,9 +18,16 @@ import { createRequire } from "node:module";
|
|
|
18
18
|
const _require = createRequire(import.meta.url);
|
|
19
19
|
const { version } = _require("../package.json");
|
|
20
20
|
|
|
21
|
-
import { statSync, watch, writeFileSync } from "node:fs";
|
|
21
|
+
import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
22
22
|
import { dirname, resolve } from "node:path";
|
|
23
|
-
import {
|
|
23
|
+
import { copyAssets } from "./asset-manager.js";
|
|
24
|
+
import {
|
|
25
|
+
handleInit,
|
|
26
|
+
handlePrep,
|
|
27
|
+
handleTest,
|
|
28
|
+
maybeMinify,
|
|
29
|
+
runCompile,
|
|
30
|
+
} from "./cli-core.js";
|
|
24
31
|
import { createDevServer, liveReloadClient } from "./dev-server.js";
|
|
25
32
|
|
|
26
33
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
@@ -44,11 +51,14 @@ Usage:
|
|
|
44
51
|
gofront <input> --watch Watch for changes and recompile
|
|
45
52
|
gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
|
|
46
53
|
gofront <input> -o out.js --serve --port 8080 Use a custom port
|
|
54
|
+
gofront <input> -o out.js --copy-assets Compile and synchronize static assets
|
|
47
55
|
gofront <input> --source-map Append inline source map to output
|
|
48
56
|
gofront <input> --minify Minify output
|
|
49
57
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
50
58
|
gofront <file.go> --ast Dump AST (debug)
|
|
51
59
|
gofront <file.go> --tokens Dump tokens (debug)
|
|
60
|
+
gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
61
|
+
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
52
62
|
gofront init [dir] Scaffold a new GoFront project
|
|
53
63
|
gofront --version Print version
|
|
54
64
|
`.trim(),
|
|
@@ -56,6 +66,39 @@ Usage:
|
|
|
56
66
|
process.exit(0);
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
// ── prep / vendor subcommand ──────────────────────────────────
|
|
70
|
+
|
|
71
|
+
if (args[0] === "prep" || args[0] === "vendor") {
|
|
72
|
+
const hasMinify = args.includes("--minify");
|
|
73
|
+
const filteredArgs = args.slice(1).filter((a) => !a.startsWith("--"));
|
|
74
|
+
const targetArg = filteredArgs[0] ?? ".";
|
|
75
|
+
const targetDir = resolve(targetArg);
|
|
76
|
+
try {
|
|
77
|
+
const vendorConfig = hasMinify ? { minify: true } : {};
|
|
78
|
+
const { assets, vendor } = await handlePrep(targetDir, {
|
|
79
|
+
vendorConfig,
|
|
80
|
+
});
|
|
81
|
+
if (assets.copied > 0 || assets.skipped > 0) {
|
|
82
|
+
console.error(
|
|
83
|
+
`gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (vendor.bundled?.length > 0) {
|
|
87
|
+
const destStr = Array.isArray(vendor.dest)
|
|
88
|
+
? vendor.dest.join(", ")
|
|
89
|
+
: vendor.dest;
|
|
90
|
+
const minStr = vendor.minify ? " (minified)" : "";
|
|
91
|
+
console.error(
|
|
92
|
+
`gofront: bundled ${vendor.bundled.length} vendor dependencies → ${destStr}${minStr}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error(`gofront: ${e.message}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
59
102
|
// ── init subcommand ──────────────────────────────────────────
|
|
60
103
|
|
|
61
104
|
if (args[0] === "init") {
|
|
@@ -75,6 +118,45 @@ if (args[0] === "init") {
|
|
|
75
118
|
process.exit(0);
|
|
76
119
|
}
|
|
77
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
|
+
|
|
78
160
|
const inputArg = args[0];
|
|
79
161
|
const outputFlag = args.indexOf("-o");
|
|
80
162
|
const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
|
|
@@ -84,6 +166,7 @@ const dumpTokens = args.includes("--tokens");
|
|
|
84
166
|
const sourceMap = args.includes("--source-map");
|
|
85
167
|
const serveMode = args.includes("--serve");
|
|
86
168
|
const watchMode = args.includes("--watch") || serveMode;
|
|
169
|
+
const copyAssetsFlag = args.includes("--copy-assets");
|
|
87
170
|
const minifyOutput = args.includes("--minify");
|
|
88
171
|
const mangleOutput = args.includes("--mangle");
|
|
89
172
|
const portFlag = args.indexOf("--port");
|
|
@@ -148,6 +231,7 @@ if (!watchMode) {
|
|
|
148
231
|
|
|
149
232
|
if (outputFile) {
|
|
150
233
|
try {
|
|
234
|
+
mkdirSync(dirname(resolve(outputFile)), { recursive: true });
|
|
151
235
|
writeFileSync(outputFile, `${js}\n`);
|
|
152
236
|
console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
|
|
153
237
|
} catch (e) {
|
|
@@ -157,6 +241,19 @@ if (!watchMode) {
|
|
|
157
241
|
} else {
|
|
158
242
|
console.log(js);
|
|
159
243
|
}
|
|
244
|
+
|
|
245
|
+
if (copyAssetsFlag) {
|
|
246
|
+
try {
|
|
247
|
+
const projectDir = resolve(".");
|
|
248
|
+
const { copied, skipped } = copyAssets(projectDir);
|
|
249
|
+
if (copied > 0 || skipped > 0) {
|
|
250
|
+
console.error(`gofront: copied ${copied} assets (${skipped} skipped)`);
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.error(`gofront: asset copy failed: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
160
257
|
process.exit(0);
|
|
161
258
|
}
|
|
162
259
|
|
|
@@ -210,6 +307,15 @@ function buildOnce(changedFile = null) {
|
|
|
210
307
|
`[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
|
|
211
308
|
);
|
|
212
309
|
}
|
|
310
|
+
|
|
311
|
+
if (copyAssetsFlag) {
|
|
312
|
+
try {
|
|
313
|
+
copyAssets(resolve("."));
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error(`gofront: asset copy failed: ${e.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
213
319
|
devServer?.notify();
|
|
214
320
|
} catch (e) {
|
|
215
321
|
console.error(`[${timestamp()}] gofront: ERROR`);
|
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
|
|