@cosmicdrift/kumiko-server-runtime 0.327.0 → 0.328.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-server-runtime",
3
- "version": "0.327.0",
3
+ "version": "0.328.0",
4
4
  "description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -62,6 +62,10 @@
62
62
  "types": "./src/extra-routes-deps.ts",
63
63
  "default": "./src/extra-routes-deps.ts"
64
64
  },
65
+ "./renderer-web-fonts": {
66
+ "types": "./src/renderer-web-fonts.ts",
67
+ "default": "./src/renderer-web-fonts.ts"
68
+ },
65
69
  "./resolve-tailwind-cli": {
66
70
  "types": "./src/resolve-tailwind-cli.ts",
67
71
  "default": "./src/resolve-tailwind-cli.ts"
@@ -76,9 +80,9 @@
76
80
  }
77
81
  },
78
82
  "dependencies": {
79
- "@cosmicdrift/kumiko-bundled-features": "0.327.0",
80
- "@cosmicdrift/kumiko-framework": "0.327.0",
81
- "@cosmicdrift/kumiko-headless": "0.327.0",
83
+ "@cosmicdrift/kumiko-bundled-features": "0.328.0",
84
+ "@cosmicdrift/kumiko-framework": "0.328.0",
85
+ "@cosmicdrift/kumiko-headless": "0.328.0",
82
86
  "temporal-polyfill": "^0.3.2"
83
87
  },
84
88
  "publishConfig": {
@@ -118,6 +118,46 @@ describe("buildProdBundle in-process (Bun.build)", () => {
118
118
  }
119
119
  });
120
120
 
