gofront 1.0.0 → 1.1.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 (41) hide show
  1. package/README.md +36 -3
  2. package/package.json +7 -7
  3. package/src/asset-manager.js +125 -0
  4. package/src/cli-core.js +74 -0
  5. package/src/codegen/expressions.js +3 -0
  6. package/src/codegen/statements.js +3 -0
  7. package/src/codegen/stdlib/builder.js +3 -0
  8. package/src/codegen/stdlib/bytes.js +3 -0
  9. package/src/codegen/stdlib/errors.js +3 -0
  10. package/src/codegen/stdlib/fmt.js +3 -0
  11. package/src/codegen/stdlib/gom.js +3 -0
  12. package/src/codegen/stdlib/html.js +3 -0
  13. package/src/codegen/stdlib/index.js +3 -0
  14. package/src/codegen/stdlib/io.js +3 -0
  15. package/src/codegen/stdlib/maps.js +3 -0
  16. package/src/codegen/stdlib/math.js +3 -0
  17. package/src/codegen/stdlib/os.js +3 -0
  18. package/src/codegen/stdlib/path.js +3 -0
  19. package/src/codegen/stdlib/rand.js +3 -0
  20. package/src/codegen/stdlib/regexp.js +3 -0
  21. package/src/codegen/stdlib/slices.js +3 -0
  22. package/src/codegen/stdlib/sort.js +3 -0
  23. package/src/codegen/stdlib/strconv.js +3 -0
  24. package/src/codegen/stdlib/strings.js +3 -0
  25. package/src/codegen/stdlib/time.js +3 -0
  26. package/src/codegen/stdlib/unicode.js +3 -0
  27. package/src/codegen/stdlib/utf8.js +3 -0
  28. package/src/codegen/templ.js +3 -0
  29. package/src/compiler.js +63 -4
  30. package/src/dev-server.js +96 -33
  31. package/src/index.js +80 -123
  32. package/src/parser/declarations.js +347 -0
  33. package/src/parser/index.js +3 -346
  34. package/src/typechecker/assignability.js +3 -0
  35. package/src/typechecker/expressions.js +9 -0
  36. package/src/typechecker/index.js +2 -1
  37. package/src/typechecker/resolve.js +9 -2
  38. package/src/typechecker/statements.js +3 -0
  39. package/src/typechecker/termination.js +3 -0
  40. package/src/typechecker/types.js +1 -0
  41. package/src/vendor.js +228 -0
package/src/compiler.js CHANGED
@@ -1,9 +1,11 @@
1
- // Multi-file GoFront package compiler.
1
+ // GoFront package compiler.
2
2
  //
3
- // compileDir(dir, options) — compile all *.go in a directory as one package
4
- // compileFiles(files, options) — compile an explicit list of .go files
3
+ // compileSingleFile(path, options) — compile one .go file
4
+ // compileDir(dir, options) — compile all *.go in a directory as one package
5
+ // compileFiles(files, options) — compile an explicit list of .go files
5
6
  //
6
- // Both return:
7
+ // compileSingleFile returns: { js } (or { tokens } / { ast } for debug modes)
8
+ // compileDir / compileFiles return:
7
9
  // {
8
10
  // pkgName: string,
9
11
  // js: string, // generated JS bundle (may include dep preamble)
