@cosmicdrift/kumiko-dev-server 0.304.0 → 0.305.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.304.0",
3
+ "version": "0.305.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -59,10 +59,10 @@
59
59
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
60
60
  },
61
61
  "dependencies": {
62
- "@cosmicdrift/kumiko-bundled-features": "0.304.0",
63
- "@cosmicdrift/kumiko-framework": "0.304.0",
64
- "@cosmicdrift/kumiko-headless": "0.304.0",
65
- "@cosmicdrift/kumiko-server-runtime": "0.304.0",
62
+ "@cosmicdrift/kumiko-bundled-features": "0.305.0",
63
+ "@cosmicdrift/kumiko-framework": "0.305.0",
64
+ "@cosmicdrift/kumiko-headless": "0.305.0",
65
+ "@cosmicdrift/kumiko-server-runtime": "0.305.0",
66
66
  "ts-morph": "^28.0.0"
67
67
  },
68
68
  "publishConfig": {
@@ -17,7 +17,11 @@ import {
17
17
  } from "@cosmicdrift/kumiko-framework/engine";
18
18
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
19
19
  import { z } from "zod";
20
- import { createKumikoServer, type KumikoServerHandle } from "../create-kumiko-server";
20
+ import {
21
+ createKumikoServer,
22
+ type KumikoServerHandle,
23
+ STYLESHEET_WATCH_ENV,
24
+ } from "../create-kumiko-server";
21
25
 
22
26
  const TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
23
27
 
@@ -471,6 +475,45 @@ describe("createKumikoServer — stylesheet tailwind failure (graceful)", () =>
471
475
  });
472
476
  });
473
477
 
