@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.
- package/dist/codegen/render.js +2 -2
- package/dist/codegen/run-codegen.js +26 -20
- package/dist/codegen/scan-events.d.ts +9 -4
- package/dist/codegen/scan-events.js +1 -1
- package/dist/create-kumiko-server.js +6 -1
- package/dist/run-dev-app.js +11 -15
- package/dist/scaffold-app.js +3 -3
- package/dist/scaffold-deploy.js +3 -0
- package/dist/schema-apply.js +2 -13
- package/package.json +6 -6
- package/src/changes.json +18 -0
package/dist/codegen/render.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
55
|
-
const handlerQns = opts.handlerQns ??
|
|
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
|
-
|
|
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:
|
|
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:
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
readonly
|
|
42
|
-
|
|
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
|
|
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
|
}
|
package/dist/run-dev-app.js
CHANGED
|
@@ -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
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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`
|
package/dist/scaffold-app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.",
|
package/dist/scaffold-deploy.js
CHANGED
|
@@ -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" };
|
package/dist/schema-apply.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
70
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
71
|
-
"@cosmicdrift/kumiko-server-runtime": "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",
|