gofront 0.0.5 → 0.0.7

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.
@@ -16,21 +16,29 @@ function vlqEncode(value) {
16
16
  return result;
17
17
  }
18
18
 
19
- export function buildSourceMap(sourceName, mappings) {
20
- // mappings: Array<{ genLine: number, srcLine: number }> (0-based)
19
+ export function buildSourceMap(sources, mappings) {
20
+ // sources: string[]
21
+ // mappings: Array<{ genLine: number, srcLine: number, srcFileIdx?: number }> (0-based)
21
22
  // Emits one segment per generated line, column 0 → source line (delta-encoded).
22
23
  const lines = [];
23
24
  let prevSrcLine = 0;
25
+ let prevSrcFile = 0;
24
26
  const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
25
27
  const byLine = new Map(mappings.map((e) => [e.genLine, e]));
26
28
  for (let g = 0; g <= maxGen; g++) {
27
29
  const entry = byLine.get(g);
28
30
  if (entry) {
31
+ const fileIdx = entry.srcFileIdx ?? 0;
32
+ const fileDelta = fileIdx - prevSrcFile;
29
33
  const srcDelta = entry.srcLine - prevSrcLine;
30
- // Segment: [genCol=0, srcFileIdx=0, srcLineDelta, srcCol=0]
34
+ // Segment: [genCol=0, srcFileIdxDelta, srcLineDelta, srcCol=0]
31
35
  lines.push(
32
- vlqEncode(0) + vlqEncode(0) + vlqEncode(srcDelta) + vlqEncode(0),
36
+ vlqEncode(0) +
37
+ vlqEncode(fileDelta) +
38
+ vlqEncode(srcDelta) +
39
+ vlqEncode(0),
33
40
  );
41
+ prevSrcFile = fileIdx;
34
42
  prevSrcLine = entry.srcLine;
35
43
  } else {
36
44
  lines.push(""); // no mapping for this generated line
@@ -38,7 +46,7 @@ export function buildSourceMap(sourceName, mappings) {
38
46
  }
39
47
  return JSON.stringify({
40
48
  version: 3,
41
- sources: [sourceName],
49
+ sources,
42
50
  names: [],
43
51
  mappings: lines.join(";"),
44
52
  });
@@ -7,6 +7,30 @@ export const statementGenMethods = {
7
7
  for (const stmt of block.stmts) this.genStmt(stmt);
8
8
  },
9
9
 
10
+ // Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
11
+ // isDefine=true → emit `let v = m[k]; let ok = k in m;`
12
+ // isDefine=false → emit `v = k in m ? m[k] : zero; ok = k in m;`
13
+ _genCommaOkMapIndex(stmt, rhsNode, isDefine) {
14
+ const mapExpr = this.genExpr(rhsNode.expr);
15
+ const keyExpr = this.genExpr(rhsNode.index);
16
+ const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
17
+ const decl = isDefine ? "let " : "";
18
+ if (vName !== "_") {
19
+ if (isDefine) {
20
+ this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
21
+ } else {
22
+ const zero = rhsNode._mapValueType
23
+ ? this.zeroValueForType(rhsNode._mapValueType)
24
+ : "undefined";
25
+ this.line(
26
+ `${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
27
+ );
28
+ }
29
+ }
30
+ if (okName !== "_")
31
+ this.line(`${decl}${okName} = (${keyExpr}) in ${mapExpr};`);
32
+ },
33
+
10
34
  genStmt(stmt) {
11
35
  // Record source mapping for the first line this statement produces
12
36
  const srcLine = stmt._line ?? null;
@@ -31,15 +55,7 @@ export const statementGenMethods = {
31
55
  rhsNode?.kind === "IndexExpr" &&
32
56
  rhsNode.expr?._type?.kind === "map"
33
57
  ) {
34
- const mapExpr = this.genExpr(rhsNode.expr);
35
- const keyExpr = this.genExpr(rhsNode.index);
36
- const [vName, okName] = stmt.lhs.map(
37
- (e) => e.name ?? this.genExpr(e),
38
- );
39
- if (vName !== "_")
40
- this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
41
- if (okName !== "_")
42
- this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
58
+ this._genCommaOkMapIndex(stmt, rhsNode, true);
43
59
  break;
44
60
  }
45
61
  const rhs = stmt.rhs.map((e) => this.genExpr(e));
@@ -117,22 +133,7 @@ export const statementGenMethods = {
117
133
  }
118
134
  // comma-ok map index: v, ok = m["key"]
119
135
  if (stmt._commaOkMap) {
120
- const rhsNode = stmt.rhs[0];
121
- const mapExpr = this.genExpr(rhsNode.expr);
122
- const keyExpr = this.genExpr(rhsNode.index);
123
- const [vName, okName] = stmt.lhs.map(
124
- (e) => e.name ?? this.genExpr(e),
125
- );
126
- if (vName !== "_") {
127
- const zero = rhsNode._mapValueType
128
- ? this.zeroValueForType(rhsNode._mapValueType)
129
- : "undefined";
130
- this.line(
131
- `${vName} = (${keyExpr}) in ${mapExpr} ? ${mapExpr}[${keyExpr}] : ${zero};`,
132
- );
133
- }
134
- if (okName !== "_")
135
- this.line(`${okName} = (${keyExpr}) in ${mapExpr};`);
136
+ this._genCommaOkMapIndex(stmt, stmt.rhs[0], false);
136
137
  break;
137
138
  }
138
139
  // comma-ok type assertion: v, ok = x.(T)
@@ -383,8 +384,11 @@ export const statementGenMethods = {
383
384
  const init = stmt.init;
384
385
  const range = init.rhs[0];
385
386
  const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
386
- const iteree = this.genExpr(range.expr);
387
387
  const iterType = range.expr._type;
388
+ const wrapField = this._namedWrapperField(iterType, range.expr);
389
+ const iteree = wrapField
390
+ ? `${this.genExpr(range.expr)}.${wrapField}`
391
+ : this.genExpr(range.expr);
388
392
 
389
393
  let iterExpr;
390
394
  if (
package/src/codegen.js CHANGED
@@ -35,10 +35,12 @@ export class CodeGen {
35
35
  this.out = [];
36
36
  this.indent = 0;
37
37
  this.structNames = new Set();
38
+ this.namedWrapperNames = new Set();
38
39
  this.jsImports = jsImports;
39
40
  this.bundledPackages = bundledPackages;
40
41
  this.namedReturnVars = null; // names of current function's named return vars
41
- this._srcMappings = []; // { genLine, srcLine } for source map
42
+ this._srcMappings = []; // { genLine, srcLine, srcFileIdx } for source map
43
+ this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
42
44
  this._boxedVars = new Set(); // address-taken scalar variables that need boxing
43
45
  // Runtime helper usage tracking — only emit helpers that are actually used
44
46
  this._usesLen = false;
@@ -67,8 +69,9 @@ export class CodeGen {
67
69
  if (srcLine != null) {
68
70
  this._srcMappings.push({
69
71
  genLine: this.out.length,
70
- srcLine: srcLine - 1,
71
- }); // 0-based
72
+ srcLine: srcLine - 1, // 0-based
73
+ srcFileIdx: this._currentSrcFileIdx,
74
+ });
72
75
  }
73
76
  this.out.push(" ".repeat(this.indent) + s);
74
77
  }
@@ -90,7 +93,7 @@ export class CodeGen {
90
93
  }
91
94
  }
92
95
 
93
- // Build a map: structName → [MethodDecl, ...]
96
+ // Build a map: typeName → [MethodDecl, ...]
94
97
  const methods = new Map();
95
98
  for (const d of program.decls) {
96
99
  if (d.kind === "MethodDecl") {
@@ -100,6 +103,18 @@ export class CodeGen {
100
103
  }
101
104
  }
102
105
 
106
+ // Collect named non-struct types that have methods (emitted as wrapper classes)
107
+ for (const d of program.decls) {
108
+ if (
109
+ d.kind === "TypeDecl" &&
110
+ d.type.kind !== "StructType" &&
111
+ d.type.kind !== "InterfaceType" &&
112
+ (methods.get(d.name) ?? []).length > 0
113
+ ) {
114
+ this.namedWrapperNames.add(d.name);
115
+ }
116
+ }
117
+
103
118
  // Emit ESM import statements for npm packages
104
119
  for (const [importPath, names] of this.jsImports) {
105
120
  if (names.length === 0) continue;
@@ -109,6 +124,7 @@ export class CodeGen {
109
124
 
110
125
  for (const d of program.decls) {
111
126
  if (d.kind === "TypeDecl") {
127
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
112
128
  this.genTypeDeclWithMethods(d, methods.get(d.name) ?? []);
113
129
  this.blank();
114
130
  }
@@ -116,10 +132,12 @@ export class CodeGen {
116
132
 
117
133
  for (const d of program.decls) {
118
134
  if (d.kind === "VarDecl") {
135
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
119
136
  this.genVarDecl(d);
120
137
  this.blank();
121
138
  }
122
139
  if (d.kind === "ConstDecl") {
140
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
123
141
  this.genConstDecl(d);
124
142
  this.blank();
125
143
  }
@@ -130,6 +148,7 @@ export class CodeGen {
130
148
  const initNames = [];
131
149
  for (const d of program.decls) {
132
150
  if (d.kind === "FuncDecl") {
151
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
133
152
  if (d.name === "init") {
134
153
  const renamed = initCount === 0 ? "init" : `init$${initCount}`;
135
154
  initNames.push(renamed);
@@ -152,29 +171,31 @@ export class CodeGen {
152
171
  // Prepend runtime helpers that were actually used
153
172
  const helpers = [];
154
173
  if (this._usesLen)
155
- helpers.push("function __len(a) { return a?.length ?? 0; }");
174
+ helpers.push(
175
+ "var __len = __len || function(a) { if (a && typeof a === 'object' && !Array.isArray(a)) return Object.keys(a).length; return a?.length ?? 0; };",
176
+ );
156
177
  if (this._usesAppend)
157
178
  helpers.push(
158
- "function __append(a, ...b) { return a ? [...a, ...b] : b; }",
179
+ "var __append = __append || function(a, ...b) { return a ? [...a, ...b] : b; };",
159
180
  );
160
181
  if (this._usesSliceGuard)
161
- helpers.push("function __s(a) { return a || []; }");
182
+ helpers.push("var __s = __s || function(a) { return a || []; };");
162
183
  if (this._usesEqual)
163
184
  helpers.push(
164
- 'function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;}',
185
+ 'var __equal = __equal || function __equal(a,b){if(a===b)return true;if(a===null||b===null)return false;if(Array.isArray(a)&&Array.isArray(b)){if(a.length!==b.length)return false;for(let i=0;i<a.length;i++)if(!__equal(a[i],b[i]))return false;return true;}if(typeof a==="object"&&typeof b==="object"){const ka=Object.keys(a),kb=Object.keys(b);if(ka.length!==kb.length)return false;for(const k of ka)if(!__equal(a[k],b[k]))return false;return true;}return false;};',
165
186
  );
166
187
  if (this._usesCmul)
167
188
  helpers.push(
168
- "function __cmul(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; }",
189
+ "var __cmul = __cmul || function(a, b) { return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re }; };",
169
190
  );
170
191
  if (this._usesCdiv)
171
192
  helpers.push(
172
- "function __cdiv(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; }",
193
+ "var __cdiv = __cdiv || function(a, b) { const d = b.re * b.re + b.im * b.im; return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d }; };",
173
194
  );
174
195
  if (this._usesSprintf)
175
196
  helpers.push(
176
197
  [
177
- "function __sprintf(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
198
+ "var __sprintf = __sprintf || function(f,...a){let i=0;return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g,(m)=>{",
178
199
  "if(m==='%%')return'%';const fl=m.slice(1,-1),verb=m.slice(-1),v=a[i++];",
179
200
  "const pad=(s,w,z)=>{w=parseInt(w)||0;if(!w)return s;const p=(z?'0':' ').repeat(Math.max(0,w-s.length));return fl.includes('-')?s+p:p+s;};",
180
201
  "const [,flags,width,prec]=m.match(/^%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)/)||[];",
@@ -194,16 +215,16 @@ export class CodeGen {
194
215
  "case'g':case'G':{const n=Number(v);return pad(prec!==''?n.toPrecision(parseInt(prec)):String(n),width,zero);}",
195
216
  "case'w':return pad(String(v==null?'<nil>':typeof v==='object'&&v.Error?v.Error():v),width,false);",
196
217
  "default:return m;}});",
197
- "}",
218
+ "};",
198
219
  ].join(""),
199
220
  );
200
221
  if (this._usesError)
201
222
  helpers.push(
202
- "function __error(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; }",
223
+ "var __error = __error || function(msg, cause) { return { Error() { return msg; }, toString() { return msg; }, _msg: msg, _cause: cause ?? null }; };",
203
224
  );
204
225
  if (this._usesErrorIs)
205
226
  helpers.push(
206
- 'function __errorIs(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; }',
227
+ 'var __errorIs = __errorIs || function(err, target) { while (err !== null && err !== undefined) { if (err === target) return true; if (typeof err === "object" && typeof target === "object" && err._msg !== undefined && target._msg !== undefined && err._msg === target._msg) return true; err = err?._cause ?? null; } return false; };',
207
228
  );
208
229
 
209
230
  if (helpers.length > 0) this.out.unshift(...helpers, "");
@@ -214,16 +235,22 @@ export class CodeGen {
214
235
  }
215
236
 
216
237
  // Generate a single bundle from multiple programs (same-package multi-file).
217
- // Merges all decls and generates as if it were one program.
238
+ // Annotates each decl with its source file index before merging.
218
239
  generateAll(programs) {
240
+ for (let i = 0; i < programs.length; i++) {
241
+ for (const decl of programs[i].decls) {
242
+ decl._srcFileIdx = i;
243
+ }
244
+ }
219
245
  const merged = { decls: programs.flatMap((p) => p.decls) };
220
246
  return this.generate(merged);
221
247
  }
222
248
 
223
249
  // Returns a source map JSON string for the last generate() call.
224
- // sourceName: the .go filename to reference in the map.
225
- getSourceMap(sourceName) {
226
- return buildSourceMap(sourceName, this._srcMappings);
250
+ // sources: string[] of source filenames (relative to the output file).
251
+ getSourceMap(sources) {
252
+ const srcArray = Array.isArray(sources) ? sources : [sources];
253
+ return buildSourceMap(srcArray, this._srcMappings);
227
254
  }
228
255
 
229
256
  // ── Type declarations ────────────────────────────────────────
@@ -234,13 +261,41 @@ export class CodeGen {
234
261
  } else if (decl.type.kind === "InterfaceType") {
235
262
  // Interfaces are compile-time only — no JS output needed.
236
263
  this.line(`// interface ${decl.name} (compile-time only)`);
264
+ } else if (methodDecls.length > 0) {
265
+ // Named non-struct type with methods — emit an ES6 wrapper class.
266
+ this.genNamedTypeClass(decl.name, methodDecls);
237
267
  } else {
238
- // Type alias — no JS equivalent needed unless it's used as a constructor.
239
- // e.g. type MyInt int → just a comment
240
268
  this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
241
269
  }
242
270
  }
243
271
 
272
+ genNamedTypeClass(name, methodDecls) {
273
+ const namedType = this.checker?.types.get(name);
274
+ const underlying = namedType?.underlying;
275
+ let field, ctorDefault;
276
+ if (underlying?.kind === "func") {
277
+ field = "_fn";
278
+ ctorDefault = "null";
279
+ } else if (underlying?.kind === "map") {
280
+ field = "_map";
281
+ ctorDefault = "{}";
282
+ } else {
283
+ field = "_items";
284
+ ctorDefault = "[]";
285
+ }
286
+ this.line(`class ${name} {`);
287
+ this.indented(() => {
288
+ this.line(
289
+ `constructor(${field} = ${ctorDefault}) { this.${field} = ${field}; }`,
290
+ );
291
+ for (const m of methodDecls) {
292
+ this.blank();
293
+ this.genMethod(m, field);
294
+ }
295
+ });
296
+ this.line("}");
297
+ }
298
+
244
299
  genStruct(name, structTypeAst, methodDecls) {
245
300
  const fields = [];
246
301
  if (this.checker) {
@@ -303,23 +358,45 @@ export class CodeGen {
303
358
  this.line("}");
304
359
  }
305
360
 
306
- genMethod(decl) {
361
+ genMethod(decl, recvField = null) {
307
362
  const params = decl.params.map((p) => p.name).join(", ");
308
363
  const asyncPrefix = decl.async ? "async " : "";
309
364
  this.line(`${asyncPrefix}${decl.name}(${params}) {`);
310
365
  const prevBoxed = this._boxedVars;
311
366
  this._boxedVars = new Set();
312
367
  this._scanAddressTaken(decl.body);
368
+ const prevUnwrapped = this._unwrappedRecv;
313
369
  this.indented(() => {
314
370
  if (decl.recvName && decl.recvName !== "_") {
315
- this.line(`const ${decl.recvName} = this;`);
371
+ if (recvField) {
372
+ this.line(`const ${decl.recvName} = this.${recvField};`);
373
+ this._unwrappedRecv = decl.recvName;
374
+ } else {
375
+ this.line(`const ${decl.recvName} = this;`);
376
+ }
316
377
  }
317
378
  this._withNamedReturns(decl, () => this._genBody(decl.body));
318
379
  });
380
+ this._unwrappedRecv = prevUnwrapped;
319
381
  this._boxedVars = prevBoxed;
320
382
  this.line("}");
321
383
  }
322
384
 
385
+ // Returns the wrapper field name ("_fn", "_items", "_map") if `type` is a named
386
+ // non-struct type emitted as a wrapper class, or null otherwise.
387
+ // Pass `expr` so we can skip unwrapping when the expression is the method receiver
388
+ // (which was already unwrapped to `this.<field>` at the top of the method body).
389
+ _namedWrapperField(type, expr = null) {
390
+ if (type?.kind !== "named") return null;
391
+ if (!this.namedWrapperNames.has(type.name)) return null;
392
+ if (expr?.kind === "Ident" && expr.name === this._unwrappedRecv)
393
+ return null;
394
+ const u = type.underlying;
395
+ if (u?.kind === "func") return "_fn";
396
+ if (u?.kind === "map") return "_map";
397
+ return "_items";
398
+ }
399
+
323
400
  // ── Function declarations ────────────────────────────────────
324
401
 
325
402
  genFuncDecl(decl, nameOverride) {
package/src/compiler.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // de-qualifies it because the dependency is inlined.
17
17
 
18
18
  import { readdirSync, readFileSync } from "node:fs";
19
- import { basename, dirname, join, resolve } from "node:path";
19
+ import { basename, dirname, join, relative, resolve } from "node:path";
20
20
  import { CodeGen } from "./codegen.js";
21
21
  import { parseDts } from "./dts-parser.js";
22
22
  import { Lexer } from "./lexer.js";
@@ -165,8 +165,15 @@ export function compileFiles(files, options = {}) {
165
165
  const codegen = new CodeGen(checker, jsImports, bundledPackages);
166
166
  const mainJs = codegen.generateAll(programs);
167
167
 
168
- const js =
169
- preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
168
+ let js = preambles.length > 0 ? `${preambles.join("\n")}\n${mainJs}` : mainJs;
169
+
170
+ if (options.sourceMap) {
171
+ const outputDir = options.outputDir ?? fromDir;
172
+ const sourceFiles = files.map((f) => relative(outputDir, f));
173
+ const map = codegen.getSourceMap(sourceFiles);
174
+ const b64 = Buffer.from(map).toString("base64");
175
+ js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
176
+ }
170
177
 
171
178
  return {
172
179
  pkgName,
@@ -0,0 +1,83 @@
1
+ // Minimal dev server for gofront --serve watch mode.
2
+ // Serves static files and pushes a reload event via SSE after each build.
3
+
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { createServer } from "node:http";
6
+ import { extname, join } from "node:path";
7
+
8
+ const MIME = {
9
+ ".html": "text/html; charset=utf-8",
10
+ ".js": "application/javascript; charset=utf-8",
11
+ ".css": "text/css; charset=utf-8",
12
+ ".json": "application/json; charset=utf-8",
13
+ ".svg": "image/svg+xml",
14
+ ".png": "image/png",
15
+ ".jpg": "image/jpeg",
16
+ ".ico": "image/x-icon",
17
+ ".woff2": "font/woff2",
18
+ ".woff": "font/woff",
19
+ };
20
+
21
+ // Injected at the bottom of compiled JS in serve mode.
22
+ export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
23
+
24
+ export function createDevServer(serveDir, port) {
25
+ const clients = new Set();
26
+
27
+ const server = createServer((req, res) => {
28
+ // SSE endpoint — browser connects here to receive reload events
29
+ if (req.url === "/_gofront/events") {
30
+ res.writeHead(200, {
31
+ "Content-Type": "text/event-stream",
32
+ "Cache-Control": "no-cache",
33
+ Connection: "keep-alive",
34
+ });
35
+ res.write(": connected\n\n");
36
+ clients.add(res);
37
+ req.on("close", () => clients.delete(res));
38
+ return;
39
+ }
40
+
41
+ let urlPath = req.url.split("?")[0];
42
+ if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
43
+
44
+ const filePath = join(serveDir, urlPath);
45
+ if (!existsSync(filePath)) {
46
+ res.writeHead(404, { "Content-Type": "text/plain" });
47
+ res.end("Not found");
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const data = readFileSync(filePath);
53
+ const mime = MIME[extname(filePath)] ?? "application/octet-stream";
54
+ res.writeHead(200, { "Content-Type": mime });
55
+ res.end(data);
56
+ } catch {
57
+ res.writeHead(500, { "Content-Type": "text/plain" });
58
+ res.end("Server error");
59
+ }
60
+ });
61
+
62
+ server.on("error", (err) => {
63
+ if (err.code === "EADDRINUSE") {
64
+ console.error(
65
+ `gofront: port ${port} already in use — try --port <number>`,
66
+ );
67
+ } else {
68
+ console.error(`gofront: dev server error: ${err.message}`);
69
+ }
70
+ process.exit(1);
71
+ });
72
+ server.listen(port, () => {
73
+ console.error(`gofront: dev server → http://localhost:${port}`);
74
+ });
75
+
76
+ function notify() {
77
+ for (const client of clients) {
78
+ client.write("event: reload\ndata: {}\n\n");
79
+ }
80
+ }
81
+
82
+ return { notify };
83
+ }
package/src/index.js CHANGED
@@ -26,9 +26,10 @@ import { createRequire } from "node:module";
26
26
  const _require = createRequire(import.meta.url);
27
27
  const { version } = _require("../package.json");
28
28
 
29
- import { basename, dirname, join, resolve } from "node:path";
29
+ import { basename, dirname, join, relative, resolve } from "node:path";
30
30
  import { CodeGen } from "./codegen.js";
31
31
  import { compileDir } from "./compiler.js";
32
+ import { createDevServer, liveReloadClient } from "./dev-server.js";
32
33
  import { parseDts } from "./dts-parser.js";
33
34
  import { Lexer } from "./lexer.js";
34
35
  import { minify } from "./minifier.js";
@@ -55,6 +56,8 @@ Usage:
55
56
  gofront <input> -o out.js Compile and write to file
56
57
  gofront <input> --check Type-check only
57
58
  gofront <input> --watch Watch for changes and recompile
59
+ gofront <input> -o out.js --serve Watch + serve with live reload (default port 3000)
60
+ gofront <input> -o out.js --serve --port 8080 Use a custom port
58
61
  gofront <input> --source-map Append inline source map to output
59
62
  gofront <input> --minify Minify output
60
63
  gofront <input> --minify --mangle Minify and mangle identifiers
@@ -120,9 +123,12 @@ const checkOnly = args.includes("--check");
120
123
  const dumpAst = args.includes("--ast");
121
124
  const dumpTokens = args.includes("--tokens");
122
125
  const sourceMap = args.includes("--source-map");
123
- const watchMode = args.includes("--watch");
126
+ const serveMode = args.includes("--serve");
127
+ const watchMode = args.includes("--watch") || serveMode;
124
128
  const minifyOutput = args.includes("--minify");
125
129
  const mangleOutput = args.includes("--mangle");
130
+ const portFlag = args.indexOf("--port");
131
+ const servePort = portFlag !== -1 ? parseInt(args[portFlag + 1], 10) : 3000;
126
132
 
127
133
  // ── Determine input mode ─────────────────────────────────────
128
134
 
@@ -139,7 +145,8 @@ try {
139
145
 
140
146
  function runCompile() {
141
147
  if (isDir) {
142
- return compileDir(inputPath);
148
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
149
+ return compileDir(inputPath, { sourceMap, outputDir });
143
150
  }
144
151
 
145
152
  // Single-file mode
@@ -223,7 +230,9 @@ function runCompile() {
223
230
  let js = cg.generate(ast);
224
231
 
225
232
  if (sourceMap) {
226
- const map = cg.getSourceMap(inputArg);
233
+ const outputDir = outputFile ? dirname(resolve(outputFile)) : resolve(".");
234
+ const srcPath = relative(outputDir, inputPath);
235
+ const map = cg.getSourceMap(srcPath);
227
236
  const b64 = Buffer.from(map).toString("base64");
228
237
  js += `\n//# sourceMappingURL=data:application/json;base64,${b64}`;
229
238
  }
@@ -291,11 +300,31 @@ function timestamp() {
291
300
  return new Date().toLocaleTimeString();
292
301
  }
293
302
 
303
+ // Start dev server before first build so the browser can connect immediately
304
+ let devServer = null;
305
+ if (serveMode) {
306
+ if (!outputFile) {
307
+ console.error("gofront: --serve requires -o <output file>");
308
+ process.exit(1);
309
+ }
310
+ const serveDir = dirname(resolve(outputFile));
311
+ devServer = createDevServer(serveDir, servePort);
312
+ }
313
+
294
314
  function buildOnce() {
295
315
  try {
296
316
  const startMs = performance.now();
297
317
  const result = runCompile();
298
- const js = maybeMinify(result.js);
318
+ let js = maybeMinify(result.js);
319
+ if (serveMode) {
320
+ // Insert live reload client before the source map comment so it stays last
321
+ const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
322
+ if (smIdx !== -1) {
323
+ js = `${js.slice(0, smIdx)}\n${liveReloadClient}${js.slice(smIdx)}`;
324
+ } else {
325
+ js += `\n${liveReloadClient}`;
326
+ }
327
+ }
299
328
  const elapsedMs = (performance.now() - startMs).toFixed(0);
300
329
  if (outputFile) {
301
330
  writeFileSync(outputFile, `${js}\n`);
@@ -308,6 +337,7 @@ function buildOnce() {
308
337
  console.log(js);
309
338
  console.error(`[${timestamp()}] gofront: OK (${elapsedMs}ms)`);
310
339
  }
340
+ devServer?.notify();
311
341
  } catch (e) {
312
342
  console.error(`[${timestamp()}] gofront: ERROR`);
313
343
  for (const line of e.message.split("\n")) console.error(` ${line}`);
package/src/parser.js CHANGED
@@ -308,16 +308,6 @@ export class Parser {
308
308
  return this.looksLikeType(next) || next.type === T.ELLIPSIS;
309
309
  }
310
310
 
311
- // Heuristic: peek2 is a param-name separator if it's a comma, rparen, or a type token
312
- isParamType(tok) {
313
- if (!tok) return false;
314
- // T.RPAREN is intentionally excluded: when peek2 is ")", the current IDENT
315
- // is the type of an unnamed param (e.g. func(int)), not a param name.
316
- return (
317
- tok.type === T.COMMA || tok.type === T.ELLIPSIS || this.looksLikeType(tok)
318
- );
319
- }
320
-
321
311
  looksLikeType(tok) {
322
312
  return (
323
313
  tok.type === T.LBRACKET ||