@cosmicdrift/kumiko-dev-server 0.55.0 → 0.56.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 +3 -3
- package/src/codegen/__tests__/render-codegen.test.ts +21 -1
- package/src/codegen/__tests__/run-codegen.test.ts +31 -0
- package/src/codegen/index.ts +1 -0
- package/src/codegen/render.ts +48 -2
- package/src/codegen/run-codegen.ts +44 -3
- package/src/codegen/watch.ts +10 -1
- package/src/create-kumiko-server.ts +2 -2
- package/src/run-dev-app.ts +8 -1
- package/src/run-prod-app.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
50
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
49
|
+
"@cosmicdrift/kumiko-bundled-features": "0.55.1",
|
|
50
|
+
"@cosmicdrift/kumiko-framework": "0.55.1",
|
|
51
51
|
"ts-morph": "^28.0.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
@@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
renderDefineFile,
|
|
7
|
+
renderInlineSchemasFile,
|
|
8
|
+
renderTypesAugmentation,
|
|
9
|
+
renderWriteHandlerTypes,
|
|
10
|
+
} from "../render";
|
|
6
11
|
import type { ScannedEvent } from "../scan-events";
|
|
7
12
|
|
|
8
13
|
describe("renderTypesAugmentation", () => {
|
|
@@ -69,4 +74,19 @@ describe("renderDefineFile", () => {
|
|
|
69
74
|
expect(out).toContain("defineWriteHandler");
|
|
70
75
|
expect(out).toContain("types.generated.d.ts");
|
|
71
76
|
});
|
|
77
|
+
|
|
78
|
+
test("emits createTypedDispatcher when handler QNs provided", () => {
|
|
79
|
+
const out = renderDefineFile(["tenant:write:create", "tenant:write:update"]);
|
|
80
|
+
expect(out).toContain("export type TypedDispatcher");
|
|
81
|
+
expect(out).toContain("export function createTypedDispatcher");
|
|
82
|
+
expect(out).toContain('export type { WriteHandlerQn } from "./types.generated"');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("renderWriteHandlerTypes", () => {
|
|
87
|
+
test("emits union type lines", () => {
|
|
88
|
+
const out = renderWriteHandlerTypes(["tenant:write:create"]);
|
|
89
|
+
expect(out).toContain("export type WriteHandlerQn =");
|
|
90
|
+
expect(out).toContain('| "tenant:write:create"');
|
|
91
|
+
});
|
|
72
92
|
});
|
|
@@ -287,6 +287,37 @@ export default defineFeature("dup", (r) => {
|
|
|
287
287
|
expect(w).toBeDefined();
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
test("emits WriteHandlerQn union and TypedDispatcher when handlerQns passed", () => {
|
|
291
|
+
const appRoot = makeAppDir();
|
|
292
|
+
write(
|
|
293
|
+
appRoot,
|
|
294
|
+
"src/feature/events.ts",
|
|
295
|
+
`import { z } from "zod";\nexport const sSchema = z.object({ id: z.string() });\n`,
|
|
296
|
+
);
|
|
297
|
+
write(
|
|
298
|
+
appRoot,
|
|
299
|
+
"src/feature/feature.ts",
|
|
300
|
+
`import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
301
|
+
import { sSchema } from "./events";
|
|
302
|
+
export default defineFeature("typed", (r) => {
|
|
303
|
+
r.defineEvent("only", sSchema);
|
|
304
|
+
});
|
|
305
|
+
`,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const result = runCodegen({
|
|
309
|
+
appRoot,
|
|
310
|
+
handlerQns: ["typed:write:create"],
|
|
311
|
+
});
|
|
312
|
+
expect(result.skipped).toBe(false);
|
|
313
|
+
|
|
314
|
+
const types = readFileSync(join(appRoot, ".kumiko", "types.generated.d.ts"), "utf-8");
|
|
315
|
+
expect(types).toContain('| "typed:write:create"');
|
|
316
|
+
|
|
317
|
+
const define = readFileSync(join(appRoot, ".kumiko", "define.ts"), "utf-8");
|
|
318
|
+
expect(define).toContain("createTypedDispatcher");
|
|
319
|
+
});
|
|
320
|
+
|
|
290
321
|
test("idempotent — second run does not re-write files", () => {
|
|
291
322
|
const appRoot = makeAppDir();
|
|
292
323
|
write(
|
package/src/codegen/index.ts
CHANGED
package/src/codegen/render.ts
CHANGED
|
@@ -169,7 +169,35 @@ export function renderInlineSchemasFile(
|
|
|
169
169
|
* the import back to "@cosmicdrift/kumiko-framework/engine" and you're back on the
|
|
170
170
|
* loose default. Migration is reversible, no behavioural surface change.
|
|
171
171
|
*/
|
|
172
|
-
export function renderDefineFile(): string {
|
|
172
|
+
export function renderDefineFile(handlerQns: readonly string[] = []): string {
|
|
173
|
+
const typedDispatcherBlock =
|
|
174
|
+
handlerQns.length > 0
|
|
175
|
+
? [
|
|
176
|
+
"",
|
|
177
|
+
`import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`,
|
|
178
|
+
"",
|
|
179
|
+
`export type { WriteHandlerQn } from "./types.generated";`,
|
|
180
|
+
"",
|
|
181
|
+
`/** Dispatcher with write() narrowed to registered handler QNs. */`,
|
|
182
|
+
`export type TypedDispatcher = Omit<Dispatcher, "write"> & {`,
|
|
183
|
+
` write<TData = unknown>(`,
|
|
184
|
+
` type: WriteHandlerQn,`,
|
|
185
|
+
` payload: unknown,`,
|
|
186
|
+
` opts?: WriteOpts,`,
|
|
187
|
+
` ): Promise<WriteResult<TData>>;`,
|
|
188
|
+
`};`,
|
|
189
|
+
"",
|
|
190
|
+
`/** Wrap any Dispatcher so write() only accepts registered QNs at compile time. */`,
|
|
191
|
+
`export function createTypedDispatcher(dispatcher: Dispatcher): TypedDispatcher {`,
|
|
192
|
+
` return {`,
|
|
193
|
+
` ...dispatcher,`,
|
|
194
|
+
` write: (type, payload, opts) => dispatcher.write(type, payload, opts),`,
|
|
195
|
+
` };`,
|
|
196
|
+
`}`,
|
|
197
|
+
"",
|
|
198
|
+
].join("\n")
|
|
199
|
+
: "";
|
|
200
|
+
|
|
173
201
|
const body = [
|
|
174
202
|
HEADER,
|
|
175
203
|
"",
|
|
@@ -228,8 +256,26 @@ export function renderDefineFile(): string {
|
|
|
228
256
|
`): QueryHandlerDefinition<TName, TSchema, TResult, KumikoEventTypeMap> {`,
|
|
229
257
|
` return fwDefineQueryHandler<TName, TSchema, TResult, KumikoEventTypeMap>(def);`,
|
|
230
258
|
`}`,
|
|
231
|
-
|
|
259
|
+
typedDispatcherBlock,
|
|
232
260
|
].join("\n");
|
|
233
261
|
|
|
234
262
|
return body;
|
|
235
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Render `WriteHandlerQn` type lines — exports a union of all registered
|
|
267
|
+
* write handler QNs so custom-screen code can type-check dispatcher.write
|
|
268
|
+
* calls at compile time:
|
|
269
|
+
*
|
|
270
|
+
* ```ts
|
|
271
|
+
* import type { WriteHandlerQn } from "@app/define";
|
|
272
|
+
* dispatcher.write<WriteHandlerQn>("tenant:write:create", payload); // ✓
|
|
273
|
+
* dispatcher.write<WriteHandlerQn>("tenant:write:creat", payload); // ✗ TS error
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
export function renderWriteHandlerTypes(handlerQns: readonly string[]): string {
|
|
277
|
+
if (handlerQns.length === 0) return "";
|
|
278
|
+
|
|
279
|
+
const lines = handlerQns.map((qn) => ` | "${qn}"`);
|
|
280
|
+
return ["", `export type WriteHandlerQn =`, ...lines, ";", ""].join("\n");
|
|
281
|
+
}
|
|
@@ -16,12 +16,22 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
renderDefineFile,
|
|
21
|
+
renderInlineSchemasFile,
|
|
22
|
+
renderTypesAugmentation,
|
|
23
|
+
renderWriteHandlerTypes,
|
|
24
|
+
} from "./render";
|
|
20
25
|
import { type ScanWarning, scanEvents } from "./scan-events";
|
|
21
26
|
|
|
22
27
|
export type CodegenOptions = {
|
|
23
28
|
/** App-Root — `<appRoot>/.kumiko/` ist der Output-Ordner. */
|
|
24
29
|
readonly appRoot: string;
|
|
30
|
+
/** Optional: alle registrierten Write-Handler-QNs. Wenn nicht gesetzt,
|
|
31
|
+
* versucht der Codegen `feature-manifest.json` im appRoot zu lesen.
|
|
32
|
+
* Die Dev-Server-Integration übergibt die QNs direkt aus der gebooteten
|
|
33
|
+
* Registry — CLI ruft sie aus dem Manifest. */
|
|
34
|
+
readonly handlerQns?: readonly string[];
|
|
25
35
|
};
|
|
26
36
|
|
|
27
37
|
export type CodegenResult = {
|
|
@@ -67,7 +77,8 @@ export function runCodegen(opts: CodegenOptions): CodegenResult {
|
|
|
67
77
|
mkdirSync(outputDir, { recursive: true });
|
|
68
78
|
|
|
69
79
|
const typesContent = renderTypesAugmentation(scan.events, outputDir);
|
|
70
|
-
const
|
|
80
|
+
const handlerQns = opts.handlerQns ?? readHandlerQnsFromManifest(opts.appRoot);
|
|
81
|
+
const defineContent = renderDefineFile(handlerQns);
|
|
71
82
|
const schemasContent = renderInlineSchemasFile(scan.events, opts.appRoot);
|
|
72
83
|
// package.json — turns `.kumiko/` into a real installable package
|
|
73
84
|
// named `@app/define`. Apps that declare
|
|
@@ -77,7 +88,13 @@ export function runCodegen(opts: CodegenOptions): CodegenResult {
|
|
|
77
88
|
// is required — yarn classic v1 ignored deps in versionless workspaces.
|
|
78
89
|
const packageJsonContent = renderKumikoPackageJson();
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
// WriteHandlerQn-Union an den types-Content anhängen (optional). Der
|
|
92
|
+
// Dev-Server übergibt handlerQns aus der lebenden Registry (genauer);
|
|
93
|
+
// der CLI-Fallback liest das feature-manifest.json falls vorhanden.
|
|
94
|
+
const finalTypesContent =
|
|
95
|
+
handlerQns.length > 0 ? `${typesContent}${renderWriteHandlerTypes(handlerQns)}` : typesContent;
|
|
96
|
+
|
|
97
|
+
const didWriteTypes = writeIfChanged(typesPath, finalTypesContent);
|
|
81
98
|
const didWriteDefine = writeIfChanged(definePath, defineContent);
|
|
82
99
|
writeIfChanged(packageJsonPath, packageJsonContent);
|
|
83
100
|
const didWriteSchemas =
|
|
@@ -155,3 +172,27 @@ function removeIfExists(path: string): boolean {
|
|
|
155
172
|
rmSync(path, { force: true });
|
|
156
173
|
return true;
|
|
157
174
|
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fallback: liest `feature-manifest.json` aus dem appRoot und extrahiert
|
|
178
|
+
* alle Write-Handler-QNs. Fehlende/stale Manifeste sind kein Fehler —
|
|
179
|
+
* dann wird schlicht keine `WriteHandlerQn`-Union generiert (CLI ohne
|
|
180
|
+
* Manifest oder CI-Setup ohne Manifest-Generator).
|
|
181
|
+
*/
|
|
182
|
+
function readHandlerQnsFromManifest(appRoot: string): readonly string[] {
|
|
183
|
+
try {
|
|
184
|
+
const manifestPath = join(appRoot, "feature-manifest.json");
|
|
185
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
186
|
+
const manifest = JSON.parse(raw) as {
|
|
187
|
+
readonly features: ReadonlyArray<{ readonly writeHandlers?: readonly string[] }>;
|
|
188
|
+
};
|
|
189
|
+
const qns: string[] = [];
|
|
190
|
+
for (const f of manifest.features) {
|
|
191
|
+
if (f.writeHandlers) qns.push(...f.writeHandlers);
|
|
192
|
+
}
|
|
193
|
+
qns.sort();
|
|
194
|
+
return qns;
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/codegen/watch.ts
CHANGED
|
@@ -21,6 +21,12 @@ import { runCodegen } from "./run-codegen";
|
|
|
21
21
|
export type WatchOptions = {
|
|
22
22
|
/** App-Wurzel — gleiche Bedeutung wie für `runCodegen`. */
|
|
23
23
|
readonly appRoot: string;
|
|
24
|
+
/** Optional: alle registrierten Write-Handler-QNs. Durchgereicht an
|
|
25
|
+
* `runCodegen` für die Generierung der `WriteHandlerQn`-Union.
|
|
26
|
+
* Der Dev-Server extrahiert sie beim Boot aus den Features und
|
|
27
|
+
* reicht sie hier rein — der Watcher reicht sie bei jedem Re-Run
|
|
28
|
+
* an `runCodegen` weiter, damit der generated-Type nicht verschwindet. */
|
|
29
|
+
readonly handlerQns?: readonly string[];
|
|
24
30
|
/** Wartet diese Millisekunden zusätzliche Events ab, bevor codegen
|
|
25
31
|
* einmal fährt. 50ms catched typische save-bursts (Editor-Saves
|
|
26
32
|
* feuern oft 2-3 Events: temp-file → rename → cleanup), bleibt aber
|
|
@@ -58,7 +64,10 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
|
|
|
58
64
|
|
|
59
65
|
const fire = () => {
|
|
60
66
|
try {
|
|
61
|
-
const result = runCodegen({
|
|
67
|
+
const result = runCodegen({
|
|
68
|
+
appRoot: opts.appRoot,
|
|
69
|
+
...(opts.handlerQns !== undefined && { handlerQns: opts.handlerQns }),
|
|
70
|
+
});
|
|
62
71
|
if (opts.onResult) {
|
|
63
72
|
opts.onResult(result);
|
|
64
73
|
} else {
|
|
@@ -48,11 +48,11 @@ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
|
48
48
|
// and only resolve the type when Bun is actually around.
|
|
49
49
|
type BunServer = typeof Bun extends undefined ? unknown : ReturnType<typeof Bun.serve>;
|
|
50
50
|
|
|
51
|
-
// biome-ignore lint/suspicious/noConsole: dev-server status logging
|
|
52
51
|
// @wrapper-known semantic-alias
|
|
52
|
+
// biome-ignore lint/suspicious/noConsole: dev-server status logging
|
|
53
53
|
const logInfo = (msg: string): void => console.log(msg);
|
|
54
|
-
// biome-ignore lint/suspicious/noConsole: dev-server error logging
|
|
55
54
|
// @wrapper-known semantic-alias
|
|
55
|
+
// biome-ignore lint/suspicious/noConsole: dev-server error logging
|
|
56
56
|
const logError = (...args: unknown[]): void => console.error(...args);
|
|
57
57
|
|
|
58
58
|
/** Multi-Entry-Mode für Apps die mehrere getrennte Bundles ausliefern
|
package/src/run-dev-app.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
30
30
|
import type { SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
31
31
|
import {
|
|
32
|
+
collectWriteHandlerQns,
|
|
32
33
|
createRegistry,
|
|
33
34
|
type EffectiveFeaturesResolver,
|
|
34
35
|
type FeatureDefinition,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
type SessionUser,
|
|
38
39
|
type TenantId,
|
|
39
40
|
type TierResolverPlugin,
|
|
41
|
+
validateAppCustomScreenWriteQns,
|
|
40
42
|
validateBoot,
|
|
41
43
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
42
44
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
@@ -191,6 +193,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
191
193
|
// CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
|
|
192
194
|
// Socket oder Watcher (codegen-Write) aufgeht.
|
|
193
195
|
validateBoot(features);
|
|
196
|
+
validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
|
|
194
197
|
|
|
195
198
|
// Codegen + File-Watcher — schreibt `<appRoot>/.kumiko/types.generated.d.ts`
|
|
196
199
|
// + `define.ts` aus den r.defineEvent-Aufrufen der App, einmal beim
|
|
@@ -202,7 +205,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
202
205
|
// Watcher läuft solange der Dev-Server lebt; close() bei Shutdown
|
|
203
206
|
// wird über das createKumikoServer-Handle implizit erledigt (Bun's
|
|
204
207
|
// process-exit räumt fs.watch-handles auf).
|
|
205
|
-
|
|
208
|
+
// Sammelt alle Write-Handler-QNs für den Codegen — damit
|
|
209
|
+
// `types.generated.d.ts` eine `WriteHandlerQn`-Union exportieren
|
|
210
|
+
// kann, die dispatcher.write-Aufrufe client-seitig typisiert.
|
|
211
|
+
const handlerQns = collectWriteHandlerQns(features);
|
|
212
|
+
watchAndRegenerate({ appRoot: process.cwd(), handlerQns: [...handlerQns] });
|
|
206
213
|
|
|
207
214
|
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
208
215
|
// tenantTierResolver-extension. If found AND user didn't supply own
|
package/src/run-prod-app.ts
CHANGED
|
@@ -47,12 +47,14 @@ import { createSseBroker, type SseBroker } from "@cosmicdrift/kumiko-framework/a
|
|
|
47
47
|
import { createDbConnection, type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
48
48
|
import {
|
|
49
49
|
buildAppSchema,
|
|
50
|
+
collectWriteHandlerQns,
|
|
50
51
|
createRegistry,
|
|
51
52
|
type EffectiveFeaturesResolver,
|
|
52
53
|
type FeatureDefinition,
|
|
53
54
|
findTierResolverUsage,
|
|
54
55
|
type TenantId,
|
|
55
56
|
type TierResolverPlugin,
|
|
57
|
+
validateAppCustomScreenWriteQns,
|
|
56
58
|
validateBoot,
|
|
57
59
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
58
60
|
import {
|
|
@@ -592,6 +594,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
592
594
|
});
|
|
593
595
|
|
|
594
596
|
validateBoot(features);
|
|
597
|
+
validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
|
|
595
598
|
const registry = createRegistry(features);
|
|
596
599
|
|
|
597
600
|
// C1 boot-mode exit: validators ran + registry built; no DB/Redis client
|