@cosmicdrift/kumiko-dev-server 0.13.0 → 0.15.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.
Files changed (34) hide show
  1. package/package.json +11 -14
  2. package/src/__tests__/build-prod-bundle.integration.ts +1 -1
  3. package/src/__tests__/build-prod-bundle.test.ts +1 -1
  4. package/src/__tests__/cache-headers.test.ts +1 -1
  5. package/src/__tests__/classify-change.test.ts +1 -1
  6. package/src/__tests__/compose-features-wiring.integration.ts +10 -9
  7. package/src/__tests__/compose-features.test.ts +1 -1
  8. package/src/__tests__/config-seed-boot.integration.ts +5 -4
  9. package/src/__tests__/crash-tracker.test.ts +1 -1
  10. package/src/__tests__/create-kumiko-server.integration.ts +5 -5
  11. package/src/__tests__/env-schema.integration.ts +1 -1
  12. package/src/__tests__/env-schema.test.ts +1 -1
  13. package/src/__tests__/few-shot-corpus.test.ts +1 -1
  14. package/src/__tests__/inject-schema.test.ts +1 -1
  15. package/src/__tests__/resolve-stylesheet.test.ts +11 -7
  16. package/src/__tests__/resolve-tailwind-cli.test.ts +1 -1
  17. package/src/__tests__/run-prod-app-spec.test.ts +1 -1
  18. package/src/__tests__/run-prod-app.integration.ts +6 -6
  19. package/src/__tests__/scaffold-app-feature.test.ts +1 -1
  20. package/src/__tests__/scaffold-app.test.ts +1 -1
  21. package/src/__tests__/scaffold-deploy.test.ts +1 -1
  22. package/src/__tests__/scaffold-feature.test.ts +1 -1
  23. package/src/__tests__/try-hono-first.test.ts +1 -1
  24. package/src/__tests__/walkthrough.integration.ts +118 -0
  25. package/src/codegen/__tests__/run-codegen.test.ts +1 -1
  26. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +1 -1
  27. package/src/codegen/__tests__/watch.test.ts +1 -1
  28. package/src/scaffold-app-feature.ts +2 -1
  29. package/src/scaffold-app.ts +139 -76
  30. package/src/{drizzle-tables-auth-mode.ts → schema-tables-auth-mode.ts} +1 -1
  31. package/templates/deploy/Dockerfile.template +15 -47
  32. package/CHANGELOG.md +0 -632
  33. package/src/drizzle-config.ts +0 -44
  34. /package/src/{drizzle-tables-minimal.ts → schema-tables-minimal.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.13.0",
3
+ "version": "0.15.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>",
@@ -30,17 +30,13 @@
30
30
  "types": "./src/compose-features.ts",
31
31
  "default": "./src/compose-features.ts"
32
32
  },
33
- "./drizzle-config": {
34
- "types": "./src/drizzle-config.ts",
35
- "default": "./src/drizzle-config.ts"
33
+ "./schema-tables-auth-mode": {
34
+ "types": "./src/schema-tables-auth-mode.ts",
35
+ "default": "./src/schema-tables-auth-mode.ts"
36
36
  },
37
- "./drizzle-tables-auth-mode": {
38
- "types": "./src/drizzle-tables-auth-mode.ts",
39
- "default": "./src/drizzle-tables-auth-mode.ts"
40
- },
41
- "./drizzle-tables-minimal": {
42
- "types": "./src/drizzle-tables-minimal.ts",
43
- "default": "./src/drizzle-tables-minimal.ts"
37
+ "./schema-tables-minimal": {
38
+ "types": "./src/schema-tables-minimal.ts",
39
+ "default": "./src/schema-tables-minimal.ts"
44
40
  }
45
41
  },
46
42
  "bin": {
@@ -49,8 +45,9 @@
49
45
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
50
46
  },
51
47
  "dependencies": {
52
- "@cosmicdrift/kumiko-bundled-features": "0.13.0",
53
- "@cosmicdrift/kumiko-framework": "0.13.0"
48
+ "@cosmicdrift/kumiko-bundled-features": "0.14.0",
49
+ "@cosmicdrift/kumiko-framework": "0.14.0",
50
+ "ts-morph": "^28.0.0"
54
51
  },
55
52
  "publishConfig": {
56
53
  "registry": "https://registry.npmjs.org",
@@ -62,4 +59,4 @@
62
59
  "README.md",
63
60
  "LICENSE"
64
61
  ]
65
- }
62
+ }
@@ -12,13 +12,13 @@
12
12
  // braucht Bun. Spawnen kumiko-build als subprocess via PATH.
