@cosmicdrift/kumiko-dev-server 0.79.0 → 0.79.3

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.0",
3
+ "version": "0.79.3",
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.0",
54
- "@cosmicdrift/kumiko-framework": "0.79.0",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.79.3",
54
+ "@cosmicdrift/kumiko-framework": "0.79.3",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -13,6 +13,11 @@ import { composeFeatures } from "../compose-features";
13
13
 
14
14
  const noopFeature = defineFeature("noop-app", () => {});
15
15
 
16
+ // Mirrors what the create-kumiko-app picker hands back when the user
17
+ // ticks an auto-mounted feature: a stub with the same name as a bundled
18
+ // one. The dedupe path drops it and warns.
19
+ const pickerAuthDupe = defineFeature("auth-email-password", () => {});
20
+
16
21
  const HMAC_SECRET = "test-secret-with-at-least-32-bytes-aaa";
17
22
 
18
23
  describe("composeFeatures", () => {
@@ -78,4 +83,29 @@ describe("composeFeatures", () => {
78
83
  expect(handlerNames).toContain("login");
79
84
  expect(handlerNames).toContain("logout");
80
85
  });
86
+
87
+ test("app feature duplicating a bundled name is dropped (no createRegistry crash)", () => {
88
+ // Bug discovered during Phase 3 recording: create-kumiko-app's picker
89
+ // hands back createAuthEmailPasswordFeature() because the user ticked
90
+ // it in the recommended set; runDevApp then adds its OWN bundled
91
+ // copy via includeBundled:true, and createRegistry throws "Duplicate
92
+ // feature: auth-email-password". The dedupe path keeps the bundled
93
+ // instance (it carries authOptions wiring) and drops the app stub.
94
+ const features = composeFeatures([pickerAuthDupe, noopFeature], {
95
+ includeBundled: true,
96
+ });
97
+ expect(features.map((f) => f.name)).toEqual([
98
+ "config",
99
+ "user",
100
+ "tenant",
101
+ "auth-email-password",
102
+ "noop-app",
103
+ ]);
104
+ // The kept instance is the bundled one — confirmed by the presence of
105
+ // login/logout handlers (the picker stub has none).
106
+ const auth = features.find((f) => f.name === "auth-email-password");
107
+ expect(auth).toBeDefined();
108
+ if (!auth) return;
109
+ expect(Object.keys(auth.writeHandlers)).toContain("login");
110
+ });
81
111
  });
@@ -41,15 +41,33 @@ export function composeFeatures(
41
41
  appFeatures: readonly FeatureDefinition[],
42
42
  options: ComposeFeaturesOptions,
43
43
  ): FeatureDefinition[] {
44
- return options.includeBundled
45
- ? [
46
- createConfigFeature(),
47
- createUserFeature(),
48
- createTenantFeature(),
49
- createAuthEmailPasswordFeature(options.authOptions ?? {}),
50
- ...appFeatures,
51
- ]
52
- : [...appFeatures];
44
+ if (!options.includeBundled) return [...appFeatures];
45
+
46
+ // Bundled foundation goes first so its instances carry the runDevApp /
47
+ // runProdApp `authOptions` (passwordReset wiring etc.). App-features that
48
+ // ALSO declare one of these names — e.g. the create-kumiko-app picker
49
+ // hands back `createAuthEmailPasswordFeature()` because the user ticked
50
+ // it — would otherwise crash createRegistry with "Duplicate feature".
51
+ // Drop the app-side duplicates and warn so the user can clean run-config.
52
+ const bundled = [
53
+ createConfigFeature(),
54
+ createUserFeature(),
55
+ createTenantFeature(),
56
+ createAuthEmailPasswordFeature(options.authOptions ?? {}),
57
+ ];
58
+ const bundledNames = new Set(bundled.map((f) => f.name));
59
+ const filteredApp: FeatureDefinition[] = [];
60
+ for (const f of appFeatures) {
61
+ if (bundledNames.has(f.name)) {
62
+ // biome-ignore lint/suspicious/noConsole: boot-time UX warning
63
+ console.warn(
64
+ `[composeFeatures] "${f.name}" already auto-mounted via includeBundled — dropping the explicit copy from APP_FEATURES. Remove it from run-config.ts to silence this warning.`,
65
+ );
66
+ continue;
67
+ }
68
+ filteredApp.push(f);
69
+ }
70
+ return [...bundled, ...filteredApp];
53
71
  }
54
72
 
55
73
  /** Shape eines beliebigen run{Prod,Dev}App-Auth-Blocks der eine