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 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/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap and release history.
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.1",
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:all": "npm run test:unit && npm run test:e2e",
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": "husky"
48
+ "prepare": "lefthook install"
46
49
  },
47
50
  "devDependencies": {
48
- "@biomejs/biome": "^2.4.12",
49
- "@playwright/test": "^1.59.1",
50
- "c8": "^11.0.0",
51
- "husky": "^9.1.7",
52
- "jsdom": "^29.0.2",
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}`;
@@ -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 (program.decls.some((d) => d.kind === "FuncDecl" && d.name === "main"))
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.
@@ -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 && "re" in v && "im" in v) {
60
- const sign = v.im >= 0 ? "+" : "";
61
- return pad("(" + v.re + sign + v.im + "i)", width, false);
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) => f.endsWith(".go") || f.endsWith(".templ"))
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
  };