gofront 1.2.2 → 1.3.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.
@@ -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 isPtr = writerType?.kind === "pointer";
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
  }
@@ -208,7 +208,7 @@ export const templGenMethods = {
208
208
 
209
209
  _genTemplRangeFor(forStmt, templBody, mountParent) {
210
210
  const init = forStmt.init;
211
- if (!init?.rhs?.[0] || init.rhs[0].kind !== "RangeExpr") {
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 a reload event via SSE after each build.
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 compiled JS in serve mode.
23
- export const liveReloadClient = `(function(){var es=new EventSource('/_gofront/events');es.addEventListener('reload',function(){location.reload();});})();`;
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
- export function handleDevRequest(req, res, serveDir, clients = new Set()) {
32
- // SSE endpoint — browser connects here to receive reload events
33
- if (req.url === "/_gofront/events") {
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": "text/event-stream",
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.write(": connected\n\n");
40
- clients.add(res);
41
- req.on?.("close", () => clients.delete(res));
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 filePath = resolve(serveDir, `.${urlPath}`);
55
- if (!isInsideDir(resolve(serveDir), filePath)) {
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
- try {
85
- const stat = statSync(filePath);
86
- if (stat.isDirectory()) {
87
- const dirIndex = join(filePath, "index.html");
88
- if (existsSync(dirIndex)) {
89
- const data = readFileSync(dirIndex);
90
- res.writeHead(200, { "Content-Type": MIME[".html"] });
91
- res.end(data);
92
- return;
93
- }
94
- const rootIndex = join(serveDir, "index.html");
95
- if (existsSync(rootIndex)) {
96
- const data = readFileSync(rootIndex);
97
- res.writeHead(200, { "Content-Type": MIME[".html"] });
98
- res.end(data);
99
- return;
100
- }
101
- res.writeHead(404, { "Content-Type": "text/plain" });
102
- res.end("Not found");
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
- const data = readFileSync(filePath);
107
- const mime = MIME[extname(filePath)] ?? "application/octet-stream";
108
- res.writeHead(200, { "Content-Type": mime });
109
- res.end(data);
110
- } catch {
111
- res.writeHead(500, { "Content-Type": "text/plain" });
112
- res.end("Server error");
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
- function notify() {
142
- for (const client of clients) {
143
- client.write("event: reload\ndata: {}\n\n");
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: () => new Promise((res) => server.close(res)),
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, liveReloadClient } from "./dev-server.js";
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 .) Compile all *.go in directory as one bundle
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,58 @@ 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
+ } catch (e) {
176
+ console.error(`gofront: ${e.message}`);
177
+ process.exit(1);
178
+ }
179
+ }
180
+
119
181
  const inputArg = args[0];
120
182
  const outputFlag = args.indexOf("-o");
121
183
  const outputFile = outputFlag !== -1 ? args[outputFlag + 1] : null;
@@ -237,20 +299,11 @@ function buildOnce(changedFile = null) {
237
299
  try {
238
300
  const startMs = performance.now();
239
301
  const result = runCompile(inputPath, isDir, { sourceMap, outputFile });
240
- let js = maybeMinify(result.js, {
302
+ const js = maybeMinify(result.js, {
241
303
  minify: minifyOutput,
242
304
  mangle: mangleOutput,
243
305
  sourceMap,
244
306
  });
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
307
  const elapsedMs = (performance.now() - startMs).toFixed(0);
255
308
  const changeNote = changedFile ? ` — ${changedFile} changed` : "";
256
309
  if (outputFile) {
@@ -279,6 +332,7 @@ function buildOnce(changedFile = null) {
279
332
  } catch (e) {
280
333
  console.error(`[${timestamp()}] gofront: ERROR`);
281
334
  for (const line of e.message.split("\n")) console.error(` ${line}`);
335
+ devServer?.notifyError?.(e);
282
336
  }
283
337
  }
284
338
 
@@ -288,10 +342,27 @@ buildOnce();
288
342
  // Determine what to watch
289
343
  const watchTarget = isDir ? inputPath : dirname(inputPath);
290
344
 
345
+ function handleCssWatch(filename) {
346
+ if (copyAssetsFlag) {
347
+ try {
348
+ copyAssets(resolve("."));
349
+ } catch (e) {
350
+ console.error(`gofront: asset copy failed: ${e.message}`);
351
+ }
352
+ }
353
+ devServer?.notifyCss?.(filename);
354
+ }
355
+
291
356
  let debounce = null;
357
+ let cssDebounce = null;
292
358
  watch(watchTarget, { recursive: true }, (_event, filename) => {
293
- if (filename && !filename.endsWith(".go") && !filename.endsWith(".templ"))
359
+ if (!filename) return;
360
+ if (filename.endsWith(".css")) {
361
+ clearTimeout(cssDebounce);
362
+ cssDebounce = setTimeout(() => handleCssWatch(filename), 50);
294
363
  return;
364
+ }
365
+ if (!filename.endsWith(".go") && !filename.endsWith(".templ")) return;
295
366
  clearTimeout(debounce);
296
367
  debounce = setTimeout(() => buildOnce(filename), 80);
297
368
  });