@cosmicdrift/kumiko-framework 0.96.0 → 0.97.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-framework",
3
- "version": "0.96.0",
3
+ "version": "0.97.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -181,7 +181,7 @@
181
181
  "zod": "^4.4.3"
182
182
  },
183
183
  "devDependencies": {
184
- "@cosmicdrift/kumiko-dispatcher-live": "0.96.0",
184
+ "@cosmicdrift/kumiko-dispatcher-live": "0.97.0",
185
185
  "bun-types": "^1.3.13",
186
186
  "pino-pretty": "^13.1.3"
187
187
  },
@@ -1920,6 +1920,28 @@ describe("boot-validator", () => {
1920
1920
  /rowAction "edit" navigate-target "ghost-screen" does not resolve/,
1921
1921
  );
1922
1922
  });
1923
+
1924
+ test("navigate-target → screen in ANOTHER feature → kein Throw (cross-feature)", () => {
1925
+ // Eine deklarative Liste im owning-Feature der Entity navigiert zum
1926
+ // Custom-Editor der Consumer-App (anderes Feature). Runtime löst die bare
1927
+ // id app-weit auf → der Validator muss das erlauben.
1928
+ const list = defineFeature("shop", (r) => {
1929
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
1930
+ r.screen({
1931
+ id: "product-list",
1932
+ type: "entityList",
1933
+ entity: "product",
1934
+ columns: ["name"],
1935
+ rowActions: [
1936
+ { kind: "navigate", id: "edit", label: "actions.edit", screen: "product-editor" },
1937
+ ],
1938
+ });
1939
+ });
1940
+ const consumer = defineFeature("app", (r) => {
1941
+ r.screen({ id: "product-editor", type: "custom", renderer: { react: "stub" } });
1942
+ });
1943
+ expect(() => validateBoot([list, consumer])).not.toThrow();
1944
+ });
1923
1945
  });
1924
1946
 
1925
1947
  // --- rowAction kind="writeHandler" handler-QN-Validierung (Tier 2.7e-1 erw.) ---
@@ -2125,6 +2147,25 @@ describe("boot-validator", () => {
2125
2147
  /toolbarAction "open-form" navigate-target "ghost-form" does not resolve/,
2126
2148
  );
2127
2149
  });
2150
+
2151
+ test("navigate-target → screen in ANOTHER feature → kein Throw (cross-feature)", () => {
2152
+ const list = defineFeature("shop", (r) => {
2153
+ r.entity("product", createEntity({ fields: { name: createTextField() } }));
2154
+ r.screen({
2155
+ id: "product-list",
2156
+ type: "entityList",
2157
+ entity: "product",
2158
+ columns: ["name"],
2159
+ toolbarActions: [
2160
+ { kind: "navigate", id: "new", label: "actions.new", screen: "product-editor" },
2161
+ ],
2162
+ });
2163
+ });
2164
+ const consumer = defineFeature("app", (r) => {
2165
+ r.screen({ id: "product-editor", type: "custom", renderer: { react: "stub" } });
2166
+ });
2167
+ expect(() => validateBoot([list, consumer])).not.toThrow();
2168
+ });
2128
2169
  });
2129
2170
 
2130
2171
  describe("entityList toolbarAction writeHandler handler-QN (Tier 2.7e-2)", () => {
@@ -88,6 +88,13 @@ export function validateScreens(
88
88
  allScreenQns: ReadonlySet<string>,
89
89
  allConfigKeyQns: ReadonlySet<string>,
90
90
  ): void {
91
+ // navigate-Targets (rowAction/toolbarAction) dürfen cross-feature zeigen —
92
+ // der Runtime-Router (create-app) löst eine bare screenId app-weit über ALLE
93
+ // Features auf (eine deklarative Liste im owning-Feature der Entity navigiert
94
+ // so zu den Custom-Editoren der Consumer-App). Der Validator spiegelt das:
95
+ // same-feature ODER irgendein Feature. (redirect/cancelTarget bleiben bewusst
96
+ // same-feature: deren Router baut die URL direkt aus der kurzen id.)
97
+ const navTargetShortIds = screenShortIdsFrom(allScreenQns);
91
98
  for (const [screenId, screen] of Object.entries(feature.screens)) {
92
99
  if (screen.type === "custom") {
93
100
  if (!screen.renderer.react && !screen.renderer.native) {
@@ -429,10 +436,10 @@ export function validateScreens(
429
436
  for (const action of screen.rowActions) {
430
437
  if (action.kind === "navigate") {
431
438
  const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
432
- if (!allScreenQns.has(candidateQn)) {
439
+ if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
433
440
  throw new Error(
434
441
  `[Feature ${feature.name}] Screen "${screenId}" (entityList) rowAction "${action.id}" ` +
435
- `navigate-target "${action.screen}" does not resolve to a registered screen in this feature.`,
442
+ `navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`,
436
443
  );
437
444
  }
438
445
  } else {
@@ -462,10 +469,10 @@ export function validateScreens(
462
469
  for (const action of screen.toolbarActions) {
463
470
  if (action.kind === "navigate") {
464
471
  const candidateQn = qualifyEntityName(feature.name, "screen", action.screen);
465
- if (!allScreenQns.has(candidateQn)) {
472
+ if (!allScreenQns.has(candidateQn) && !navTargetShortIds.has(action.screen)) {
466
473
  throw new Error(
467
474
  `[Feature ${feature.name}] Screen "${screenId}" (entityList) toolbarAction "${action.id}" ` +
468
- `navigate-target "${action.screen}" does not resolve to a registered screen in this feature.`,
475
+ `navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`,
469
476
  );
470
477
  }
471
478
  } else {
@@ -615,6 +622,19 @@ export function collectScreenQns(features: readonly FeatureDefinition[]): Set<st
615
622
  return set;
616
623
  }
617
624
 
625
+ // Bare Screen-ids (ohne `<feature>:screen:`-Prefix) aus den qualifizierten
626
+ // QNs — für die app-weite Auflösung von navigate-Targets (s. validateScreens).
627
+ // Spiegelt den Runtime-Router, der bare ids feature-übergreifend matcht.
628
+ export function screenShortIdsFrom(allScreenQns: ReadonlySet<string>): Set<string> {
629
+ const marker = ":screen:";
630
+ const set = new Set<string>();
631
+ for (const qn of allScreenQns) {
632
+ const at = qn.indexOf(marker);
633
+ if (at !== -1) set.add(qn.slice(at + marker.length));
634
+ }
635
+ return set;
636
+ }
637
+
618
638
  // Sammelt alle qualifizierten Write-Handler-QNs (`<feature>:write:<short>`).
619
639
  // Wird vom actionForm-Screen-Validator genutzt um zu prüfen ob der
620
640
  // im Schema deklarierte handler tatsächlich registriert ist —