121
+ test("renderer-web stylesheet → woff2 files land where the CSS points, no external font URL", async () => {
122
+ const dir = await mkdtemp(join(REPO_ROOT, ".inproc-fonts-"));
123
+ const cwd = join(dir, "app");
124
+ try {
125
+ await mkdir(join(cwd, "src"), { recursive: true });
126
+ await writeFile(
127
+ join(cwd, "src/client.ts"),
128
+ `const root = document.getElementById("root"); if (root) root.textContent = "hi";`,
129
+ );
130
+ await writeFile(
131
+ join(cwd, "src/styles.css"),
132
+ `@import "@cosmicdrift/kumiko-renderer-web/styles.css";\n`,
133
+ );
134
+ await mkdir(join(cwd, "public"), { recursive: true });
135
+ await writeFile(
136
+ join(cwd, "public/index.html"),
137
+ `<!doctype html><html><head><link rel="stylesheet" href="/styles.css" /></head><body><div id="root"></div><script type="module" src="/client.js"></script></body></html>`,
138
+ );
139
+ await writeFile(join(cwd, "package.json"), `{"name":"inproc-fonts","private":true}`);
140
+
141
+ const result = await buildProdBundle({ cwd, stylesheet: "src/styles.css" });
142
+
143
+ const css = await readFile(join(cwd, "dist", result.manifest["styles.css"] ?? ""), "utf8");
144
+ const referenced = [
145
+ ...css.matchAll(/url\("?(\/assets\/kumiko\/fonts\/[a-z0-9-]+\.woff2)"?\)/g),
146
+ ].map((m) => m[1] ?? "");
147
+ expect(referenced.length).toBeGreaterThanOrEqual(10);
148
+ for (const url of referenced) {
149
+ const file = join(cwd, "dist", url);
150
+ expect(existsSync(file)).toBe(true);
151
+ // woff2 magic number
152
+ expect((await readFile(file)).subarray(0, 4).toString("latin1")).toBe("wOF2");
153
+ }
154
+ expect(css).not.toMatch(/url\(\s*["']?(https?:)?\/\//);
155
+ expect(css).not.toContain("data:font");
156
+ } finally {
157
+ await rm(dir, { recursive: true, force: true });
158
+ }
159
+ });
160
+
121
161
  test("missing stylesheet override → tailwind rejects", async () => {
122
162
  const dir = await mkdtemp(join(REPO_ROOT, ".inproc-styles-miss-"));
123
163
  const cwd = join(dir, "app");
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
  import { runTailwindOnce } from "../build-prod-bundle";
12
12
 
13
13
  const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..");
14
- const FRAMEWORK_LIGHT_PRIMARY = "--color-primary:#171717";
14
+ const FRAMEWORK_LIGHT_PRIMARY = "--color-primary:#1a1c1e";
15
15
 
16
16
  function bunAvailable(): boolean {
17
17
  try {
@@ -0,0 +1,43 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
4
+ import { runPiiEventBackfill } from "@cosmicdrift/kumiko-framework/migrations";
5
+
6
+ export const SKIP_PII_BACKFILL_ENV = "KUMIKO_SKIP_PII_BACKFILL";
7
+
8
+ type ShutdownRegistry = {
9
+ registerShutdownHook(name: string, fn: () => Promise<void>): void;
10
+ };
11
+
12
+ // Runs in the background instead of blocking boot: the first run after a bump
13
+ // scans the whole store and would delay readiness in proportion to its size.
14
+ // Every batch is its own transaction, so an abort at any point is safe.
15
+ export function startPiiEventBackfillOnBoot(opts: {
16
+ readonly db: DbConnection;
17
+ readonly registry: Registry;
18
+ readonly lifecycle: ShutdownRegistry;
19
+ readonly envSource: Readonly<Record<string, string | undefined>>;
20
+ }): void {
21
+ if (opts.envSource[SKIP_PII_BACKFILL_ENV] === "1") {
22
+ // skip: operator opted out via KUMIKO_SKIP_PII_BACKFILL
23
+ return;
24
+ }
25
+
26
+ const log = createFallbackLogger("pii-event-backfill");
27
+ const abortController = new AbortController();
28
+ const run = runPiiEventBackfill(opts.db, opts.registry, {
29
+ signal: abortController.signal,
30
+ }).then(
31
+ () => undefined,
32
+ (e: unknown) => {
33
+ log.error("PII event backfill crashed; it will be retried on the next boot", {
34
+ error: e instanceof Error ? e.message : String(e),
35
+ });
36
+ },
37
+ );
38
+
39
+ opts.lifecycle.registerShutdownHook("piiEventBackfill", async () => {
40
+ abortController.abort();
41
+ await run;
42
+ });
43
+ }
@@ -53,6 +53,11 @@ import { tmpdir } from "node:os";
53
53
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
54
54
  import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
55
55
  import { Temporal } from "temporal-polyfill";
56
+ import {
57
+ RENDERER_WEB_FONT_FILE_PATTERN,
58
+ RENDERER_WEB_FONTS_DIST_DIR,
59
+ resolveRendererWebFontsDir,
60
+ } from "./renderer-web-fonts";
56
61
  import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
57
62
 
58
63
  // Bun-Runtime-Check als module-level Konstante: alle Build-Schritte
@@ -186,6 +191,7 @@ export async function buildProdBundle(options: BuildProdBundleOptions = {}): Pro
186
191
  const filename = `styles-${hash}.css`;
187
192
  await writeFile(join(assetsDir, filename), css);
188
193
  manifest["styles.css"] = `/${ASSETS_DIR}/${filename}`;
194
+ await copyRendererWebFonts(cwd, outDir);
189
195
  }
190
196
 
191
197
  // 4. Bun.build pro Entry (multi-entry produces N bundles + shared chunks).
@@ -641,6 +647,22 @@ export function assertRendererWebShellPresent(css: string, stylesheet: ResolvedS
641
647
  );
642
648
  }
643
649
 
650
+ // The compiled CSS points at absolute /assets/kumiko/fonts/<file>.woff2 URLs
651
+ // (see renderer-web-fonts.ts), so the files have to sit at exactly that path in
652
+ // dist. Skipped when renderer-web isn't resolvable — then no such CSS exists.
653
+ async function copyRendererWebFonts(cwd: string, outDir: string): Promise<void> {
654
+ const fontsDir = resolveRendererWebFontsDir([cwd]);
655
+ // skip: renderer-web not resolvable, so no font-referencing CSS exists.
656
+ if (fontsDir === undefined || !existsSync(fontsDir)) return;
657
+ const target = join(outDir, RENDERER_WEB_FONTS_DIST_DIR);
658
+ await mkdir(target, { recursive: true });
659
+ for (const file of readdirSync(fontsDir)) {
660
+ if (RENDERER_WEB_FONT_FILE_PATTERN.test(file)) {
661
+ await cp(join(fontsDir, file), join(target, file));
662
+ }
663
+ }
664
+ }
665
+
644
666
  // @internal — exported nur für Unit-Tests.
645
667
  export function discoverHtmlTemplate(cwd: string): string | undefined {
646
668
  for (const candidate of ["index.html", "public/index.html"]) {
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.328.0",
4
+ "type": "improvement",
5
+ "title": "Production build copies the renderer-web fonts",
6
+ "detail": "`buildProdBundle` copies the IBM Plex woff2 files to `dist/assets/kumiko/fonts/`, where the compiled CSS expects them. The new export `@cosmicdrift/kumiko-server-runtime/renderer-web-fonts` holds the URL prefix, the file name allowlist and the resolver, and the woff2 MIME type is known to the static file server."
7
+ },
2
8
  {
3
9
  "version": "0.323.0",
4
10
  "type": "breaking",
@@ -0,0 +1,33 @@
1
+ // The renderer-web stylesheet references its IBM Plex woff2 files by absolute
2
+ // same-origin URL. Relative url() would 404: the CSS is compiled into a temp
3
+ // dir (dev) or hashed into dist/assets (prod), away from the source fonts.
4
+ // data: URIs are out because apps run with `font-src 'self'`.
5
+
6
+ import { dirname, join } from "node:path";
7
+
8
+ export const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
9
+
10
+ /** dist-relative folder the prod build copies the fonts into. */
11
+ export const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
12
+
13
+ /** Allowlist for servable font file names; also rules out path segments. */
14
+ export const RENDERER_WEB_FONT_FILE_PATTERN = /^[a-z0-9-]+\.woff2$/;
15
+
16
+ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
17
+
18
+ /** Directory of the woff2 files next to renderer-web's styles.css, or
19
+ * undefined when the package can't be resolved from any `searchFrom` dir
20
+ * (or outside Bun, which does the package-exports resolution). */
21
+ export function resolveRendererWebFontsDir(searchFrom: readonly string[]): string | undefined {
22
+ if (!hasBun) return undefined;
23
+ const bun = (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }).Bun;
24
+ for (const from of searchFrom) {
25
+ try {
26
+ const stylesheet = bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", from);
27
+ return join(dirname(stylesheet), "fonts");
28
+ } catch {
29
+ // try the next search root
30
+ }
31
+ }
32
+ return undefined;
33
+ }
@@ -110,6 +110,8 @@ export function mimeTypeFor(filePath: string): string {
110
110
  return "application/xml; charset=utf-8";
111
111
  case "webmanifest":
112
112
  return "application/manifest+json";
113
+ case "woff2":
114
+ return "font/woff2";
113
115
  default:
114
116
  return "application/octet-stream";
115
117
  }
@@ -149,6 +149,7 @@ import Redis from "ioredis";
149
149
  import { applyBootSeeds } from "./boot/apply-boot-seeds";
150
150
  import { resolveBootCrypto } from "./boot/boot-crypto";
151
151
  import { jobRunLoggerCallbacks } from "./boot/job-run-logger";
152
+ import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot";
152
153
  import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-serve-options";
153
154
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
154
155
  import { makeDispatchSystemWrite, type SystemWireDeps } from "./extra-routes-deps";
@@ -1301,6 +1302,12 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
1301
1302
  }
1302
1303
 
1303
1304
  await entrypoint.start();
1305
+ startPiiEventBackfillOnBoot({
1306
+ db,
1307
+ registry,
1308
+ lifecycle: entrypoint.lifecycle,
1309
+ envSource,
1310
+ });
1304
1311
 
1305
1312
  // 11. Build the fetch-handler. Static-fallback for non-/api/ paths
1306
1313
  // wired via a wrapper so Hono owns /api/* + extraRoutes and disk
@@ -60,6 +60,7 @@ import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
60
60
  import Redis from "ioredis";
61
61
  import { resolveBootCrypto } from "./boot/boot-crypto";
62
62
  import { jobRunLoggerCallbacks } from "./boot/job-run-logger";
63
+ import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot";
63
64
  import { composeFeatures } from "./compose-features";
64
65
  import { makeDispatchSystemWrite, type SystemWireDeps } from "./extra-routes-deps";
65
66
  import { assertPiiBootInvariants } from "./pii-boot-gate";
@@ -320,6 +321,12 @@ export async function runWorkerApp(options: RunWorkerAppOptions): Promise<Worker
320
321
  };
321
322
 
322
323
  await entrypoint.start();
324
+ startPiiEventBackfillOnBoot({
325
+ db,
326
+ registry,
327
+ lifecycle: entrypoint.lifecycle,
328
+ envSource,
329
+ });
323
330
 
324
331
  if (options.wireComponents) {
325
332
  await options.wireComponents({