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/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 createDevServer(serveDir, port) {
26
- const clients = new Set();
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
- const server = createServer((req, res) => {
29
- // SSE endpoint — browser connects here to receive reload events
30
- if (req.url === "/_gofront/events") {
31
- res.writeHead(200, {
32
- "Content-Type": "text/event-stream",
33
- "Cache-Control": "no-cache",
34
- Connection: "keep-alive",
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
- let urlPath = req.url.split("?")[0];
43
- if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
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
- const filePath = join(serveDir, urlPath);
46
- if (!existsSync(filePath)) {
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
- try {
53
- const data = readFileSync(filePath);
54
- const mime = MIME[extname(filePath)] ?? "application/octet-stream";
55
- res.writeHead(200, { "Content-Type": mime });
56
- res.end(data);
57
- } catch {
58
- res.writeHead(500, { "Content-Type": "text/plain" });
59
- res.end("Server error");
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
- console.error(`gofront: dev server → http://localhost:${port}`);
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 { notify };
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 { handleInit, maybeMinify, runCompile } from "./cli-core.js";
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
@@ -47,6 +47,7 @@ const BUILTIN_PACKAGES = new Set([
47
47
  "regexp",
48
48
  "path",
49
49
  "path/filepath",
50
+ "testing",
50
51
  ]);
51
52
 
52
53
  // Returns true for local relative paths (start with ./ or ../)
@@ -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
+ }
@@ -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) this.checkTopDecl(d, this.globals);
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