@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.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/bin/kumiko-schema-check.ts +7 -0
- package/package.json +12 -7
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/build-server-bundle.test.ts +62 -0
- package/src/__tests__/compose-stacks.test.ts +271 -0
- package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
- package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
- package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/few-shot-corpus.test.ts +16 -11
- package/src/__tests__/merge-extra-context.test.ts +56 -0
- package/src/__tests__/resolve-stylesheet.test.ts +16 -0
- package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
- package/src/__tests__/scaffold-app-feature.test.ts +59 -6
- package/src/__tests__/scaffold-app.test.ts +34 -2
- package/src/__tests__/schema-apply.integration.test.ts +5 -2
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
- package/src/__tests__/walkthrough.integration.test.ts +22 -6
- package/src/build-server-bundle.ts +33 -15
- package/src/build.ts +1 -2
- package/src/codegen/__tests__/render-codegen.test.ts +2 -1
- package/src/codegen/__tests__/run-codegen.test.ts +2 -2
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
- package/src/codegen/__tests__/watch.test.ts +1 -2
- package/src/codegen/render.ts +6 -1
- package/src/codegen/scan-events.ts +7 -5
- package/src/codegen/watch.ts +7 -4
- package/src/compose-stacks.ts +184 -0
- package/src/create-kumiko-server.ts +28 -5
- package/src/few-shot-corpus.ts +4 -3
- package/src/index.ts +30 -12
- package/src/run-dev-app.ts +195 -61
- package/src/scaffold-app-feature.ts +118 -15
- package/src/scaffold-app.ts +151 -79
- package/src/scaffold-demo-tasks.ts +233 -0
- package/src/schema-apply.ts +15 -2
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +61 -0
- package/src/welcome-banner.ts +1 -4
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -311
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -129
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -840
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/apply-boot-seeds.ts +0 -18
- package/src/build-prod-bundle.ts +0 -697
- package/src/compose-features.ts +0 -145
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -24
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app.ts +0 -1492
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
|
@@ -13,6 +13,9 @@ const SCAFFOLD_FILES = [
|
|
|
13
13
|
"bunfig.toml",
|
|
14
14
|
"bunfig.ci.toml",
|
|
15
15
|
"src/run-config.ts",
|
|
16
|
+
"src/features/tasks/feature.ts",
|
|
17
|
+
"src/features/tasks/index.ts",
|
|
18
|
+
"src/seed.ts",
|
|
16
19
|
"kumiko/schema.ts",
|
|
17
20
|
"bin/main.ts",
|
|
18
21
|
"bin/dev.ts",
|
|
@@ -89,6 +92,7 @@ describe("scaffoldApp", () => {
|
|
|
89
92
|
|
|
90
93
|
const kumikoBin = readFileSync(join(dest, "bin/kumiko.ts"), "utf-8");
|
|
91
94
|
expect(kumikoBin).toContain("runSchemaCli");
|
|
95
|
+
expect(kumikoBin).toContain("runConsumerCli");
|
|
92
96
|
expect(kumikoBin).toContain("includeBundled: HAS_AUTH");
|
|
93
97
|
});
|
|
94
98
|
|
|
@@ -112,9 +116,13 @@ describe("scaffoldApp", () => {
|
|
|
112
116
|
expect(client).toContain("createKumikoApp");
|
|
113
117
|
expect(client).toContain("emailPasswordClient");
|
|
114
118
|
expect(client).toContain("DefaultAppShell");
|
|
115
|
-
expect(client).toContain("shell:
|
|
119
|
+
expect(client).toContain("shell: AppShell");
|
|
120
|
+
expect(client).toContain("function AppShell(");
|
|
121
|
+
expect(client).toContain("brand={<span");
|
|
122
|
+
expect(client).toContain(">my-shop</span>");
|
|
116
123
|
expect(client).toContain('from "@cosmicdrift/kumiko-renderer-web"');
|
|
117
124
|
expect(client).toContain('from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web"');
|
|
125
|
+
expect(client).toContain("tasksClient");
|
|
118
126
|
});
|
|
119
127
|
|
|
120
128
|
test(".env.example carries KUMIKO_DEV_DB_NAME default so reboots are persistent", async () => {
|
|
@@ -178,6 +186,27 @@ describe("scaffoldApp", () => {
|
|
|
178
186
|
expect(readme).toContain("- `delivery`");
|
|
179
187
|
});
|
|
180
188
|
|
|
189
|
+
test("callExpression/export-callability mismatch throws instead of silently mis-instantiating", async () => {
|
|
190
|
+
const dest = join(tmp, "my-shop");
|
|
191
|
+
await expect(
|
|
192
|
+
scaffoldApp({
|
|
193
|
+
name: "my-shop",
|
|
194
|
+
destination: dest,
|
|
195
|
+
features: [
|
|
196
|
+
{
|
|
197
|
+
name: "delivery",
|
|
198
|
+
importPath: "@cosmicdrift/kumiko-bundled-features/delivery",
|
|
199
|
+
exportName: "createDeliveryFeature",
|
|
200
|
+
// createDeliveryFeature is a factory function, but this claims it's
|
|
201
|
+
// a plain object export (no trailing "()") — must not silently push
|
|
202
|
+
// the function itself as a FeatureDefinition.
|
|
203
|
+
callExpression: "createDeliveryFeature",
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
}),
|
|
207
|
+
).rejects.toThrow(/is.*callable but callExpression/);
|
|
208
|
+
});
|
|
209
|
+
|
|
181
210
|
test("bin/main.ts contains runProdApp + auth.admin stub + staticDir", async () => {
|
|
182
211
|
const dest = join(tmp, "my-shop");
|
|
183
212
|
await scaffoldApp({ name: "my-shop", destination: dest });
|
|
@@ -202,13 +231,15 @@ describe("scaffoldApp", () => {
|
|
|
202
231
|
);
|
|
203
232
|
});
|
|
204
233
|
|
|
205
|
-
test("src/run-config.ts mounts secrets + sessions + HAS_AUTH", async () => {
|
|
234
|
+
test("src/run-config.ts mounts secrets + sessions + tasks + HAS_AUTH", async () => {
|
|
206
235
|
const dest = join(tmp, "my-shop");
|
|
207
236
|
await scaffoldApp({ name: "my-shop", destination: dest });
|
|
208
237
|
|
|
209
238
|
const runConfig = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
|
|
210
239
|
expect(runConfig).toContain("createSecretsFeature()");
|
|
211
240
|
expect(runConfig).toContain("createSessionsFeature()");
|
|
241
|
+
expect(runConfig).toContain("tasksFeature");
|
|
242
|
+
expect(runConfig).toContain('from "./features/tasks"');
|
|
212
243
|
expect(runConfig).toContain("export const APP_FEATURES");
|
|
213
244
|
expect(runConfig).toContain("export const HAS_AUTH");
|
|
214
245
|
});
|
|
@@ -362,6 +393,7 @@ describe("scaffoldApp", () => {
|
|
|
362
393
|
const cfg = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
|
|
363
394
|
expect(cfg).not.toContain("createSecretsFeature");
|
|
364
395
|
expect(cfg).not.toContain("createSessionsFeature");
|
|
396
|
+
expect(cfg).toContain("tasksFeature");
|
|
365
397
|
});
|
|
366
398
|
|
|
367
399
|
test("deterministic tenantId for same name (reproducible boots)", async () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `relation "kumiko_events" does not exist`. Dieser Test fährt den echten
|
|
7
7
|
// Pfad gegen eine leere DB + den idempotenten Re-Run.
|
|
8
8
|
|
|
9
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
|
|
10
10
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
@@ -76,7 +76,7 @@ describe("runSchemaApply", () => {
|
|
|
76
76
|
expect(await tableExists(conn.db, "public.read_thing")).toBe(true);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test("rebuild-Marker für nicht-registrierte Tabelle → kein Crash, 0", async () => {
|
|
79
|
+
test("rebuild-Marker für nicht-registrierte Tabelle → kein Crash, 0, aber laut warnen (522/3)", async () => {
|
|
80
80
|
writeFileSync(
|
|
81
81
|
join(migDir, "0002_more.sql"),
|
|
82
82
|
`CREATE TABLE "read_more" ("id" text PRIMARY KEY);`,
|
|
@@ -86,7 +86,10 @@ describe("runSchemaApply", () => {
|
|
|
86
86
|
JSON.stringify({ version: 1, tables: ["read_more"] }),
|
|
87
87
|
);
|
|
88
88
|
|
|
89
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
89
90
|
expect(await runSchemaApply({ ...APPLY, appCwd })).toBe(0);
|
|
90
91
|
expect(await tableExists(conn.db, "public.read_more")).toBe(true);
|
|
92
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Table "read_more"'));
|
|
93
|
+
warn.mockRestore();
|
|
91
94
|
});
|
|
92
95
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { setupTestStackFromFeatures } from "../setup-test-stack-from-features";
|
|
4
|
+
|
|
5
|
+
const noopFeature = defineFeature("noop-app", () => {});
|
|
6
|
+
|
|
7
|
+
describe("setupTestStackFromFeatures", () => {
|
|
8
|
+
test("includeBundled prepends config/user/tenant/auth", async () => {
|
|
9
|
+
const stack = await setupTestStackFromFeatures([noopFeature], { includeBundled: true });
|
|
10
|
+
try {
|
|
11
|
+
for (const name of ["config", "user", "tenant", "auth-email-password", "noop-app"]) {
|
|
12
|
+
expect(stack.registry.getFeature(name)).toBeDefined();
|
|
13
|
+
}
|
|
14
|
+
} finally {
|
|
15
|
+
await stack.cleanup();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("config preset wires configResolver in extraContext", async () => {
|
|
20
|
+
const stack = await setupTestStackFromFeatures([noopFeature], {
|
|
21
|
+
includeBundled: true,
|
|
22
|
+
presets: ["config"],
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
expect(stack.registry.getFeature("config")).toBeDefined();
|
|
26
|
+
} finally {
|
|
27
|
+
await stack.cleanup();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -13,10 +13,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
13
13
|
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
17
|
+
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
16
18
|
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
17
19
|
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
18
20
|
import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
-
import { composeFeatures } from "
|
|
21
|
+
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
20
22
|
import { scaffoldApp } from "../scaffold-app";
|
|
21
23
|
import { scaffoldAppFeature } from "../scaffold-app-feature";
|
|
22
24
|
|
|
@@ -77,18 +79,32 @@ describe("walkthrough — DX-3.1 snapshot", () => {
|
|
|
77
79
|
// a dummy defineFeature here — the scaffold-side of "notesFeature"
|
|
78
80
|
// (file-content) is pinned in test 2; this test pins the runtime-side
|
|
79
81
|
// (composeFeatures auto-prepend behaviour the walkthrough claims).
|
|
82
|
+
//
|
|
83
|
+
// sessions now requires auth-foundation (#1370/#1371), which itself
|
|
84
|
+
// needs a tokenVerifier provider mounted (#1368) — PAT here, mirroring
|
|
85
|
+
// the scaffold picker's real-world default. Not part of the walkthrough's
|
|
86
|
+
// advertised 7-feature count (that count is about includeBundled's
|
|
87
|
+
// auto-mount, unrelated to this coupling), so both are added on top.
|
|
80
88
|
const notesFeature = defineFeature("notes", () => {});
|
|
81
|
-
const APP_FEATURES = [
|
|
89
|
+
const APP_FEATURES = [
|
|
90
|
+
createSecretsFeature(),
|
|
91
|
+
authFoundationFeature,
|
|
92
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
93
|
+
createSessionsFeature(),
|
|
94
|
+
notesFeature,
|
|
95
|
+
];
|
|
82
96
|
|
|
83
97
|
const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
|
|
84
|
-
//
|
|
85
|
-
expect(composed.length).toBe(
|
|
98
|
+
// 5 explicit + 4 auto-mounted bundled = 9 total features.
|
|
99
|
+
expect(composed.length).toBe(9);
|
|
86
100
|
|
|
87
101
|
const composedNames = composed.map((f) => f.name).sort();
|
|
88
102
|
expect(composedNames).toEqual([
|
|
89
103
|
"auth-email-password",
|
|
104
|
+
"auth-foundation",
|
|
90
105
|
"config",
|
|
91
106
|
"notes",
|
|
107
|
+
"personal-access-tokens",
|
|
92
108
|
"secrets",
|
|
93
109
|
"sessions",
|
|
94
110
|
"tenant",
|
|
@@ -97,9 +113,9 @@ describe("walkthrough — DX-3.1 snapshot", () => {
|
|
|
97
113
|
|
|
98
114
|
// validateBoot must pass (no missing-requires, no schema-errors).
|
|
99
115
|
expect(() => validateBoot(composed)).not.toThrow();
|
|
100
|
-
// Registry must contain all
|
|
116
|
+
// Registry must contain all 9 features.
|
|
101
117
|
const registry = createRegistry(composed);
|
|
102
|
-
expect(registry.features.size).toBe(
|
|
118
|
+
expect(registry.features.size).toBe(9);
|
|
103
119
|
});
|
|
104
120
|
|
|
105
121
|
test("bin/main.ts contains the auth.admin stub the walkthrough relies on", async () => {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
39
39
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
40
40
|
import { dirname, join, resolve } from "node:path";
|
|
41
|
+
import { parseJsonOrThrow, parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
41
42
|
|
|
42
43
|
const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
43
44
|
|
|
@@ -46,6 +47,12 @@ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
|
46
47
|
// ioredis), dynamic-require (postgres, temporal-polyfill) — alles was unter
|
|
47
48
|
// Bun-bundling bricht. (drizzle-kit/drizzle-orm sind raus: der Migrate-Pfad
|
|
48
49
|
// nutzt jetzt den framework-eigenen runMigrationsFromDir, kein drizzle-kit.)
|
|
50
|
+
// meilisearch: deliberately here, not in BUILD_ONLY_EXTERNALS below — apps
|
|
51
|
+
// that import createMeilisearchAdapter (kumiko-framework/search/meilisearch)
|
|
52
|
+
// reference the package at runtime (no native binding, plain HTTP client,
|
|
53
|
+
// but a real runtime import). Found 2026-07-19: money-horse prod crash
|
|
54
|
+
// "Cannot find package 'meilisearch'" because it used to sit in
|
|
55
|
+
// BUILD_ONLY_EXTERNALS instead.
|
|
49
56
|
const RUNTIME_EXTERNALS = [
|
|
50
57
|
"@node-rs/argon2",
|
|
51
58
|
"bullmq",
|
|
@@ -54,6 +61,7 @@ const RUNTIME_EXTERNALS = [
|
|
|
54
61
|
"temporal-polyfill",
|
|
55
62
|
"pino",
|
|
56
63
|
"pino-pretty",
|
|
64
|
+
"meilisearch",
|
|
57
65
|
] as const;
|
|
58
66
|
|
|
59
67
|
// Pakete die nur im Build-Stack erscheinen (transitive Imports im Framework),
|
|
@@ -62,7 +70,6 @@ const RUNTIME_EXTERNALS = [
|
|
|
62
70
|
// Bundle — der Marker schaltet nur das resolution-during-build ab. NICHT in
|
|
63
71
|
// runtime-deps.
|
|
64
72
|
const BUILD_ONLY_EXTERNALS = [
|
|
65
|
-
"meilisearch",
|
|
66
73
|
"@planetscale/database",
|
|
67
74
|
"@libsql/client",
|
|
68
75
|
"better-sqlite3",
|
|
@@ -178,7 +185,7 @@ export async function buildServerBundle(
|
|
|
178
185
|
entries.sort((a, b) => a.file.localeCompare(b.file));
|
|
179
186
|
const totalBytes = [...entries, ...chunks].reduce((sum, e) => sum + e.sizeBytes, 0);
|
|
180
187
|
|
|
181
|
-
const runtimeDeps = await resolveRuntimeDepsVersions(repoRoot, [
|
|
188
|
+
const runtimeDeps = await resolveRuntimeDepsVersions(cwd, repoRoot, [
|
|
182
189
|
...RUNTIME_EXTERNALS,
|
|
183
190
|
...(options.extraRuntimeExternals ?? []),
|
|
184
191
|
]);
|
|
@@ -217,29 +224,35 @@ function findRepoRoot(start: string): string | undefined {
|
|
|
217
224
|
return undefined;
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
// Versionen für RUNTIME_EXTERNALS auflösen: erst aus
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
227
|
+
// Versionen für RUNTIME_EXTERNALS auflösen: erst aus den installierten
|
|
228
|
+
// node_modules/@cosmicdrift/*-Packages (relativ zu cwd — funktioniert für
|
|
229
|
+
// jede Consumer-App), dann als Zusatz aus packages/framework + bundled-
|
|
230
|
+
// features (Workspace-Checkout des Frameworks selbst). Fallback auf "*"
|
|
231
|
+
// wenn keine Quelle den Namen kennt.
|
|
224
232
|
async function resolveRuntimeDepsVersions(
|
|
233
|
+
cwd: string,
|
|
225
234
|
repoRoot: string | undefined,
|
|
226
235
|
packages: readonly string[],
|
|
227
236
|
): Promise<Record<string, string>> {
|
|
228
237
|
const out: Record<string, string> = {};
|
|
229
|
-
if (!repoRoot) {
|
|
230
|
-
for (const pkg of packages) out[pkg] = "*";
|
|
231
|
-
return out;
|
|
232
|
-
}
|
|
233
238
|
|
|
234
239
|
const pinSources = [
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
...(repoRoot
|
|
241
|
+
? [
|
|
242
|
+
join(repoRoot, "packages/framework/package.json"),
|
|
243
|
+
join(repoRoot, "packages/bundled-features/package.json"),
|
|
244
|
+
]
|
|
245
|
+
: []),
|
|
246
|
+
// node_modules applied last so its versions win over the repo-root
|
|
247
|
+
// fallback — see the function comment above (#1217 bug class).
|
|
248
|
+
join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
|
|
249
|
+
join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
|
|
237
250
|
];
|
|
238
251
|
const allDeps: Record<string, string> = {};
|
|
239
252
|
for (const path of pinSources) {
|
|
240
253
|
if (!existsSync(path)) continue;
|
|
241
254
|
const raw = await readFile(path, "utf-8");
|
|
242
|
-
const parsed =
|
|
255
|
+
const parsed = parseJsonOrThrow<{ dependencies?: Record<string, string> }>(raw, path);
|
|
243
256
|
Object.assign(allDeps, parsed.dependencies ?? {});
|
|
244
257
|
}
|
|
245
258
|
for (const pkg of packages) {
|
|
@@ -251,12 +264,17 @@ async function resolveRuntimeDepsVersions(
|
|
|
251
264
|
function derivePkgName(cwd: string): string {
|
|
252
265
|
const pkgJson = join(cwd, "package.json");
|
|
253
266
|
if (!existsSync(pkgJson)) return "kumiko-app-runtime";
|
|
267
|
+
// existsSync/readFileSync race (deleted between the two calls) or a
|
|
268
|
+
// permission error must fall back like a missing file, not crash the
|
|
269
|
+
// build — read here too, not just the JSON.parse below.
|
|
270
|
+
let raw: string;
|
|
254
271
|
try {
|
|
255
|
-
|
|
256
|
-
return parsed.name ? `${parsed.name}-runtime` : "kumiko-app-runtime";
|
|
272
|
+
raw = readFileSync(pkgJson, "utf-8");
|
|
257
273
|
} catch {
|
|
258
274
|
return "kumiko-app-runtime";
|
|
259
275
|
}
|
|
276
|
+
const parsed = parseJsonSafe<{ name?: string }>(raw, {});
|
|
277
|
+
return parsed.name ? `${parsed.name}-runtime` : "kumiko-app-runtime";
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
export function formatServerBuildResult(
|
package/src/build.ts
CHANGED
|
@@ -10,11 +10,10 @@ export {
|
|
|
10
10
|
buildProdBundle,
|
|
11
11
|
type ClientEntry,
|
|
12
12
|
discoverClientEntries,
|
|
13
|
-
discoverClientEntry,
|
|
14
13
|
discoverHtmlTemplate,
|
|
15
14
|
formatBuildResult,
|
|
16
15
|
injectAssetTags,
|
|
17
|
-
} from "
|
|
16
|
+
} from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
|
|
18
17
|
|
|
19
18
|
export {
|
|
20
19
|
type BuildServerBundleEntry,
|
|
@@ -79,7 +79,8 @@ describe("renderDefineFile", () => {
|
|
|
79
79
|
const out = renderDefineFile(["tenant:write:create", "tenant:write:update"]);
|
|
80
80
|
expect(out).toContain("export type TypedDispatcher");
|
|
81
81
|
expect(out).toContain("export function createTypedDispatcher");
|
|
82
|
-
expect(out).toContain('
|
|
82
|
+
expect(out).toContain('import type { WriteHandlerQn } from "./types.generated"');
|
|
83
|
+
expect(out).toContain("export type { WriteHandlerQn };");
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
86
|
|
|
@@ -283,7 +283,7 @@ export default defineFeature("dup", (r) => {
|
|
|
283
283
|
const result = runCodegen({ appRoot });
|
|
284
284
|
expect(result.eventCount).toBe(1);
|
|
285
285
|
expect(result.warnings.length).toBeGreaterThanOrEqual(1);
|
|
286
|
-
const w = result.warnings.find((w) => w.
|
|
286
|
+
const w = result.warnings.find((w) => w.message.includes("duplicate"));
|
|
287
287
|
expect(w).toBeDefined();
|
|
288
288
|
});
|
|
289
289
|
|
|
@@ -368,7 +368,7 @@ export default defineFeature("inline", (r) => {
|
|
|
368
368
|
const result = runCodegen({ appRoot });
|
|
369
369
|
expect(result.eventCount).toBe(0);
|
|
370
370
|
expect(result.warnings.length).toBe(1);
|
|
371
|
-
expect(result.warnings[0]?.
|
|
371
|
+
expect(result.warnings[0]?.message).toMatch(/not a named import nor an inline z\.\* call/);
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
test("skips test files, node_modules, .kumiko, dist", () => {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
4
|
+
// TS7's package.json exports only version.cjs at ".", so the classic
|
|
5
|
+
// createProgram/ScriptTarget Compiler API is unreachable via `import *
|
|
6
|
+
// as ts from "typescript"` anymore; ts-morph's vendored (older) compiler
|
|
7
|
+
// build is the only way to get that surface, so diagnostics here run
|
|
8
|
+
// against a different compiler than the repo's own tsc.
|
|
9
|
+
import { ts } from "ts-morph";
|
|
5
10
|
import { runCodegen } from "../run-codegen";
|
|
6
11
|
|
|
7
12
|
const REPO_ROOT = join(__dirname, "../../../../..");
|
|
@@ -138,8 +138,7 @@ export default defineFeature("orders", (r) => {
|
|
|
138
138
|
writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("nope", "evt"));
|
|
139
139
|
const handle = watchAndRegenerate({ appRoot, onResult: () => {} });
|
|
140
140
|
handle.close();
|
|
141
|
-
handle.close();
|
|
142
|
-
expect(true).toBe(true);
|
|
141
|
+
expect(() => handle.close()).not.toThrow();
|
|
143
142
|
});
|
|
144
143
|
|
|
145
144
|
test("non-ts file changes do not trigger codegen", async () => {
|
package/src/codegen/render.ts
CHANGED
|
@@ -175,8 +175,13 @@ export function renderDefineFile(handlerQns: readonly string[] = []): string {
|
|
|
175
175
|
? [
|
|
176
176
|
"",
|
|
177
177
|
`import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`,
|
|
178
|
+
`import type { WriteHandlerQn } from "./types.generated";`,
|
|
178
179
|
"",
|
|
179
|
-
`export type { WriteHandlerQn } from "./types.generated"
|
|
180
|
+
// Plain `export type { WriteHandlerQn } from "./types.generated"`
|
|
181
|
+
// only re-exports — with verbatimModuleSyntax it does NOT bind the
|
|
182
|
+
// name locally, so the `TypedDispatcher` reference below would fail
|
|
183
|
+
// to resolve. Import above + bare re-export here does both.
|
|
184
|
+
`export type { WriteHandlerQn };`,
|
|
180
185
|
"",
|
|
181
186
|
`/** Dispatcher with write() narrowed to registered handler QNs. */`,
|
|
182
187
|
`export type TypedDispatcher = Omit<Dispatcher, "write"> & {`,
|
|
@@ -93,7 +93,7 @@ export type SchemaSource =
|
|
|
93
93
|
export type ScanWarning = {
|
|
94
94
|
readonly file: string;
|
|
95
95
|
readonly line: number;
|
|
96
|
-
readonly
|
|
96
|
+
readonly message: string;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
export type ScanResult = {
|
|
@@ -152,7 +152,7 @@ function collectTsFiles(dir: string, out: string[]): void {
|
|
|
152
152
|
try {
|
|
153
153
|
entries = readdirSync(dir);
|
|
154
154
|
} catch {
|
|
155
|
-
//
|
|
155
|
+
// skip: directory unreadable, nothing to scan there
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
for (const entry of entries) {
|
|
@@ -224,8 +224,9 @@ function collectFromDefineEvent(
|
|
|
224
224
|
warnings.push({
|
|
225
225
|
file: filePath,
|
|
226
226
|
line: call.getStartLineNumber(),
|
|
227
|
-
|
|
227
|
+
message: "r.defineEvent: cannot read event-name + schema statically",
|
|
228
228
|
});
|
|
229
|
+
// skip: defineEvent call shape not statically parseable, already recorded as warning
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -241,8 +242,9 @@ function collectFromDefineEvent(
|
|
|
241
242
|
warnings.push({
|
|
242
243
|
file: filePath,
|
|
243
244
|
line: call.getStartLineNumber(),
|
|
244
|
-
|
|
245
|
+
message: `r.defineEvent("${parsed.eventName}"): schema "${parsed.schemaNode.getText()}" — not a named import nor an inline z.* call, skipped`,
|
|
245
246
|
});
|
|
247
|
+
// skip: schema source not resolvable, already recorded as warning
|
|
246
248
|
return;
|
|
247
249
|
}
|
|
248
250
|
|
|
@@ -544,7 +546,7 @@ function dedupe(events: ScannedEvent[], warnings: ScanWarning[]): ScannedEvent[]
|
|
|
544
546
|
warnings.push({
|
|
545
547
|
file: ev.source.file,
|
|
546
548
|
line: ev.source.line,
|
|
547
|
-
|
|
549
|
+
message: `duplicate r.defineEvent("${ev.qualifiedName}") — first declared at ${existing.source.file}:${existing.source.line}, ignored here`,
|
|
548
550
|
});
|
|
549
551
|
continue;
|
|
550
552
|
}
|
package/src/codegen/watch.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
|
|
|
74
74
|
if (result.warnings.length > 0) {
|
|
75
75
|
for (const w of result.warnings) {
|
|
76
76
|
// biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
|
|
77
|
-
console.warn(`[codegen] ${w.file}:${w.line} — ${w.
|
|
77
|
+
console.warn(`[codegen] ${w.file}:${w.line} — ${w.message}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -95,17 +95,19 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
|
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
97
|
watcher = watch(srcDir, { recursive: true }, (_eventType, filename) => {
|
|
98
|
+
// skip: watcher closed or no filename reported, nothing to act on
|
|
98
99
|
if (closed || !filename) return;
|
|
99
100
|
// node liefert filename relativ zu srcDir, kann aber posix oder
|
|
100
101
|
// windows-style separators haben. Wir prüfen substring-tolerant.
|
|
101
102
|
const normalised = `/${filename.toString().replace(/\\/g, "/")}`;
|
|
103
|
+
// skip: path matches an excluded segment (node_modules/dist/etc.)
|
|
102
104
|
if (SKIP_SUBSTRINGS.some((seg) => normalised.includes(seg))) return;
|
|
103
|
-
// Nur .ts/.tsx interessieren — alles andere (CSS, MD, JSON) hat
|
|
105
|
+
// skip: Nur .ts/.tsx interessieren — alles andere (CSS, MD, JSON) hat
|
|
104
106
|
// keinen Einfluss auf r.defineEvent-Calls.
|
|
105
107
|
if (!normalised.endsWith(".ts") && !normalised.endsWith(".tsx")) return;
|
|
106
|
-
// .d.ts ausgenommen — die kommen meistens vom codegen selbst.
|
|
108
|
+
// skip: .d.ts ausgenommen — die kommen meistens vom codegen selbst.
|
|
107
109
|
if (normalised.endsWith(".d.ts")) return;
|
|
108
|
-
// .test.ts/.test.tsx ausgenommen — Tests definieren keine
|
|
110
|
+
// skip: .test.ts/.test.tsx ausgenommen — Tests definieren keine
|
|
109
111
|
// Production-Features.
|
|
110
112
|
if (normalised.endsWith(".test.ts") || normalised.endsWith(".test.tsx")) return;
|
|
111
113
|
|
|
@@ -127,6 +129,7 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
|
|
|
127
129
|
|
|
128
130
|
return {
|
|
129
131
|
close: () => {
|
|
132
|
+
// skip: already closed, avoid double-close
|
|
130
133
|
if (closed) return;
|
|
131
134
|
closed = true;
|
|
132
135
|
if (timer) clearTimeout(timer);
|