@cosmicdrift/kumiko-dev-server 0.385.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.
@@ -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
- // Zwei reload-Trigger:
65
- // - explizites `reload`-Event vom Server beim hot-reload (rebuild + send)
66
- // - implizites: jede SSE-Connection bekommt beim Connect ein `boot`-Event
67
- // mit der bootId des aktuellen Server-Process. Das Snippet merkt sich
68
- // die erste bootId; wenn nach einem Reconnect (Server-Restart!) eine
69
- // ANDERE bootId kommt, refresh — sonst bleibt der Browser ewig auf
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` (fw#1918): without it, a mobile
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
- // Injiziert <link rel="stylesheet" href="/styles.css"> in den <head>,
163
- // wenn es noch nicht da ist. Wird nur aufgerufen wenn die App das
164
- // stylesheet-Option genutzt hat — andernfalls kommt keine CSS-Route.
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
- // AbortSignal wird vom Server-stop() ausgelöst: ohne den Abort liefe
284
- // die for-await-Schleife bis zum Process-Exit weiter. Im Test-Setup
285
- // (afterEach räumt tmpdir mit rmSync auf) sähe der Watcher dann das
286
- // rmSync, klassifizierte's als "restart" und riefe process.exit(75) —
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() wirft AbortError aus dem async-iterator; das ist
297
- // gewollt und kein Fehler. Andere Errors weiterreichen.
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
- // Klassifiziert eine geänderte Datei: `hot-reload` für Client-Side
305
- // (Browser-Bundle rebuild + reload), `restart` für Server-Side (Bun-
306
- // Module-Cache zwingt einen Process-Restart durch), `ignore` für
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
- // Heuristik:
310
- // - Tests (`__tests__/` oder `*.test.ts(x)`) → ignore
311
- // - `.ts` / `.tsx` außer Tests:
312
- // - Client-side-Dirs (`/web/`, `/admin/`, `/public/`, `/client/`)
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
- // Warum mehrere Dirs für client-side: in Kumiko-Samples gibt's keine
318
- // Convention. publicstatus splittet `/admin/` (Admin-Bundle) und
319
- // `/public/` (Anonymous-Bundle); beammycar nutzt `/web/` für seine
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
- // Exportiert für Tests; intern wird's von der Watcher-Loop gerufen.
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
- // Plattformpfad-agnostisch: prüfen auf POSIX und Windows-Trenner.
340
- // Wir matchen sowohl `<sep><dir><sep>` als auch trailing-`<sep><dir>`
341
- // (für Watcher-Filenames die als relativer Pfad ankommen).
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
- // Expandiert watchDirs-Patterns auf konkrete Verzeichnisse. Ein Eintrag
365
- // ohne `*` wird als gewöhnlicher Pfad resolved; mit `*` wird er per
366
- // glob expanded und alle Treffer die Verzeichnisse sind übernommen.
367
- // Erlaubt z.B. `"../../../packages/*/src"` statt vier hart-kodierte
368
- // Pfade. Glob ist sync — wird einmal beim Boot ausgewertet, nicht
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 folgt Package-Exports — "./styles.css" in renderer/web's
425
- // package.json. Das Monorepo auflöst direkt auf den Workspace-File, eine
426
- // installierte Fremd-App auf den node_modules-File. Kein `fileURLToPath`
427
- // nötig, Bun gibt schon einen absoluten FS-Pfad zurück.
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
- // Unit-Tests unter vitest/Node landen hier. Ohne Bun können wir die
430
- // Package-Export-Resolution nicht machen — und im Test-Kontext gibt's
431
- // keine echte Tailwind-Pipeline. Skip still, keine Fehlermeldung nötig.
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
- // Initial-Build blockend, damit der erste /styles.css-Request kein
474
- // 404 bekommt. Dann den watcher im Hintergrund mit unref() — sonst
475
- // hing er beim Parent-Crash als orphan-process.
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
- // schon tot oder nie gestartet — nicht weiter relevant
495
+ // Already stopped or never started; no further action is needed.
509
496
  }
510
497
  },
511
498
  };
512
499
  }
513
- /** URL-Pfad unter dem ein Entry ausgeliefert wird. "client" → /client.js
514
- * (Single-Mode-Default), sonst "/client-<name>.js". Single-Source-of-Truth
515
- * damit Routing + Logging dieselbe Konvention nutzen. */
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 oder multi-entry über dieselbe Map) ---
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-Entry: ein Template (htmlPath oder DEFAULT_HTML) für alles.
572
- // Multi-Entry: pro Entry ein Template (entry.htmlPath ?? options.htmlPath
573
- // ?? DEFAULT_HTML). Der hostDispatch wählt zur Request-Zeit.
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
- // Der nackte ioredis-Client (nicht der TestRedis-Wrapper) —
629
- // Parität mit runProdApp, App-Code soll in dev+prod dasselbe sehen.
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 für Caller-spezifische Tables + Seed. Läuft nach den Entity-
638
- // Tabellen damit das Sample auf `stack.db` / `stack.dispatcher`
639
- // zugreifen kann, und VOR dem Server-Start damit der erste HTTP-Request
640
- // bereits gegen einen gefüllten State läuft. Idempotenz ist Caller-
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 konfiguriert den eventDispatcher, startet ihn aber
646
- // NICHT — Integration-Tests drain'en deterministisch via runOnce().
647
- // Ein Dev-Server will das laufende Polling, damit SSE-Broadcasts
648
- // (system-hook sse, Priorität 1001) von selbst an connected Clients
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 (kumiko-framework#1232).
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 (kumiko-framework#2553).
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-middleware still needs a valid JWT to let the request past.
675
- // Nicht genutzt wenn `options.auth` gesetzt ist — dann macht der Client
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 identifiziert diese spezifische Server-Process-Instanz. Wird
689
- // beim Connect an jeden Browser geschickt; Browser merkt sich den
690
- // ersten Wert und refresht wenn beim Reconnect ein anderer kommt
691
- // (= Server wurde restartet, alter JS-Bundle ist stale). Siehe
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 (#3026).
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-Pfad-Lookup: für jede Entry serven wir
785
- // GET /client[-name].js → JS-Bundle
786
- // GET /client[-name].js.map → Sourcemap
787
- // assetPathFor() ist die Single-Source-of-Truth für die URL-Form.
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-Variante: gleicher Lookup mit /.map abgeschnitten.
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-Event: Browser-Snippet vergleicht das mit der ersten
876
- // bootId. Verschiedener Wert nach Reconnect = Server wurde
877
- // restartet → location.reload().
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: any GET to a non-API, non-asset path returns the
896
- // HTML shell. The client-side router then reads location.pathname
897
- // and mounts the right screen. The "no dot" filter skips
898
- // /favicon.ico etc. (let the stack's 404 handler respond).
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 that live outside /api (currently just /sse) have
901
- // to be excluded explicitly, otherwise the catch-all would shadow
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
- // Plus: r.httpRoute-deklarierte Feature-Routes (z.B. /legal/*) liegen
906
- // ebenfalls außerhalb /api und matchen sonst diesen catch-all. Wir
907
- // probieren daher ZUERST stack.app.fetch — wenn Hono eine matchende
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 mitnehmen — prod (runProdApp) fällt für GET UND HEAD auf die
914
- // SPA zurück; ohne das liefert dev 404 wo prod 200 liefert.
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-Dispatch — symmetric zu prod. Ohne hostDispatch
922
- // landet das im Single-Entry-Default ("client" + Schema-Inject).
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 we skip Bun.serve entirely — the handle's
963
- // .fetch() is the test surface. Real dev runs under Bun, where
964
- // Bun.serve wires the listener.
965
- // Bun.serve-Options kommen aus buildBunServeOptions (run-prod-app.ts)
966
- // damit Dev und Prod genau dieselben SSE-relevanten Defaults nutzen
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, oder process-restart bei Schema-Änderungen ---
974
- // Heuristik: alles in `web/` oder `__tests__/` ist client-side oder
975
- // test-only — Hot-Reload reicht (rebuild + broadcast reload). Alles
976
- // andere ist server-side; Bun cached die Module-Imports, also würde ein
977
- // Schema-Change in feature.ts nicht durchschlagen ohne process-restart.
978
- // Wir exiten dann mit Code 75 (EX_TEMPFAIL) — `kumiko-dev` Wrapper
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
- // watcherAbort wird beim stop() ausgelöst → fs.watch beendet die
982
- // async-iteration → kein Watcher überlebt einen Test-Teardown und
983
- // klassifiziert ein rmSync(tmpdir) als "restart needed".
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-Dirs: alle entry-Verzeichnisse (deduped) plus die explizit
987
- // angegebenen watchDirs. In Multi-Entry-Setups liegen die Entries
988
- // oft im selben src/-Verzeichnis (`src/client-admin.tsx` +
989
- // `src/client-public.tsx`) — der Set kollabiert das auf einen
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
- // Alle Entries rebuilden — auch wenn nur eine Datei sich
1012
- // ändert, wir wissen nicht welche Entries sie importieren.
1013
- // Bei zwei Entries mit shared Code triggert ein Edit der
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
- // Watcher zuerst stoppen damit kein onChange während des Teardowns
1054
- // mehr feuert (sonst können tmpdir-rmSync ein process.exit(75)
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 fire on Ctrl-C / kill. Without them, repeated dev
1073
- // restarts leak Postgres pools, lassen Tailwind-Watcher als orphan
1074
- // hängen und (in persistent mode) hinterlassen temp Clients.
1075
- // uncaughtException + unhandledRejection: Crashes hatten den Tailwind-
1076
- // Watcher nicht gekillt, der lief munter weiter im Hintergrund. Jetzt
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"]) {
@@ -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 (Sprint 9.5 Phase 4). */
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;
@@ -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 (kumiko-framework#2340).
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
- // Sprint 9.2 is purely additive: the schema is exposed, but the call-sites
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 — Phase 2 (bundled-features).
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 (Sprint 9.5 Phase 4). */
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 für den Kumiko-Dev-Server. Zwei Schichten:
1
+ // Public API for the Kumiko development server, with two layers:
2
2
  //
3
3
  // - createKumikoServer (low-level)
4
- // Bun.serve-Wrapper der Client-Bundle, /styles.css, AppSchema-
5
- // Injection, SSE-Reload und einen Auto-Mint-JWT-Modus liefert. Nimmt
6
- // features + clientEntry direkt an, kein Auth-Auto-Wiring. Wer
7
- // einen ungewöhnlichen Auth-Setup braucht (alternative Membership-
8
- // Query, eigener Rate-Limiter, custom Login-Routes) geht hier rein.
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
- // Mischt die Standard-Features (config/user/tenant/auth-email-
12
- // password) automatisch dazu wenn `auth` gesetzt ist, wired die
13
- // Login-Routes + Error-Map, ruft seedAdminGuarded im onAfterSetup. Default
14
- // für Sample-Apps und Showcases — 5-10 Zeilen Bootstrap statt 50.
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
- // Build-Toolchain (buildProdBundle, Bun.build, Tailwind-Pipeline, ts-morph) lebt
17
- // im Sub-Path-Export `@cosmicdrift/kumiko-dev-server/build`. Damit zieht der Main-Barrel
18
- // kein Bun-Toolchain-Bundle mehr in Production-Reads (z.B. wenn drizzle-kit
19
- // die App-Config unter Node lädt).
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";