@cosmicdrift/kumiko-dev-server 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @cosmicdrift/kumiko-dev-server
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b8e1d48: scaffoldApp baut `src/run-config.ts` + `bin/main.ts` jetzt via ts-morph
8
+ (AST) statt template-strings. Selbes Tool wie scaffoldAppFeature →
9
+ ein konsistenter Mechanismus für generate + later modify. Plus:
10
+ ts-morph als explicit dependency aufgenommen (war bisher nur via
11
+ hoisted root-dep verfügbar; broken bei publish).
12
+
13
+ ### Patch Changes
14
+
15
+ - ce23d48: `walkthrough.integration.ts` — DX-3.1 walkthrough-snapshot-test. Pins
16
+ scaffoldApp + scaffoldAppFeature output gegen die Behauptungen in
17
+ docs.kumiko.so/en/walkthrough/. Catches doc-drift ohne actual
18
+ `bunx … && yarn install && bun run boot` CI-run.
19
+
20
+ 5 Tests: file-list, auto-mount-diff, run-config text-content,
21
+ composeFeatures(includeBundled:true) = 7 features, bin/main auth.admin
22
+ stub.
23
+
24
+ - @cosmicdrift/kumiko-framework@0.14.0
25
+ - @cosmicdrift/kumiko-bundled-features@0.14.0
26
+
3
27
  ## 0.13.0
4
28
 
5
29
  ### Minor Changes
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.14.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>",
@@ -49,8 +49,9 @@
49
49
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
50
50
  },
51
51
  "dependencies": {
52
- "@cosmicdrift/kumiko-bundled-features": "0.13.0",
53
- "@cosmicdrift/kumiko-framework": "0.13.0"
52
+ "@cosmicdrift/kumiko-bundled-features": "0.14.0",
53
+ "@cosmicdrift/kumiko-framework": "0.14.0",
54
+ "ts-morph": "^28.0.0"
54
55
  },
55
56
  "publishConfig": {
56
57
  "registry": "https://registry.npmjs.org",
@@ -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 { mkdtempSync, readFileSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
16
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
17
+ import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
18
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
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
+ });
@@ -6,16 +6,14 @@
6
6
  // stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
7
7
  // README.
8
8
  //
9
- // Intentionally NOT included in DX-1.0:
10
- // - drizzle/ setup (DX-1.1 needs FEATURE_IMPORT_REGISTRY decision from DX-4)
11
- // - deploy/Dockerfile (already covered by scaffoldDeploy separate cmd)
12
- // - first feature scaffold (use scaffoldFeature after this)
13
- //
14
- // The generated app is born "boots cleanly, mounts nothing fancy". User
15
- // runs `kumiko add feature` (DX-2) or hand-edits src/run-config.ts to grow.
9
+ // .ts files are built via ts-morph (same tool [[scaffoldAppFeature]] uses
10
+ // to auto-mount features). Means a single AST representation for both
11
+ // generate + later modify no template-string ts-morph divergence.
12
+ // Static files (package.json, tsconfig, .env, README) stay text-based.
16
13
 
17
14
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
18
15
  import { join, resolve } from "node:path";
16
+ import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
19
17
 
20
18
  export type ScaffoldAppOptions = {
21
19
  /** kebab-case app name (e.g. "my-shop"). Becomes package-name + folder. */
@@ -122,72 +120,143 @@ function renderTsconfig(): string {
122
120
  )}\n`;
123
121
  }
124
122
 
125
- function renderRunConfig(): string {
126
- return `// Single source of truth für die Feature-Komposition deiner App.
127
- // Bundled-Foundation: secrets + sessions. config/user/tenant/auth-email-password
128
- // werden via composeFeatures(includeBundled:true) automatisch ergänzt
129
- // wenn runProdApp mit \`auth: {…}\` aufgerufen wird (siehe bin/main.ts).
130
- //
131
- // Neue features hinzufügen:
132
- // - bunx kumiko add feature <name> (DX-2, automatisch)
133
- // - oder: hand-edit + import unten ergänzen
134
-
135
- import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
136
- import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
123
+ function newTsProject(): Project {
124
+ return new Project({
125
+ useInMemoryFileSystem: true,
126
+ compilerOptions: { target: 99, module: 99, strict: true },
127
+ manipulationSettings: { indentationText: IndentationText.TwoSpaces },
128
+ });
129
+ }
137
130
 