478
+ describe("createKumikoServer — stylesheet watch disabled via env", () => {
479
+ test("KUMIKO_DEV_STYLESHEET_WATCH=0 serves the built CSS and never spawns a --watch process", async () => {
480
+ const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-css-no-watch-"));
481
+ const entryCss = join(tmpDir, "styles.css");
482
+ // Plain CSS, no `@import "tailwindcss"` — the marker property survives
483
+ // the Tailwind CLI/lightningcss pass unchanged, unlike a color value
484
+ // which lightningcss may renormalize.
485
+ writeFileSync(entryCss, ".kumiko-probe { --probe-marker: 42; }\n");
486
+ const previousWatchEnv = process.env[STYLESHEET_WATCH_ENV];
487
+ process.env[STYLESHEET_WATCH_ENV] = "0";
488
+ try {
489
+ handle = await createKumikoServer({
490
+ features: [probeFeature],
491
+ port: 0,
492
+ installSignalHandlers: false,
493
+ stylesheet: entryCss,
494
+ });
495
+
496
+ const cssRes = await handle.fetch(new Request("http://localhost/styles.css"));
497
+ expect(cssRes.status).toBe(200);
498
+ const css = await cssRes.text();
499
+ expect(css).toContain("--probe-marker: 42");
500
+
501
+ // -ww: unwrapped, full argv — a truncated args column would make the
502
+ // negative assert below pass vacuously. The pid check proves ps
503
+ // actually returned live process rows on this machine/CI runner.
504
+ const ps = Bun.spawnSync(["ps", "-A", "-ww", "-o", "pid=,args="]);
505
+ const psOutput = ps.stdout.toString();
506
+ const lines = psOutput.split("\n");
507
+ expect(lines.some((line) => line.includes(String(process.pid)))).toBe(true);
508
+ expect(lines.some((line) => line.includes(entryCss) && line.includes("--watch"))).toBe(false);
509
+ } finally {
510
+ if (previousWatchEnv === undefined) delete process.env[STYLESHEET_WATCH_ENV];
511
+ else process.env[STYLESHEET_WATCH_ENV] = previousWatchEnv;
512
+ rmSync(tmpDir, { recursive: true, force: true });
513
+ }
514
+ });
515
+ });
516
+
474
517
  describe("createKumikoServer — real Bun.build (buildClient)", () => {
475
518
  test("clientEntry without _buildBundle produces a JS bundle via Bun.build", async () => {
476
519
  const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-real-build-"));
@@ -11,7 +11,11 @@ import { describe, expect, test } from "bun:test";
11
11
  import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
- import { resolveStylesheet } from "../create-kumiko-server";
14
+ import {
15
+ resolveStylesheet,
16
+ resolveStylesheetWatch,
17
+ STYLESHEET_WATCH_ENV,
18
+ } from "../create-kumiko-server";
15
19
 
16
20
  describe("resolveStylesheet", () => {
17
21
  test("string → resolved absolute path", () => {
@@ -108,3 +112,21 @@ describe("resolveStylesheet", () => {
108
112
  }
109
113
  });
110
114
  });
115
+
116
+ describe("resolveStylesheetWatch", () => {
117
+ test("an explicit option wins over the env", () => {
118
+ expect(resolveStylesheetWatch({ stylesheetWatch: false }, {})).toBe(false);
119
+ expect(resolveStylesheetWatch({ stylesheetWatch: true }, { [STYLESHEET_WATCH_ENV]: "0" })).toBe(
120
+ true,
121
+ );
122
+ });
123
+
124
+ test("no option, env=0 → false", () => {
125
+ expect(resolveStylesheetWatch({}, { [STYLESHEET_WATCH_ENV]: "0" })).toBe(false);
126
+ });
127
+
128
+ test("no option, no or other env value → true", () => {
129
+ expect(resolveStylesheetWatch({}, {})).toBe(true);
130
+ expect(resolveStylesheetWatch({}, { [STYLESHEET_WATCH_ENV]: "1" })).toBe(true);
131
+ });
132
+ });
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.305.0",
4
+ "type": "improvement",
5
+ "title": "createKumikoServer/runDevApp gain stylesheetWatch (env KUMIKO_DEV_STYLESHEET_WATCH=0) to build Tailwind CSS once without a --watch process",
6
+ "detail": "`defineAppE2eConfig` sets `KUMIKO_DEV_STYLESHEET_WATCH=0`, so the dev-server it boots builds CSS once and stops instead of keeping a Tailwind `--watch` process alive. Tailwind v4's watcher subscribes to the whole app cwd recursively with no gitignore filter, so every Playwright artifact write under `test-results/` previously counted as a rebuild trigger."
7
+ },
2
8
  {
3
9
  "version": "0.301.0",
4
10
  "type": "fix",
@@ -162,18 +162,24 @@ export type CreateKumikoServerOptions = {
162
162
  readonly js: string;
163
163
  readonly map: string;
164
164
  }>;
165
- /** Absolute path to the CSS entry (typischerweise styles.css mit
166
- * @import "tailwindcss"). Der dev-server startet dann den
167
- * Tailwind-CLI als watcher und servt das kompilierte CSS unter
168
- * /styles.css.
165
+ /** Absolute path to the CSS entry (typically styles.css with
166
+ * @import "tailwindcss"). The dev-server builds it with the Tailwind
167
+ * CLI and serves the result at /styles.css; `stylesheetWatch` decides
168
+ * whether the CLI keeps running with `--watch` afterwards.
169
169
  *
170
- * Wenn `undefined` UND `clientEntry` gesetzt: resolve die
171
- * `@cosmicdrift/kumiko-renderer-web/styles.css`-Default via Package-Exports.
172
- * So muss kein Sample mehr den monorepo-relativen Pfad
173
- * ../../packages/renderer-web/src/styles.css hardcoden.
170
+ * When `undefined` and a client entry is set, the app's own
171
+ * src/styles.css wins, otherwise `@cosmicdrift/kumiko-renderer-web/styles.css`
172
+ * resolves via package exports, so no sample hardcodes a monorepo-relative path.
174
173
  *
175
- * `stylesheet: false` → CSS-Pipeline explizit deaktivieren. */
174
+ * `stylesheet: false` disables the CSS pipeline. */
176
175
  readonly stylesheet?: string | false;
176
+ /** `true`: the Tailwind CLI keeps running with `--watch` after the
177
+ * initial build. `false`: CSS is built once at boot, no watcher.
178
+ * When `undefined`, env `KUMIKO_DEV_STYLESHEET_WATCH=0` means `false`,
179
+ * anything else `true`. `defineAppE2eConfig` sets that env: Tailwind v4
180
+ * subscribes to the whole app cwd without a gitignore filter, so every
181
+ * Playwright artifact write under test-results/ would trigger a rebuild. */
182
+ readonly stylesheetWatch?: boolean;
177
183
  /** Optional HTML template served at `GET /`. The dev-server injects
178
184
  * a `<script type="module" src="/client.js">` and a reload-listener
179
185
  * snippet into `</body>` if those aren't already there. Defaults to a
@@ -259,6 +265,10 @@ export type KumikoServerHandle = {
259
265
  const CSRF_COOKIE = "kumiko_csrf";
260
266
  const AUTH_COOKIE = "kumiko_auth";
261
267
 
268
+ // Duplicated in packages/testing/src/e2e/constants.ts: the Playwright config
269
+ // runs under Node and cannot import this module.
270
+ export const STYLESHEET_WATCH_ENV = "KUMIKO_DEV_STYLESHEET_WATCH";
271
+
262
272
  // Reload snippet injected into every page-load so the browser
263
273
  // subscribes to /_reload without the HTML needing to hard-code it.
264
274
  //
@@ -661,12 +671,24 @@ export function resolveStylesheet(options: CreateKumikoServerOptions): string |
661
671
  }
662
672
  }
663
673
 
664
- // Startet den Tailwind-CLI als watch-Prozess. Failure-Mode ist
665
- // non-fatal (return undefined): kann der CLI nicht resolved werden
666
- // oder failt der initial-Build (z.B. flakiges Netz, fehlende
667
- // Dependency), bootet der Server ohne CSS statt zu sterben.
668
- async function startTailwindWatcher(
674
+ // The E2E template can only reach an app's own server entry through its env,
675
+ // so the env has to count whenever the option is left undefined.
676
+ //
677
+ // @internal — exported for unit tests only, not re-exported from the index.
678
+ export function resolveStylesheetWatch(
679
+ options: Pick<CreateKumikoServerOptions, "stylesheetWatch">,
680
+ env: Readonly<Record<string, string | undefined>> = process.env,
681
+ ): boolean {
682
+ if (options.stylesheetWatch !== undefined) return options.stylesheetWatch;
683
+ return env[STYLESHEET_WATCH_ENV] !== "0";
684
+ }
685
+
686
+ // Non-fatal by design (returns undefined): if the CLI can't be resolved or
687
+ // the initial build fails (flaky network, missing dependency), the server
688
+ // boots without CSS instead of dying.
689
+ async function startTailwindPipeline(
669
690
  entryCss: string,
691
+ { watch }: { readonly watch: boolean },
670
692
  ): Promise<{ outPath: string; kill: () => void } | undefined> {
671
693
  const bunResolver = hasBun
672
694
  ? (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }).Bun
@@ -681,7 +703,11 @@ async function startTailwindWatcher(
681
703
  }
682
704
  const outDir = mkdtempSync(join(tmpdir(), "kumiko-tw-"));
683
705
  const outPath = join(outDir, "styles.css");
684
- logInfo(`[kumiko-server] tailwind watcher → ${outPath}`);
706
+ logInfo(
707
+ watch
708
+ ? `[kumiko-server] tailwind watcher → ${outPath}`
709
+ : `[kumiko-server] tailwind one-shot build → ${outPath}`,
710
+ );
685
711
  const bunPath = process.argv[0] ?? "bun";
686
712
  // Initial-Build blockend, damit der erste /styles.css-Request kein
687
713
  // 404 bekommt. Dann den watcher im Hintergrund mit unref() — sonst
@@ -701,6 +727,9 @@ async function startTailwindWatcher(
701
727
  logError("[kumiko-server] tailwind one-shot-build fehlgeschlagen — booting ohne CSS:", err);
702
728
  return undefined;
703
729
  }
730
+ if (!watch) {
731
+ return { outPath, kill: () => {} };
732
+ }
704
733
  const watcher = spawn(bunPath, ["run", cliPath, "-i", entryCss, "-o", outPath, "--watch"], {
705
734
  stdio: "inherit",
706
735
  });
@@ -775,21 +804,20 @@ export async function createKumikoServer(
775
804
  }
776
805
 
777
806
  // --- Tailwind stylesheet (optional) ---
778
- // Tailwind-CLI läuft im watch-mode, schreibt in ein temp-file, der
779
- // dev-server liest es bei jedem /styles.css-Request frisch. Nicht
780
- // Super-Performant, aber keine in-memory-Signal-Gymnastik nötig
781
- // und der Browser-Reload kommt eh nur nach Bundle-Rebuild.
807
+ // The Tailwind CLI writes to a temp file that every /styles.css request
808
+ // reads fresh: no in-memory signalling needed, and the browser only
809
+ // reloads after a bundle rebuild anyway.
782
810
  //
783
- // Default-Resolution: wenn kein `stylesheet` übergeben und ein
784
- // `clientEntry` existiert, resolve die styles.css aus
785
- // `@cosmicdrift/kumiko-renderer-web` via Package-Exports. Bun.resolveSync liefert
786
- // einen absoluten Pfad — funktioniert sowohl im Monorepo (Workspace-
787
- // Link) als auch in einer installierten Fremd-App (node_modules).
811
+ // Without an explicit `stylesheet`, the default resolves through the
812
+ // package exports of `@cosmicdrift/kumiko-renderer-web`, which works both
813
+ // for a workspace link and for an installed app (node_modules).
788
814
  let stylesheetPath: string | undefined;
789
815
  let killTailwind: (() => void) | undefined;
790
816
  const resolvedStylesheet = resolveStylesheet(options);
791
817
  if (resolvedStylesheet !== undefined) {
792
- const handle = await startTailwindWatcher(resolvedStylesheet);
818
+ const handle = await startTailwindPipeline(resolvedStylesheet, {
819
+ watch: resolveStylesheetWatch(options),
820
+ });
793
821
  if (handle !== undefined) {
794
822
  stylesheetPath = handle.outPath;
795
823
  killTailwind = handle.kill;
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ export {
55
55
  createKumikoServer,
56
56
  type KumikoServerHandle,
57
57
  resolveStylesheet,
58
+ STYLESHEET_WATCH_ENV,
58
59
  } from "./create-kumiko-server";
59
60
  export { type FrameworkCoreEnv, frameworkCoreEnvSchema } from "./env-schema";
60
61
  export type {
@@ -196,6 +196,8 @@ export type RunDevAppOptions = {
196
196
  /** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer-web/styles.css`
197
197
  * wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
198
198
  readonly stylesheet?: string | false;
199
+ /** Tailwind `--watch` process on/off, see `CreateKumikoServerOptions.stylesheetWatch`. */
200
+ readonly stylesheetWatch?: boolean;
199
201
  /** Eigenes HTML-Template; sonst minimal-Default (#root + client.js).
200
202
  * Im Multi-Entry-Mode ist es das Fallback-Template, wenn ein einzelner
201
203
  * Entry kein eigenes htmlPath setzt. */
@@ -479,6 +481,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
479
481
  ...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
480
482
  ...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
481
483
  ...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
484
+ ...(options.stylesheetWatch !== undefined && { stylesheetWatch: options.stylesheetWatch }),
482
485
  ...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
483
486
  ...(options.port !== undefined && { port: options.port }),
484
487
  ...(options.watchDirs !== undefined && { watchDirs: options.watchDirs }),