@cosmicdrift/kumiko-dev-server 0.334.0 → 0.336.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.
@@ -71,7 +71,7 @@ export function renderTypesAugmentation(events, outputDirAbs) {
71
71
  }
72
72
  const importLines = [];
73
73
  // `z` from zod is needed for `z.infer<typeof X>`; we import it once.
74
- importLines.push(`import type { z } from "zod";`);
74
+ importLines.push(`import type * as z from "zod";`);
75
75
  // Stable order — sort module paths alphabetically; identifiers within
76
76
  // a module also alphabetically. Idempotent output.
77
77
  for (const [modPath, idents] of [...importsByPath.entries()].sort(([a], [b]) => a.localeCompare(b))) {
@@ -124,7 +124,7 @@ export function renderInlineSchemasFile(events, appRootAbs) {
124
124
  "// build`) or let the dev-server regenerate it — otherwise the z.infer",
125
125
  "// type drifts from the runtime schema.",
126
126
  "",
127
- `import { z } from "zod";`,
127
+ `import * as z from "zod";`,
128
128
  "",
129
129
  ];
130
130
  // Sort by const-name for stable output.
@@ -15,6 +15,7 @@
15
15
  // kein leeres Verzeichnis.
16
16
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
+ import * as z from "zod";
18
19
  import { renderDefineFile, renderInlineSchemasFile, renderTypesAugmentation, renderWriteHandlerTypes, TYPED_DISPATCHER_MARKER, WRITE_HANDLER_QN_MARKER, } from "./render.js";
19
20
  import { scanEvents } from "./scan-events.js";
20
21
  export function runCodegen(opts) {
@@ -51,9 +52,8 @@ export function runCodegen(opts) {
51
52
  // "the manifest doesn't exist" — both come back as an empty array —
52
53
  // so an empty result here is a known-lossy read, not authoritative
53
54
  // data the way run-dev-app's live registry is.
54
- const usingManifestFallback = opts.handlerQns === undefined;
55
- const handlerQns = opts.handlerQns ?? readHandlerQnsFromManifest(opts.appRoot);
56
- const hasHandlerQns = handlerQns.length > 0;
55
+ const manifestRead = opts.handlerQns === undefined ? readHandlerQnsFromManifest(opts.appRoot) : undefined;
56
+ const handlerQns = opts.handlerQns ?? (manifestRead?.kind === "ok" ? manifestRead.qns : []);
57
57
  const defineContent = renderDefineFile(handlerQns);
58
58
  const schemasContent = renderInlineSchemasFile(scan.events, opts.appRoot);
59
59
  // package.json — turns `.kumiko/` into a real installable package
@@ -73,7 +73,9 @@ export function runCodegen(opts) {
73
73
  // not a missing-manifest gap, so it clears the block like before.
74
74
  // Only the manifest fallback coming back empty is ambiguous enough to
75
75
  // warrant carrying the previous block forward instead of dropping it.
76
- const shouldPreserveStaleBlock = !hasHandlerQns && usingManifestFallback;
76
+ // A valid manifest that lists zero handlers is real data and clears the block;
77
+ // only a missing or unreadable manifest is ambiguous.
78
+ const shouldPreserveStaleBlock = manifestRead !== undefined && manifestRead.kind !== "ok";
77
79
  const finalTypesContent = shouldPreserveStaleBlock
78
80
  ? withPreservedBlock(typesContent, typesPath, WRITE_HANDLER_QN_MARKER, LEGACY_WRITE_HANDLER_QN_ANCHOR)
79
81
  : `${typesContent}${renderWriteHandlerTypes(handlerQns)}`;
@@ -90,7 +92,9 @@ export function runCodegen(opts) {
90
92
  ? [
91
93
  ...scan.warnings,
92
94
  {
93
- message: "No feature-manifest.json found — the WriteHandlerQn/TypedDispatcher block could not be determined from here; run the dev-server to get the exact handler list.",
95
+ message: manifestRead?.kind === "invalid"
96
+ ? `feature-manifest.json invalid: ${manifestRead.reason} — the WriteHandlerQn/TypedDispatcher block could not be determined from here; fix or regenerate the manifest, or run the dev-server.`
97
+ : "No feature-manifest.json found — the WriteHandlerQn/TypedDispatcher block could not be determined from here; run the dev-server to get the exact handler list.",
94
98
  },
95
99
  ]
96
100
  : scan.warnings;
@@ -194,26 +198,28 @@ function removeIfExists(path) {
194
198
  rmSync(path, { force: true });
195
199
  return true;
196
200
  }
201
+ const featureManifestSchema = z.object({
202
+ features: z.array(z.object({ writeHandlers: z.array(z.string()).optional() })),
203
+ });
197
204
  /**
198
- * Fallback: liest `feature-manifest.json` aus dem appRoot und extrahiert
199
- * alle Write-Handler-QNs. Fehlende/stale Manifeste sind kein Fehler —
200
- * dann wird schlicht keine `WriteHandlerQn`-Union generiert (CLI ohne
201
- * Manifest oder CI-Setup ohne Manifest-Generator).
205
+ * Fallback: reads `feature-manifest.json` from appRoot and extracts all
206
+ * write-handler QNs. A missing manifest is not an error (CLI without a
207
+ * manifest generator); a broken one is reported separately. Either way the
208
+ * existing block is left untouched.
202
209
  */
203
210
  function readHandlerQnsFromManifest(appRoot) {
211
+ const manifestPath = join(appRoot, "feature-manifest.json");
212
+ if (!existsSync(manifestPath))
213
+ return { kind: "missing" };
204
214
  try {
205
- const manifestPath = join(appRoot, "feature-manifest.json");
206
- const raw = readFileSync(manifestPath, "utf-8");
207
- const manifest = JSON.parse(raw);
208
- const qns = [];
209
- for (const f of manifest.features) {
210
- if (f.writeHandlers)
211
- qns.push(...f.writeHandlers);
212
- }
215
+ const parsed = featureManifestSchema.safeParse(JSON.parse(readFileSync(manifestPath, "utf-8")));
216
+ if (!parsed.success)
217
+ return { kind: "invalid", reason: z.prettifyError(parsed.error) };
218
+ const qns = parsed.data.features.flatMap((f) => f.writeHandlers ?? []);
213
219
  qns.sort();
214
- return qns;
220
+ return { kind: "ok", qns };
215
221
  }
216
- catch {
217
- return [];
222
+ catch (e) {
223
+ return { kind: "invalid", reason: e instanceof Error ? e.message : String(e) };
218
224
  }
219
225
  }
@@ -35,11 +35,16 @@ export type SchemaSource = {
35
35
  * rename them. */
36
36
  readonly generatedConstName: string;
37
37
  };
38
+ /** `file` + `line` come as a pair; the location-less variant covers warnings
39
+ * that aren't tied to a scanned source line (e.g. a codegen-level warning
40
+ * about a missing manifest file). */
38
41
  export type ScanWarning = {
39
- /** Absent for warnings that aren't tied to a scanned source line
40
- * (e.g. a codegen-level warning about a missing manifest file). */
41
- readonly file?: string;
42
- readonly line?: number;
42
+ readonly file: string;
43
+ readonly line: number;
44
+ readonly message: string;
45
+ } | {
46
+ readonly file?: undefined;
47
+ readonly line?: undefined;
43
48
  readonly message: string;
44
49
  };
45
50
  export type ScanResult = {
@@ -357,7 +357,7 @@ function looksLikeZodCall(call) {
357
357
  // Walk the callee head — `z.something(...)` or `z.something.foo(...)`,
358
358
  // anything that traces back to an Identifier `z`. Conservative: we
359
359
  // don't try to verify it's the actual zod-import (the runtime check
360
- // happens through the schemas-file's `import { z } from "zod"` anyway,
360
+ // happens through the schemas-file's `import * as z from "zod"` anyway,
361
361
  // which fails loudly if the user's `z` is something else).
362
362
  let cur = call.getExpression();
363
363
  while (cur.asKind(SyntaxKind.PropertyAccessExpression) || cur.asKind(SyntaxKind.CallExpression)) {
@@ -187,6 +187,10 @@ function resolvePublicFilePath(pathname, publicDir) {
187
187
  catch {
188
188
  return undefined;
189
189
  }
190
+ // A NUL byte makes readFile throw ERR_INVALID_ARG_VALUE (not ENOENT), which
191
+ // would surface as a 500 instead of a router miss.
192
+ if (decoded.includes("\0"))
193
+ return undefined;
190
194
  const resolved = resolve(publicDir, `.${decoded}`);
191
195
  return resolved === publicDir || resolved.startsWith(publicDir + sep) ? resolved : undefined;
192
196
  }
@@ -235,8 +239,9 @@ async function servePublicFile(pathname, publicDir) {
235
239
  }
236
240
  catch (err) {
237
241
  const code = err.code;
238
- if (code === "ENOENT" || code === "EISDIR" || code === "ENOTDIR")
242
+ if (code === "ENOENT" || code === "EISDIR" || code === "ENOTDIR" || code === "ENAMETOOLONG") {
239
243
  return undefined;
244
+ }
240
245
  throw err;
241
246
  }
242
247
  }
@@ -51,26 +51,22 @@ export async function runDevApp(options) {
51
51
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
52
52
  });
53
53
  // An explicitly wired file provider (options.files) satisfies the
54
- // FILE_STORAGE_PROVIDER boot gate — set it before validateBoot runs. Only
55
- // if WE set it (532/2): deleting it after use instead of leaving a
56
- // permanent process.env mutation, so a second runDevApp call in the same
57
- // process (no files this time) doesn't fall through the gate falsely.
58
- const setFileStorageProviderEnv = options.files !== undefined && process.env[FILE_STORAGE_PROVIDER_ENV] === undefined;
59
- if (setFileStorageProviderEnv) {
60
- process.env[FILE_STORAGE_PROVIDER_ENV] = FILE_STORAGE_PROVIDER_BOOT_SENTINEL;
61
- }
54
+ // FILE_STORAGE_PROVIDER boot gate. Passed via the validateBoot env option
55
+ // instead of mutating process.env, so nothing leaks into later runDevApp
56
+ // calls in the same process.
57
+ const baseBootEnv = options.validateBootOptions?.env ?? process.env;
58
+ const bootOptions = options.files !== undefined && baseBootEnv[FILE_STORAGE_PROVIDER_ENV] === undefined
59
+ ? {
60
+ ...options.validateBootOptions,
61
+ env: { ...baseBootEnv, [FILE_STORAGE_PROVIDER_ENV]: FILE_STORAGE_PROVIDER_BOOT_SENTINEL },
62
+ }
63
+ : options.validateBootOptions;
62
64
  // Boot-Validation als allererstes — vor fs-Watcher und Server. Dieselbe
63
65
  // Fehlerklasse (unqualifizierte nav-/handler-QNs, screen-access etc.),
64
66
  // die früher nur runProdApp fing und sonst erst den Prod-Pod im
65
67
  // CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
66
68
  // Socket oder Watcher (codegen-Write) aufgeht.
67
- try {
68
- validateBoot(features, options.validateBootOptions);
69
- }
70
- finally {
71
- if (setFileStorageProviderEnv)
72
- delete process.env[FILE_STORAGE_PROVIDER_ENV];
73
- }
69
+ validateBoot(features, bootOptions);
74
70
  warnIfNonUtcServerTimeZone();
75
71
  validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
76
72
  // Codegen + File-Watcher — schreibt `<appRoot>/.kumiko/types.generated.d.ts`
@@ -563,7 +563,7 @@ function renderClient(appName) {
563
563
  "// here — symmetric to APP_FEATURES on the server side.",
564
564
  "",
565
565
  'import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";',
566
- `import { tasksClient } from "./features/tasks/web";`,
566
+ 'import { tasksClient } from "./features/tasks/web";',
567
567
  'import { type AppSchema, createKumikoApp, DefaultAppShell } from "@cosmicdrift/kumiko-renderer-web";',
568
568
  'import type { ReactNode } from "react";',
569
569
  "",
@@ -742,7 +742,7 @@ function renderKumikoSchema() {
742
742
  'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
743
743
  'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
744
744
  'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
745
- `import { APP_FEATURES, HAS_AUTH } from "../src/run-config";`,
745
+ 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
746
746
  "",
747
747
  "export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
748
748
  " includeBundled: HAS_AUTH,",
@@ -764,7 +764,7 @@ function renderBinKumiko() {
764
764
  'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
765
765
  'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
766
766
  'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
767
- `import { APP_FEATURES, HAS_AUTH } from "../src/run-config";`,
767
+ 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
768
768
  "",
769
769
  "const [, , cmd, ...rest] = Bun.argv;",
770
770
  "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
@@ -170,6 +170,9 @@ function resolveDeployConfig(deployConfigRaw, appName) {
170
170
  // over-long appName fails loud here instead of producing a DB user
171
171
  // Postgres itself would reject at migrate-time.
172
172
  if (!DB_USER_RE.test(dbUser)) {
173
+ if (config.dbUser === undefined) {
174
+ throw new Error(`scaffoldDeploy: appName "${appName}" cannot serve as the default DB user (must match ${DB_USER_RE}); set package.json#kumiko.deploy.dbUser`);
175
+ }
173
176
  throw new Error(`scaffoldDeploy: invalid package.json#kumiko.deploy.dbUser — effective value "${dbUser}" (defaulted from appName) does not match ${DB_USER_RE}`);
174
177
  }
175
178
  return { dbUser, stackNetwork: config.stackNetwork ?? "discover" };
@@ -10,7 +10,7 @@ import { join } from "node:path";
10
10
  import { createDbConnection, runMigrationsFromDir } from "@cosmicdrift/kumiko-framework/db";
11
11
  import { createRegistry } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
13
- import { queueRebuildsFromMarkers, runPendingRebuilds, } from "@cosmicdrift/kumiko-framework/migrations";
13
+ import { queueRebuildsFromMarkers, reportPendingRebuildRun, runPendingRebuilds, } from "@cosmicdrift/kumiko-framework/migrations";
14
14
  import { createEventConsumerStateTable, createProjectionStateTable, } from "@cosmicdrift/kumiko-framework/pipeline";
15
15
  import { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
16
16
  export async function runSchemaApply(opts) {
@@ -62,18 +62,7 @@ export async function runSchemaApply(opts) {
62
62
  });
63
63
  const registry = createRegistry(composeFeatures([...opts.features], opts));
64
64
  const rebuildRun = await runPendingRebuilds(db, registry, { thisRunTables });
65
- if (rebuildRun.rebuilt.length > 0) {
66
- console.log(` Rebuild ${rebuildRun.rebuilt.length} Projection(s)…`);
67
- for (const r of rebuildRun.rebuilt) {
68
- console.log(` ↻ ${r.projection} (${r.eventsProcessed} events)`);
69
- }
70
- console.log("");
71
- }
72
- if (rebuildRun.failed.length > 0) {
73
- throw new Error(`Projection rebuild failed for: ${rebuildRun.failed
74
- .map((f) => `${f.projection} (${f.error})`)
75
- .join("; ")}. Table(s) stay queued in kumiko_pending_rebuilds — retried on the next apply.`);
76
- }
65
+ reportPendingRebuildRun(rebuildRun, console.log);
77
66
  return 0;
78
67
  }
79
68
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.334.0",
3
+ "version": "0.336.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>",
@@ -64,11 +64,11 @@
64
64
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
65
65
  },
66
66
  "dependencies": {
67
- "@cosmicdrift/kumiko-bundled-features": "0.334.0",
68
- "@cosmicdrift/kumiko-framework": "0.334.0",
69
- "@cosmicdrift/kumiko-headless": "0.334.0",
70
- "@cosmicdrift/kumiko-renderer-web": "0.334.0",
71
- "@cosmicdrift/kumiko-server-runtime": "0.334.0",
67
+ "@cosmicdrift/kumiko-bundled-features": "0.336.0",
68
+ "@cosmicdrift/kumiko-framework": "0.336.0",
69
+ "@cosmicdrift/kumiko-headless": "0.336.0",
70
+ "@cosmicdrift/kumiko-renderer-web": "0.336.0",
71
+ "@cosmicdrift/kumiko-server-runtime": "0.336.0",
72
72
  "bun-types": "^1.3.13",
73
73
  "ts-morph": "^28.0.0",
74
74
  "zod": "^4.4.3"
package/src/changes.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.335.0",
4
+ "type": "fix",
5
+ "title": "Codegen validates feature-manifest.json and only keeps the stale handler block when it is missing or invalid",
6
+ "detail": "`kumiko-build` codegen now validates `feature-manifest.json`. A valid manifest that lists no write handlers removes the generated `WriteHandlerQn` and typed-dispatcher block instead of keeping a stale one. A broken manifest keeps the block and warns with its own \"feature-manifest.json invalid\" message."
7
+ },
8
+ {
9
+ "version": "0.335.0",
10
+ "type": "fix",
11
+ "title": "Codegen emits namespace zod imports",
12
+ "detail": "Generated `schemas.generated.ts` and `types.generated.d.ts` now import zod as a namespace (`import * as z from \"zod\"`), matching the framework's own convention. Regenerate with `kumiko build` to pick it up."
13
+ },
14
+ {
15
+ "version": "0.335.0",
16
+ "type": "fix",
17
+ "title": "Dev public-file fallback returns 404 for NUL or over-long paths, deploy scaffold blames appName for an invalid default DB user",
18
+ "detail": "The dev-server public-file fallback answers a request path containing an encoded NUL byte (`%00`) or an over-long file name with the normal 404 instead of a 500. The scaffold deploy step now names `appName` as the cause when it is too long to serve as the default DB user."
19
+ },
2
20
  {
3
21
  "version": "0.330.0",
4
22
  "type": "improvement",