@cosmicdrift/kumiko-dev-server 0.174.0 → 0.176.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.174.0",
3
+ "version": "0.176.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Not shipped into production node_modules — see @cosmicdrift/kumiko-server-runtime for the prod boot path.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -54,9 +54,9 @@
54
54
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
55
55
  },
56
56
  "dependencies": {
57
- "@cosmicdrift/kumiko-bundled-features": "0.174.0",
58
- "@cosmicdrift/kumiko-framework": "0.174.0",
59
- "@cosmicdrift/kumiko-server-runtime": "0.174.0",
57
+ "@cosmicdrift/kumiko-bundled-features": "0.176.0",
58
+ "@cosmicdrift/kumiko-framework": "0.176.0",
59
+ "@cosmicdrift/kumiko-server-runtime": "0.176.0",
60
60
  "ts-morph": "^28.0.0"
61
61
  },
62
62
  "publishConfig": {
@@ -29,8 +29,8 @@ describe("composeStacks", () => {
29
29
  ]);
30
30
  });
31
31
 
32
- test("composePagesStack → text-content + legal-pages", () => {
33
- expect(stackFeatureNames(composePagesStack())).toEqual(["text-content", "legal-pages"]);
32
+ test("composePagesStack → legal-pages (template-resolver comes from the renderer stack)", () => {
33
+ expect(stackFeatureNames(composePagesStack())).toEqual(["legal-pages"]);
34
34
  });
35
35
 
36
36
  test("composeMailStack mounts foundation + selected transports", () => {
@@ -134,6 +134,8 @@ describe("composeStacks boots for real", () => {
134
134
  createPersonalAccessTokensFeature({ scopes: {} }),
135
135
  ...composeOpsStack({ rateLimiting: true }),
136
136
  ...composePagesStack(),
137
+ // legal-pages requires template-resolver, which ships in the renderer stack.
138
+ ...composeRendererStack(),
137
139
  ...composeMailStack({ transports: ["inmemory"] }),
138
140
  ...composeFileStack({ providers: ["inmemory"] }),
139
141
  ...composeGdprStack({ order: "compliance-first", sessions: true }),
@@ -192,6 +194,8 @@ describe("composeStacks intended block names (snapshot, not live parity)", () =>
192
194
  const names = stackFeatureNames([
193
195
  ...composeOpsStack({ rateLimiting: true }),
194
196
  ...composePagesStack(),
197
+ // Pages content sits on template-resolver, which the renderer stack mounts.
198
+ ...composeRendererStack(),
195
199
  ...composeMailStack({ transports: ["inmemory", "smtp"] }),
196
200
  ...composeFileStack({ providers: ["inmemory", "s3"] }),
197
201
  ...composeGdprStack({ order: "compliance-first" }),
@@ -202,7 +206,7 @@ describe("composeStacks intended block names (snapshot, not live parity)", () =>
202
206
  "audit",
203
207
  "jobs",
204
208
  "rate-limiting",
205
- "text-content",
209
+ "template-resolver",
206
210
  "legal-pages",
207
211
  "mail-foundation",
208
212
  "file-foundation",
@@ -226,7 +230,7 @@ describe("composeStacks intended block names (snapshot, not live parity)", () =>
226
230
  ...composeUserDataRightsStack(),
227
231
  ]);
228
232
  for (const expected of [
229
- "text-content",
233
+ "template-resolver",
230
234
  "legal-pages",
231
235
  "data-retention",
232
236
  "compliance-profiles",
@@ -250,7 +254,7 @@ describe("composeStacks intended block names (snapshot, not live parity)", () =>
250
254
  ...composeUserDataRightsStack(),
251
255
  ]);
252
256
  for (const expected of [
253
- "text-content",
257
+ "template-resolver",
254
258
  "legal-pages",
255
259
  "tenant-lifecycle",
256
260
  "audit",
@@ -462,18 +462,14 @@ describe("createKumikoServer — hot-reload broadcast", () => {
462
462
  const initialBuilds = builds;
463
463
  writeFileSync(join(webDir, "page.tsx"), "export const x = 1;\n");
464
464
 
465
- const deadline = Date.now() + 3000;
466
- let sawReload = false;
467
- while (Date.now() < deadline && !sawReload) {
468
- const readPromise = reader.read();
469
- const timeout = new Promise<{ done: true; value: undefined }>((resolve) =>
470
- setTimeout(() => resolve({ done: true, value: undefined }), 200),
471
- );
472
- const { value, done } = await Promise.race([readPromise, timeout]);
473
- if (done || value === undefined) continue;
474
- const chunk = new TextDecoder().decode(value);
475
- if (chunk.includes("event: reload")) sawReload = true;
476
- }
465
+ const readAll = (async () => {
466
+ for (;;) {
467
+ const { value, done } = await reader.read();
468
+ if (done) return false;
469
+ if (new TextDecoder().decode(value).includes("event: reload")) return true;
470
+ }
471
+ })();
472
+ const sawReload = await Promise.race([readAll, Bun.sleep(3000).then(() => false)]);
477
473
  await reader.cancel();
478
474
 
479
475
  expect(builds).toBeGreaterThan(initialBuilds);
@@ -3,7 +3,7 @@ import { mergeExtraContext } from "../setup-test-stack-from-features";
3
3
 
4
4
  // #882/4: the integration test only asserted getFeature("config") — which
5
5
  // includeBundled already provides — never that presets:["config"] actually
6
- // merges configResolver/textContent into extraContext. Unit-test the merge
6
+ // merges configResolver/templateResolver into extraContext. Unit-test the merge
7
7
  // directly instead.
8
8
 
9
9
  const fakeDeps = {
@@ -41,16 +41,16 @@ describe("mergeExtraContext", () => {
41
41
  expect(result["configResolver"]).toBeDefined();
42
42
  });
43
43
 
44
- test("text-content preset merges textContent, built from deps.db", () => {
45
- const merged = mergeExtraContext(undefined, ["text-content"]);
44
+ test("template-resolver preset merges templateResolver, built from deps.db", () => {
45
+ const merged = mergeExtraContext(undefined, ["template-resolver"]);
46
46
  const result = (merged as (deps: typeof fakeDeps) => Record<string, unknown>)(fakeDeps);
47
- expect(result["textContent"]).toBeDefined();
47
+ expect(result["templateResolver"]).toBeDefined();
48
48
  });
49
49
 
50
50
  test("both presets merge both fields", () => {
51
- const merged = mergeExtraContext(undefined, ["config", "text-content"]);
51
+ const merged = mergeExtraContext(undefined, ["config", "template-resolver"]);
52
52
  const result = (merged as (deps: typeof fakeDeps) => Record<string, unknown>)(fakeDeps);
53
53
  expect(result["configResolver"]).toBeDefined();
54
- expect(result["textContent"]).toBeDefined();
54
+ expect(result["templateResolver"]).toBeDefined();
55
55
  });
56
56
  });
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
9
  import {
10
- requireTextContent,
11
- type TextContentApi,
12
- } from "@cosmicdrift/kumiko-bundled-features/text-content";
10
+ requireTemplateResolver,
11
+ TEXT_BLOCK_KIND,
12
+ type TemplateResolverApi,
13
+ } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
13
14
  import {
14
15
  createEntity,
15
16
  createTextField,
@@ -111,28 +112,47 @@ describe("runDevApp — auth allowedOrigins forwarding (#399/1)", () => {
111
112
  });
112
113
 
113
114
  describe("runDevApp — extraContext merge order: app values win over boot defaults (707/1)", () => {
114
- test("a caller-supplied extraContext.textContent wins over the auto-wired boot default", async () => {
115
+ test("a caller-supplied extraContext.templateResolver wins over the auto-wired boot default", async () => {
115
116
  // runDevApp's extraContext factory does `{ ...boot, ...base }` — boot's
116
- // auto-wired textContent must lose to a caller-supplied override, exactly
117
+ // auto-wired templateResolver must lose to a caller-supplied override, exactly
117
118
  // the "app values win" parity claim this PR made against runProdApp. Every
118
119
  // OTHER test in this file only checks that runDevApp boots without
119
120
  // throwing; none dispatch a request that actually reads the merged value.
120
- const sentinel: TextContentApi = {
121
- getBlock: async () => ({
121
+ const sentinel: TemplateResolverApi = {
122
+ findExact: async () => ({
123
+ id: "sentinel",
124
+ version: 1,
125
+ tenantId: TestUsers.systemAdmin.tenantId,
122
126
  slug: "sentinel",
123
- lang: "en",
127
+ kind: TEXT_BLOCK_KIND,
128
+ locale: "en",
124
129
  title: "from caller extraContext",
125
- body: null,
130
+ folder: null,
131
+ content: "",
132
+ contentFormat: "markdown",
133
+ variableSchema: {},
134
+ linkedResources: {},
135
+ scope: "system",
136
+ parentTemplateId: null,
137
+ status: "active",
126
138
  updatedAt: new Date(0),
127
139
  }),
140
+ resolveTemplate: async () => {
141
+ throw new Error("not used in this test");
142
+ },
128
143
  };
129
144
  const readBlockQuery = defineQueryHandler({
130
145
  name: "read-block",
131
146
  schema: z.object({}),
132
147
  access: { openToAll: true },
133
148
  handler: async (_query, ctx) => {
134
- const api = requireTextContent(ctx, "textcheck:query:read-block");
135
- return api.getBlock({ tenantId: TestUsers.systemAdmin.tenantId, slug: "x", lang: "en" });
149
+ const api = requireTemplateResolver(ctx, "textcheck:query:read-block");
150
+ return api.findExact({
151
+ tenantId: TestUsers.systemAdmin.tenantId,
152
+ slug: "x",
153
+ kind: TEXT_BLOCK_KIND,
154
+ locale: "en",
155
+ });
136
156
  },
137
157
  });
138
158
  const textcheckFeature = defineFeature("textcheck", (r) => {
@@ -142,7 +162,7 @@ describe("runDevApp — extraContext merge order: app values win over boot defau
142
162
  handle = await runDevApp({
143
163
  features: [validFeature(), textcheckFeature],
144
164
  port: 0,
145
- extraContext: { textContent: sentinel },
165
+ extraContext: { templateResolver: sentinel },
146
166
  });
147
167
 
148
168
  const res = await handle.stack.http.queryOk<{ title: string } | null>(
@@ -127,5 +127,14 @@ describe("walkthrough — DX-3.1 snapshot", () => {
127
127
  expect(main).toContain("admin: {");
128
128
  expect(main).toContain("memberships:");
129
129
  expect(main).toContain("runProdApp");
130
+ // Prod writer stays untouched — the verify flow must run there (#1687).
131
+ expect(main).not.toContain("emailVerified");
132
+ });
133
+
134
+ test("bin/dev.ts seeds the admin as emailVerified: true so login doesn't 422", async () => {
135
+ await scaffoldApp({ name: "my-notes", destination: appRoot });
136
+ const dev = readFileSync(join(appRoot, "bin/dev.ts"), "utf-8");
137
+ expect(dev).toContain("admin: {");
138
+ expect(dev).toContain("emailVerified: true,");
130
139
  });
131
140
  });
@@ -30,7 +30,7 @@ import { createRendererSimpleFeature } from "@cosmicdrift/kumiko-bundled-feature
30
30
  import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
31
31
  import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
32
32
  import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
33
- import { createTextContentFeature } from "@cosmicdrift/kumiko-bundled-features/text-content";
33
+
34
34
  import {
35
35
  createUserDataRightsFeature,
36
36
  type UserDataRightsOptions,
@@ -98,8 +98,12 @@ export function stackFeatureNames(features: readonly FeatureDefinition[]): strin
98
98
  }
99
99
 
100
100
  export function composePagesStack(options: PagesStackOptions = {}): FeatureDefinition[] {
101
+ // Pages content lives in template-resolver (kind text-block), which the
102
+ // renderer stack already mounts — an app composing pages WITHOUT the renderer
103
+ // stack has to add createTemplateResolverFeature() itself, or legal-pages'
104
+ // r.requires fails the boot. Mounting it here instead would collide with
105
+ // composeRendererStack (createRegistry rejects duplicate features).
101
106
  return [
102
- createTextContentFeature(),
103
107
  createLegalPagesFeature(
104
108
  options.wrapLayout !== undefined ? { wrapLayout: options.wrapLayout } : {},
105
109
  ),
@@ -204,7 +204,7 @@ export type RunDevAppOptions = {
204
204
  /** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
205
205
  * Array-Reihenfolge. */
206
206
  readonly seeds?: readonly SeedFn[];
207
- /** Extra-AppContext-Keys. `textContent` (immer) + `secrets` (wenn das
207
+ /** Extra-AppContext-Keys. `templateResolver` (immer) + `secrets` (wenn das
208
208
  * secrets-Feature gemountet ist) + `configResolver` (auth-mode) werden
209
209
  * automatisch ergänzt — App-Werte gewinnen. Symmetrisch zu runProdApp. */
210
210
  readonly extraContext?: CreateKumikoServerOptions["extraContext"];
@@ -379,7 +379,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
379
379
  bootCrypto.configCipher,
380
380
  )
381
381
  : options.extraContext;
382
- // Auto-wire textContent (immer) + secrets (feature-gated), symmetrisch zu
382
+ // Auto-wire templateResolver (immer) + secrets (feature-gated), symmetrisch zu
383
383
  // runProdApp. Anders als prod existiert die db hier erst im Factory-deps
384
384
  // (createKumikoServer baut den Stack), darum als Factory: buildBootExtraContext
385
385
  // mit hasAuth:false (configResolver kommt schon aus cfgExtra), App-Werte
@@ -568,6 +568,10 @@ function renderDev(appName: string): string {
568
568
  writer.writeLine(`email: "admin@${appName}.local",`);
569
569
  writer.writeLine(`password: "changeme",`);
570
570
  writer.writeLine(`displayName: "Admin",`);
571
+ // Dev-only: skip the email-verify login gate for the seeded
572
+ // admin. Prod's scaffold (renderMain) leaves this unset on
573
+ // purpose — the verify flow must run there.
574
+ writer.writeLine("emailVerified: true,");
571
575
  writer.write("memberships: [");
572
576
  writer.indent(() => {
573
577
  writer.inlineBlock(() => {
@@ -2,7 +2,7 @@
2
2
  // Apps pass the same feature list as run-config (APP_FEATURES / buildAppFeatures).
3
3
 
4
4
  import { createConfigResolver } from "@cosmicdrift/kumiko-bundled-features/config";
5
- import { createTextContentApi } from "@cosmicdrift/kumiko-bundled-features/text-content";
5
+ import { createTemplateResolverApi } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
6
6
  import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import {
8
8
  setupTestStack,
@@ -14,7 +14,7 @@ import {
14
14
  composeFeatures,
15
15
  } from "@cosmicdrift/kumiko-server-runtime/compose-features";
16
16
 
17
- export type TestStackPreset = "config" | "text-content";
17
+ export type TestStackPreset = "config" | "template-resolver";
18
18
 
19
19
  export type SetupTestStackFromFeaturesOptions = Omit<TestStackOptions, "features"> & {
20
20
  readonly includeBundled?: boolean;
@@ -37,8 +37,8 @@ export function mergeExtraContext(
37
37
  const configResolver = createConfigResolver();
38
38
  merged["configResolver"] = configResolver;
39
39
  }
40
- if (presets.includes("text-content")) {
41
- merged["textContent"] = createTextContentApi(deps.db);
40
+ if (presets.includes("template-resolver")) {
41
+ merged["templateResolver"] = createTemplateResolverApi(deps.db);
42
42
  }
43
43
 
44
44
  return merged;