@cosmicdrift/kumiko-dev-server 0.79.3 → 0.81.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-dev-server",
3
- "version": "0.79.3",
3
+ "version": "0.81.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>",
@@ -50,8 +50,8 @@
50
50
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@cosmicdrift/kumiko-bundled-features": "0.79.3",
54
- "@cosmicdrift/kumiko-framework": "0.79.3",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.81.0",
54
+ "@cosmicdrift/kumiko-framework": "0.81.0",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -27,6 +27,7 @@ describe("scaffoldApp", () => {
27
27
  "src/run-config.ts",
28
28
  "bin/main.ts",
29
29
  "bin/dev.ts",
30
+ "src/client.tsx",
30
31
  ".env.example",
31
32
  "README.md",
32
33
  ]);
@@ -48,11 +49,12 @@ describe("scaffoldApp", () => {
48
49
  expect(pkg.dependencies["@cosmicdrift/kumiko-bundled-features"]).toBe("^0.13.0");
49
50
  expect(pkg.dependencies["@cosmicdrift/kumiko-dev-server"]).toBe("^0.13.0");
50
51
  expect(pkg.dependencies["@cosmicdrift/kumiko-framework"]).toBe("^0.13.0");
52
+ expect(pkg.dependencies["@cosmicdrift/kumiko-renderer-web"]).toBe("^0.13.0");
51
53
  expect(pkg.scripts["boot"]).toContain("KUMIKO_DRY_RUN_ENV=boot");
52
54
  expect(pkg.scripts["dev"]).toBe("bun --watch bin/dev.ts");
53
55
  });
54
56
 
