bosia 0.8.1 → 0.8.2

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/core/dev.ts +81 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "description": "A fast, batteries-included fullstack framework — SSR · Svelte 5 Runes · Bun · ElysiaJS. File-based routing No Node.js, no Vite, no adapters.",
6
6
  "keywords": [
package/src/core/dev.ts CHANGED
@@ -17,6 +17,13 @@ const SHELL_ENV_SNAPSHOT: Record<string, string | undefined> = { ...process.env
17
17
 
18
18
  loadEnv("development");
19
19
 
20
+ // Host-managed mode: when a host (rukoku) sets BOSIA_DEV_MANAGED=1 in the unit
21
+ // env, this dev server never self-triggers builds on file change — the host is
22
+ // the single clock and drives one rebuild per turn via POST /__bosia/rebuild
23
+ // (avoids the watcher compiling a half-written file mid-edit). Unset → normal
24
+ // `bosia dev` is byte-for-byte unchanged.
25
+ const MANAGED = process.env.BOSIA_DEV_MANAGED === "1";
26
+
20
27
  function reloadEnv() {
21
28
  for (const k of Object.keys(process.env)) delete process.env[k];
22
29
  for (const [k, v] of Object.entries(SHELL_ENV_SNAPSHOT)) {
@@ -216,25 +223,30 @@ let buildTimer: ReturnType<typeof setTimeout> | null = null;
216
223
  let building = false;
217
224
  let buildPending = false;
218
225
 
219
- async function buildAndRestart() {
226
+ async function buildAndRestart(): Promise<boolean> {
220
227
  if (building) {
221
228
  buildPending = true;
222
- return;
229
+ // ponytail: coalesced into the in-flight build. Managed mode fires one
230
+ // rebuild per turn so overlap is rare; report optimistic rather than a
231
+ // false build failure to the host.
232
+ return true;
223
233
  }
224
234
  building = true;
225
235
  try {
236
+ let ok = true;
226
237
  do {
227
238
  buildPending = false;
228
- const ok = await runBuild();
239
+ ok = await runBuild();
229
240
  if (!ok) {
230
241
  console.error("❌ Build failed — fix errors and save again");
231
- return;
242
+ return false;
232
243
  }
233
244
  await startAppServer();
234
245
  // Give the app server a moment to bind its port
235
246
  await Bun.sleep(200);
236
247
  broadcastReload();
237
248
  } while (buildPending);
249
+ return ok;
238
250
  } finally {
239
251
  building = false;
240
252
  }
@@ -331,6 +343,17 @@ const devServer = Bun.serve({
331
343
  return Response.json({ ok: true, held: false, flushed });
332
344
  }
333
345
 
346
+ // Host-managed rebuild trigger. In managed mode the watcher is off, so the
347
+ // host (rukoku) POSTs here once per turn after all file writes are done.
348
+ // Reuses the single-flight building/buildPending guards. The POST awaits the
349
+ // full build; `{ ok }` is the real build result (compiler errors stream to
350
+ // stdout/stderr → journald, which the host tails on ok:false). Exists in
351
+ // both modes; simply unused when the watcher is on.
352
+ if (url.pathname === "/__bosia/rebuild" && req.method === "POST") {
353
+ const ok = await buildAndRestart();
354
+ return Response.json({ ok });
355
+ }
356
+
334
357
  // Proxy everything else to the app server. Inject X-Forwarded-Host/Proto so
335
358
  // the app's CSRF origin check (gated behind TRUST_PROXY=true, also set in the
336
359
  // app env above) reconstructs the public-facing origin from the dev proxy
@@ -426,18 +449,27 @@ const SRC_DIR = join(process.cwd(), "src");
426
449
  const MTIME_POLL_MS = 5_000;
427
450
  const mtimes = new Map<string, number>();
428
451
 
429
- const srcWatcher = watch(join(process.cwd(), "src"), { recursive: true }, (_event, filename) => {
430
- if (!filename) return;
431
- const abs = join(process.cwd(), "src", filename);
432
- if (isGenerated(abs)) return;
433
- console.log(`[watch] changed: ${filename}`);
434
- try {
435
- mtimes.set(abs, statSync(abs).mtimeMs);
436
- } catch {
437
- mtimes.delete(abs);
438
- }
439
- scheduleBuild();
440
- });
452
+ // Managed mode: the host is the single clock, so never self-trigger on file
453
+ // change. Leave both watchers unstarted (null) — boot build + startAppServer
454
+ // still run, so the server builds once and serves; it just waits for the host's
455
+ // POST /__bosia/rebuild instead of watching.
456
+ let srcWatcher: ReturnType<typeof watch> | null = null;
457
+ let mtimePoll: ReturnType<typeof setInterval> | null = null;
458
+
459
+ if (!MANAGED) {
460
+ srcWatcher = watch(join(process.cwd(), "src"), { recursive: true }, (_event, filename) => {
461
+ if (!filename) return;
462
+ const abs = join(process.cwd(), "src", filename);
463
+ if (isGenerated(abs)) return;
464
+ console.log(`[watch] changed: ${filename}`);
465
+ try {
466
+ mtimes.set(abs, statSync(abs).mtimeMs);
467
+ } catch {
468
+ mtimes.delete(abs);
469
+ }
470
+ scheduleBuild();
471
+ });
472
+ }
441
473
 
442
474
  function walkSrc(out: Map<string, number>): void {
443
475
  const stack: string[] = [SRC_DIR];
@@ -471,37 +503,39 @@ function walkSrc(out: Map<string, number>): void {
471
503
  // Seed the map without firing — first sweep just records existing mtimes.
472
504
  walkSrc(mtimes);
473
505
 
474
- const mtimePoll = setInterval(() => {
475
- const fresh = new Map<string, number>();
476
- walkSrc(fresh);
506
+ if (!MANAGED) {
507
+ mtimePoll = setInterval(() => {
508
+ const fresh = new Map<string, number>();
509
+ walkSrc(fresh);
477
510
 
478
- let changed: string | null = null;
511
+ let changed: string | null = null;
479
512
 
480
- for (const [path, ts] of fresh) {
481
- const prev = mtimes.get(path);
482
- if (prev === undefined || prev !== ts) {
483
- changed = path;
484
- break;
485
- }
486
- }
487
- if (!changed) {
488
- for (const path of mtimes.keys()) {
489
- if (!fresh.has(path)) {
513
+ for (const [path, ts] of fresh) {
514
+ const prev = mtimes.get(path);
515
+ if (prev === undefined || prev !== ts) {
490
516
  changed = path;
491
517
  break;
492
518
  }
493
519
  }
494
- }
520
+ if (!changed) {
521
+ for (const path of mtimes.keys()) {
522
+ if (!fresh.has(path)) {
523
+ changed = path;
524
+ break;
525
+ }
526
+ }
527
+ }
495
528
 
496
- if (changed) {
497
- const rel = changed.startsWith(SRC_DIR) ? changed.slice(SRC_DIR.length + 1) : changed;
498
- console.log(`[poll] changed: ${rel}`);
499
- mtimes.clear();
500
- for (const [p, t] of fresh) mtimes.set(p, t);
501
- scheduleBuild();
502
- }
503
- }, MTIME_POLL_MS);
504
- mtimePoll.unref?.();
529
+ if (changed) {
530
+ const rel = changed.startsWith(SRC_DIR) ? changed.slice(SRC_DIR.length + 1) : changed;
531
+ console.log(`[poll] changed: ${rel}`);
532
+ mtimes.clear();
533
+ for (const [p, t] of fresh) mtimes.set(p, t);
534
+ scheduleBuild();
535
+ }
536
+ }, MTIME_POLL_MS);
537
+ mtimePoll.unref?.();
538
+ }
505
539
 
506
540
  // ─── .env Watcher ─────────────────────────────────────────
507
541
  // Reset to shell-env snapshot and re-run loadEnv so removed/renamed
@@ -517,7 +551,11 @@ const envWatcher = watch(process.cwd(), { recursive: false }, (_event, filename)
517
551
  scheduleBuild();
518
552
  });
519
553
 
520
- console.log("👀 Watching src/ for changes...\n");
554
+ console.log(
555
+ MANAGED
556
+ ? "🛰️ Host-managed mode — builds driven by POST /__bosia/rebuild\n"
557
+ : "👀 Watching src/ for changes...\n",
558
+ );
521
559
 
522
560
  // ─── Shutdown ─────────────────────────────────────────────
523
561
  // Own SIGINT/SIGTERM so we can cleanly stop the child app server.
@@ -534,8 +572,8 @@ async function shutdown() {
534
572
  if (buildTimer) clearTimeout(buildTimer);
535
573
  if (restartTimer) clearTimeout(restartTimer);
536
574
  if (healthyTimer) clearTimeout(healthyTimer);
537
- clearInterval(mtimePoll);
538
- srcWatcher.close();
575
+ if (mtimePoll) clearInterval(mtimePoll);
576
+ srcWatcher?.close();
539
577
  envWatcher.close();
540
578
  devServer.stop(true); // closes SSE conns → abort listeners clear ping intervals
541
579