138
- export const APP_FEATURES = [
139
- createSecretsFeature(),
140
- createSessionsFeature(),
141
- ] as const;
142
- `;
131
+ function renderRunConfig(): string {
132
+ const project = newTsProject();
133
+ const sf = project.createSourceFile("run-config.ts", "");
134
+
135
+ sf.addImportDeclaration({
136
+ moduleSpecifier: "@cosmicdrift/kumiko-bundled-features/secrets",
137
+ namedImports: ["createSecretsFeature"],
138
+ });
139
+ sf.addImportDeclaration({
140
+ moduleSpecifier: "@cosmicdrift/kumiko-bundled-features/sessions",
141
+ namedImports: ["createSessionsFeature"],
142
+ });
143
+
144
+ sf.addVariableStatement({
145
+ declarationKind: VariableDeclarationKind.Const,
146
+ isExported: true,
147
+ declarations: [
148
+ {
149
+ name: "APP_FEATURES",
150
+ initializer: "[createSecretsFeature(), createSessionsFeature()] as const",
151
+ },
152
+ ],
153
+ });
154
+
155
+ sf.insertText(
156
+ 0,
157
+ [
158
+ "// Single source of truth für die Feature-Komposition deiner App.",
159
+ "// Bundled-Foundation: secrets + sessions. config/user/tenant/auth-email-password",
160
+ "// werden via composeFeatures(includeBundled:true) automatisch ergänzt",
161
+ "// wenn runProdApp mit `auth: {…}` aufgerufen wird (siehe bin/main.ts).",
162
+ "//",
163
+ "// Neue features hinzufügen:",
164
+ "// - bunx @cosmicdrift/kumiko-cli add feature <name> (DX-2, automatisch)",
165
+ "// - oder: hand-edit + import unten ergänzen",
166
+ "",
167
+ "",
168
+ ].join("\n"),
169
+ );
170
+
171
+ return sf.getFullText();
143
172
  }
144
173
 
145
174
  function renderMain(appName: string): string {
146
- // Deterministic tenant-UUID derived from appName for the seed-admin
147
- // membership. Reproducible across boots; tenants table sees the same
148
- // ID. Format: 8-4-4-4-12 hex chars, version-4 marker at position 14.
149
- // We hash the name into the digits using a tiny PRNG so two apps
150
- // get different IDs without bun's crypto dependency.
151
175
  const tenantId = deriveTenantId(appName);
152
- return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
153
- // composeFeatures + validateBoot + createRegistry without DB/Redis-connect
154
- // (siehe @cosmicdrift/kumiko-dev-server runProdApp). Echter Dev-Boot
155
- // passiert via \`bunx kumiko dev\` mit Docker-stack — DX-1.0 deckt nur
156
- // den boot-mode-Pfad ab; \`kumiko dev\` kommt in einer späteren DX-Phase.
157
-
158
- import { frameworkCoreEnvSchema, runProdApp } from "@cosmicdrift/kumiko-dev-server";
159
- import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
160
- import { composeEnvSchema } from "@cosmicdrift/kumiko-framework/env";
161
- import { APP_FEATURES } from "../src/run-config";
162
-
163
- const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
164
-
165
- const envSchema = composeEnvSchema({
166
- core: frameworkCoreEnvSchema,
167
- features: APP_FEATURES,
168
- });
169
-
170
- await runProdApp({
171
- features: APP_FEATURES,
172
- envSchema,
173
- migrations: false,
174
- auth: {
175
- admin: {
176
- email: "admin@${appName}.local",
177
- password: "change-me-on-first-deploy",
178
- displayName: "Admin",
179
- memberships: [
180
- {
181
- tenantId: DEFAULT_TENANT_ID,
182
- tenantKey: "${appName}",
183
- tenantName: "${appName}",
184
- roles: ["TenantAdmin"],
185
- },
186
- ],
187
- },
188
- },
189
- });
190
- `;
176
+ const project = newTsProject();
177
+ const sf = project.createSourceFile("main.ts", "");
178
+
179
+ sf.addImportDeclaration({
180
+ moduleSpecifier: "@cosmicdrift/kumiko-dev-server",
181
+ namedImports: ["frameworkCoreEnvSchema", "runProdApp"],
182
+ });
183
+ sf.addImportDeclaration({
184
+ moduleSpecifier: "@cosmicdrift/kumiko-framework/engine",
185
+ isTypeOnly: true,
186
+ namedImports: ["TenantId"],
187
+ });
188
+ sf.addImportDeclaration({
189
+ moduleSpecifier: "@cosmicdrift/kumiko-framework/env",
190
+ namedImports: ["composeEnvSchema"],
191
+ });
192
+ sf.addImportDeclaration({
193
+ moduleSpecifier: "../src/run-config",
194
+ namedImports: ["APP_FEATURES"],
195
+ });
196
+
197
+ sf.addVariableStatement({
198
+ declarationKind: VariableDeclarationKind.Const,
199
+ declarations: [
200
+ {
201
+ name: "DEFAULT_TENANT_ID",
202
+ initializer: `"${tenantId}" as TenantId`,
203
+ },
204
+ ],
205
+ });
206
+
207
+ sf.addVariableStatement({
208
+ declarationKind: VariableDeclarationKind.Const,
209
+ declarations: [
210
+ {
211
+ name: "envSchema",
212
+ initializer: "composeEnvSchema({ core: frameworkCoreEnvSchema, features: APP_FEATURES })",
213
+ },
214
+ ],
215
+ });
216
+
217
+ sf.addStatements((writer) => {
218
+ writer
219
+ .write("await runProdApp(")
220
+ .inlineBlock(() => {
221
+ writer.writeLine("features: APP_FEATURES,");
222
+ writer.writeLine("envSchema,");
223
+ writer.writeLine("migrations: false,");
224
+ writer.write("auth: ").inlineBlock(() => {
225
+ writer.write("admin: ").inlineBlock(() => {
226
+ writer.writeLine(`email: "admin@${appName}.local",`);
227
+ writer.writeLine(`password: "change-me-on-first-deploy",`);
228
+ writer.writeLine(`displayName: "Admin",`);
229
+ writer.write("memberships: [");
230
+ writer.indent(() => {
231
+ writer.inlineBlock(() => {
232
+ writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
233
+ writer.writeLine(`tenantKey: "${appName}",`);
234
+ writer.writeLine(`tenantName: "${appName}",`);
235
+ writer.writeLine(`roles: ["TenantAdmin"],`);
236
+ });
237
+ writer.write(",");
238
+ });
239
+ writer.write("],");
240
+ });
241
+ });
242
+ })
243
+ .write(");");
244
+ });
245
+
246
+ sf.insertText(
247
+ 0,
248
+ [
249
+ "// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
250
+ "// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
251
+ "// (siehe @cosmicdrift/kumiko-dev-server runProdApp). Echter Dev-Boot",
252
+ "// passiert via `yarn kumiko dev` (in-repo dev-tool) mit Docker-stack — DX-1.0 deckt nur",
253
+ "// den boot-mode-Pfad ab; `kumiko dev` kommt in einer späteren DX-Phase.",
254
+ "",
255
+ "",
256
+ ].join("\n"),
257
+ );
258
+
259
+ return sf.getFullText();
191
260
  }
