gofront 1.4.0 → 1.6.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.
Files changed (77) hide show
  1. package/README.md +174 -403
  2. package/package.json +16 -19
  3. package/src/{codegen → backend/js}/expressions.js +265 -77
  4. package/src/{codegen → backend/js}/index.js +120 -89
  5. package/src/{codegen → backend/js}/runtime.js +102 -0
  6. package/src/{codegen → backend/js}/source-map.js +32 -0
  7. package/src/{codegen → backend/js}/statements.js +139 -64
  8. package/src/backend/js/stdlib/bits.js +32 -0
  9. package/src/{codegen → backend/js}/stdlib/errors.js +2 -2
  10. package/src/{codegen → backend/js}/stdlib/fmt.js +26 -8
  11. package/src/{codegen → backend/js}/stdlib/index.js +6 -0
  12. package/src/{codegen → backend/js}/stdlib/maps.js +1 -1
  13. package/src/{codegen → backend/js}/stdlib/path.js +2 -2
  14. package/src/backend/js/stdlib/shared.js +17 -0
  15. package/src/{codegen → backend/js}/stdlib/slices.js +1 -1
  16. package/src/{codegen → backend/js}/stdlib/sort.js +1 -1
  17. package/src/backend/js/stdlib/strconv.js +58 -0
  18. package/src/{codegen → backend/js}/stdlib/strings.js +4 -1
  19. package/src/{codegen → backend/js}/stdlib/testing.js +2 -2
  20. package/src/{codegen → backend/js}/stdlib/time.js +3 -3
  21. package/src/{codegen → backend/js}/templ.js +3 -3
  22. package/src/backend/wasm/boundary.js +1798 -0
  23. package/src/backend/wasm/emit-builtins.js +1103 -0
  24. package/src/backend/wasm/emit-exprs.js +1908 -0
  25. package/src/backend/wasm/emit-maps.js +1863 -0
  26. package/src/backend/wasm/emit-shared.js +519 -0
  27. package/src/backend/wasm/emit-stdlib.js +3006 -0
  28. package/src/backend/wasm/emit-stmts.js +2472 -0
  29. package/src/backend/wasm/emit.js +1376 -0
  30. package/src/backend/wasm/encode.js +937 -0
  31. package/src/backend/wasm/glue.js +144 -0
  32. package/src/backend/wasm/index.js +2790 -0
  33. package/src/backend/wasm/monomorph.js +711 -0
  34. package/src/backend/wasm/optimize.js +227 -0
  35. package/src/backend/wasm/types.js +544 -0
  36. package/src/backend/wasm/wat.js +302 -0
  37. package/src/cli-core.js +323 -75
  38. package/src/compiler.js +447 -14
  39. package/src/dev-server.js +56 -15
  40. package/src/index.js +99 -120
  41. package/src/lexer.js +18 -0
  42. package/src/lower/boxing.js +35 -0
  43. package/src/lower/captures.js +172 -0
  44. package/src/lower/embedding.js +30 -0
  45. package/src/lower/escape.js +164 -0
  46. package/src/lower/functions.js +302 -0
  47. package/src/lower/globals.js +316 -0
  48. package/src/lower/index.js +118 -0
  49. package/src/lower/ownership.js +137 -0
  50. package/src/lower/range.js +67 -0
  51. package/src/lower/walk.js +25 -0
  52. package/src/minifier.js +2 -0
  53. package/src/parser/index.js +8 -3
  54. package/src/resolver.js +3 -1
  55. package/src/test-runner.js +94 -20
  56. package/src/typechecker/assignability.js +42 -1
  57. package/src/typechecker/expressions.js +182 -3
  58. package/src/typechecker/index.js +261 -5
  59. package/src/typechecker/resolve.js +35 -6
  60. package/src/typechecker/stdlib/core.js +34 -19
  61. package/src/typechecker/stdlib/extended.js +18 -3
  62. package/src/typechecker/stdlib/shared.js +53 -0
  63. package/src/typechecker/stdlib.js +2 -0
  64. package/src/typechecker/types.js +259 -6
  65. package/src/vendor.js +2 -2
  66. package/src/codegen/stdlib/strconv.js +0 -33
  67. /package/src/{codegen → backend/js}/stdlib/builder.js +0 -0
  68. /package/src/{codegen → backend/js}/stdlib/bytes.js +0 -0
  69. /package/src/{codegen → backend/js}/stdlib/gom.js +0 -0
  70. /package/src/{codegen → backend/js}/stdlib/html.js +0 -0
  71. /package/src/{codegen → backend/js}/stdlib/io.js +0 -0
  72. /package/src/{codegen → backend/js}/stdlib/math.js +0 -0
  73. /package/src/{codegen → backend/js}/stdlib/os.js +0 -0
  74. /package/src/{codegen → backend/js}/stdlib/rand.js +0 -0
  75. /package/src/{codegen → backend/js}/stdlib/regexp.js +0 -0
  76. /package/src/{codegen → backend/js}/stdlib/unicode.js +0 -0
  77. /package/src/{codegen → backend/js}/stdlib/utf8.js +0 -0