@@ -146,6 +148,63 @@ export function resolveImports(
146
148
 
147
149
  // ── Main entry points ─────────────────────────────────────────
148
150
 
151
+ export function compileSingleFile(inputPath, options = {}) {
152
+ const {
153
+ sourceMap = false,
154
+ outputFile = null,
155
+ dumpTokens = false,
156
+ dumpAst = false,
157
+ } = options;
158
+
159
+ let source;
160
+ try {
161
+ source = readFileSync(inputPath, "utf8");
162
+ } catch (e) {
163
+ throw new Error(`cannot read '${inputPath}': ${e.message}`);
164
+ }
165
+
166
+ const tokens = new Lexer(source, basename(inputPath)).tokenize();
167
+ if (dumpTokens) return { tokens };
168
+
169
+ const ast = new Parser(tokens, basename(inputPath), source).parse();
170
+ ast._source = source;
171
+ if (dumpAst) return { ast };
172
+
173
+ const checker = new TypeChecker();
174
+ const jsImports = new Map();
175
+ const bundledPackages = new Set();
176
+ const preambles = [];
177
+
178
+ resolveImports(
179
+ [ast],
180
+ inputPath,
181
+ checker,
182
+ jsImports,
183
+ bundledPackages,
184
+ preambles,
185
+ );
186
+
187
+ const errors = checker.check(ast);
188
+ checker.reportUnusedImports();
189
+ if (errors.length > 0) {
190
+ throw new Error(errors.map((e) => e.message).join("\n"));
191
+ }
192
+
193
+ const cg = new CodeGen(checker, jsImports, bundledPackages);
194
+ let js = cg.generate(ast);
195
+
196
+ if (sourceMap) {
197
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
198
+ const srcPath = relative(outputDir, inputPath);
199
+ const map = cg.getSourceMap(srcPath, [source]);
200
+ const b64 = Buffer.from(map).toString("base64");
201
+ js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
202
+ }
203
+
204
+ const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
205
+ return { js: output };
206
+ }
207
+
149
208
  export function compileDir(dir, options = {}) {
150
209
  const files = gwFilesIn(dir);
151
210
  if (files.length === 0) throw new Error(`No .go files found in ${dir}`);
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
@@ -13,27 +13,16 @@
13
13
  // gofront <file.go> --tokens — dump tokens of first file (debug)
14
14
  // gofront init [dir] — scaffold a new GoFront project
15
15
 
16
- import {
17
- existsSync,
18
- mkdirSync,
19
- readFileSync,
20
- statSync,
21
- watch,
22
- writeFileSync,
23
- } from "node:fs";
24
16
  import { createRequire } from "node:module";
25
17
 
26
18
  const _require = createRequire(import.meta.url);
27
19
  const { version } = _require("../package.json");
28
20
 
29
- import { basename, dirname, join, relative, resolve } from "node:path";
30
- import { CodeGen } from "./codegen/index.js";
31
- import { compileDir, resolveImports } from "./compiler.js";
21
+ import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
22
+ import { dirname, resolve } from "node:path";
23
+ import { copyAssets } from "./asset-manager.js";
24
+ import { handleInit, handlePrep, maybeMinify, runCompile } from "./cli-core.js";
32
25
  import { createDevServer, liveReloadClient } from "./dev-server.js";
33
- import { Lexer } from "./lexer.js";
34
- import { minify } from "./minifier.js";
35
- import { Parser } from "./parser/index.js";
36
- import { TypeChecker } from "./typechecker/index.js";
37
26
 
38
27
  // ── Parse CLI args ───────────────────────────────────────────
39
28
 
@@ -56,11 +45,13 @@ Usage:
56
45
  gofront <input> --watch Watch for changes and recompile
57
46
  gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
58
47
  gofront <input> -o out.js --serve --port 8080 Use a custom port
48
+ gofront <input> -o out.js --copy-assets Compile and synchronize static assets
59
49
  gofront <input> --source-map Append inline source map to output
60
50
  gofront <input> --minify Minify output
61
51
  gofront <input> --minify --mangle Minify and mangle identifiers
62
52
  gofront <file.go> --ast Dump AST (debug)
63
53
  gofront <file.go> --tokens Dump tokens (debug)
54
+ gofront prep [dir] Copy static assets and bundle vendor dependencies
64
55
  gofront init [dir] Scaffold a new GoFront project
65
56
  gofront --version Print version
66
57
  `.trim(),
@@ -68,45 +59,42 @@ Usage:
68
59
  process.exit(0);
69
60
  }
70
61
 
71
- // ── init subcommand ──────────────────────────────────────────
62
+ // ── prep / vendor subcommand ──────────────────────────────────
72
63
 
73
- if (args[0] === "init") {
64
+ if (args[0] === "prep" || args[0] === "vendor") {
74
65
  const targetArg = args[1] ?? ".";
75
66
  const targetDir = resolve(targetArg);
76
-
77
- if (targetArg !== ".") {
78
- try {
79
- mkdirSync(targetDir, { recursive: true });
80
- } catch (e) {
81
- console.error(`gofront: cannot create '${targetArg}': ${e.message}`);
82
- process.exit(1);
67
+ try {
68
+ const { assets, vendor } = await handlePrep(targetDir);
69
+ if (assets.copied > 0 || assets.skipped > 0) {
70
+ console.error(
71
+ `gofront: copied ${assets.copied} assets (${assets.skipped} skipped)`,
72
+ );
83
73
  }
84
- }
85
-
86
- const mainPath = join(targetDir, "main.go");
87
- if (existsSync(mainPath)) {
88
- console.error(`gofront: ${mainPath} already exists — nothing written`);
74
+ if (vendor.bundled?.length > 0) {
75
+ console.error(
76
+ `gofront: bundled ${vendor.bundled.length} vendor dependencies → ${vendor.dest}`,
77
+ );
78
+ }
79
+ } catch (e) {
80
+ console.error(`gofront: ${e.message}`);
89
81
  process.exit(1);
90
82
  }
83
+ process.exit(0);
84
+ }
91
85
 
92
- const pkgName =
93
- targetArg === "." ? basename(resolve(".")) : basename(targetDir);
94
- // package names must be valid identifiers
95
- const safePkg = pkgName.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[0-9]/, "_");
96
-
97
- const template = `package ${safePkg}
86
+ // ── init subcommand ──────────────────────────────────────────
98
87
 
99
- func main() {
100
- \tconsole.log("Hello from GoFront!")
101
- }
102
- `;
88
+ if (args[0] === "init") {
89
+ const targetArg = args[1] ?? ".";
90
+ const targetDir = resolve(targetArg);
91
+ let mainPath;
103
92
  try {
104
- writeFileSync(mainPath, template);
93
+ ({ mainPath } = handleInit(targetDir));
105
94
  } catch (e) {
106
- console.error(`gofront: cannot write '${mainPath}': ${e.message}`);
95
+ console.error(`gofront: ${e.message}`);
107
96
  process.exit(1);
108
97
  }
109
-
110
98
  console.error(`gofront: created ${mainPath}`);
111
99
  console.error(
112
100
  `gofront: run gofront ${targetArg === "." ? "main.go" : `${targetArg}/main.go`} to compile`,
@@ -123,6 +111,7 @@ const dumpTokens = args.includes("--tokens");
123
111
  const sourceMap = args.includes("--source-map");
124
112
  const serveMode = args.includes("--serve");
125
113
  const watchMode = args.includes("--watch") || serveMode;
114
+ const copyAssetsFlag = args.includes("--copy-assets");
126
115
  const minifyOutput = args.includes("--minify");
127
116
  const mangleOutput = args.includes("--mangle");
128
117
  const portFlag = args.indexOf("--port");
@@ -139,95 +128,32 @@ try {
139
128
  process.exit(1);
140
129
  }
141
130
 
142
- // ── Compile function (used for initial build and re-builds) ──
143
-
144
- function runCompile() {
145
- if (isDir) {
146
- const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
147
- return compileDir(inputPath, { sourceMap, outputDir });
148
- }
149
-
150
- // Single-file mode
151
- let source;
152
- try {
153
- source = readFileSync(inputPath, "utf8");
154
- } catch (e) {
155
- throw new Error(`cannot read '${inputArg}': ${e.message}`);
156
- }
157
-
158
- const tokens = new Lexer(source, basename(inputPath)).tokenize();
159
-
160
- if (dumpTokens) {
161
- for (const tok of tokens) console.log(tok.toString());
162
- process.exit(0);
163
- }
164
-
165
- const ast = new Parser(tokens, basename(inputPath), source).parse();
166
- ast._source = source;
167
-
168
- if (dumpAst) {
169
- console.log(JSON.stringify(ast, null, 2));
170
- process.exit(0);
171
- }
172
-
173
- const checker = new TypeChecker();
174
- const jsImports = new Map();
175
- const bundledPackages = new Set();
176
- const preambles = [];
177
-
178
- resolveImports(
179
- [ast],
180
- inputPath,
181
- checker,
182
- jsImports,
183
- bundledPackages,
184
- preambles,
185
- );
186
-
187
- const errors = checker.check(ast);
188
- checker.reportUnusedImports();
189
- if (errors.length > 0) {
190
- throw new Error(errors.map((e) => e.message).join("\n"));
191
- }
192
-
193
- const cg = new CodeGen(checker, jsImports, bundledPackages);
194
- let js = cg.generate(ast);
195
-
196
- if (sourceMap) {
197
- const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
198
- const srcPath = relative(outputDir, inputPath);
199
- const map = cg.getSourceMap(srcPath, [source]);
200
- const b64 = Buffer.from(map).toString("base64");
201
- js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
202
- }
203
-
204
- const output = preambles.length > 0 ? `${preambles.join("\n")}\n${js}` : js;
205
- return { js: output };
206
- }
207
-
208
- // ── Minify helper ────────────────────────────────────────────
209
-
210
- function maybeMinify(js) {
211
- if (!minifyOutput) return js;
212
- if (sourceMap) {
213
- console.error("gofront: --source-map and --minify cannot be used together");
214
- process.exit(1);
215
- }
216
- return minify(js, { mangle: mangleOutput });
217
- }
218
-
219
131
  // ── Single-shot mode ─────────────────────────────────────────
220
132
 
221
133
  if (!watchMode) {
222
134
  let result;
223
135
  const startMs = performance.now();
224
136
  try {
225
- result = runCompile();
137
+ result = runCompile(inputPath, isDir, {
138
+ sourceMap,
139
+ outputFile,
140
+ dumpTokens,
141
+ dumpAst,
142
+ });
226
143
  } catch (e) {
227
144
  console.error(`gofront: ${e.message}`);
228
145
  process.exit(1);
229
146
  }
230
147
 
148
+ if (result.tokens) {
149
+ for (const tok of result.tokens) console.log(tok.toString());
150
+ process.exit(0);
151
+ }
152
+ if (result.ast) {
153
+ console.log(JSON.stringify(result.ast, null, 2));
154
+ process.exit(0);
155
+ }
156
+
231
157
  if (checkOnly) {
232
158
  const elapsedMs = (performance.now() - startMs).toFixed(0);
233
159
  console.error(`gofront: ${inputArg} — OK (${elapsedMs}ms)`);
@@ -236,7 +162,11 @@ if (!watchMode) {
236
162
 
237
163
  let js;
238
164
  try {
239
- js = maybeMinify(result.js);
165
+ js = maybeMinify(result.js, {
166
+ minify: minifyOutput,
167
+ mangle: mangleOutput,
168
+ sourceMap,
169
+ });
240
170
  } catch (e) {
241
171
  console.error(`gofront: minify failed: ${e.message}`);
242
172
  process.exit(1);
@@ -246,6 +176,7 @@ if (!watchMode) {
246
176
 
247
177
  if (outputFile) {
248
178
  try {
179
+ mkdirSync(dirname(resolve(outputFile)), { recursive: true });
249
180
  writeFileSync(outputFile, `${js}\n`);
250
181
  console.error(`gofront: wrote ${outputFile} (${elapsedMs}ms)`);
251
182
  } catch (e) {
@@ -255,6 +186,19 @@ if (!watchMode) {
255
186
  } else {
256
187
  console.log(js);
257
188
  }
189
+
190
+ if (copyAssetsFlag) {
191
+ try {
192
+ const projectDir = resolve(".");
193
+ const { copied, skipped } = copyAssets(projectDir);
194
+ if (copied > 0 || skipped > 0) {
195
+ console.error(`gofront: copied ${copied} assets (${skipped} skipped)`);
196
+ }
197
+ } catch (e) {
198
+ console.error(`gofront: asset copy failed: ${e.message}`);
199
+ }
200
+ }
201
+
258
202
  process.exit(0);
259
203
  }
260
204
 
@@ -278,8 +222,12 @@ if (serveMode) {
278
222
  function buildOnce(changedFile = null) {
279
223
  try {
280
224
  const startMs = performance.now();
281
- const result = runCompile();
282
- let js = maybeMinify(result.js);
225
+ const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
226
+ let js = maybeMinify(result.js, {
227
+ minify: minifyOutput,
228
+ mangle: mangleOutput,
229
+ sourceMap,
230
+ });
283
231
  if (serveMode) {
284
232
  // Insert live reload client before the source map comment so it stays last
285
233
  const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
@@ -304,6 +252,15 @@ function buildOnce(changedFile = null) {
304
252
  `[${timestamp()}] gofront: OK (${elapsedMs}ms${changeNote})`,
305
253
  );
306
254
  }
255
+
256
+ if (copyAssetsFlag) {
257
+ try {
258
+ copyAssets(resolve("."));
259
+ } catch (e) {
260
+ console.error(`gofront: asset copy failed: ${e.message}`);
261
+ }
262
+ }
263
+
307
264
  devServer?.notify();
308
265
  } catch (e) {
309
266
  console.error(`[${timestamp()}] gofront: ERROR`);