192
261
 
193
262
  function renderEnvExample(): string {
@@ -208,7 +277,7 @@ function renderReadme(appName: string): string {
208
277
  return `# ${appName}
209
278
 
210
279
  Scaffolded by \`kumiko new app\`. Boots out-of-the-box with secrets + sessions
211
- mounted (foundation set). Add features with \`bunx kumiko add feature <name>\`.
280
+ mounted (foundation set). Add features with \`bunx @cosmicdrift/kumiko-cli add feature <name>\`.
212
281
 
213
282
  ## First boot
214
283
 
@@ -224,7 +293,7 @@ Expected: \`[runProdApp] boot validation OK (… features, … registry entries)
224
293
  ## Adding features
225
294
 
226
295
  \`\`\`sh
227
- bunx kumiko add feature my-domain
296
+ bunx @cosmicdrift/kumiko-cli add feature my-domain
228
297
  # → editiert src/run-config.ts automatisch + scaffolded src/features/my-domain/
229
298
  \`\`\`
230
299
 
@@ -241,10 +310,6 @@ For full docs see https://docs.kumiko.so.
241
310
  // version-marker at the right spot. NOT cryptographically random —
242
311
  // just a stable per-app default the user can change later.
243
312
  function deriveTenantId(name: string): string {
244
- // Tiny xorshift PRNG seeded from the name's char-codes. Same name →
245
- // same ID. Sufficient for "give every scaffolded app a deterministic
246
- // default tenant" — production sets its own via the create-tenant
247
- // flow anyway.
248
313
  let state = 2166136261;
249
314
  for (const ch of name) {
250
315
  state ^= ch.charCodeAt(0);
@@ -255,11 +320,9 @@ function deriveTenantId(name: string): string {
255
320
  state ^= state << 13;
256
321
  state >>>= 0;
257
322
  const b = hex(state, 4);
258
- // version-4 marker at first char of 3rd group:
259
323
  state ^= state >>> 17;
260
324
  state >>>= 0;
261
325
  const c = `4${hex(state, 3)}`;
262
- // RFC 4122 variant: 10xx (set top two bits of 4th group to 10):
263
326
  state ^= state << 5;
264
327
  state >>>= 0;
265
328
  const d4 = (0x8 | (state & 0x3)).toString(16);