package/src/dev-server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Minimal dev server for gofront --serve watch mode.
2
2
  // Serves static files and pushes reload / error / css-update events via SSE.
3
3
 
4
- import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
5
5
  import { createServer } from "node:http";
6
6
  import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { colors, log } from "./colors.js";
@@ -9,8 +9,10 @@ import { colors, log } from "./colors.js";
9
9
  export const MIME = {
10
10
  ".html": "text/html; charset=utf-8",
11
11
  ".js": "application/javascript; charset=utf-8",
12
+ ".wasm": "application/wasm",
12
13
  ".css": "text/css; charset=utf-8",
13
14
  ".json": "application/json; charset=utf-8",
15
+ ".map": "application/json; charset=utf-8",
14
16
  ".go": "text/plain; charset=utf-8",
15
17
  ".svg": "image/svg+xml",
16
18
  ".png": "image/png",
@@ -151,7 +153,11 @@ function handleSseRequest(req, res, clients, lastError = null) {
151
153
  if (lastError)
152
154
  res.write(`event: build-error\ndata: ${JSON.stringify(lastError)}\n\n`);
153
155
  clients.add(res);
154
- req.on?.("close", () => clients.delete(res));
156
+ const cleanup = () => clients.delete(res);
157
+ req.on?.("close", cleanup);
158
+ res.on?.("close", cleanup);
159
+ res.on?.("finish", cleanup);
160
+ res.on?.("error", cleanup);
155
161
  }
156
162
 
157
163
  function resolveStaticPath(serveDir, urlPath) {
@@ -161,49 +167,73 @@ function resolveStaticPath(serveDir, urlPath) {
161
167
  : urlPath.startsWith("/")
162
168
  ? urlPath
163
169
  : `/${urlPath}`;
164
- const filePath = resolve(serveDir, `.${cleanPath}`);
165
- if (!isInsideDir(resolve(serveDir), filePath)) return { forbidden: true };
170
+ const rootPath = existsSync(serveDir)
171
+ ? realpathSync(resolve(serveDir))
172
+ : resolve(serveDir);
173
+ const filePath = resolve(rootPath, `.${cleanPath}`);
174
+ if (!isInsideDir(rootPath, filePath)) return { forbidden: true };
166
175
 
167
176
  if (existsSync(filePath)) {
168
- const stat = statSync(filePath);
177
+ const realTarget = realpathSync(filePath);
178
+ if (!isInsideDir(rootPath, realTarget)) return { forbidden: true };
179
+ const stat = statSync(realTarget);
169
180
  if (stat.isDirectory()) {
170
- const dirIndex = join(filePath, "index.html");
171
- if (existsSync(dirIndex)) return { filePath: dirIndex };
172
- const rootIndex = join(serveDir, "index.html");
173
- if (existsSync(rootIndex)) return { filePath: rootIndex };
181
+ const dirIndex = join(realTarget, "index.html");
182
+ if (
183
+ existsSync(dirIndex) &&
184
+ isInsideDir(rootPath, realpathSync(dirIndex))
185
+ ) {
186
+ return { filePath: dirIndex };
187
+ }
188
+ const rootIndex = join(rootPath, "index.html");
189
+ if (
190
+ existsSync(rootIndex) &&
191
+ isInsideDir(rootPath, realpathSync(rootIndex))
192
+ ) {
193
+ return { filePath: rootIndex };
194
+ }
174
195
  return { notFound: true };
175
196
  }
176
- return { filePath };
197
+ return { filePath: realTarget };
177
198
  }
178
199
 
179
200
  // SPA fallback: clean paths with no file extension fall back to index.html
180
201
  if (!extname(cleanPath)) {
181
- const indexPath = join(serveDir, "index.html");
202
+ const indexPath = join(rootPath, "index.html");
182
203
  if (existsSync(indexPath)) return { filePath: indexPath };
183
204
  }
184
205
 
185
206
  return { notFound: true };
186
207
  }
187
208
 
188
- function sendStaticFile(res, filePath, shouldInject) {
209
+ function sendStaticFile(res, filePath, shouldInject, req = null) {
189
210
  try {
190
211
  const ext = extname(filePath);
212
+ const isHead = req?.method === "HEAD";
191
213
  if (ext === ".html") {
192
214
  const data = readFileSync(filePath, "utf8");
193
215
  const body = shouldInject ? injectLiveReload(data) : data;
194
216
  res.writeHead(200, {
195
217
  "Content-Type": MIME[".html"],
218
+ "X-Content-Type-Options": "nosniff",
196
219
  "Cache-Control": "no-cache, no-store, must-revalidate",
197
220
  });
198
- res.end(body);
221
+ if (isHead) res.end();
222
+ else res.end(body);
199
223
  return;
200
224
  }
201
225
 
202
226
  const data = readFileSync(filePath);
203
227
  res.writeHead(200, {
204
228
  "Content-Type": MIME[ext] ?? "application/octet-stream",
205
- "Cache-Control": "no-cache, no-store, must-revalidate",
229
+ "X-Content-Type-Options": "nosniff",
230
+ "Cache-Control":
231
+ ext === ".wasm" ? "no-store" : "no-cache, no-store, must-revalidate",
206
232
  });
233
+ if (isHead) {
234
+ res.end();
235
+ return;
236
+ }
207
237
  res.end(data);
208
238
  } catch {
209
239
  res.writeHead(500, { "Content-Type": "text/plain" });
@@ -218,7 +248,18 @@ export function handleDevRequest(
218
248
  clients = new Set(),
219
249
  options = {},
220
250
  ) {
251
+ if (req.method && req.method !== "GET" && req.method !== "HEAD") {
252
+ res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET, HEAD" });
253
+ res.end("Method Not Allowed");
254
+ return;
255
+ }
256
+
221
257
  if (req.url === "/_gofront/events") {
258
+ if (req.method && req.method !== "GET") {
259
+ res.writeHead(405, { "Content-Type": "text/plain", Allow: "GET" });
260
+ res.end("Method Not Allowed");
261
+ return;
262
+ }
222
263
  handleSseRequest(req, res, clients, options.getLastError?.() ?? null);
223
264
  return;
224
265
  }
@@ -240,7 +281,7 @@ export function handleDevRequest(
240
281
  return;
241
282
  }
242
283
 
243
- sendStaticFile(res, resolved.filePath, options.injectReload ?? true);
284
+ sendStaticFile(res, resolved.filePath, options.injectReload ?? true, req);
244
285
  }
245
286
 
246
287
  function broadcastToClients(clients, event, payload) {
package/src/index.js CHANGED
@@ -18,12 +18,13 @@ import { createRequire } from "node:module";
18
18
  const _require = createRequire(import.meta.url);
19
19
  const { version } = _require("../package.json");
20
20
 
21
- import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
21
+ import { statSync } from "node:fs";
22
22
  import { dirname, relative, resolve } from "node:path";
23
23
  import { copyAssets } from "./asset-manager.js";
24
24
  import {
25
25
  formatBuildSummary,
26
26
  formatPrepSummary,
27
+ formatWrittenDesc,
27
28
  handleBuild,
28
29
  handleCheck,
29
30
  handleDev,
@@ -34,12 +35,13 @@ import {
34
35
  parseBuildArgs,
35
36
  parseCheckArgs,
36
37
  parseDevArgs,
38
+ parseLegacyArgs,
37
39
  parsePrepArgs,
38
40
  parseTestArgs,
39
41
  runCompile,
42
+ writeCompileOutput,
40
43
  } from "./cli-core.js";
41
44
  import { colors, formatDiagnostic, log, ms, stamp } from "./colors.js";
42
- import { createDevServer } from "./dev-server.js";
43
45
 
44
46
  // ── Parse CLI args ───────────────────────────────────────────
45
47
 
@@ -52,18 +54,20 @@ if (args[0] === "--version" || args[0] === "-v") {
52
54
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
53
55
  console.log(
54
56
  `
55
- GoFront — a Go-inspired language that compiles to JavaScript
57
+ GoFront — a Go-inspired language that compiles to JavaScript and WebAssembly
56
58
 
57
59
  Usage:
58
60
  gofront dev [dir] [options] Start dev server with live reload (default port 3000)
59
- gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify)
61
+ gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify, --emit-wat, --release, --wasm-opt, --js-only)
60
62
  gofront check [dir|dir/...] Type-check only (dir/... recurses into every package)
61
63
  gofront test [dir|dir/...] [--dom] Run unit tests (-v verbose, -run <regex>)
62
64
  gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
63
65
  gofront init [dir] Scaffold a new GoFront project
64
66
  gofront <file.go> Compile single file and print to stdout
65
67
  gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
66
- gofront <input> -o out.js Compile and write to file
68
+ gofront <input> -o out.js Compile and write to file (wasm packages also emit app.wasm)
69
+ gofront <input> --release Production build with minification and Binaryen wasm optimization
70
+ gofront <input> --wasm-opt Optimize wasm output with Binaryen (-O3 + GUFA)
67
71
  gofront <input> --check Type-check only
68
72
  gofront <input> --watch Watch for changes and recompile
69
73
  gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
@@ -129,9 +133,9 @@ if (args[0] === "test") {
129
133
  // ── check subcommand ──────────────────────────────────────────
130
134
 
131
135
  if (args[0] === "check") {
132
- const { targetDir } = parseCheckArgs(args.slice(1));
136
+ const { targetDir, forceTarget } = parseCheckArgs(args.slice(1));
133
137
  try {
134
- const { elapsedMs, packages } = handleCheck(targetDir);
138
+ const { elapsedMs, packages } = handleCheck(targetDir, { forceTarget });
135
139
  const okLine = (label, t) =>
136
140
  log.info(`${colors.cyan(label)} — ${colors.green("OK")} ${ms(t)}`);
137
141
  if (packages) {
@@ -180,7 +184,7 @@ if (args[0] === "dev") {
180
184
  if (dev.initialError) {
181
185
  log.error(dev.initialError.message);
182
186
  } else {
183
- log.ok(`— wrote ${colors.cyan(dev.outputFile)}`);
187
+ log.ok(`— wrote ${formatWrittenDesc(dev.written, dev.outputFile)}`);
184
188
  }
185
189
  log.info(
186
190
  `dev server running → ${colors.cyan(`http://localhost:${dev.port}`)}`,
@@ -200,20 +204,22 @@ if (args[0] === "dev") {
200
204
  }
201
205
  }
202
206
 
203
- const inputArg = args[0];
204
- const outputFlag = args.indexOf("-o");
205
- const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
206
- const checkOnly = args.includes("--check");
207
- const dumpAst = args.includes("--ast");
208
- const dumpTokens = args.includes("--tokens");
209
- const sourceMap = args.includes("--source-map");
210
- const serveMode = args.includes("--serve");
211
- const watchMode = args.includes("--watch") || serveMode;
212
- const copyAssetsFlag = args.includes("--copy-assets");
213
- const minifyOutput = args.includes("--minify");
214
- const mangleOutput = args.includes("--mangle");
215
- const portFlag = args.indexOf("--port");
216
- const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
207
+ const {
208
+ inputArg,
209
+ outputFile,
210
+ checkOnly,
211
+ dumpAst,
212
+ dumpTokens,
213
+ sourceMap,
214
+ serve: serveMode,
215
+ watch: watchMode,
216
+ copyAssets: copyAssetsFlag,
217
+ wasmOpt,
218
+ minify: minifyOutput,
219
+ mangle: mangleOutput,
220
+ port: servePort,
221
+ forceTarget,
222
+ } = parseLegacyArgs(args);
217
223
 
218
224
  // ── Determine input mode ─────────────────────────────────────
219
225
 
@@ -226,6 +232,17 @@ try {
226
232
  process.exit(1);
227
233
  }
228
234
 
235
+ function copyProjectAssets(verbose) {
236
+ try {
237
+ const { copied, skipped } = copyAssets(resolve("."));
238
+ if (verbose && (copied > 0 || skipped > 0)) {
239
+ log.info(`copied ${copied} assets (${skipped} skipped)`);
240
+ }
241
+ } catch (e) {
242
+ log.warn(`asset copy failed: ${e.message}`);
243
+ }
244
+ }
245
+
229
246
  // ── Single-shot mode ─────────────────────────────────────────
230
247
 
231
248
  if (!watchMode) {
@@ -237,6 +254,8 @@ if (!watchMode) {
237
254
  outputFile,
238
255
  dumpTokens,
239
256
  dumpAst,
257
+ wasmOpt,
258
+ forceTarget,
240
259
  });
241
260
  } catch (e) {
242
261
  log.fail(e.message);
@@ -276,121 +295,81 @@ if (!watchMode) {
276
295
 
277
296
  if (outputFile) {
278
297
  try {
279
- mkdirSync(dirname(resolve(outputFile)), { recursive: true });
280
- writeFileSync(outputFile, `${js}\n`);
281
- log.info(`wrote ${colors.cyan(outputFile)} ${ms(elapsedMs)}`);
298
+ const written = writeCompileOutput(resolve(outputFile), result, js);
299
+ log.info(
300
+ `wrote ${formatWrittenDesc(written, outputFile)} ${ms(elapsedMs)}`,
301
+ );
302
+ if (result.wasmOptInfo && result.wasmOptInfo.originalSize > 0) {
303
+ const origKb = (result.wasmOptInfo.originalSize / 1024).toFixed(1);
304
+ const optKb = (result.wasmOptInfo.optimizedSize / 1024).toFixed(1);
305
+ log.info(
306
+ `wasm-opt: app.wasm ${origKb} kB → ${optKb} kB (-${result.wasmOptInfo.percentSaved}%) [${result.wasmOptInfo.engine}]`,
307
+ );
308
+ }
282
309
  } catch (e) {
283
310
  log.fail(`cannot write '${outputFile}': ${e.message}`);
284
311
  process.exit(1);
285
312
  }
286
313
  } else {
287
314
  console.log(js);
288
- }
289
-
290
- if (copyAssetsFlag) {
291
- try {
292
- const projectDir = resolve(".");
293
- const { copied, skipped } = copyAssets(projectDir);
294
- if (copied > 0 || skipped > 0) {
295
- log.info(`copied ${copied} assets (${skipped} skipped)`);
296
- }
297
- } catch (e) {
298
- log.warn(`asset copy failed: ${e.message}`);
315
+ if (result.wasm) {
316
+ log.warn(
317
+ "wasm packages were linked but app.wasm needs -o <file> to be written",
318
+ );
299
319
  }
300
320
  }
301
321
 
322
+ if (copyAssetsFlag) copyProjectAssets(true);
323
+
302
324
  process.exit(0);
303
325
  }
304
326
 
305
327
  // ── Watch mode ───────────────────────────────────────────────
306
328
 
307
- // Start dev server before first build so the browser can connect immediately
308
- let devServer = null;
309
- if (serveMode) {
310
- if (!outputFile) {
311
- log.fail("--serve requires -o <output file>");
312
- process.exit(1);
313
- }
314
- const serveDir = dirname(resolve(outputFile));
315
- devServer = createDevServer(serveDir, servePort);
329
+ if (serveMode && !outputFile) {
330
+ log.fail("--serve requires -o <output file>");
331
+ process.exit(1);
316
332
  }
317
333
 
318
- function buildOnce(changedFile = null) {
319
- try {
320
- const startMs = performance.now();
321
- const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
322
- const js = maybeMinify(result.js, {
323
- minify: minifyOutput,
324
- mangle: mangleOutput,
325
- sourceMap,
326
- });
327
- const elapsedMs = (performance.now() - startMs).toFixed(0);
328
- const changeNote = changedFile ? ` — ${changedFile} changed` : "";
329
- const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
330
- if (outputFile) {
331
- writeFileSync(outputFile, `${js}\n`);
332
- console.error(
333
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} — wrote ${colors.cyan(outputFile)} ${timing}`,
334
- );
335
- } else {
336
- // Clear screen then print
337
- process.stdout.write("\x1Bc");
338
- console.log(js);
339
- console.error(
340
- `${stamp()} ${colors.bold("gofront:")} ${colors.green("OK")} ${timing}`,
341
- );
342
- }
334
+ const prefix = () => `${stamp()} ${colors.bold("gofront:")}`;
343
335
 
344
- if (copyAssetsFlag) {
345
- try {
346
- copyAssets(resolve("."));
347
- } catch (e) {
348
- log.warn(`asset copy failed: ${e.message}`);
336
+ try {
337
+ await handleDev(".", {
338
+ srcDir: inputPath,
339
+ outputFile,
340
+ serve: serveMode,
341
+ serveDir: outputFile ? dirname(resolve(outputFile)) : undefined,
342
+ port: servePort,
343
+ silent: false,
344
+ sourceMap,
345
+ minify: minifyOutput,
346
+ mangle: mangleOutput,
347
+ copyAssets: copyAssetsFlag,
348
+ forceTarget,
349
+ onBuild: ({ js, written, elapsedMs, changedFile }) => {
350
+ const changeNote = changedFile ? ` — ${changedFile} changed` : "";
351
+ const timing = colors.dim(`(${elapsedMs}ms${changeNote})`);
352
+ if (outputFile) {
353
+ console.error(
354
+ `${prefix()} ${colors.green("OK")} — wrote ${formatWrittenDesc(written, outputFile)} ${timing}`,
355
+ );
356
+ } else {
357
+ // Clear screen then print
358
+ process.stdout.write("\x1Bc");
359
+ console.log(js);
360
+ console.error(`${prefix()} ${colors.green("OK")} ${timing}`);
349
361
  }
350
- }
351
-
352
- devServer?.notify();
353
- } catch (e) {
354
- console.error(
355
- `${stamp()} ${colors.bold("gofront:")} ${colors.bold(colors.red("ERROR"))}`,
356
- );
357
- for (const line of formatDiagnostic(e.message).split("\n"))
358
- console.error(` ${line}`);
359
- devServer?.notifyError?.(e);
360
- }
361
- }
362
-
363
- // Initial build
364
- buildOnce();
365
-
366
- // Determine what to watch
367
- const watchTarget = isDir ? inputPath : dirname(inputPath);
368
-
369
- function handleCssWatch(filename) {
370
- if (copyAssetsFlag) {
371
- try {
372
- copyAssets(resolve("."));
373
- } catch (e) {
374
- log.warn(`asset copy failed: ${e.message}`);
375
- }
376
- }
377
- devServer?.notifyCss?.(filename);
362
+ if (copyAssetsFlag) copyProjectAssets(false);
363
+ },
364
+ onError: (e) => {
365
+ console.error(`${prefix()} ${colors.bold(colors.red("ERROR"))}`);
366
+ for (const line of formatDiagnostic(e.message).split("\n"))
367
+ console.error(` ${line}`);
368
+ },
369
+ });
370
+ } catch (e) {
371
+ log.fail(e.message);
372
+ process.exit(1);
378
373
  }
379
374
 
380
- let debounce = null;
381
- let cssDebounce = null;
382
- watch(watchTarget, { recursive: true }, (_event, filename) => {
383
- if (!filename) return;
384
- if (filename.endsWith(".css")) {
385
- clearTimeout(cssDebounce);
386
- cssDebounce = setTimeout(() => handleCssWatch(filename), 50);
387
- return;
388
- }
389
- if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
390
- clearTimeout(debounce);
391
- debounce = setTimeout(() => buildOnce(filename), 80);
392
- });
393
-
394
- console.error(
395
- `${stamp()} ${colors.bold("gofront:")} watching ${colors.cyan(inputArg)} ...`,
396
- );
375
+ console.error(`${prefix()} watching ${colors.cyan(inputArg)} ...`);
package/src/lexer.js CHANGED
@@ -111,6 +111,8 @@ export class Lexer {
111
111
  this.col = 1;
112
112
  this.tokens = [];
113
113
  this.filename = filename;
114
+ this.target = null;
115
+ this.targetDirective = null;
114
116
  }
115
117
 
116
118
  // ── Primitives ──────────────────────────────────────────────
@@ -150,8 +152,22 @@ export class Lexer {
150
152
  if (ch === " " || ch === "\t" || ch === "\r") {
151
153
  this.advance();
152
154
  } else if (ch === "/" && this.peek(1) === "/") {
155
+ const start = this.pos;
156
+ const line = this.line,
157
+ col = this.col;
153
158
  while (this.pos < this.src.length && this.peek() !== "\n")
154
159
  this.advance();
160
+ const comment = this.src.slice(start, this.pos);
161
+ const match = comment.match(/^\/\/gofront:target\s+(\S+)/);
162
+ if (match) {
163
+ this.target = match[1];
164
+ this.targetDirective = {
165
+ value: match[1],
166
+ line,
167
+ col,
168
+ beforePackage: this.tokens.length === 0,
169
+ };
170
+ }
155
171
  } else if (ch === "/" && this.peek(1) === "*") {
156
172
  const startLine = this.line,
157
173
  startCol = this.col;
@@ -472,6 +488,8 @@ export class Lexer {
472
488
 
473
489
  if (this.shouldSemi()) this.push(T.SEMICOLON, ";", this.line, this.col);
474
490
  this.push(T.EOF, "", this.line, this.col);
491
+ this.tokens.target = this.target;
492
+ this.tokens.targetDirective = this.targetDirective;
475
493
  return this.tokens;
476
494
  }
477
495
  _readPlusOp(l, c) {
@@ -0,0 +1,35 @@
1
+ // src/lower/boxing.js
2
+ // Identifies address-taken scalar variables that require heap boxing ({ value: x }).
3
+
4
+ import { forEachChild } from "./walk.js";
5
+
6
+ export function isReferenceType(t) {
7
+ if (!t) return false;
8
+ const base = t.kind === "named" ? t.underlying : t;
9
+ return (
10
+ base?.kind === "struct" ||
11
+ base?.kind === "slice" ||
12
+ base?.kind === "map" ||
13
+ base?.kind === "func" ||
14
+ base?.kind === "interface"
15
+ );
16
+ }
17
+
18
+ // Scan AST node for _addressTaken idents on scalars and populate a Set of boxed variable names.
19
+ export function scanAddressTaken(node, boxedVars = new Set()) {
20
+ if (!node || typeof node !== "object") return boxedVars;
21
+ if (Array.isArray(node)) {
22
+ for (const child of node) scanAddressTaken(child, boxedVars);
23
+ return boxedVars;
24
+ }
25
+ // &x — the operand ident will have _addressTaken set by typechecker
26
+ if (node.kind === "Ident" && node._addressTaken) {
27
+ const t = node._type;
28
+ if (t && !isReferenceType(t)) {
29
+ boxedVars.add(node.name);
30
+ }
31
+ }
32
+ // Recurse into FuncLit too — closures may take address of outer vars
33
+ forEachChild(node, (child) => scanAddressTaken(child, boxedVars));
34
+ return boxedVars;
35
+ }