55
- test("bin/dev.ts contains runDevApp + welcomeBanner + admin login", () => {
57
+ test("bin/dev.ts contains runDevApp + welcomeBanner + admin login + clientEntry", () => {
56
58
  const dest = join(tmp, "my-shop");
57
59
  scaffoldApp({ name: "my-shop", destination: dest });
58
60
 
@@ -61,6 +63,25 @@ describe("scaffoldApp", () => {
61
63
  expect(dev).toContain("welcomeBanner: true");
62
64
  expect(dev).toContain("admin@my-shop.local");
63
65
  expect(dev).toContain(`password: "changeme"`);
66
+ // Without clientEntry the default HTML still <script src="/client.js">'s,
67
+ // and the dev-server 404s on it — every fresh scaffold rendered blank.
68
+ expect(dev).toContain(`clientEntry: "./src/client.tsx"`);
69
+ });
70
+
71
+ test("src/client.tsx bundles createKumikoApp + emailPasswordClient + DefaultAppShell", () => {
72
+ const dest = join(tmp, "my-shop");
73
+ scaffoldApp({ name: "my-shop", destination: dest });
74
+
75
+ const client = readFileSync(join(dest, "src/client.tsx"), "utf-8");
76
+ expect(client).toContain("createKumikoApp");
77
+ expect(client).toContain("emailPasswordClient");
78
+ // Without shell:DefaultAppShell post-login renders only the active
79
+ // screen (no sidebar / topbar) — the e2e hero-demo proved this dead-
80
+ // ends on "Screen not found" because routing has no layout chrome.
81
+ expect(client).toContain("DefaultAppShell");
82
+ expect(client).toContain("shell: DefaultAppShell");
83
+ expect(client).toContain('from "@cosmicdrift/kumiko-renderer-web"');
84
+ expect(client).toContain('from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web"');
64
85
  });
65
86
 
66
87
  test(".env.example carries KUMIKO_DEV_DB_NAME default so reboots are persistent", () => {
@@ -71,6 +92,18 @@ describe("scaffoldApp", () => {
71
92
  expect(env).toContain("KUMIKO_DEV_DB_NAME=my_shop_dev");
72
93
  });
73
94
 
95
+ test(".env.example lists both TEST_DATABASE_URL (bun dev) + DATABASE_URL (prod)", () => {
96
+ const dest = join(tmp, "my-shop");
97
+ scaffoldApp({ name: "my-shop", destination: dest });
98
+
99
+ const env = readFileSync(join(dest, ".env.example"), "utf-8");
100
+ // runDevApp → setupTestStack needs TEST_DATABASE_URL; previously the
101
+ // template only listed DATABASE_URL, so every fresh `bun dev` crashed
102
+ // with "Missing required env var: TEST_DATABASE_URL".
103
+ expect(env).toContain("TEST_DATABASE_URL=");
104
+ expect(env).toContain("DATABASE_URL=");
105
+ });
106
+
74
107
  test("README lists the mounted features dynamically", () => {
75
108
  const dest = join(tmp, "my-shop");
76
109
  scaffoldApp({
@@ -173,25 +206,25 @@ describe("scaffoldApp", () => {
173
206
  name: "custom-features",
174
207
  destination: dest,
175
208
  features: [
176
- {
177
- name: "tenant",
178
- importPath: "@cosmicdrift/kumiko-bundled-features/tenant",
179
- exportName: "createTenantFeature",
180
- callExpression: "createTenantFeature()",
181
- },
182
209
  {
183
210
  name: "billing-foundation",
184
211
  importPath: "@cosmicdrift/kumiko-bundled-features/billing-foundation",
185
212
  exportName: "billingFoundationFeature",
186
213
  callExpression: "billingFoundationFeature",
187
214
  },
215
+ {
216
+ name: "delivery",
217
+ importPath: "@cosmicdrift/kumiko-bundled-features/delivery",
218
+ exportName: "createDeliveryFeature",
219
+ callExpression: "createDeliveryFeature()",
220
+ },
188
221
  ],
189
222
  });
190
223
  const cfg = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
191
- expect(cfg).toContain('from "@cosmicdrift/kumiko-bundled-features/tenant"');
192
224
  expect(cfg).toContain('from "@cosmicdrift/kumiko-bundled-features/billing-foundation"');
193
- expect(cfg).toContain("createTenantFeature()");
225
+ expect(cfg).toContain('from "@cosmicdrift/kumiko-bundled-features/delivery"');
194
226
  expect(cfg).toContain("billingFoundationFeature");
227
+ expect(cfg).toContain("createDeliveryFeature()");
195
228
  expect(cfg).not.toContain("createSecretsFeature");
196
229
  expect(cfg).not.toContain("createSessionsFeature");
197
230
  });
@@ -204,6 +237,55 @@ describe("scaffoldApp", () => {
204
237
  expect(cfg).toContain("createSessionsFeature()");
205
238
  });
206
239
 
240
+ test("features-param: composeFeatures auto-mounted (config/user/tenant/auth-email-password) are filtered out — no boot-time dedupe-warn spam", () => {
241
+ const dest = join(tmp, "filtered");
242
+ scaffoldApp({
243
+ name: "filtered",
244
+ destination: dest,
245
+ features: [
246
+ // Picker's recommended set includes the 4 auto-mounted bundled
247
+ // features; without the filter they'd land in APP_FEATURES and
248
+ // composeFeatures would log 4 dedupe warnings on every boot.
249
+ {
250
+ name: "config",
251
+ importPath: "@cosmicdrift/kumiko-bundled-features/config",
252
+ exportName: "createConfigFeature",
253
+ callExpression: "createConfigFeature()",
254
+ },
255
+ {
256
+ name: "user",
257
+ importPath: "@cosmicdrift/kumiko-bundled-features/user",
258
+ exportName: "createUserFeature",
259
+ callExpression: "createUserFeature()",
260
+ },
261
+ {
262
+ name: "tenant",
263
+ importPath: "@cosmicdrift/kumiko-bundled-features/tenant",
264
+ exportName: "createTenantFeature",
265
+ callExpression: "createTenantFeature()",
266
+ },
267
+ {
268
+ name: "auth-email-password",
269
+ importPath: "@cosmicdrift/kumiko-bundled-features/auth-email-password",
270
+ exportName: "createAuthEmailPasswordFeature",
271
+ callExpression: "createAuthEmailPasswordFeature()",
272
+ },
273
+ {
274
+ name: "delivery",
275
+ importPath: "@cosmicdrift/kumiko-bundled-features/delivery",
276
+ exportName: "createDeliveryFeature",
277
+ callExpression: "createDeliveryFeature()",
278
+ },
279
+ ],
280
+ });
281
+ const cfg = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
282
+ expect(cfg).not.toContain("createConfigFeature");
283
+ expect(cfg).not.toContain("createUserFeature");
284
+ expect(cfg).not.toContain("createTenantFeature");
285
+ expect(cfg).not.toContain("createAuthEmailPasswordFeature");
286
+ expect(cfg).toContain("createDeliveryFeature()");
287
+ });
288
+
207
289
  test("deterministic tenantId for same name (reproducible boots)", () => {
208
290
  const a = join(tmp, "a");
209
291
  const b = join(tmp, "b");
@@ -40,6 +40,7 @@ describe("walkthrough — DX-3.1 snapshot", () => {
40
40
  "src/run-config.ts",
41
41
  "bin/main.ts",
42
42
  "bin/dev.ts",
43
+ "src/client.tsx",
43
44
  ".env.example",
44
45
  "README.md",
45
46
  ]);
@@ -83,6 +83,9 @@ export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
83
83
  write(join(destination, "bin", "dev.ts"), renderDev(options.name));
84
84
  files.push("bin/dev.ts");
85
85
 
86
+ write(join(destination, "src", "client.tsx"), renderClient());
87
+ files.push("src/client.tsx");
88
+
86
89
  write(join(destination, ".env.example"), renderEnvExample(options.name));
87
90
  files.push(".env.example");
88
91
 
@@ -113,6 +116,7 @@ function renderPackageJson(name: string, version: string): string {
113
116
  "@cosmicdrift/kumiko-bundled-features": version,
114
117
  "@cosmicdrift/kumiko-dev-server": version,
115
118
  "@cosmicdrift/kumiko-framework": version,
119
+ "@cosmicdrift/kumiko-renderer-web": version,
116
120
  zod: "^4.4.3",
117
121
  },
118
122
  },
@@ -168,11 +172,20 @@ const FOUNDATION_FEATURES: ReadonlyArray<ScaffoldFeatureEntry> = [
168
172
  },
169
173
  ];
170
174
 
175
+ // composeFeatures({ includeBundled: true }) auto-mountet diese 4 Foundation-
176
+ // Features. Sie hier nochmal in APP_FEATURES zu schreiben löste den dedupe-
177
+ // warn-Spam im scaffolded `bun dev` aus (PR #599 hat den createRegistry-
178
+ // Crash gefangen, der Spam blieb bis hier). Filter wirkt defensiv: auch wenn
179
+ // jemand scaffoldApp() direkt mit allen Bundled-Feature-Entries aufruft
180
+ // rutschen die 4 nicht in run-config.ts.
181
+ const COMPOSE_AUTO_MOUNTED_NAMES = new Set(["config", "user", "tenant", "auth-email-password"]);
182
+
171
183
  function renderRunConfig(features?: ReadonlyArray<ScaffoldFeatureEntry>): string {
172
184
  const project = newTsProject();
173
185
  const sf = project.createSourceFile("run-config.ts", "");
174
186
 
175
- const effective = features && features.length > 0 ? features : FOUNDATION_FEATURES;
187
+ const filtered = (features ?? []).filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
188
+ const effective = filtered.length > 0 ? filtered : FOUNDATION_FEATURES;
176
189
  const grouped = new Map<string, string[]>();
177
190
  for (const entry of effective) {
178
191
  const existing = grouped.get(entry.importPath) ?? [];
@@ -352,6 +365,7 @@ function renderDev(appName: string): string {
352
365
  .inlineBlock(() => {
353
366
  writer.writeLine("features: APP_FEATURES,");
354
367
  writer.writeLine("welcomeBanner: true,");
368
+ writer.writeLine(`clientEntry: "./src/client.tsx",`);
355
369
  writer.write("auth: ").inlineBlock(() => {
356
370
  writer.write("admin: ").inlineBlock(() => {
357
371
  writer.writeLine(`email: "admin@${appName}.local",`);
@@ -391,8 +405,39 @@ function renderDev(appName: string): string {
391
405
  return sf.getFullText();
392
406
  }
393
407
 
408
+ function renderClient(): string {
409
+ return [
410
+ "// Browser-Entry. runDevApp's clientEntry-Option bundlet diese Datei zu",
411
+ "// /client.js und das Default-HTML lädt sie. createKumikoApp liest das",
412
+ "// Schema aus dem window-globalen (das injectSchema im dev-server setzt)",
413
+ "// und mountet die Routen.",
414
+ "//",
415
+ "// DefaultAppShell liefert die Sidebar + Topbar — ohne `shell` rendert",
416
+ "// createKumikoApp das aktive Screen ohne Layout-Wrapper (= nach Login",
417
+ "// nur ein nackter Banner statt der App). emailPasswordClient() bringt",
418
+ "// Login-Screen + Session-Provider — ohne ihn bliebe /login leer.",
419
+ "//",
420
+ "// Neue Client-Plugins (z.B. notificationsClient()) hier in clientFeatures",
421
+ "// hinzu — symmetrisch zu APP_FEATURES auf der Server-Seite.",
422
+ "",
423
+ 'import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";',
424
+ 'import { createKumikoApp, DefaultAppShell } from "@cosmicdrift/kumiko-renderer-web";',
425
+ "",
426
+ "createKumikoApp({",
427
+ " shell: DefaultAppShell,",
428
+ " clientFeatures: [emailPasswordClient()],",
429
+ "});",
430
+ "",
431
+ ].join("\n");
432
+ }
433
+
394
434
  function renderEnvExample(appName: string): string {
395
- return `# Required env-vars für boot-mode + dev. Production: über Pulumi/k8s-Secrets.
435
+ const devDb = `${appName.replace(/-/g, "_")}_dev`;
436
+ return `# bun dev (runDevApp → setupTestStack) braucht TEST_DATABASE_URL.
437
+ # Production (bun bin/main.ts → runProdApp) braucht DATABASE_URL.
438
+ # Beide zeigen im Default auf denselben lokalen Postgres — runDevApp legt
439
+ # darunter eine eigene "<KUMIKO_DEV_DB_NAME>"-Datenbank an.
440
+ TEST_DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres
396
441
  DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app
397
442
  REDIS_URL=redis://127.0.0.1:6379
398
443
 
@@ -406,7 +451,7 @@ KUMIKO_SECRETS_MASTER_KEY_V1=
406
451
  # Dev-only: persistente DB für \`bun dev\`. Ohne diesen Var startet jeder Reboot
407
452
  # eine frische kumiko_test_<random>-DB → Admin-Login + Daten weg bei jedem Edit.
408
453
  # Mit Var bleibt die DB zwischen Reboots erhalten (Schema-Pushes sind idempotent).
409
- KUMIKO_DEV_DB_NAME=${appName.replace(/-/g, "_")}_dev
454
+ KUMIKO_DEV_DB_NAME=${devDb}
410
455
  `;
411
456
  }
412
457