@cosmicdrift/kumiko-dev-server 0.386.0 → 0.387.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.
- package/LICENSE +21 -57
- package/dist/build-server-bundle.d.ts +6 -6
- package/dist/build-server-bundle.js +51 -51
- package/dist/build.js +3 -3
- package/dist/compose-stacks.d.ts +2 -2
- package/dist/compose-stacks.js +1 -2
- package/dist/crash-tracker.d.ts +3 -7
- package/dist/crash-tracker.js +4 -13
- package/dist/create-kumiko-server.d.ts +47 -51
- package/dist/create-kumiko-server.js +122 -152
- package/dist/env-schema.d.ts +1 -1
- package/dist/env-schema.js +4 -4
- package/dist/index.js +14 -14
- package/dist/run-dev-app.d.ts +48 -46
- package/dist/run-dev-app.js +57 -55
- package/dist/scaffold-app-feature.js +5 -4
- package/dist/scaffold-app.js +5 -7
- package/dist/schema-apply.d.ts +2 -2
- package/dist/schema-apply.js +12 -12
- package/dist/schema-tables-auth-mode.js +16 -15
- package/dist/schema-tables-minimal.js +7 -7
- package/dist/setup-test-stack-from-features.js +1 -1
- package/dist/welcome-banner.js +2 -2
- package/package.json +10 -10
|
@@ -61,14 +61,12 @@ export const PROD_BUNDLES_ENV = "KUMIKO_DEV_PROD_BUNDLES";
|
|
|
61
61
|
// Reload snippet injected into every page-load so the browser
|
|
62
62
|
// subscribes to /_reload without the HTML needing to hard-code it.
|
|
63
63
|
//
|
|
64
|
-
//
|
|
65
|
-
// -
|
|
66
|
-
// -
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// dem alten Bundle hängen wenn der Watcher classifyChange="restart"
|
|
71
|
-
// gewählt hat oder der User Ctrl-C/bun dev gemacht hat.
|
|
64
|
+
// Two reload triggers:
|
|
65
|
+
// - explicit `reload` event from the server after hot reload (rebuild + send)
|
|
66
|
+
// - implicit `boot` event on every SSE connection, containing the current
|
|
67
|
+
// server process's bootId. The snippet remembers the first bootId; if a
|
|
68
|
+
// reconnect receives a different one, it refreshes. Otherwise the
|
|
69
|
+
// browser could remain on a stale bundle after a server restart.
|
|
72
70
|
const RELOAD_SNIPPET = `
|
|
73
71
|
<script>
|
|
74
72
|
(() => {
|
|
@@ -95,7 +93,7 @@ const RELOAD_SNIPPET = `
|
|
|
95
93
|
// replaces `window.history` and every pushState afterwards throws. Prod
|
|
96
94
|
// (build-prod-bundle) has always emitted the module form.
|
|
97
95
|
//
|
|
98
|
-
// `interactive-widget=resizes-content
|
|
96
|
+
// `interactive-widget=resizes-content`: without it, a mobile
|
|
99
97
|
// keyboard shrinks only the visual viewport, not the layout viewport that
|
|
100
98
|
// `position: fixed` anchors to — a fixed bottom action bar would stay
|
|
101
99
|
// pinned behind the keyboard instead of above it. iOS Safari ignores the
|
|
@@ -159,9 +157,9 @@ function injectReload(html) {
|
|
|
159
157
|
? html.replace("</body>", `${RELOAD_SNIPPET}</body>`)
|
|
160
158
|
: html + RELOAD_SNIPPET;
|
|
161
159
|
}
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
160
|
+
// Injects <link rel="stylesheet" href="/styles.css"> into <head> if it
|
|
161
|
+
// is not already present. Called only when the stylesheet option is used;
|
|
162
|
+
// otherwise no CSS route is provided.
|
|
165
163
|
function injectStylesheet(html) {
|
|
166
164
|
if (html.includes('href="/styles.css"'))
|
|
167
165
|
return html;
|
|
@@ -280,11 +278,10 @@ async function tryServePublicAsset(req, pathname, app, publicDir, socketAddress)
|
|
|
280
278
|
return honoTry.response;
|
|
281
279
|
}
|
|
282
280
|
async function watchDir(dir, onChange, signal, watchEvents) {
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
// bubbles als unhandled error in vitest hoch.
|
|
281
|
+
// The server's stop() aborts the signal; without it, the for-await loop
|
|
282
|
+
// would continue until process exit. During test teardown, removing the
|
|
283
|
+
// temporary directory could then be classified as a restart and call
|
|
284
|
+
// process.exit(75), surfacing as an unhandled Vitest error.
|
|
288
285
|
const watcher = watchEvents(dir, { recursive: true, signal });
|
|
289
286
|
try {
|
|
290
287
|
for await (const ev of watcher) {
|
|
@@ -293,37 +290,28 @@ async function watchDir(dir, onChange, signal, watchEvents) {
|
|
|
293
290
|
}
|
|
294
291
|
}
|
|
295
292
|
catch (err) {
|
|
296
|
-
// signal.abort()
|
|
297
|
-
//
|
|
293
|
+
// signal.abort() causes the async iterator to throw AbortError during
|
|
294
|
+
// expected teardown; other errors must propagate.
|
|
298
295
|
// skip: AbortSignal fired the abort, this is expected teardown not a real error
|
|
299
296
|
if (typeof err === "object" && err !== null && "name" in err && err.name === "AbortError")
|
|
300
297
|
return;
|
|
301
298
|
throw err;
|
|
302
299
|
}
|
|
303
300
|
}
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
// alles was den Server nicht beeinflusst (Tests, .css, .json…).
|
|
301
|
+
// Classifies a changed file as `hot-reload` for client-side changes,
|
|
302
|
+
// `restart` for server-side changes whose cached Bun modules must be
|
|
303
|
+
// reloaded, or `ignore` when the server is unaffected.
|
|
308
304
|
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// oder die client-entry-Datei selbst → hot-reload
|
|
314
|
-
// - sonst → restart (könnte Schema/Feature-Definition sein)
|
|
315
|
-
// - andere Dateitypen → ignore (kein TS rebuild nötig)
|
|
305
|
+
// Tests and non-TypeScript files are ignored. TypeScript files under
|
|
306
|
+
// `/web/`, `/admin/`, `/public/`, or `/client/`, as well as the client
|
|
307
|
+
// entry itself, trigger hot reload; other TypeScript files trigger a
|
|
308
|
+
// restart because they may contain schema or feature definitions.
|
|
316
309
|
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
// Feature-Web-Code; ältere Samples haben einfach `/client.tsx` neben
|
|
321
|
-
// dem Server. Der Watcher muss alle drei verstehen, sonst löst ein
|
|
322
|
-
// Edit der Bridge-Component einen kompletten Server-Restart aus —
|
|
323
|
-
// kostet 2-3s, droppt die Test-DB im ephemeral-Modus, reseed läuft
|
|
324
|
-
// erneut. Ineffektiv und für der User verwirrend.
|
|
310
|
+
// The multiple client directories are intentional: samples use different
|
|
311
|
+
// layouts. Restarting for a client-only edit would be slower, drop the
|
|
312
|
+
// ephemeral test database, and reseed it unnecessarily.
|
|
325
313
|
//
|
|
326
|
-
//
|
|
314
|
+
// Exported for tests; internally called by the watcher loop.
|
|
327
315
|
export function classifyChange(filename) {
|
|
328
316
|
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx"))
|
|
329
317
|
return "ignore";
|
|
@@ -336,9 +324,9 @@ export function classifyChange(filename) {
|
|
|
336
324
|
filename.endsWith(".e2e.ts")) {
|
|
337
325
|
return "ignore";
|
|
338
326
|
}
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
327
|
+
// Platform-independent: checks both POSIX and Windows separators.
|
|
328
|
+
// Matches `<sep><dir><sep>` and trailing-`<sep><dir>` for watcher
|
|
329
|
+
// filenames supplied as relative paths.
|
|
342
330
|
const clientSubdirs = ["web", "admin", "public", "client"];
|
|
343
331
|
for (const dir of clientSubdirs) {
|
|
344
332
|
if (filename.includes(`/${dir}/`) ||
|
|
@@ -361,12 +349,11 @@ export function classifyWatchedChange(watchedDir, filename, entrySourceFiles) {
|
|
|
361
349
|
? "hot-reload"
|
|
362
350
|
: classifyChange(filename);
|
|
363
351
|
}
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
// während der Watcher läuft.
|
|
352
|
+
// Expands watchDirs patterns into concrete directories. Entries without
|
|
353
|
+
// `*` are resolved as ordinary paths; entries with `*` are glob-expanded
|
|
354
|
+
// and only directory matches are retained, so `"../../../packages/*/src"`
|
|
355
|
+
// replaces a list of hard-coded paths. Expansion runs once at boot,
|
|
356
|
+
// not while the watcher is running.
|
|
370
357
|
function expandWatchPatterns(patterns) {
|
|
371
358
|
const out = [];
|
|
372
359
|
for (const p of patterns) {
|
|
@@ -421,14 +408,14 @@ export function resolveStylesheet(options) {
|
|
|
421
408
|
const local = resolve(process.cwd(), "src/styles.css");
|
|
422
409
|
if (existsSync(local))
|
|
423
410
|
return local;
|
|
424
|
-
// Bun.resolveSync
|
|
425
|
-
// package.json
|
|
426
|
-
//
|
|
427
|
-
//
|
|
411
|
+
// Bun.resolveSync follows package exports: `"./styles.css"` in
|
|
412
|
+
// renderer/web's package.json resolves to the workspace file in the
|
|
413
|
+
// monorepo or the node_modules file in an installed app. Bun already
|
|
414
|
+
// returns an absolute filesystem path, so `fileURLToPath` is unnecessary.
|
|
428
415
|
if (!hasBun) {
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
416
|
+
// Node-based unit tests reach this branch. Without Bun, package-export
|
|
417
|
+
// resolution and a real Tailwind pipeline are unavailable, so skip
|
|
418
|
+
// silently rather than reporting an error.
|
|
432
419
|
return undefined;
|
|
433
420
|
}
|
|
434
421
|
try {
|
|
@@ -470,9 +457,9 @@ async function startTailwindPipeline(entryCss, { watch }) {
|
|
|
470
457
|
? `[kumiko-server] tailwind watcher → ${outPath}`
|
|
471
458
|
: `[kumiko-server] tailwind one-shot build → ${outPath}`);
|
|
472
459
|
const bunPath = process.argv[0] ?? "bun";
|
|
473
|
-
//
|
|
474
|
-
// 404
|
|
475
|
-
//
|
|
460
|
+
// Wait for the initial build so the first /styles.css request does not
|
|
461
|
+
// return 404. Then run the watcher in the background with unref();
|
|
462
|
+
// otherwise it could remain as an orphan after the parent crashes.
|
|
476
463
|
try {
|
|
477
464
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
478
465
|
const child = spawn(bunPath, ["run", cliPath, "-i", entryCss, "-o", outPath], {
|
|
@@ -505,14 +492,14 @@ async function startTailwindPipeline(entryCss, { watch }) {
|
|
|
505
492
|
watcher.kill("SIGTERM");
|
|
506
493
|
}
|
|
507
494
|
catch {
|
|
508
|
-
//
|
|
495
|
+
// Already stopped or never started; no further action is needed.
|
|
509
496
|
}
|
|
510
497
|
},
|
|
511
498
|
};
|
|
512
499
|
}
|
|
513
|
-
/** URL
|
|
514
|
-
* (
|
|
515
|
-
*
|
|
500
|
+
/** URL path used to serve an entry: `"client"` → `/client.js`
|
|
501
|
+
* (the single-entry default), otherwise `/client-<name>.js`. Keeping
|
|
502
|
+
* this in one place ensures routing and logging use the same convention. */
|
|
516
503
|
function assetPathFor(entryName) {
|
|
517
504
|
return entryName === "client" ? "/client.js" : `/client-${entryName}.js`;
|
|
518
505
|
}
|
|
@@ -537,7 +524,7 @@ function normalizeEntries(options) {
|
|
|
537
524
|
}
|
|
538
525
|
export async function createKumikoServer(options) {
|
|
539
526
|
const port = options.port ?? Number(process.env["PORT"] ?? 4173);
|
|
540
|
-
// --- client bundles (single-entry
|
|
527
|
+
// --- client bundles (single-entry or multi-entry through the same map) ---
|
|
541
528
|
const entries = normalizeEntries(options);
|
|
542
529
|
const buildBundle = options._buildBundle ?? buildClient;
|
|
543
530
|
const clientBundles = new Map();
|
|
@@ -568,9 +555,9 @@ export async function createKumikoServer(options) {
|
|
|
568
555
|
}
|
|
569
556
|
}
|
|
570
557
|
// --- HTML templates ---
|
|
571
|
-
// Single-
|
|
572
|
-
// Multi-
|
|
573
|
-
// ?? DEFAULT_HTML)
|
|
558
|
+
// Single-entry uses one template (`htmlPath` or DEFAULT_HTML).
|
|
559
|
+
// Multi-entry uses one per entry (`entry.htmlPath ?? options.htmlPath
|
|
560
|
+
// ?? DEFAULT_HTML`); hostDispatch selects it per request.
|
|
574
561
|
const defaultTemplate = options.htmlPath !== undefined
|
|
575
562
|
? await readFile(resolve(options.htmlPath), "utf-8")
|
|
576
563
|
: DEFAULT_HTML;
|
|
@@ -625,8 +612,8 @@ export async function createKumikoServer(options) {
|
|
|
625
612
|
if (options.wire !== undefined) {
|
|
626
613
|
await options.wire({
|
|
627
614
|
db: stack.db,
|
|
628
|
-
//
|
|
629
|
-
//
|
|
615
|
+
// The raw ioredis client, not the TestRedis wrapper, matching runProdApp
|
|
616
|
+
// so application code sees the same interface in development and production.
|
|
630
617
|
redis: stack.redis.redis,
|
|
631
618
|
registry: stack.registry,
|
|
632
619
|
metricsFor: createMetricsForFactory(stack.observability.meter),
|
|
@@ -634,20 +621,17 @@ export async function createKumikoServer(options) {
|
|
|
634
621
|
dispatchSystemQuery: makeDispatchSystemQuery(stack.dispatcher),
|
|
635
622
|
});
|
|
636
623
|
}
|
|
637
|
-
// Hook
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
// Verantwortung (persistent-DB-Modus läuft es bei jedem Boot).
|
|
624
|
+
// Hook for caller-specific tables and seeding. It runs after entity tables
|
|
625
|
+
// are created, so the hook can use `stack.db` / `stack.dispatcher`, and
|
|
626
|
+
// before the server starts, so the first request sees initialized state. Idempotence is the caller's responsibility because
|
|
627
|
+
// persistent-DB mode runs the hook on every boot.
|
|
642
628
|
if (options.onAfterSetup !== undefined) {
|
|
643
629
|
await options.onAfterSetup(stack);
|
|
644
630
|
}
|
|
645
|
-
// setupTestStack
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
// fließen. Ohne start() bleiben alle Events in der events-Tabelle
|
|
650
|
-
// liegen und die Tabs sehen nichts.
|
|
631
|
+
// setupTestStack configures but does not start the event dispatcher so
|
|
632
|
+
// integration tests can drain it deterministically with runOnce(). The
|
|
633
|
+
// dev server starts polling so SSE broadcasts reach connected clients;
|
|
634
|
+
// otherwise events remain in the events table.
|
|
651
635
|
if (stack.eventDispatcher) {
|
|
652
636
|
await stack.eventDispatcher.start();
|
|
653
637
|
}
|
|
@@ -658,22 +642,21 @@ export async function createKumikoServer(options) {
|
|
|
658
642
|
// The stack's own AppContext, not a `{ db, registry }` literal: these
|
|
659
643
|
// runners consume the lanes, and a job reaching for ctx.files /
|
|
660
644
|
// ctx.notify / ctx.config would otherwise die here while the very same
|
|
661
|
-
// handler works on the request path
|
|
645
|
+
// handler works on the request path.
|
|
662
646
|
context: stack.context,
|
|
663
647
|
redisUrl,
|
|
664
648
|
// Same dispatcher the request path writes through — without this,
|
|
665
649
|
// ctx.write/ctx.writeAs/ctx.queryAs inside a dev-run job throw on their
|
|
666
|
-
// first call
|
|
650
|
+
// first call.
|
|
667
651
|
dispatcher: stack.dispatcher,
|
|
668
652
|
// Same isolation reasoning as setupTestStack's jobs.queueNamePrefix
|
|
669
653
|
// above — both must use the same prefix so enqueuer and consumer agree
|
|
670
654
|
// on queue names.
|
|
671
655
|
queueNamePrefix: jobQueueNamePrefix,
|
|
672
656
|
});
|
|
673
|
-
// Dev user = TestUsers.admin. Demo features are openToAll but the
|
|
674
|
-
// auth
|
|
675
|
-
//
|
|
676
|
-
// selbst den Login.
|
|
657
|
+
// Dev user = TestUsers.admin. Demo features are openToAll, but the
|
|
658
|
+
// auth middleware still requires a valid JWT. When `options.auth` is set,
|
|
659
|
+
// the client performs login instead.
|
|
677
660
|
const autoMintJwt = options.auth === undefined;
|
|
678
661
|
const devUser = TestUsers.admin;
|
|
679
662
|
// Build once at boot purely to surface authoring warnings (settings-hub
|
|
@@ -685,11 +668,10 @@ export async function createKumikoServer(options) {
|
|
|
685
668
|
searchAdapterMissing: !stack.context.searchAdapter,
|
|
686
669
|
});
|
|
687
670
|
// --- SSE reload ---
|
|
688
|
-
// bootId
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
// RELOAD_SNIPPET oben.
|
|
671
|
+
// bootId identifies this server process instance. It is sent on connect;
|
|
672
|
+
// the browser remembers the first value and refreshes when a reconnect
|
|
673
|
+
// receives a different one, indicating that the server restarted and
|
|
674
|
+
// the old bundle is stale. See RELOAD_SNIPPET above.
|
|
693
675
|
const bootId = String(Date.now());
|
|
694
676
|
const reloadClients = new Set();
|
|
695
677
|
// JSON-encoded so a filename can never break the SSE framing.
|
|
@@ -734,7 +716,7 @@ export async function createKumikoServer(options) {
|
|
|
734
716
|
});
|
|
735
717
|
// resolvePageHead goes through the same headless resolveAndInjectPageHead
|
|
736
718
|
// that runProdApp uses, so dev and e2e exercise the one timeout and
|
|
737
|
-
// fallback path instead of a second copy of it
|
|
719
|
+
// fallback path instead of a second copy of it.
|
|
738
720
|
const htmlResponse = async (entryName, req, socketAddress, status = 200) => {
|
|
739
721
|
const template = htmlTemplates.get(entryName) ?? defaultTemplate;
|
|
740
722
|
const headers = new Headers();
|
|
@@ -781,10 +763,10 @@ export async function createKumikoServer(options) {
|
|
|
781
763
|
});
|
|
782
764
|
};
|
|
783
765
|
// --- Fetch handler (runtime-neutral) ---
|
|
784
|
-
// Bundle
|
|
785
|
-
// GET /client[-name].js → JS
|
|
786
|
-
// GET /client[-name].js.map →
|
|
787
|
-
// assetPathFor()
|
|
766
|
+
// Bundle path lookup: each entry is served at
|
|
767
|
+
// GET /client[-name].js → JS bundle
|
|
768
|
+
// GET /client[-name].js.map → sourcemap
|
|
769
|
+
// assetPathFor() is the single source of truth for the URL form.
|
|
788
770
|
const bundleByAssetPath = new Map();
|
|
789
771
|
for (const e of entries)
|
|
790
772
|
bundleByAssetPath.set(assetPathFor(e.name), e.name);
|
|
@@ -822,7 +804,7 @@ export async function createKumikoServer(options) {
|
|
|
822
804
|
});
|
|
823
805
|
}
|
|
824
806
|
}
|
|
825
|
-
// .js.map
|
|
807
|
+
// .js.map variant: perform the same lookup after removing /.map.
|
|
826
808
|
if (url.pathname.endsWith(".js.map")) {
|
|
827
809
|
const jsPath = url.pathname.slice(0, -".map".length);
|
|
828
810
|
const mapName = bundleByAssetPath.get(jsPath);
|
|
@@ -872,9 +854,9 @@ export async function createKumikoServer(options) {
|
|
|
872
854
|
const entry = { controller, encoder, closed: false };
|
|
873
855
|
reloadClients.add(entry);
|
|
874
856
|
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
875
|
-
// boot
|
|
876
|
-
// bootId.
|
|
877
|
-
//
|
|
857
|
+
// boot event: the browser snippet compares this with the first
|
|
858
|
+
// bootId. A different value after reconnect means the server
|
|
859
|
+
// restarted, so location.reload() is called.
|
|
878
860
|
controller.enqueue(encoder.encode(`event: boot\ndata: ${bootId}\n\n`));
|
|
879
861
|
},
|
|
880
862
|
cancel() {
|
|
@@ -892,34 +874,28 @@ export async function createKumikoServer(options) {
|
|
|
892
874
|
},
|
|
893
875
|
});
|
|
894
876
|
}
|
|
895
|
-
// SPA catch-all:
|
|
896
|
-
// HTML shell
|
|
897
|
-
//
|
|
898
|
-
//
|
|
877
|
+
// SPA catch-all: GET or HEAD requests to non-API, non-asset paths return
|
|
878
|
+
// the HTML shell so the client-side router can mount the screen for
|
|
879
|
+
// location.pathname. Paths containing a dot are left to the stack's
|
|
880
|
+
// 404 handling.
|
|
899
881
|
//
|
|
900
|
-
// Backend routes
|
|
901
|
-
//
|
|
902
|
-
// the real Hono route with HTML and EventSource would never
|
|
903
|
-
// connect.
|
|
882
|
+
// Backend routes outside /api, currently including /sse, must be
|
|
883
|
+
// excluded so the catch-all does not shadow Hono routes.
|
|
904
884
|
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
// Route hat, gewinnt sie. 404 vom Hono-stack → SPA-fallback wie
|
|
909
|
-
// bisher. Das spiegelt runProdApp's doc-intent ("Hono matched VOR
|
|
910
|
-
// staticDir-fallback") und macht r.httpRoute mit non-/api paths im
|
|
911
|
-
// dev-server symmetrisch zu prod.
|
|
885
|
+
// Feature routes declared with r.httpRoute can also be outside /api.
|
|
886
|
+
// Try stack.app.fetch first so a matching Hono route wins; only a Hono
|
|
887
|
+
// 404 proceeds to the SPA fallback, matching production behavior.
|
|
912
888
|
if (
|
|
913
|
-
// HEAD
|
|
914
|
-
//
|
|
889
|
+
// Include HEAD: runProdApp falls back to the SPA for both GET and HEAD;
|
|
890
|
+
// omitting it would make development return 404 where production returns 200.
|
|
915
891
|
isRoutableGetOrHead(req, url.pathname) &&
|
|
916
892
|
!url.pathname.includes(".")) {
|
|
917
893
|
const honoTry = await tryHonoFirst(stack.app, req, socketAddress);
|
|
918
894
|
if (honoTry.matched) {
|
|
919
895
|
return honoTry.response;
|
|
920
896
|
}
|
|
921
|
-
// Discriminated
|
|
922
|
-
//
|
|
897
|
+
// Discriminated dispatch mirrors production. Without hostDispatch, use
|
|
898
|
+
// the single-entry default ("client").
|
|
923
899
|
if (options.hostDispatch !== undefined) {
|
|
924
900
|
const dispatch = await options.hostDispatch(req, {
|
|
925
901
|
systemQuery: buildDevSystemQuery(req, socketAddress),
|
|
@@ -959,35 +935,32 @@ export async function createKumikoServer(options) {
|
|
|
959
935
|
};
|
|
960
936
|
const handleFetch = options.wrapFetch ? options.wrapFetch(routeFetch) : routeFetch;
|
|
961
937
|
// --- HTTP server (Bun only) ---
|
|
962
|
-
// Under Node/vitest
|
|
963
|
-
// .
|
|
964
|
-
// Bun.serve
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
// (idleTimeout: 0). Spec-Test in run-prod-app-spec.test.ts pinst das.
|
|
938
|
+
// Under Node/vitest, skip Bun.serve; the handle's .fetch() is the test
|
|
939
|
+
// surface. Under Bun, Bun.serve wires the listener.
|
|
940
|
+
// Bun.serve options come from buildBunServeOptions (run-prod-app.ts) so
|
|
941
|
+
// development and production share the same SSE-relevant defaults
|
|
942
|
+
// (idleTimeout: 0); run-prod-app-spec.test.ts pins that.
|
|
968
943
|
const server = hasBun
|
|
969
944
|
? Bun.serve(buildBunServeOptions(port, handleFetch, resolveDerivedMaxRequestBodySize(stack.registry), {
|
|
970
945
|
upgradeFetch: async (req, env) => stripNoRouteMatchHeader(await stack.app.fetch(req, env)),
|
|
971
946
|
}))
|
|
972
947
|
: undefined;
|
|
973
|
-
// --- file watcher → rebundle + reload,
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
// detected das und respawnt.
|
|
948
|
+
// --- file watcher → rebundle + reload, or process restart for schema changes ---
|
|
949
|
+
// Heuristic: files in `web/` or `__tests__/` are client-side or test-only,
|
|
950
|
+
// so hot reload is sufficient. Other files are server-side; because Bun
|
|
951
|
+
// caches module imports, a schema change in feature.ts requires a process
|
|
952
|
+
// restart. Exit with code 75 (EX_TEMPFAIL) so the `kumiko-dev` wrapper
|
|
953
|
+
// can respawn the process.
|
|
980
954
|
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
955
|
+
// stop() aborts watcherAbort, which ends fs.watch's async iteration. This
|
|
956
|
+
// prevents a watcher from surviving test teardown and classifying an
|
|
957
|
+
// rmSync(tmpdir) as "restart needed".
|
|
984
958
|
const watcherAbort = new AbortController();
|
|
985
959
|
if (entries.length > 0) {
|
|
986
|
-
// Watch
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
// Watcher pro Verzeichnis.
|
|
960
|
+
// Watch directories: all entry directories (deduplicated) plus the explicit
|
|
961
|
+
// watchDirs. In multi-entry setups, entries often share a src/ directory
|
|
962
|
+
// (`src/client-admin.tsx` + `src/client-public.tsx`), so the Set keeps
|
|
963
|
+
// one watcher per directory.
|
|
991
964
|
const entryDirs = new Set();
|
|
992
965
|
for (const e of entries)
|
|
993
966
|
entryDirs.add(resolve(e.sourceFile, ".."));
|
|
@@ -1008,10 +981,9 @@ export async function createKumikoServer(options) {
|
|
|
1008
981
|
process.exit(75);
|
|
1009
982
|
}
|
|
1010
983
|
try {
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
// gemeinsamen Datei beide Bundles neu, das ist gewollt.
|
|
984
|
+
// Rebuild all entries—even when only one file changes, because we do not
|
|
985
|
+
// know which entries import it. With shared code, an edit triggers
|
|
986
|
+
// both bundles; this is intentional.
|
|
1015
987
|
for (const e of entries) {
|
|
1016
988
|
const rebuilt = await buildBundle(e.sourceFile);
|
|
1017
989
|
clientBundles.set(e.name, rebuilt);
|
|
@@ -1050,9 +1022,8 @@ export async function createKumikoServer(options) {
|
|
|
1050
1022
|
}
|
|
1051
1023
|
};
|
|
1052
1024
|
const stop = async () => {
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
// auslösen).
|
|
1025
|
+
// Stop the watcher first so no onChange callback runs during teardown;
|
|
1026
|
+
// otherwise removing the temporary directory could trigger process.exit(75).
|
|
1056
1027
|
watcherAbort.abort();
|
|
1057
1028
|
if (killTailwind)
|
|
1058
1029
|
killTailwind();
|
|
@@ -1069,12 +1040,11 @@ export async function createKumikoServer(options) {
|
|
|
1069
1040
|
await deleteEphemeralJobQueues(rawRedis);
|
|
1070
1041
|
};
|
|
1071
1042
|
// --- graceful shutdown ---
|
|
1072
|
-
// Signal handlers
|
|
1073
|
-
// restarts leak Postgres pools,
|
|
1074
|
-
//
|
|
1075
|
-
// uncaughtException
|
|
1076
|
-
//
|
|
1077
|
-
// räumen wir auch im Fehlerfall auf bevor wir mit non-zero exit'n.
|
|
1043
|
+
// Signal handlers run on Ctrl-C / kill. Without them, repeated dev
|
|
1044
|
+
// restarts leak Postgres pools, leave the Tailwind watcher as an orphan,
|
|
1045
|
+
// and leave temporary clients behind in persistent mode.
|
|
1046
|
+
// uncaughtException and unhandledRejection previously left the Tailwind
|
|
1047
|
+
// watcher running in the background; clean it up before exiting nonzero.
|
|
1078
1048
|
const installHandlers = options.installSignalHandlers ?? true;
|
|
1079
1049
|
if (installHandlers) {
|
|
1080
1050
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
package/dist/env-schema.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as z from "zod";
|
|
|
4
4
|
*
|
|
5
5
|
* PORT-default "3000" is the same fallback as
|
|
6
6
|
* `packages/dev-server/src/run-prod-app.ts:533` — keep in sync when the
|
|
7
|
-
* call-site is refactored to consume parsed env
|
|
7
|
+
* call-site is refactored to consume parsed env. */
|
|
8
8
|
export declare const frameworkCoreEnvSchema: z.ZodObject<{
|
|
9
9
|
PORT: z.ZodDefault<z.ZodString>;
|
|
10
10
|
DATABASE_URL: z.ZodURL;
|
package/dist/env-schema.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// @runtime runtime
|
|
2
2
|
// Consumed at prod boot via composeEnvSchema (same as compose-stacks),
|
|
3
|
-
// not only by tooling — keep runtime-isolation-safe
|
|
3
|
+
// not only by tooling — keep runtime-isolation-safe.
|
|
4
4
|
//
|
|
5
5
|
// Framework-core env-schema — the vars that runProdApp + buildServer
|
|
6
6
|
// + the connection-pool read directly from process.env today. Apps merge
|
|
7
7
|
// this into their app-wide schema via `composeEnvSchema({ core, ... })`.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// The schema is purely additive: it is exposed, but the call-sites
|
|
10
10
|
// in run-prod-app.ts (requireEnv("DATABASE_URL"), …) and api/server.ts
|
|
11
11
|
// (process.env["KUMIKO_INSTANCE_ID"]) keep reading process.env directly.
|
|
12
12
|
// Apps that opt into the schema get aggregated boot-validation errors
|
|
13
13
|
// BEFORE those legacy reads run; apps that don't, behave as before.
|
|
14
14
|
//
|
|
15
15
|
// Feature-specific vars (JWT_SECRET, KUMIKO_SECRETS_MASTER_KEY_*) live
|
|
16
|
-
// in their owning feature's envSchema
|
|
16
|
+
// in their owning feature's envSchema (bundled-features).
|
|
17
17
|
import * as z from "zod";
|
|
18
18
|
/** Env-vars read by framework-core (api/server, db/connection,
|
|
19
19
|
* dev-server/run-prod-app). NOT including feature-specific vars.
|
|
20
20
|
*
|
|
21
21
|
* PORT-default "3000" is the same fallback as
|
|
22
22
|
* `packages/dev-server/src/run-prod-app.ts:533` — keep in sync when the
|
|
23
|
-
* call-site is refactored to consume parsed env
|
|
23
|
+
* call-site is refactored to consume parsed env. */
|
|
24
24
|
export const frameworkCoreEnvSchema = z.object({
|
|
25
25
|
PORT: z
|
|
26
26
|
.string()
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// Public API
|
|
1
|
+
// Public API for the Kumiko development server, with two layers:
|
|
2
2
|
//
|
|
3
3
|
// - createKumikoServer (low-level)
|
|
4
|
-
// Bun.serve
|
|
5
|
-
//
|
|
6
|
-
// features + clientEntry
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Bun.serve wrapper that provides the client bundle, /styles.css,
|
|
5
|
+
// AppSchema injection, SSE reload, and auto-minted JWTs. Accepts
|
|
6
|
+
// features + clientEntry directly without automatic auth wiring. Use it
|
|
7
|
+
// for unusual auth setups (alternative Membership query, custom rate
|
|
8
|
+
// limiter, or custom login routes).
|
|
9
9
|
//
|
|
10
10
|
// - runDevApp (high-level)
|
|
11
|
-
//
|
|
12
|
-
// password)
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// Automatically adds the standard features (config/user/tenant/
|
|
12
|
+
// auth-email-password) when `auth` is set, wires the login routes and
|
|
13
|
+
// error map, and calls seedAdminGuarded in onAfterSetup. The default for
|
|
14
|
+
// sample apps and showcases: 5–10 lines of bootstrap instead of 50.
|
|
15
15
|
export { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// The build toolchain (buildProdBundle, Bun.build, Tailwind pipeline, ts-morph)
|
|
17
|
+
// lives in the subpath export `@cosmicdrift/kumiko-dev-server/build`. This keeps
|
|
18
|
+
// the Bun toolchain bundle out of production imports from the main barrel (for
|
|
19
|
+
// example, when drizzle-kit loads the app config under Node).
|
|
20
20
|
export { composeAuthStack, composeFileStack, composeGdprStack, composeIdentityStack, composeMailStack, composeOpsStack, composePagesStack, composeRendererStack, composeSaasStack, composeSecurityStack, composeUserDataRightsStack, stackFeatureNames, } from "./compose-stacks.js";
|
|
21
21
|
export { createKumikoServer, PROD_BUNDLES_ENV, resolveStylesheet, STYLESHEET_WATCH_ENV, } from "./create-kumiko-server.js";
|
|
22
22
|
export { frameworkCoreEnvSchema } from "./env-schema.js";
|