13
13
  // Skipped wenn `bun` nicht erreichbar — selten, aber sauber.
14
14
 
15
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
15
16
  import { execFileSync } from "node:child_process";
16
17
  import { existsSync } from "node:fs";
17
18
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
18
19
  import { tmpdir } from "node:os";
19
20
  import { dirname, join, resolve } from "node:path";
20
21
  import { fileURLToPath } from "node:url";
21
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
22
22
  import { buildProdBundle } from "../build-prod-bundle";
23
23
 
24
24
  const __filename = fileURLToPath(import.meta.url);
@@ -6,11 +6,11 @@
6
6
  // laufen im CI als `yarn build` auf der Showcase-App; das ist der
7
7
  // ehrlichere Smoke-Test.
8
8
 
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
10
  import { existsSync } from "node:fs";
10
11
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
11
12
  import { tmpdir } from "node:os";
12
13
  import { join } from "node:path";
13
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
14
14
  import {
15
15
  type ClientEntry,
16
16
  discoverClientEntries,
@@ -7,7 +7,7 @@
7
7
  // /manifest.json, /sw.js → no-cache
8
8
  // alles andere → kein expliziter Header
9
9
 
10
- import { describe, expect, test } from "vitest";
10
+ import { describe, expect, test } from "bun:test";
11
11
  import { cacheHeadersFor } from "../run-prod-app";
12
12
 
13
13
  describe("cacheHeadersFor", () => {
@@ -5,7 +5,7 @@
5
5
  // Restarts (DX schlecht) oder echte Schema-Änderungen schlagen
6
6
  // nicht durch (UX broken). Beide sind teuer — daher pinnen wir.
7
7
 
8
- import { describe, expect, test } from "vitest";
8
+ import { describe, expect, test } from "bun:test";
9
9
  import { classifyChange } from "../create-kumiko-server";
10
10
 
11
11
  describe("classifyChange", () => {
@@ -21,6 +21,7 @@
21
21
  // schreibt in ein lokales Array — gewollter Capture-Spy ohne vitest-
22
22
  // Mock-API (CLAUDE.md "Kein Mock in *.integration.ts").
23
23
 
24
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
24
25
  import { randomBytes } from "node:crypto";
25
26
  import {
26
27
  AuthErrors,
@@ -34,6 +35,7 @@ import {
34
35
  import { tenantEntity, tenantMembershipsTable } from "@cosmicdrift/kumiko-bundled-features/tenant";
35
36
  import { seedTenantMembership } from "@cosmicdrift/kumiko-bundled-features/tenant/testing";
36
37
  import { UserHandlers, userEntity, userTable } from "@cosmicdrift/kumiko-bundled-features/user";
38
+ import { deleteMany } from "@cosmicdrift/kumiko-framework/bun-db";
37
39
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
38
40
  import {
39
41
  setupTestStack,
@@ -42,7 +44,6 @@ import {
42
44
  unsafeCreateEntityTable,
43
45
  unsafePushTables,
44
46
  } from "@cosmicdrift/kumiko-framework/stack";
45
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
46
47
  import { composeFeatures } from "../compose-features";
47
48
 
48
49
  const RESET_HMAC = randomBytes(32).toString("base64");
@@ -151,8 +152,8 @@ describe("composeFeatures wiring — passwordReset", () => {
151
152
  });
152
153
 
153
154
  beforeEach(async () => {
154
- await suite.stack.db.delete(userTable);
155
- await suite.stack.db.delete(tenantMembershipsTable);
155
+ await deleteMany(suite.stack.db, userTable, {});
156
+ await deleteMany(suite.stack.db, tenantMembershipsTable, {});
156
157
  suite.resetEmails.length = 0;
157
158
  suite.verifyEmails.length = 0;
158
159
  });
@@ -225,8 +226,8 @@ describe("composeFeatures wiring — emailVerification", () => {
225
226
  });
226
227
 
227
228
  beforeEach(async () => {
228
- await suite.stack.db.delete(userTable);
229
- await suite.stack.db.delete(tenantMembershipsTable);
229
+ await deleteMany(suite.stack.db, userTable, {});
230
+ await deleteMany(suite.stack.db, tenantMembershipsTable, {});
230
231
  suite.resetEmails.length = 0;
231
232
  suite.verifyEmails.length = 0;
232
233
  });
@@ -271,8 +272,8 @@ describe("composeFeatures wiring — asymmetric activation", () => {
271
272
  });
272
273
 
273
274
  beforeEach(async () => {
274
- await suite.stack.db.delete(userTable);
275
- await suite.stack.db.delete(tenantMembershipsTable);
275
+ await deleteMany(suite.stack.db, userTable, {});
276
+ await deleteMany(suite.stack.db, tenantMembershipsTable, {});
276
277
  suite.resetEmails.length = 0;
277
278
  suite.verifyEmails.length = 0;
278
279
  });
@@ -325,8 +326,8 @@ describe("composeFeatures wiring — fail-closed ohne authOptions", () => {
325
326
  });
326
327
 
327
328
  afterEach(async () => {
328
- await suite.stack.db.delete(userTable);
329
- await suite.stack.db.delete(tenantMembershipsTable);
329
+ await deleteMany(suite.stack.db, userTable, {});
330
+ await deleteMany(suite.stack.db, tenantMembershipsTable, {});
330
331
  suite.resetEmails.length = 0;
331
332
  });
332
333
 
@@ -7,8 +7,8 @@
7
7
  // aber die Handler fehlen → POST /api/auth/request-password-reset
8
8
  // dispatched ins Leere → 500.
9
9
 
10
+ import { describe, expect, test } from "bun:test";
10
11
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
11
- import { describe, expect, test } from "vitest";
12
12
  import { composeFeatures } from "../compose-features";
13
13
 
14
14
  const noopFeature = defineFeature("noop-app", () => {});
@@ -12,12 +12,14 @@
12
12
  // 3. an admin set on top of a seed wins the resolver cascade; coexistence
13
13
  // vs. override semantics depend on the admin user's tenantId.
14
14
 
15
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
15
16
  import {
16
17
  configValuesTable,
17
18
  createConfigAccessorFactory,
18
19
  createConfigFeature,
19
20
  createConfigResolver,
20
21
  } from "@cosmicdrift/kumiko-bundled-features/config";
22
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
21
23
  import {
22
24
  access,
23
25
  createSystemConfig,
@@ -32,7 +34,6 @@ import {
32
34
  TestUsers,
33
35
  unsafePushTables,
34
36
  } from "@cosmicdrift/kumiko-framework/stack";
35
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
36
37
  import { applyBootSeeds } from "../boot/apply-boot-seeds";
37
38
 
38
39
  const bootSeedsFeature = defineFeature("boot-seeds-test", (r) => {
@@ -82,7 +83,7 @@ describe("config-seed boot wiring", () => {
82
83
  test("first boot: applyBootSeeds writes one row per seed", async () => {
83
84
  await applyBootSeeds({ registry: stack.registry, db: stack.db });
84
85
 
85
- const rows = await stack.db.select().from(configValuesTable);
86
+ const rows = await selectMany(stack.db, configValuesTable);
86
87
  expect(rows.length).toBe(2);
87
88
 
88
89
  const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
@@ -111,7 +112,7 @@ describe("config-seed boot wiring", () => {
111
112
  test("re-boot: idempotent — every seed already on disk → no extra rows", async () => {
112
113
  await applyBootSeeds({ registry: stack.registry, db: stack.db });
113
114
 
114
- const rows = await stack.db.select().from(configValuesTable);
115
+ const rows = await selectMany(stack.db, configValuesTable);
115
116
  expect(rows.length).toBe(2);
116
117
  });
117
118
 
@@ -151,7 +152,7 @@ describe("config-seed boot wiring", () => {
151
152
  // - 3 rows = coexistence path (admin tenantId !== SYSTEM_TENANT_ID,
152
153
  // new specific-tenant row sits next to the seed system-row).
153
154
  // Either is correct as long as the resolver picks the admin value.
154
- const rows = await stack.db.select().from(configValuesTable);
155
+ const rows = await selectMany(stack.db, configValuesTable);
155
156
  expect([2, 3]).toContain(rows.length);
156
157
  });
157
158
  });
@@ -3,7 +3,7 @@
3
3
  // Wrapper aufgibt oder noch eine Runde respawnt; off-by-one am Fenster-
4
4
  // Rand würde im Bin-Skript schlecht auffallen.
5
5
 
6
- import { describe, expect, test } from "vitest";
6
+ import { describe, expect, test } from "bun:test";
7
7
  import { createCrashTracker } from "../crash-tracker";
8
8
 
9
9
  describe("createCrashTracker", () => {
@@ -1,14 +1,14 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
1
2
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
5
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
4
6
  import {
5
7
  createBooleanField,
6
8
  createEntity,
7
9
  createTextField,
8
10
  defineFeature,
9
11
  } from "@cosmicdrift/kumiko-framework/engine";
10
- import { sql } from "drizzle-orm";
11
- import { afterEach, describe, expect, test } from "vitest";
12
12
  import { createKumikoServer, type KumikoServerHandle } from "../create-kumiko-server";
13
13
 
14
14
  // Integration-Test: bootet createKumikoServer mit echtem Postgres,
@@ -50,10 +50,10 @@ async function boot(): Promise<KumikoServerHandle> {
50
50
  describe("createKumikoServer", () => {
51
51
  test("bootet den Kumiko-Stack + legt die Feature-Tables an", async () => {
52
52
  const h = await boot();
53
- const rows = await h.stack.db.execute<{ exists: boolean }>(
54
- sql`SELECT to_regclass('public.kumiko_server_probe') IS NOT NULL AS exists`,
53
+ const rows = await asRawClient(h.stack.db).unsafe(
54
+ `SELECT to_regclass('public.kumiko_server_probe') IS NOT NULL AS "exists"`,
55
55
  );
56
- expect(rows[0]?.exists).toBe(true);
56
+ expect((rows as Array<Record<string, unknown>>)[0]?.["exists"]).toBe(true);
57
57
  });
58
58
 
59
59
  test("GET / → HTML + kumiko_auth/kumiko_csrf Set-Cookie", async () => {
@@ -4,9 +4,9 @@
4
4
  // throws (via bootErrorReporter override) and KUMIKO_DRY_RUN_ENV
5
5
  // returns the dry-run handle without booting.
6
6
 
7
+ import { describe, expect, it } from "bun:test";
7
8
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
8
9
  import { composeEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
- import { describe, expect, it } from "vitest";
10
10
  import { z } from "zod";
11
11
  import { frameworkCoreEnvSchema } from "../env-schema";
12
12
  import * as devServerPublicApi from "../index";
@@ -1,5 +1,5 @@
1
+ import { describe, expect, it } from "bun:test";
1
2
  import { composeEnvSchema, KumikoBootError, parseEnv } from "@cosmicdrift/kumiko-framework/env";
2
- import { describe, expect, it } from "vitest";
3
3
  import { z } from "zod";
4
4
  import { type FrameworkCoreEnv, frameworkCoreEnvSchema } from "../env-schema";
5
5
 
@@ -13,11 +13,11 @@
13
13
  // because the corpus JSON now lives there — framework is public and must
14
14
  // not carry the eval baseline.
15
15
 
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
16
17
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
18
  import { tmpdir } from "node:os";
18
19
  import { join } from "node:path";
19
20
  import { buildFewShotCorpus, pathToId } from "@cosmicdrift/kumiko-dev-server";
20
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
21
21
 
22
22
  let workdir: string;
23
23
 
@@ -4,7 +4,7 @@
4
4
  // nicht und mountet leer. Tests pinnen die Idempotenz + die zwei
5
5
  // Insertion-Punkte (vor /client.js-Tag oder vor </body>).
6
6
 
7
- import { describe, expect, test } from "vitest";
7
+ import { describe, expect, test } from "bun:test";
8
8
  import { injectSchema } from "../inject-schema";
9
9
 
10
10
  const SCHEMA = '{"features":[]}';
@@ -7,10 +7,10 @@
7
7
  // die anderen Tests dort booten ein TestStack (DB + Redis), das brauchen
8
8
  // wir für reine Resolver-Logik nicht.
9
9
 
10
+ import { describe, expect, test } from "bun:test";
10
11
  import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
11
12
  import { tmpdir } from "node:os";
12
13
  import { join } from "node:path";
13
- import { describe, expect, test } from "vitest";
14
14
  import { resolveStylesheet } from "../create-kumiko-server";
15
15
 
16
16
  describe("resolveStylesheet", () => {
@@ -48,16 +48,20 @@ describe("resolveStylesheet", () => {
48
48
  expect(out).toBeUndefined();
49
49
  });
50
50
 
51
- test("undefined + clientEntry unter Node (ohne Bun): undefined (silent skip)", () => {
52
- // hasBun ist false in vitest-Node — Default-Resolution wird übersprungen,
53
- // ohne Throw. Wer im Test eine echte CSS will, übergibt explicit string.
54
- // Unter Bun (production) würde Bun.resolveSync den Path liefern; das
55
- // testet der bestehende create-kumiko-server.integration.ts.
51
+ test("undefined + clientEntry: undefined (silent skip)", () => {
52
+ // Bun resolveSync findet packages/renderer-web/src/styles.css im
53
+ // Repo. Das ist der prod-Pfad unter Bun (real oder CI) ist der
54
+ // Wert ein absoluter path; unter Node (von wo der Test portiert
55
+ // wurde) wäre es undefined. Wir akzeptieren beides.
56
56
  const out = resolveStylesheet({
57
57
  features: [],
58
58
  clientEntry: "./entry.tsx",
59
59
  });
60
- expect(out).toBeUndefined();
60
+ if (typeof Bun === "undefined") {
61
+ expect(out).toBeUndefined();
62
+ } else {
63
+ expect(out).toMatch(/renderer-web\/src\/styles\.css$/);
64
+ }
61
65
  });
62
66
 
63
67
  test("undefined + clientEntry + src/styles.css existiert → returns App-Theme-Override", () => {
@@ -2,7 +2,7 @@
2
2
  // genau das, was den dev-server-Crash bei flakigem Netz verhindert:
3
3
  // kein Bun → undefined, Package nicht installiert → undefined.
4
4
 
5
- import { describe, expect, test } from "vitest";
5
+ import { describe, expect, test } from "bun:test";
6
6
  import { resolveTailwindCli } from "../resolve-tailwind-cli";
7
7
 
8
8
  describe("resolveTailwindCli", () => {
@@ -4,8 +4,8 @@
4
4
  // "looks like a leak"-Reverts oder "bisschen rauf, sollte reichen"-
5
5
  // Tweaks.
6
6
 
7
+ import { describe, expect, test } from "bun:test";
7
8
  import { SSE_HEARTBEAT_INTERVAL_MS } from "@cosmicdrift/kumiko-framework/api";
8
- import { describe, expect, test } from "vitest";
9
9
  import { buildBunServeOptions } from "../run-prod-app";
10
10
 
11
11
  describe("Bun.serve options for production", () => {
@@ -9,9 +9,11 @@
9
9
  // fetch direkt. Bun.serve-Wiring ist in Production-Coolify selbst
10
10
  // getestet wenn der Container hochfährt.
11
11
 
12
+ import { afterEach, beforeAll, describe, expect, test } from "bun:test";
12
13
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
13
14
  import { tmpdir } from "node:os";
14
15
  import { dirname, join } from "node:path";
16
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
15
17
  import { createDbConnection } from "@cosmicdrift/kumiko-framework/db";
16
18
  import {
17
19
  createBooleanField,
@@ -28,9 +30,7 @@ import {
28
30
  createProjectionStateTable,
29
31
  } from "@cosmicdrift/kumiko-framework/pipeline";
30
32
  import { unsafeEnsureEntityTable } from "@cosmicdrift/kumiko-framework/stack";
31
- import { sql } from "drizzle-orm";
32
33
  import postgres from "postgres";
33
- import { afterEach, beforeAll, describe, expect, test } from "vitest";
34
34
  import { z } from "zod";
35
35
  import { type ProdAppHandle, runProdApp } from "../run-prod-app";
36
36
 
@@ -448,7 +448,7 @@ describe("runProdApp", () => {
448
448
  });
449
449
 
450
450
  expect(invocations).toBe(1);
451
- expect(factoryDeps).toEqual({ db: true, redis: true, registry: true });
451
+ expect(factoryDeps!).toEqual({ db: true, redis: true, registry: true });
452
452
  // Smoke: handle is functional (boot completed without error).
453
453
  expect(handle.entrypoint).toBeDefined();
454
454
  });
@@ -465,9 +465,9 @@ describe("runProdApp", () => {
465
465
  seedInvocations++;
466
466
  // Seed-side idempotence: check before inserting. runProdApp doesn't
467
467
  // gate seeds — the seed itself is responsible.
468
- const existing = await db.execute(sql`SELECT 1 FROM prod_widgets LIMIT 1`);
469
- if (existing.length > 0) return;
470
- await db.execute(sql`INSERT INTO prod_widgets (id, tenant_id, name) VALUES
468
+ const existing = await asRawClient(db).unsafe(`SELECT 1 FROM prod_widgets LIMIT 1`);
469
+ if ((existing as Array<Record<string, unknown>>).length > 0) return;
470
+ await asRawClient(db).unsafe(`INSERT INTO prod_widgets (id, tenant_id, name) VALUES
471
471
  (gen_random_uuid(), '00000000-0000-4000-8000-000000000001', 'seeded')`);
472
472
  inserted = true;
473
473
  };
@@ -1,9 +1,9 @@
1
1
  // scaffoldAppFeature unit-tests (DX-2).
2
2
 
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
4
  import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
7
7
  import { scaffoldApp } from "../scaffold-app";
8
8
  import { scaffoldAppFeature } from "../scaffold-app-feature";
9
9
 
@@ -1,9 +1,9 @@
1
1
  // scaffoldApp unit-tests (DX-1.0).
2
2
 
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
4
  import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
7
7
  import { scaffoldApp } from "../scaffold-app";
8
8
 
9
9
  describe("scaffoldApp", () => {
@@ -1,7 +1,7 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
1
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
5
  import { scaffoldDeploy } from "../scaffold-deploy";
6
6
 
7
7
  describe("scaffoldDeploy", () => {
@@ -8,12 +8,12 @@
8
8
  // 4. Validation: bad names fail loudly, existing destination refuses
9
9
  // to overwrite
10
10
 
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
12
  import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
12
13
  import { tmpdir } from "node:os";
13
14
  import { join } from "node:path";
14
15
  import { parseSourceFile, VERSION_HEADER } from "@cosmicdrift/kumiko-framework/engine";
15
16
  import { Project } from "ts-morph";
16
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
17
17
  import { scaffoldFeature } from "../scaffold-feature";
18
18
 
19
19
  let workdir: string;
@@ -5,7 +5,7 @@
5
5
  // sich ändert (z.B. "matched" auch für 4xx anders als 404), MÜSSEN
6
6
  // beide Pfade synchron updaten.
7
7
 
8
- import { describe, expect, test } from "vitest";
8
+ import { describe, expect, test } from "bun:test";
9
9
  import { type HonoLikeApp, tryHonoFirst } from "../try-hono-first";
10
10
 
11
11
  function makeApp(response: Response): HonoLikeApp {
@@ -0,0 +1,118 @@
1
+ // DX-3.1 — walkthrough-snapshot-test. Reproduces the 3-command path from
2
+ // docs.kumiko.so/en/walkthrough/ in-process and asserts what the walkthrough
3
+ // claims. Catches drift in scaffoldApp + scaffoldAppFeature against the
4
+ // docs without an actual `bunx … && yarn install && bun run boot` CI run.
5
+ //
6
+ // What this test pins:
7
+ // - scaffoldApp produces the 6 files the walkthrough lists
8
+ // - scaffoldAppFeature scaffolds + auto-mounts (the diff-block shown)
9
+ // - composeFeatures(includeBundled:true) yields the exact feature-count
10
+ // the walkthrough advertises in "Expected output"
11
+
12
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
17
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
18
+ import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { composeFeatures } from "../compose-features";
20
+ import { scaffoldApp } from "../scaffold-app";
21
+ import { scaffoldAppFeature } from "../scaffold-app-feature";
22
+
23
+ describe("walkthrough — DX-3.1 snapshot", () => {
24
+ let tmp: string;
25
+ let appRoot: string;
26
+
27
+ beforeEach(() => {
28
+ tmp = mkdtempSync(join(tmpdir(), "walkthrough-"));
29
+ appRoot = join(tmp, "my-notes");
30
+ });
31
+ afterEach(() => {
32
+ rmSync(tmp, { recursive: true, force: true });
33
+ });
34
+
35
+ test("Step 1 (kumiko new app) — produces walkthrough's 6 files", () => {
36
+ const result = scaffoldApp({ name: "my-notes", destination: appRoot });
37
+ expect(result.files).toEqual([
38
+ "package.json",
39
+ "tsconfig.json",
40
+ "src/run-config.ts",
41
+ "bin/main.ts",
42
+ ".env.example",
43
+ "README.md",
44
+ ]);
45
+ });
46
+
47
+ test("Step 2 (kumiko add feature) — auto-mounts + walkthrough diff matches", () => {
48
+ scaffoldApp({ name: "my-notes", destination: appRoot });
49
+ const result = scaffoldAppFeature({ name: "notes", appRoot });
50
+ expect(result.autoMounted).toBe(true);
51
+
52
+ const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
53
+ // Walkthrough's diff-block claims:
54
+ // + import { notesFeature } from "./features/notes";
55
+ // + notesFeature (in APP_FEATURES)
56
+ expect(runConfig).toContain(`import { notesFeature } from "./features/notes";`);
57
+ expect(runConfig).toContain("notesFeature");
58
+ // Foundation still mounted (createSecretsFeature + createSessionsFeature).
59
+ expect(runConfig).toContain("createSecretsFeature()");
60
+ expect(runConfig).toContain("createSessionsFeature()");
61
+ });
62
+
63
+ test("Step 3 (boot validation) — scaffolded run-config matches walkthrough's APP_FEATURES claim", () => {
64
+ scaffoldApp({ name: "my-notes", destination: appRoot });
65
+ scaffoldAppFeature({ name: "notes", appRoot });
66
+
67
+ // Text-assert: scaffolded run-config.ts contains exactly the 3 features
68
+ // the walkthrough's diff-block shows (secrets + sessions + notesFeature).
69
+ // Dynamic-import would fail because /tmp can't resolve @cosmicdrift/*
70
+ // workspace symlinks — instead we reproduce the equivalent APP_FEATURES
71
+ // array in-process below.
72
+ const runConfig = readFileSync(join(appRoot, "src/run-config.ts"), "utf-8");
73
+ expect(runConfig).toContain("createSecretsFeature()");
74
+ expect(runConfig).toContain("createSessionsFeature()");
75
+ expect(runConfig).toContain("notesFeature");
76
+ });
77
+
78
+ test("Step 3 (composeFeatures) — 3 explicit + 4 auto-mounted = 7 features", () => {
79
+ // Reproduces the scaffolded APP_FEATURES in-process. notesFeature gets
80
+ // a dummy defineFeature here — the scaffold-side of "notesFeature"
81
+ // (file-content) is pinned in test 2; this test pins the runtime-side
82
+ // (composeFeatures auto-prepend behaviour the walkthrough claims).
83
+ const notesFeature = defineFeature("notes", () => {});
84
+ const APP_FEATURES = [createSecretsFeature(), createSessionsFeature(), notesFeature];
85
+
86
+ const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
87
+ // 3 explicit + 4 auto-mounted bundled = 7 total features.
88
+ expect(composed.length).toBe(7);
89
+
90
+ const composedNames = composed.map((f) => f.name).sort();
91
+ expect(composedNames).toEqual([
92
+ "auth-email-password",
93
+ "config",
94
+ "notes",
95
+ "secrets",
96
+ "sessions",
97
+ "tenant",
98
+ "user",
99
+ ]);
100
+
101
+ // validateBoot must pass (no missing-requires, no schema-errors).
102
+ expect(() => validateBoot(composed)).not.toThrow();
103
+ // Registry must contain all 7 features.
104
+ const registry = createRegistry(composed);
105
+ expect(registry.features.size).toBe(7);
106
+ });
107
+
108
+ test("bin/main.ts contains the auth.admin stub the walkthrough relies on", () => {
109
+ scaffoldApp({ name: "my-notes", destination: appRoot });
110
+ const main = readFileSync(join(appRoot, "bin/main.ts"), "utf-8");
111
+ // composeFeatures(includeBundled:true)-trigger is `auth: { admin: { … } }`.
112
+ // Walkthrough explicitly says this is what auto-mounts the 4 bundled features.
113
+ expect(main).toContain("auth: {");
114
+ expect(main).toContain("admin: {");
115
+ expect(main).toContain("memberships:");
116
+ expect(main).toContain("runProdApp");
117
+ });
118
+ });
@@ -16,10 +16,10 @@
16
16
  // - skipped-Flag wenn 0 Events UND kein .kumiko/ schon existiert
17
17
  // - schemas.generated.ts wird erzeugt wenn inline-Schemas, sonst nicht
18
18
 
19
+ import { describe, expect, test } from "bun:test";
19
20
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
20
21
  import { tmpdir } from "node:os";
21
22
  import { join } from "node:path";
22
- import { describe, expect, test } from "vitest";
23
23
  import { runCodegen } from "../run-codegen";
24
24
 
25
25
  function makeAppDir(): string {
@@ -1,7 +1,7 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
3
4
  import * as ts from "typescript";
4
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
5
5
  import { runCodegen } from "../run-codegen";
6
6
 
7
7
  const REPO_ROOT = join(__dirname, "../../../../..");
@@ -8,9 +8,9 @@
8
8
  // 'zod' nicht direct nutzen, runCodegen scant feature-files die
9
9
  // `import { z } from "zod"` haben.
10
10
 
11
+ import { afterAll, describe, expect, test } from "bun:test";
11
12
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
13
  import { dirname, join } from "node:path";
13
- import { afterAll, describe, expect, test } from "vitest";
14
14
  import type { CodegenResult } from "../run-codegen";
15
15
  import { watchAndRegenerate } from "../watch";
16
16
 
@@ -114,7 +114,8 @@ function mountInRunConfig(runConfigPath: string, name: string): boolean {
114
114
 
115
115
  // 1. Prepend import after the last existing import.
116
116
  const imports = sf.getImportDeclarations();
117
- const insertIndex = imports.length > 0 ? imports[imports.length - 1]!.getChildIndex() + 1 : 0;
117
+ const insertIndex =
118
+ imports.length > 0 ? (imports[imports.length - 1]?.getChildIndex() ?? 0) + 1 : 0;
118
119
  sf.insertImportDeclaration(insertIndex, {
119
120
  moduleSpecifier: `./features/${name}`,
120
121
  namedImports: [`${camel}Feature`],