bosia 0.8.0 → 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.
- package/package.json +1 -1
- package/src/core/client/prefetch.ts +5 -0
- package/src/core/dev.ts +81 -43
package/package.json
CHANGED
|
@@ -97,6 +97,11 @@ export function consumePrefetch(path: string): any | null {
|
|
|
97
97
|
|
|
98
98
|
/** Prefetches data for a path and stores in cache. No-op if already cached/in-flight. */
|
|
99
99
|
export async function prefetchPath(path: string): Promise<void> {
|
|
100
|
+
// Warm the route's +loading.svelte chunk alongside its data, so the skeleton
|
|
101
|
+
// paints instantly on click instead of cold-importing after the old page lingers.
|
|
102
|
+
const warmMatch = findMatch(clientRoutes, new URL(path, window.location.origin).pathname);
|
|
103
|
+
(warmMatch?.route as { loading?: (() => Promise<unknown>) | null } | undefined)?.loading?.();
|
|
104
|
+
|
|
100
105
|
const existing = prefetchCache.get(path);
|
|
101
106
|
if (existing && Date.now() - existing.ts <= 30_000) return;
|
|
102
107
|
if (existing) prefetchCache.delete(path);
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
506
|
+
if (!MANAGED) {
|
|
507
|
+
mtimePoll = setInterval(() => {
|
|
508
|
+
const fresh = new Map<string, number>();
|
|
509
|
+
walkSrc(fresh);
|
|
477
510
|
|
|
478
|
-
|
|
511
|
+
let changed: string | null = null;
|
|
479
512
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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(
|
|
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
|
|
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
|
|