gofront 1.2.3 → 1.3.1
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/package.json +6 -4
- package/src/cli-core.js +520 -15
- package/src/codegen/expressions.js +229 -30
- package/src/codegen/index.js +38 -20
- package/src/codegen/runtime.js +14 -2
- package/src/codegen/statements.js +95 -15
- package/src/codegen/stdlib/builder.js +2 -2
- package/src/codegen/stdlib/bytes.js +1 -1
- package/src/codegen/stdlib/fmt.js +3 -4
- package/src/codegen/stdlib/io.js +1 -2
- package/src/codegen/templ.js +1 -1
- package/src/dev-server.js +279 -83
- package/src/index.js +88 -16
- package/src/parser/expressions.js +11 -0
- package/src/pwa.js +138 -0
- package/src/typechecker/assignability.js +61 -8
- package/src/typechecker/resolve.js +37 -3
- package/src/typechecker/stdlib/core.js +16 -13
- package/src/typechecker/stdlib/extended.js +11 -2
- package/src/typechecker/stdlib/web.js +418 -0
- package/src/typechecker/stdlib.js +2 -0
- package/src/typechecker/types.js +78 -17
- package/src/vendor.js +158 -43
package/src/codegen/stdlib/io.js
CHANGED
|
@@ -14,8 +14,7 @@ export const ioMethods = {
|
|
|
14
14
|
(writerType?.kind === "pointer" ? writerType.base?.name : null);
|
|
15
15
|
const w = this.genExpr(writerArg);
|
|
16
16
|
const s = this.genExpr(expr.args[1]);
|
|
17
|
-
const
|
|
18
|
-
const base = isPtr ? `${w}.value` : w;
|
|
17
|
+
const base = `(${w}?.value ?? ${w})`;
|
|
19
18
|
if (typeName === "strings.Builder") {
|
|
20
19
|
return `((b,s) => { b._buf += s; return [s.length, null]; })(${base}, ${s})`;
|
|
21
20
|
}
|
package/src/codegen/templ.js
CHANGED
|
@@ -208,7 +208,7 @@ export const templGenMethods = {
|
|
|
208
208
|
|
|
209
209
|
_genTemplRangeFor(forStmt, templBody, mountParent) {
|
|
210
210
|
const init = forStmt.init;
|
|
211
|
-
if (
|
|
211
|
+
if (init?.rhs?.[0]?.kind !== "RangeExpr") {
|
|
212
212
|
// Plain for loop (cond-only or three-clause)
|
|
213
213
|
const condJs = forStmt.cond ? this.genExpr(forStmt.cond) : "true";
|
|
214
214
|
this.line(`while (${condJs}) {`);
|
package/src/dev-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Minimal dev server for gofront --serve watch mode.
|
|
2
|
-
// Serves static files and pushes
|
|
2
|
+
// Serves static files and pushes reload / error / css-update events via SSE.
|
|
3
3
|
|
|
4
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createServer } from "node:http";
|
|
@@ -19,8 +19,120 @@ export const MIME = {
|
|
|
19
19
|
".woff": "font/woff",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// Injected at the bottom of
|
|
23
|
-
export const liveReloadClient = `(function()
|
|
22
|
+
// Injected at the bottom of served HTML before </body> in serve mode.
|
|
23
|
+
export const liveReloadClient = `(function() {
|
|
24
|
+
var retryDelay = 1000;
|
|
25
|
+
var maxDelay = 16000;
|
|
26
|
+
var es = null;
|
|
27
|
+
var overlayId = 'gofront-error-overlay';
|
|
28
|
+
|
|
29
|
+
function dismissErrorOverlay() {
|
|
30
|
+
var overlay = document.getElementById(overlayId);
|
|
31
|
+
if (overlay) overlay.remove();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function showErrorOverlay(data) {
|
|
35
|
+
var msg = typeof data === 'string' ? data : (data && data.message ? data.message : JSON.stringify(data, null, 2));
|
|
36
|
+
var overlay = document.getElementById(overlayId);
|
|
37
|
+
if (!overlay) {
|
|
38
|
+
overlay = document.createElement('div');
|
|
39
|
+
overlay.id = overlayId;
|
|
40
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;background:rgba(15,23,42,0.85);backdrop-filter:blur(4px);z-index:999999;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;';
|
|
41
|
+
(document.body || document.documentElement).appendChild(overlay);
|
|
42
|
+
}
|
|
43
|
+
overlay.innerHTML = '<div style="background:#18181b;color:#f43f5e;border:1px solid #e11d48;border-radius:8px;max-width:850px;width:100%;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 25px 50px -12px rgba(0,0,0,0.7);overflow:hidden;box-sizing:border-box;">'
|
|
44
|
+
+ '<div style="display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-bottom:1px solid #27272a;background:#27272a;">'
|
|
45
|
+
+ '<div style="font-weight:600;font-size:14px;color:#fda4af;display:flex;align-items:center;gap:8px;">'
|
|
46
|
+
+ '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#f43f5e;"></span>'
|
|
47
|
+
+ 'GoFront Compilation Error</div>'
|
|
48
|
+
+ '<button id="gofront-close-overlay" style="background:transparent;border:none;color:#a1a1aa;cursor:pointer;font-size:18px;line-height:1;padding:4px 8px;border-radius:4px;">✕</button>'
|
|
49
|
+
+ '</div>'
|
|
50
|
+
+ '<div style="padding:20px;overflow:auto;">'
|
|
51
|
+
+ '<pre style="margin:0;white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.6;color:#fecdd3;background:#09090b;padding:16px;border-radius:6px;border:1px solid #27272a;font-family:inherit;"></pre>'
|
|
52
|
+
+ '</div>'
|
|
53
|
+
+ '</div>';
|
|
54
|
+
var closeBtn = overlay.querySelector('#gofront-close-overlay');
|
|
55
|
+
if (closeBtn) closeBtn.onclick = dismissErrorOverlay;
|
|
56
|
+
var pre = overlay.querySelector('pre');
|
|
57
|
+
if (pre) pre.textContent = msg;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateCss(file) {
|
|
61
|
+
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
62
|
+
var fileName = file ? file.split(/[\\\\/]/).pop() : null;
|
|
63
|
+
var anyMatched = false;
|
|
64
|
+
for (var i = 0; i < links.length; i++) {
|
|
65
|
+
var link = links[i];
|
|
66
|
+
var rawHref = link.getAttribute('href');
|
|
67
|
+
if (!rawHref) continue;
|
|
68
|
+
if (fileName) {
|
|
69
|
+
var cleanHref = rawHref.split('?')[0].split('#')[0];
|
|
70
|
+
var base = cleanHref.split(/[\\\\/]/).pop();
|
|
71
|
+
if (base !== fileName) continue;
|
|
72
|
+
}
|
|
73
|
+
anyMatched = true;
|
|
74
|
+
var url = new URL(link.href, window.location.href);
|
|
75
|
+
url.searchParams.set('t', Date.now().toString());
|
|
76
|
+
link.href = url.toString();
|
|
77
|
+
}
|
|
78
|
+
if (!anyMatched && !fileName) {
|
|
79
|
+
for (var j = 0; j < links.length; j++) {
|
|
80
|
+
var l = links[j];
|
|
81
|
+
var u = new URL(l.href, window.location.href);
|
|
82
|
+
u.searchParams.set('t', Date.now().toString());
|
|
83
|
+
l.href = u.toString();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function handleBuildError(e) {
|
|
89
|
+
try {
|
|
90
|
+
showErrorOverlay(JSON.parse(e.data));
|
|
91
|
+
} catch (_) {
|
|
92
|
+
showErrorOverlay(e.data);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function connect() {
|
|
97
|
+
if (es) {
|
|
98
|
+
try { es.close(); } catch (_) {}
|
|
99
|
+
}
|
|
100
|
+
es = new EventSource('/_gofront/events');
|
|
101
|
+
es.onopen = function() {
|
|
102
|
+
retryDelay = 1000;
|
|
103
|
+
};
|
|
104
|
+
es.onerror = function() {
|
|
105
|
+
es.close();
|
|
106
|
+
setTimeout(function() {
|
|
107
|
+
retryDelay = Math.min(retryDelay * 2, maxDelay);
|
|
108
|
+
connect();
|
|
109
|
+
}, retryDelay);
|
|
110
|
+
};
|
|
111
|
+
es.addEventListener('reload', function() {
|
|
112
|
+
dismissErrorOverlay();
|
|
113
|
+
location.reload();
|
|
114
|
+
});
|
|
115
|
+
es.addEventListener('build-error', handleBuildError);
|
|
116
|
+
es.addEventListener('css-update', function(e) {
|
|
117
|
+
try {
|
|
118
|
+
var data = JSON.parse(e.data);
|
|
119
|
+
updateCss(data.file);
|
|
120
|
+
} catch (_) {
|
|
121
|
+
updateCss();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
connect();
|
|
127
|
+
})();`;
|
|
128
|
+
|
|
129
|
+
export function injectLiveReload(html) {
|
|
130
|
+
const script = `<script id="gofront-live-reload">${liveReloadClient}</script>`;
|
|
131
|
+
if (/<\/body>/i.test(html)) {
|
|
132
|
+
return html.replace(/<\/body>/i, () => `${script}</body>`);
|
|
133
|
+
}
|
|
134
|
+
return `${html}${script}`;
|
|
135
|
+
}
|
|
24
136
|
|
|
25
137
|
// True when `target` is `root` itself or a path inside it (no `..` escape).
|
|
26
138
|
function isInsideDir(root, target) {
|
|
@@ -28,109 +140,184 @@ function isInsideDir(root, target) {
|
|
|
28
140
|
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
29
141
|
}
|
|
30
142
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
143
|
+
function handleSseRequest(req, res, clients) {
|
|
144
|
+
res.writeHead(200, {
|
|
145
|
+
"Content-Type": "text/event-stream",
|
|
146
|
+
"Cache-Control": "no-cache",
|
|
147
|
+
Connection: "keep-alive",
|
|
148
|
+
});
|
|
149
|
+
res.write(": connected\n\n");
|
|
150
|
+
clients.add(res);
|
|
151
|
+
req.on?.("close", () => clients.delete(res));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveStaticPath(serveDir, urlPath) {
|
|
155
|
+
const cleanPath =
|
|
156
|
+
urlPath === "/" || urlPath === ""
|
|
157
|
+
? "/index.html"
|
|
158
|
+
: urlPath.startsWith("/")
|
|
159
|
+
? urlPath
|
|
160
|
+
: `/${urlPath}`;
|
|
161
|
+
const filePath = resolve(serveDir, `.${cleanPath}`);
|
|
162
|
+
if (!isInsideDir(resolve(serveDir), filePath)) return { forbidden: true };
|
|
163
|
+
|
|
164
|
+
if (existsSync(filePath)) {
|
|
165
|
+
const stat = statSync(filePath);
|
|
166
|
+
if (stat.isDirectory()) {
|
|
167
|
+
const dirIndex = join(filePath, "index.html");
|
|
168
|
+
if (existsSync(dirIndex)) return { filePath: dirIndex };
|
|
169
|
+
const rootIndex = join(serveDir, "index.html");
|
|
170
|
+
if (existsSync(rootIndex)) return { filePath: rootIndex };
|
|
171
|
+
return { notFound: true };
|
|
172
|
+
}
|
|
173
|
+
return { filePath };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// SPA fallback: clean paths with no file extension fall back to index.html
|
|
177
|
+
if (!extname(cleanPath)) {
|
|
178
|
+
const indexPath = join(serveDir, "index.html");
|
|
179
|
+
if (existsSync(indexPath)) return { filePath: indexPath };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { notFound: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sendStaticFile(res, filePath, shouldInject) {
|
|
186
|
+
try {
|
|
187
|
+
const ext = extname(filePath);
|
|
188
|
+
if (ext === ".html") {
|
|
189
|
+
const data = readFileSync(filePath, "utf8");
|
|
190
|
+
const body = shouldInject ? injectLiveReload(data) : data;
|
|
191
|
+
res.writeHead(200, {
|
|
192
|
+
"Content-Type": MIME[".html"],
|
|
193
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
194
|
+
});
|
|
195
|
+
res.end(body);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const data = readFileSync(filePath);
|
|
34
200
|
res.writeHead(200, {
|
|
35
|
-
"Content-Type": "
|
|
36
|
-
"Cache-Control": "no-cache",
|
|
37
|
-
Connection: "keep-alive",
|
|
201
|
+
"Content-Type": MIME[ext] ?? "application/octet-stream",
|
|
202
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
38
203
|
});
|
|
39
|
-
res.
|
|
40
|
-
|
|
41
|
-
|
|
204
|
+
res.end(data);
|
|
205
|
+
} catch {
|
|
206
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
207
|
+
res.end("Server error");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function handleDevRequest(
|
|
212
|
+
req,
|
|
213
|
+
res,
|
|
214
|
+
serveDir,
|
|
215
|
+
clients = new Set(),
|
|
216
|
+
options = {},
|
|
217
|
+
) {
|
|
218
|
+
if (req.url === "/_gofront/events") {
|
|
219
|
+
handleSseRequest(req, res, clients);
|
|
42
220
|
return;
|
|
43
221
|
}
|
|
44
222
|
|
|
45
223
|
let urlPath = req.url.split("?")[0].split("#")[0];
|
|
46
224
|
try {
|
|
47
225
|
urlPath = decodeURIComponent(urlPath);
|
|
48
|
-
} catch {
|
|
49
|
-
// Ignore malformed URL encoding and use raw path
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (urlPath === "/" || urlPath === "") urlPath = "/index.html";
|
|
226
|
+
} catch {}
|
|
53
227
|
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
228
|
+
const resolved = resolveStaticPath(serveDir, urlPath);
|
|
229
|
+
if (resolved.forbidden) {
|
|
56
230
|
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
57
231
|
res.end("Forbidden");
|
|
58
232
|
return;
|
|
59
233
|
}
|
|
60
|
-
|
|
61
|
-
if (!existsSync(filePath)) {
|
|
62
|
-
// SPA fallback: clean paths with no file extension fall back to index.html
|
|
63
|
-
const ext = extname(urlPath);
|
|
64
|
-
if (!ext) {
|
|
65
|
-
const indexPath = join(serveDir, "index.html");
|
|
66
|
-
if (existsSync(indexPath)) {
|
|
67
|
-
try {
|
|
68
|
-
const data = readFileSync(indexPath);
|
|
69
|
-
res.writeHead(200, { "Content-Type": MIME[".html"] });
|
|
70
|
-
res.end(data);
|
|
71
|
-
return;
|
|
72
|
-
} catch {
|
|
73
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
74
|
-
res.end("Server error");
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
234
|
+
if (resolved.notFound) {
|
|
79
235
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
80
236
|
res.end("Not found");
|
|
81
237
|
return;
|
|
82
238
|
}
|
|
83
239
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
240
|
+
sendStaticFile(res, resolved.filePath, options.injectReload ?? true);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function broadcastToClients(clients, event, payload) {
|
|
244
|
+
const body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
245
|
+
for (const client of clients) {
|
|
246
|
+
try {
|
|
247
|
+
client.write(`event: ${event}\ndata: ${body}\n\n`);
|
|
248
|
+
} catch {
|
|
249
|
+
clients.delete(client);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sendPings(clients) {
|
|
255
|
+
for (const client of clients) {
|
|
256
|
+
try {
|
|
257
|
+
client.write(": ping\n\n");
|
|
258
|
+
} catch {
|
|
259
|
+
clients.delete(client);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createNotify(clients) {
|
|
265
|
+
return function notify(arg) {
|
|
266
|
+
if (arg?.type) {
|
|
267
|
+
broadcastToClients(clients, arg.type, arg);
|
|
103
268
|
return;
|
|
104
269
|
}
|
|
270
|
+
broadcastToClients(clients, "reload", {});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
105
273
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
274
|
+
function handleServerError(err, port) {
|
|
275
|
+
if (err.code === "EADDRINUSE") {
|
|
276
|
+
console.error(`gofront: port ${port} already in use — try --port <number>`);
|
|
277
|
+
} else {
|
|
278
|
+
console.error(`gofront: dev server error: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildErrorPayload(err) {
|
|
284
|
+
let message = "";
|
|
285
|
+
let loc = null;
|
|
286
|
+
if (typeof err === "string") {
|
|
287
|
+
message = err;
|
|
288
|
+
} else if (err) {
|
|
289
|
+
message = err.message || String(err);
|
|
290
|
+
if (err.loc) {
|
|
291
|
+
loc = err.loc;
|
|
292
|
+
} else if (err.line !== undefined || err.file !== undefined) {
|
|
293
|
+
loc = { line: err.line, col: err.column ?? err.col, file: err.file };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const payload = { type: "build-error", message };
|
|
297
|
+
if (loc) payload.loc = loc;
|
|
298
|
+
return payload;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function closeServer(server, clients, timer) {
|
|
302
|
+
if (timer) clearInterval(timer);
|
|
303
|
+
for (const client of clients) {
|
|
304
|
+
try {
|
|
305
|
+
client.end();
|
|
306
|
+
} catch {}
|
|
113
307
|
}
|
|
308
|
+
clients.clear();
|
|
309
|
+
return new Promise((res) => server.close(res));
|
|
114
310
|
}
|
|
115
311
|
|
|
116
|
-
export function createDevServer(serveDir, port = 3000) {
|
|
312
|
+
export function createDevServer(serveDir, port = 3000, options = {}) {
|
|
117
313
|
const clients = new Set();
|
|
314
|
+
const heartbeatMs = options.heartbeatInterval ?? 15000;
|
|
118
315
|
|
|
119
316
|
const server = createServer((req, res) => {
|
|
120
|
-
handleDevRequest(req, res, serveDir, clients);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
server.on("error", (err) => {
|
|
124
|
-
if (err.code === "EADDRINUSE") {
|
|
125
|
-
console.error(
|
|
126
|
-
`gofront: port ${port} already in use — try --port <number>`,
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
console.error(`gofront: dev server error: ${err.message}`);
|
|
130
|
-
}
|
|
131
|
-
process.exit(1);
|
|
317
|
+
handleDevRequest(req, res, serveDir, clients, options);
|
|
132
318
|
});
|
|
133
319
|
|
|
320
|
+
server.on("error", (err) => handleServerError(err, port));
|
|
134
321
|
server.listen(port, () => {
|
|
135
322
|
const actualPort = server.address()?.port ?? port;
|
|
136
323
|
if (port !== 0) {
|
|
@@ -138,15 +325,24 @@ export function createDevServer(serveDir, port = 3000) {
|
|
|
138
325
|
}
|
|
139
326
|
});
|
|
140
327
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
328
|
+
const heartbeatTimer =
|
|
329
|
+
heartbeatMs > 0 ? setInterval(() => sendPings(clients), heartbeatMs) : null;
|
|
330
|
+
heartbeatTimer?.unref?.();
|
|
331
|
+
|
|
332
|
+
const notify = createNotify(clients);
|
|
146
333
|
|
|
147
334
|
return {
|
|
335
|
+
broadcast: (event, data) => broadcastToClients(clients, event, data),
|
|
148
336
|
notify,
|
|
337
|
+
notifyCss: (file) =>
|
|
338
|
+
broadcastToClients(clients, "css-update", { type: "css-update", file }),
|
|
339
|
+
notifyError: (err) =>
|
|
340
|
+
broadcastToClients(clients, "build-error", buildErrorPayload(err)),
|
|
341
|
+
notifyBuildError: (err) =>
|
|
342
|
+
broadcastToClients(clients, "build-error", buildErrorPayload(err)),
|
|
343
|
+
ping: () => sendPings(clients),
|
|
344
|
+
clients,
|
|
149
345
|
server,
|
|
150
|
-
close: () =>
|
|
346
|
+
close: () => closeServer(server, clients, heartbeatTimer),
|
|
151
347
|
};
|
|
152
348
|
}
|
package/src/index.js
CHANGED
|
@@ -22,16 +22,23 @@ import { mkdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
|
22
22
|
import { dirname, resolve } from "node:path";
|
|
23
23
|
import { copyAssets } from "./asset-manager.js";
|
|
24
24
|
import {
|
|
25
|
+
formatBuildSummary,
|
|
25
26
|
formatPrepSummary,
|
|
27
|
+
handleBuild,
|
|
28
|
+
handleCheck,
|
|
29
|
+
handleDev,
|
|
26
30
|
handleInit,
|
|
27
31
|
handlePrep,
|
|
28
32
|
handleTest,
|
|
29
33
|
maybeMinify,
|
|
34
|
+
parseBuildArgs,
|
|
35
|
+
parseCheckArgs,
|
|
36
|
+
parseDevArgs,
|
|
30
37
|
parsePrepArgs,
|
|
31
38
|
parseTestArgs,
|
|
32
39
|
runCompile,
|
|
33
40
|
} from "./cli-core.js";
|
|
34
|
-
import { createDevServer
|
|
41
|
+
import { createDevServer } from "./dev-server.js";
|
|
35
42
|
|
|
36
43
|
// ── Parse CLI args ───────────────────────────────────────────
|
|
37
44
|
|
|
@@ -47,8 +54,14 @@ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
|
47
54
|
GoFront — a Go-inspired language that compiles to JavaScript
|
|
48
55
|
|
|
49
56
|
Usage:
|
|
57
|
+
gofront dev [dir] [options] Start dev server with live reload (default port 3000)
|
|
58
|
+
gofront build [dir] [options] Build production bundle (-o <dir>, --pwa, --minify)
|
|
59
|
+
gofront check [dir] Type-check only
|
|
60
|
+
gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
61
|
+
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
62
|
+
gofront init [dir] Scaffold a new GoFront project
|
|
50
63
|
gofront <file.go> Compile single file and print to stdout
|
|
51
|
-
gofront <dir> (or gofront .)
|
|
64
|
+
gofront <dir> (or gofront .) Compile all *.go in directory as one bundle
|
|
52
65
|
gofront <input> -o out.js Compile and write to file
|
|
53
66
|
gofront <input> --check Type-check only
|
|
54
67
|
gofront <input> --watch Watch for changes and recompile
|
|
@@ -60,9 +73,6 @@ Usage:
|
|
|
60
73
|
gofront <input> --minify --mangle Minify and mangle identifiers
|
|
61
74
|
gofront <file.go> --ast Dump AST (debug)
|
|
62
75
|
gofront <file.go> --tokens Dump tokens (debug)
|
|
63
|
-
gofront test [dir] [--dom] Run unit tests (-v verbose, -run <regex>)
|
|
64
|
-
gofront prep [dir] [--minify] Copy static assets and bundle vendor dependencies
|
|
65
|
-
gofront init [dir] Scaffold a new GoFront project
|
|
66
76
|
gofront --version Print version
|
|
67
77
|
`.trim(),
|
|
68
78
|
);
|
|
@@ -116,6 +126,59 @@ if (args[0] === "test") {
|
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
// ── check subcommand ──────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
if (args[0] === "check") {
|
|
132
|
+
const { targetDir } = parseCheckArgs(args.slice(1));
|
|
133
|
+
try {
|
|
134
|
+
const { elapsedMs } = handleCheck(targetDir);
|
|
135
|
+
console.error(`gofront: ${targetDir} — OK (${elapsedMs}ms)`);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(`gofront: ${e.message}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── build subcommand ──────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
if (args[0] === "build") {
|
|
146
|
+
const buildOptions = parseBuildArgs(args.slice(1));
|
|
147
|
+
try {
|
|
148
|
+
const result = await handleBuild(buildOptions.targetDir, buildOptions);
|
|
149
|
+
for (const line of formatBuildSummary(result)) {
|
|
150
|
+
console.error(`gofront: ${line}`);
|
|
151
|
+
}
|
|
152
|
+
process.exit(0);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`gofront: ${e.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── dev subcommand ────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
if (args[0] === "dev") {
|
|
162
|
+
const devOptions = parseDevArgs(args.slice(1));
|
|
163
|
+
try {
|
|
164
|
+
const dev = await handleDev(devOptions.targetDir, devOptions);
|
|
165
|
+
const port = dev.port ?? devOptions.port ?? 3000;
|
|
166
|
+
console.error(`gofront: dev server running → http://localhost:${port}`);
|
|
167
|
+
console.error(`gofront: watching ${dev.srcDir} for changes...`);
|
|
168
|
+
|
|
169
|
+
const shutdown = async () => {
|
|
170
|
+
await dev.close();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
};
|
|
173
|
+
process.on("SIGINT", shutdown);
|
|
174
|
+
process.on("SIGTERM", shutdown);
|
|
175
|
+
await new Promise(() => {});
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error(`gofront: ${e.message}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
119
182
|
const inputArg = args[0];
|
|
120
183
|
const outputFlag = args.indexOf("-o");
|
|
121
184
|
const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
|
|
@@ -237,20 +300,11 @@ function buildOnce(changedFile = null) {
|
|
|
237
300
|
try {
|
|
238
301
|
const startMs = performance.now();
|
|
239
302
|
const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
|
|
240
|
-
|
|
303
|
+
const js = maybeMinify(result.js, {
|
|
241
304
|
minify: minifyOutput,
|
|
242
305
|
mangle: mangleOutput,
|
|
243
306
|
sourceMap,
|
|
244
307
|
});
|
|
245
|
-
if (serveMode) {
|
|
246
|
-
// Insert live reload client before the source map comment so it stays last
|
|
247
|
-
const smIdx = js.lastIndexOf("\n//# sourceMappingURL=");
|
|
248
|
-
if (smIdx !== -1) {
|
|
249
|
-
js = `${js.slice(0, smIdx)}\n${liveReloadClient}${js.slice(smIdx)}`;
|
|
250
|
-
} else {
|
|
251
|
-
js += `\n${liveReloadClient}`;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
308
|
const elapsedMs = (performance.now() - startMs).toFixed(0);
|
|
255
309
|
const changeNote = changedFile ? ` — ${changedFile} changed` : "";
|
|
256
310
|
if (outputFile) {
|
|
@@ -279,6 +333,7 @@ function buildOnce(changedFile = null) {
|
|
|
279
333
|
} catch (e) {
|
|
280
334
|
console.error(`[${timestamp()}] gofront: ERROR`);
|
|
281
335
|
for (const line of e.message.split("\n")) console.error(` ${line}`);
|
|
336
|
+
devServer?.notifyError?.(e);
|
|
282
337
|
}
|
|
283
338
|
}
|
|
284
339
|
|
|
@@ -288,10 +343,27 @@ buildOnce();
|
|
|
288
343
|
// Determine what to watch
|
|
289
344
|
const watchTarget = isDir ? inputPath : dirname(inputPath);
|
|
290
345
|
|
|
346
|
+
function handleCssWatch(filename) {
|
|
347
|
+
if (copyAssetsFlag) {
|
|
348
|
+
try {
|
|
349
|
+
copyAssets(resolve("."));
|
|
350
|
+
} catch (e) {
|
|
351
|
+
console.error(`gofront: asset copy failed: ${e.message}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
devServer?.notifyCss?.(filename);
|
|
355
|
+
}
|
|
356
|
+
|
|
291
357
|
let debounce = null;
|
|
358
|
+
let cssDebounce = null;
|
|
292
359
|
watch(watchTarget, { recursive: true }, (_event, filename) => {
|
|
293
|
-
if (
|
|
360
|
+
if (!filename) return;
|
|
361
|
+
if (filename.endsWith(".css")) {
|
|
362
|
+
clearTimeout(cssDebounce);
|
|
363
|
+
cssDebounce = setTimeout(() => handleCssWatch(filename), 50);
|
|
294
364
|
return;
|
|
365
|
+
}
|
|
366
|
+
if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
|
|
295
367
|
clearTimeout(debounce);
|
|
296
368
|
debounce = setTimeout(() => buildOnce(filename), 80);
|
|
297
369
|
});
|