@shipfox/client-shell 11.0.0 → 12.0.1

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 (51) hide show
  1. package/dist/compose/compose-client-features.d.ts +2 -1
  2. package/dist/compose/compose-client-features.d.ts.map +1 -1
  3. package/dist/compose/compose-client-features.js +5 -3
  4. package/dist/compose/compose-client-features.js.map +1 -1
  5. package/dist/compose/compose-routes.d.ts +6 -2
  6. package/dist/compose/compose-routes.d.ts.map +1 -1
  7. package/dist/compose/compose-routes.js +137 -2
  8. package/dist/compose/compose-routes.js.map +1 -1
  9. package/dist/compose/errors.d.ts +4 -0
  10. package/dist/compose/errors.d.ts.map +1 -1
  11. package/dist/compose/errors.js +6 -0
  12. package/dist/compose/errors.js.map +1 -1
  13. package/dist/compose/validate-registries.d.ts +9 -2
  14. package/dist/compose/validate-registries.d.ts.map +1 -1
  15. package/dist/compose/validate-registries.js +83 -2
  16. package/dist/compose/validate-registries.js.map +1 -1
  17. package/dist/contract.d.ts +23 -3
  18. package/dist/contract.d.ts.map +1 -1
  19. package/dist/contract.js.map +1 -1
  20. package/dist/runtime/anchor-paths.d.ts +2 -0
  21. package/dist/runtime/anchor-paths.d.ts.map +1 -1
  22. package/dist/runtime/anchor-paths.js +15 -0
  23. package/dist/runtime/anchor-paths.js.map +1 -1
  24. package/dist/runtime/assemble-route-tree.d.ts +3 -2
  25. package/dist/runtime/assemble-route-tree.d.ts.map +1 -1
  26. package/dist/runtime/assemble-route-tree.js +76 -5
  27. package/dist/runtime/assemble-route-tree.js.map +1 -1
  28. package/dist/runtime/index.d.ts +1 -0
  29. package/dist/runtime/index.d.ts.map +1 -1
  30. package/dist/runtime/index.js +1 -0
  31. package/dist/runtime/index.js.map +1 -1
  32. package/dist/runtime/layout-navigation.d.ts +7 -0
  33. package/dist/runtime/layout-navigation.d.ts.map +1 -0
  34. package/dist/runtime/layout-navigation.js +18 -0
  35. package/dist/runtime/layout-navigation.js.map +1 -0
  36. package/dist/runtime/provider-stack.d.ts.map +1 -1
  37. package/dist/runtime/provider-stack.js +7 -3
  38. package/dist/runtime/provider-stack.js.map +1 -1
  39. package/dist/runtime/registries.d.ts +2 -1
  40. package/dist/runtime/registries.d.ts.map +1 -1
  41. package/dist/runtime/registries.js +10 -0
  42. package/dist/runtime/registries.js.map +1 -1
  43. package/dist/tsconfig.test.tsbuildinfo +1 -1
  44. package/dist/vite/generate.d.ts +3 -2
  45. package/dist/vite/generate.d.ts.map +1 -1
  46. package/dist/vite/generate.js +108 -20
  47. package/dist/vite/generate.js.map +1 -1
  48. package/dist/vite/plugin.d.ts.map +1 -1
  49. package/dist/vite/plugin.js +7 -3
  50. package/dist/vite/plugin.js.map +1 -1
  51. package/package.json +3 -3
@@ -1,8 +1,9 @@
1
1
  import type { ClientFeature, NavTabEntry, SettingsSectionEntry } from '#contract.js';
2
- import { type ComposedRoute } from './compose-routes.js';
2
+ import { type ComposedLayout, type ComposedRoute } from './compose-routes.js';
3
3
  import { mergeConfigShapes } from './merge-config.js';
4
4
  export interface ComposedClientFeatures {
5
5
  configShape: ReturnType<typeof mergeConfigShapes>;
6
+ layouts: ComposedLayout[];
6
7
  navigation: NavTabEntry[];
7
8
  routes: ComposedRoute[];
8
9
  settingsSections: SettingsSectionEntry[];
@@ -1 +1 @@
1
- {"version":3,"file":"compose-client-features.d.ts","sourceRoot":"","sources":["../../src/compose/compose-client-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAC,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAIpD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IAClD,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,sBAAsB,CAYhG"}
1
+ {"version":3,"file":"compose-client-features.d.ts","sourceRoot":"","sources":["../../src/compose/compose-client-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,WAAW,EAAE,oBAAoB,EAAC,MAAM,cAAc,CAAC;AAEnF,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAGnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAIpD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IAClD,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,UAAU,EAAE,WAAW,EAAE,CAAC;IAC1B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;CAC1C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,sBAAsB,CAchG"}
@@ -1,15 +1,17 @@
1
1
  import { navigationEntries, settingsEntries } from '#runtime/registries.js';
2
- import { composeRoutes } from './compose-routes.js';
2
+ import { composeLayouts, composeRoutes } from './compose-routes.js';
3
3
  import { mergeConfigShapes } from './merge-config.js';
4
4
  import { validateProviderIds } from './validate-providers.js';
5
5
  import { validateNavigation, validateSettingsSections } from './validate-registries.js';
6
6
  export function composeClientFeatures(features) {
7
- const routes = composeRoutes(features);
7
+ const layouts = composeLayouts(features);
8
+ const routes = composeRoutes(features, layouts);
8
9
  validateProviderIds(features);
9
- validateNavigation(features, routes);
10
+ validateNavigation(features, routes, layouts);
10
11
  validateSettingsSections(features, routes);
11
12
  return {
12
13
  configShape: mergeConfigShapes(features),
14
+ layouts,
13
15
  navigation: navigationEntries(features),
14
16
  routes,
15
17
  settingsSections: settingsEntries(features)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/compose-client-features.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {navigationEntries, settingsEntries} from '#runtime/registries.js';\nimport {type ComposedRoute, composeRoutes} from './compose-routes.js';\nimport {mergeConfigShapes} from './merge-config.js';\nimport {validateProviderIds} from './validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from './validate-registries.js';\n\nexport interface ComposedClientFeatures {\n configShape: ReturnType<typeof mergeConfigShapes>;\n navigation: NavTabEntry[];\n routes: ComposedRoute[];\n settingsSections: SettingsSectionEntry[];\n}\n\nexport function composeClientFeatures(features: readonly ClientFeature[]): ComposedClientFeatures {\n const routes = composeRoutes(features);\n validateProviderIds(features);\n validateNavigation(features, routes);\n validateSettingsSections(features, routes);\n\n return {\n configShape: mergeConfigShapes(features),\n navigation: navigationEntries(features),\n routes,\n settingsSections: settingsEntries(features),\n };\n}\n"],"names":["navigationEntries","settingsEntries","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","composeClientFeatures","features","routes","configShape","navigation","settingsSections"],"mappings":"AACA,SAAQA,iBAAiB,EAAEC,eAAe,QAAO,yBAAyB;AAC1E,SAA4BC,aAAa,QAAO,sBAAsB;AACtE,SAAQC,iBAAiB,QAAO,oBAAoB;AACpD,SAAQC,mBAAmB,QAAO,0BAA0B;AAC5D,SAAQC,kBAAkB,EAAEC,wBAAwB,QAAO,2BAA2B;AAStF,OAAO,SAASC,sBAAsBC,QAAkC;IACtE,MAAMC,SAASP,cAAcM;IAC7BJ,oBAAoBI;IACpBH,mBAAmBG,UAAUC;IAC7BH,yBAAyBE,UAAUC;IAEnC,OAAO;QACLC,aAAaP,kBAAkBK;QAC/BG,YAAYX,kBAAkBQ;QAC9BC;QACAG,kBAAkBX,gBAAgBO;IACpC;AACF"}
1
+ {"version":3,"sources":["../../src/compose/compose-client-features.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, SettingsSectionEntry} from '#contract.js';\nimport {navigationEntries, settingsEntries} from '#runtime/registries.js';\nimport {\n type ComposedLayout,\n type ComposedRoute,\n composeLayouts,\n composeRoutes,\n} from './compose-routes.js';\nimport {mergeConfigShapes} from './merge-config.js';\nimport {validateProviderIds} from './validate-providers.js';\nimport {validateNavigation, validateSettingsSections} from './validate-registries.js';\n\nexport interface ComposedClientFeatures {\n configShape: ReturnType<typeof mergeConfigShapes>;\n layouts: ComposedLayout[];\n navigation: NavTabEntry[];\n routes: ComposedRoute[];\n settingsSections: SettingsSectionEntry[];\n}\n\nexport function composeClientFeatures(features: readonly ClientFeature[]): ComposedClientFeatures {\n const layouts = composeLayouts(features);\n const routes = composeRoutes(features, layouts);\n validateProviderIds(features);\n validateNavigation(features, routes, layouts);\n validateSettingsSections(features, routes);\n\n return {\n configShape: mergeConfigShapes(features),\n layouts,\n navigation: navigationEntries(features),\n routes,\n settingsSections: settingsEntries(features),\n };\n}\n"],"names":["navigationEntries","settingsEntries","composeLayouts","composeRoutes","mergeConfigShapes","validateProviderIds","validateNavigation","validateSettingsSections","composeClientFeatures","features","layouts","routes","configShape","navigation","settingsSections"],"mappings":"AACA,SAAQA,iBAAiB,EAAEC,eAAe,QAAO,yBAAyB;AAC1E,SAGEC,cAAc,EACdC,aAAa,QACR,sBAAsB;AAC7B,SAAQC,iBAAiB,QAAO,oBAAoB;AACpD,SAAQC,mBAAmB,QAAO,0BAA0B;AAC5D,SAAQC,kBAAkB,EAAEC,wBAAwB,QAAO,2BAA2B;AAUtF,OAAO,SAASC,sBAAsBC,QAAkC;IACtE,MAAMC,UAAUR,eAAeO;IAC/B,MAAME,SAASR,cAAcM,UAAUC;IACvCL,oBAAoBI;IACpBH,mBAAmBG,UAAUE,QAAQD;IACrCH,yBAAyBE,UAAUE;IAEnC,OAAO;QACLC,aAAaR,kBAAkBK;QAC/BC;QACAG,YAAYb,kBAAkBS;QAC9BE;QACAG,kBAAkBb,gBAAgBQ;IACpC;AACF"}
@@ -1,7 +1,11 @@
1
- import type { ClientFeature, RouteContribution } from '#contract.js';
1
+ import type { ClientFeature, LayoutContribution, RouteContribution } from '#contract.js';
2
+ export interface ComposedLayout extends LayoutContribution {
3
+ featureId: string;
4
+ }
2
5
  export interface ComposedRoute extends RouteContribution {
3
6
  featureId: string;
4
7
  ownerFeatureId: string;
5
8
  }
6
- export declare function composeRoutes(features: readonly ClientFeature[]): ComposedRoute[];
9
+ export declare function composeLayouts(features: readonly ClientFeature[]): ComposedLayout[];
10
+ export declare function composeRoutes(features: readonly ClientFeature[], providedLayouts?: readonly ComposedLayout[]): ComposedRoute[];
7
11
  //# sourceMappingURL=compose-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAInE,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAqDjF"}
1
+ {"version":3,"file":"compose-routes.d.ts","sourceRoot":"","sources":["../../src/compose/compose-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,cAAc,CAAC;AAKtB,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AA0ID,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,EAAE,CAanF;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,aAAa,EAAE,CAqFjB"}
@@ -1,6 +1,127 @@
1
- import { RouteCompositionError } from './errors.js';
1
+ import { anchorPaths } from '#runtime/anchor-paths.js';
2
+ import { LayoutCompositionError, RouteCompositionError } from './errors.js';
2
3
  import { normalizeRoutePath } from './normalize-route-path.js';
3
- export function composeRoutes(features) {
4
+ function isShellAnchor(parent) {
5
+ return Object.hasOwn(anchorPaths, parent);
6
+ }
7
+ function layoutParentPath(parent, layouts) {
8
+ if (isShellAnchor(parent)) return anchorPaths[parent];
9
+ return layouts.get(parent)?.path;
10
+ }
11
+ function resolvedShellAnchor(parent, layouts) {
12
+ let current = parent;
13
+ const visited = new Set();
14
+ while(!isShellAnchor(current)){
15
+ if (visited.has(current)) return undefined;
16
+ visited.add(current);
17
+ const next = layouts.get(current)?.parent;
18
+ if (next === undefined) return undefined;
19
+ current = next;
20
+ }
21
+ return current;
22
+ }
23
+ function validateNestedRoute(path, parent, layouts, featureId) {
24
+ if (resolvedShellAnchor(parent, layouts) === 'root') {
25
+ const protectedAnchor = protectedAnchorForRootPath(path);
26
+ if (protectedAnchor) {
27
+ const [anchor, anchorPath] = protectedAnchor;
28
+ throw new RouteCompositionError(path, `Route "${path}" in feature "${featureId}" cannot use root parent inside reserved anchor "${anchor}" (${anchorPath}). Use parent "${anchor}".`, [
29
+ featureId
30
+ ]);
31
+ }
32
+ }
33
+ const parentPath = layoutParentPath(parent, layouts);
34
+ if (!parentPath) {
35
+ if (isShellAnchor(parent)) return;
36
+ throw new RouteCompositionError(path, `Route "${path}" in feature "${featureId}" targets missing layout parent "${parent}".`, [
37
+ featureId
38
+ ]);
39
+ }
40
+ if (parentPath !== '/' && path !== parentPath && !path.startsWith(`${parentPath}/`)) {
41
+ const parentKind = isShellAnchor(parent) ? 'anchor' : 'layout';
42
+ throw new RouteCompositionError(path, `Route "${path}" must be nested under ${parentKind} "${parent}" (${parentPath}).`, [
43
+ featureId
44
+ ]);
45
+ }
46
+ }
47
+ const anchorPathValues = new Set(Object.values(anchorPaths));
48
+ const protectedAnchorPaths = Object.entries(anchorPaths).filter(([anchor])=>anchor !== 'root').sort(([, leftPath], [, rightPath])=>rightPath.length - leftPath.length);
49
+ function protectedAnchorForRootPath(path) {
50
+ return protectedAnchorPaths.find(([, anchorPath])=>path === anchorPath || path.startsWith(`${anchorPath}/`));
51
+ }
52
+ function validateLayoutParents(layouts) {
53
+ const byId = new Map();
54
+ for (const layout of layouts){
55
+ if (isShellAnchor(layout.id)) {
56
+ throw new LayoutCompositionError(layout.id, `Layout id "${layout.id}" in feature "${layout.featureId}" is reserved by the shell.`, [
57
+ layout.featureId
58
+ ]);
59
+ }
60
+ if (anchorPathValues.has(layout.path)) {
61
+ throw new LayoutCompositionError(layout.id, `Layout "${layout.id}" in feature "${layout.featureId}" targets path "${layout.path}" which is reserved by a shell anchor.`, [
62
+ layout.featureId
63
+ ]);
64
+ }
65
+ const existing = byId.get(layout.id);
66
+ if (existing) {
67
+ throw new LayoutCompositionError(layout.id, `Layout id "${layout.id}" is contributed by both features "${existing.featureId}" and "${layout.featureId}".`, [
68
+ existing.featureId,
69
+ layout.featureId
70
+ ]);
71
+ }
72
+ byId.set(layout.id, layout);
73
+ }
74
+ const visiting = new Set();
75
+ const visited = new Set();
76
+ function visit(layout) {
77
+ if (visited.has(layout.id) || isShellAnchor(layout.parent)) return;
78
+ if (visiting.has(layout.id)) {
79
+ throw new LayoutCompositionError(layout.id, `Layout "${layout.id}" in feature "${layout.featureId}" has a cyclic parent reference.`, [
80
+ layout.featureId
81
+ ]);
82
+ }
83
+ const parent = byId.get(layout.parent);
84
+ if (!parent) {
85
+ throw new LayoutCompositionError(layout.id, `Layout "${layout.id}" in feature "${layout.featureId}" targets missing layout parent "${layout.parent}".`, [
86
+ layout.featureId
87
+ ]);
88
+ }
89
+ visiting.add(layout.id);
90
+ visit(parent);
91
+ visiting.delete(layout.id);
92
+ visited.add(layout.id);
93
+ }
94
+ for (const layout of layouts)visit(layout);
95
+ return byId;
96
+ }
97
+ export function composeLayouts(features) {
98
+ const layouts = features.flatMap((feature)=>(feature.layouts ?? []).map((layout)=>({
99
+ ...layout,
100
+ path: normalizeRoutePath(layout.path),
101
+ featureId: feature.id
102
+ })));
103
+ const layoutById = validateLayoutParents(layouts);
104
+ for (const layout of layouts){
105
+ validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);
106
+ }
107
+ return layouts;
108
+ }
109
+ export function composeRoutes(features, providedLayouts) {
110
+ const layouts = providedLayouts ? [
111
+ ...providedLayouts
112
+ ] : composeLayouts(features);
113
+ const layoutById = validateLayoutParents(layouts);
114
+ const layoutPaths = new Map();
115
+ for (const layout of layouts){
116
+ const existing = layoutPaths.get(layout.path);
117
+ if (existing) {
118
+ throw new RouteCompositionError(layout.path, `Route "${layout.path}" is contributed by both features "${existing.featureId}" and "${layout.featureId}".`, [
119
+ existing.featureId,
120
+ layout.featureId
121
+ ]);
122
+ }
123
+ layoutPaths.set(layout.path, layout);
124
+ }
4
125
  const routes = new Map();
5
126
  for (const feature of features){
6
127
  for (const contribution of feature.routes ?? []){
@@ -8,6 +129,14 @@ export function composeRoutes(features) {
8
129
  ...contribution,
9
130
  path: normalizeRoutePath(contribution.path)
10
131
  };
132
+ const layoutAtPath = layoutPaths.get(normalizedContribution.path);
133
+ if (layoutAtPath) {
134
+ const message = normalizedContribution.override ? `Route override for "${normalizedContribution.path}" from feature "${feature.id}" cannot replace layout "${layoutAtPath.id}" contributed by feature "${layoutAtPath.featureId}".` : `Route "${normalizedContribution.path}" from feature "${feature.id}" conflicts with layout "${layoutAtPath.id}" contributed by feature "${layoutAtPath.featureId}". Routes cannot replace layouts.`;
135
+ throw new RouteCompositionError(normalizedContribution.path, message, [
136
+ layoutAtPath.featureId,
137
+ feature.id
138
+ ]);
139
+ }
11
140
  const existing = routes.get(normalizedContribution.path);
12
141
  if (!existing) {
13
142
  if (normalizedContribution.override) {
@@ -47,6 +176,12 @@ export function composeRoutes(features) {
47
176
  });
48
177
  }
49
178
  }
179
+ for (const layout of layouts){
180
+ validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);
181
+ }
182
+ for (const route of routes.values()){
183
+ validateNestedRoute(route.path, route.parent, layoutById, route.featureId);
184
+ }
50
185
  return [
51
186
  ...routes.values()
52
187
  ];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {ClientFeature, RouteContribution} from '#contract.js';\nimport {RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n ownerFeatureId: string;\n}\n\nexport function composeRoutes(features: readonly ClientFeature[]): ComposedRoute[] {\n const routes = new Map<string, ComposedRoute>();\n for (const feature of features) {\n for (const contribution of feature.routes ?? []) {\n const normalizedContribution = {\n ...contribution,\n path: normalizeRoutePath(contribution.path),\n };\n const existing = routes.get(normalizedContribution.path);\n if (!existing) {\n if (normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" has no route to replace.`,\n [feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: feature.id,\n });\n continue;\n }\n if (!normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" is contributed by both features \"${existing.featureId}\" and \"${feature.id}\". Set override: true to replace it explicitly.`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" has competing overrides from features \"${existing.featureId}\" and \"${feature.id}\".`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.parent !== normalizedContribution.parent) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot change anchor from \"${existing.parent}\" in feature \"${existing.featureId}\" to \"${normalizedContribution.parent}\".`,\n [existing.featureId, feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: existing.ownerFeatureId,\n });\n }\n }\n return [...routes.values()];\n}\n"],"names":["RouteCompositionError","normalizeRoutePath","composeRoutes","features","routes","Map","feature","contribution","normalizedContribution","path","existing","get","override","id","set","featureId","ownerFeatureId","parent","values"],"mappings":"AACA,SAAQA,qBAAqB,QAAO,cAAc;AAClD,SAAQC,kBAAkB,QAAO,4BAA4B;AAO7D,OAAO,SAASC,cAAcC,QAAkC;IAC9D,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,WAAWH,SAAU;QAC9B,KAAK,MAAMI,gBAAgBD,QAAQF,MAAM,IAAI,EAAE,CAAE;YAC/C,MAAMI,yBAAyB;gBAC7B,GAAGD,YAAY;gBACfE,MAAMR,mBAAmBM,aAAaE,IAAI;YAC5C;YACA,MAAMC,WAAWN,OAAOO,GAAG,CAACH,uBAAuBC,IAAI;YACvD,IAAI,CAACC,UAAU;gBACb,IAAIF,uBAAuBI,QAAQ,EAAE;oBACnC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,0BAA0B,CAAC,EAC3G;wBAACP,QAAQO,EAAE;qBAAC;gBAEhB;gBACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;oBACtC,GAAGD,sBAAsB;oBACzBO,WAAWT,QAAQO,EAAE;oBACrBG,gBAAgBV,QAAQO,EAAE;gBAC5B;gBACA;YACF;YACA,IAAI,CAACL,uBAAuBI,QAAQ,EAAE;gBACpC,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,mCAAmC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,+CAA+C,CAAC,EAClK;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASE,QAAQ,EAAE;gBACrB,MAAM,IAAIZ,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,OAAO,EAAED,uBAAuBC,IAAI,CAAC,yCAAyC,EAAEC,SAASK,SAAS,CAAC,OAAO,EAAET,QAAQO,EAAE,CAAC,EAAE,CAAC,EAC3H;oBAACH,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACA,IAAIH,SAASO,MAAM,KAAKT,uBAAuBS,MAAM,EAAE;gBACrD,MAAM,IAAIjB,sBACRQ,uBAAuBC,IAAI,EAC3B,CAAC,oBAAoB,EAAED,uBAAuBC,IAAI,CAAC,gBAAgB,EAAEH,QAAQO,EAAE,CAAC,6BAA6B,EAAEH,SAASO,MAAM,CAAC,cAAc,EAAEP,SAASK,SAAS,CAAC,MAAM,EAAEP,uBAAuBS,MAAM,CAAC,EAAE,CAAC,EAC3M;oBAACP,SAASK,SAAS;oBAAET,QAAQO,EAAE;iBAAC;YAEpC;YACAT,OAAOU,GAAG,CAACN,uBAAuBC,IAAI,EAAE;gBACtC,GAAGD,sBAAsB;gBACzBO,WAAWT,QAAQO,EAAE;gBACrBG,gBAAgBN,SAASM,cAAc;YACzC;QACF;IACF;IACA,OAAO;WAAIZ,OAAOc,MAAM;KAAG;AAC7B"}
1
+ {"version":3,"sources":["../../src/compose/compose-routes.ts"],"sourcesContent":["import type {\n AnchorId,\n ClientFeature,\n LayoutContribution,\n RouteContribution,\n RouteParentId,\n} from '#contract.js';\nimport {anchorPaths} from '#runtime/anchor-paths.js';\nimport {LayoutCompositionError, RouteCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\nexport interface ComposedLayout extends LayoutContribution {\n featureId: string;\n}\n\nexport interface ComposedRoute extends RouteContribution {\n featureId: string;\n ownerFeatureId: string;\n}\n\nfunction isShellAnchor(parent: RouteParentId): parent is AnchorId {\n return Object.hasOwn(anchorPaths, parent);\n}\n\nfunction layoutParentPath(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): string | undefined {\n if (isShellAnchor(parent)) return anchorPaths[parent];\n return layouts.get(parent)?.path;\n}\n\nfunction resolvedShellAnchor(\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n): AnchorId | undefined {\n let current = parent;\n const visited = new Set<string>();\n while (!isShellAnchor(current)) {\n if (visited.has(current)) return undefined;\n visited.add(current);\n const next = layouts.get(current)?.parent;\n if (next === undefined) return undefined;\n current = next;\n }\n return current;\n}\n\nfunction validateNestedRoute(\n path: string,\n parent: RouteParentId,\n layouts: ReadonlyMap<string, ComposedLayout>,\n featureId: string,\n): void {\n if (resolvedShellAnchor(parent, layouts) === 'root') {\n const protectedAnchor = protectedAnchorForRootPath(path);\n if (protectedAnchor) {\n const [anchor, anchorPath] = protectedAnchor;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" cannot use root parent inside reserved anchor \"${anchor}\" (${anchorPath}). Use parent \"${anchor}\".`,\n [featureId],\n );\n }\n }\n const parentPath = layoutParentPath(parent, layouts);\n if (!parentPath) {\n if (isShellAnchor(parent)) return;\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" in feature \"${featureId}\" targets missing layout parent \"${parent}\".`,\n [featureId],\n );\n }\n if (parentPath !== '/' && path !== parentPath && !path.startsWith(`${parentPath}/`)) {\n const parentKind = isShellAnchor(parent) ? 'anchor' : 'layout';\n throw new RouteCompositionError(\n path,\n `Route \"${path}\" must be nested under ${parentKind} \"${parent}\" (${parentPath}).`,\n [featureId],\n );\n }\n}\n\nconst anchorPathValues: ReadonlySet<string> = new Set(Object.values(anchorPaths));\nconst protectedAnchorPaths = Object.entries(anchorPaths)\n .filter(([anchor]) => anchor !== 'root')\n .sort(([, leftPath], [, rightPath]) => rightPath.length - leftPath.length) as Array<\n [Exclude<AnchorId, 'root'>, string]\n>;\n\nfunction protectedAnchorForRootPath(\n path: string,\n): readonly [Exclude<AnchorId, 'root'>, string] | undefined {\n return protectedAnchorPaths.find(\n ([, anchorPath]) => path === anchorPath || path.startsWith(`${anchorPath}/`),\n );\n}\n\nfunction validateLayoutParents(layouts: readonly ComposedLayout[]): Map<string, ComposedLayout> {\n const byId = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n if (isShellAnchor(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" in feature \"${layout.featureId}\" is reserved by the shell.`,\n [layout.featureId],\n );\n }\n if (anchorPathValues.has(layout.path)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets path \"${layout.path}\" which is reserved by a shell anchor.`,\n [layout.featureId],\n );\n }\n const existing = byId.get(layout.id);\n if (existing) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout id \"${layout.id}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n byId.set(layout.id, layout);\n }\n\n const visiting = new Set<string>();\n const visited = new Set<string>();\n function visit(layout: ComposedLayout): void {\n if (visited.has(layout.id) || isShellAnchor(layout.parent)) return;\n if (visiting.has(layout.id)) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" has a cyclic parent reference.`,\n [layout.featureId],\n );\n }\n const parent = byId.get(layout.parent);\n if (!parent) {\n throw new LayoutCompositionError(\n layout.id,\n `Layout \"${layout.id}\" in feature \"${layout.featureId}\" targets missing layout parent \"${layout.parent}\".`,\n [layout.featureId],\n );\n }\n visiting.add(layout.id);\n visit(parent);\n visiting.delete(layout.id);\n visited.add(layout.id);\n }\n\n for (const layout of layouts) visit(layout);\n return byId;\n}\n\nexport function composeLayouts(features: readonly ClientFeature[]): ComposedLayout[] {\n const layouts = features.flatMap((feature) =>\n (feature.layouts ?? []).map((layout) => ({\n ...layout,\n path: normalizeRoutePath(layout.path),\n featureId: feature.id,\n })),\n );\n const layoutById = validateLayoutParents(layouts);\n for (const layout of layouts) {\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n return layouts;\n}\n\nexport function composeRoutes(\n features: readonly ClientFeature[],\n providedLayouts?: readonly ComposedLayout[],\n): ComposedRoute[] {\n const layouts = providedLayouts ? [...providedLayouts] : composeLayouts(features);\n const layoutById = validateLayoutParents(layouts);\n const layoutPaths = new Map<string, ComposedLayout>();\n for (const layout of layouts) {\n const existing = layoutPaths.get(layout.path);\n if (existing) {\n throw new RouteCompositionError(\n layout.path,\n `Route \"${layout.path}\" is contributed by both features \"${existing.featureId}\" and \"${layout.featureId}\".`,\n [existing.featureId, layout.featureId],\n );\n }\n layoutPaths.set(layout.path, layout);\n }\n\n const routes = new Map<string, ComposedRoute>();\n for (const feature of features) {\n for (const contribution of feature.routes ?? []) {\n const normalizedContribution = {\n ...contribution,\n path: normalizeRoutePath(contribution.path),\n };\n const layoutAtPath = layoutPaths.get(normalizedContribution.path);\n if (layoutAtPath) {\n const message = normalizedContribution.override\n ? `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot replace layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\".`\n : `Route \"${normalizedContribution.path}\" from feature \"${feature.id}\" conflicts with layout \"${layoutAtPath.id}\" contributed by feature \"${layoutAtPath.featureId}\". Routes cannot replace layouts.`;\n throw new RouteCompositionError(normalizedContribution.path, message, [\n layoutAtPath.featureId,\n feature.id,\n ]);\n }\n const existing = routes.get(normalizedContribution.path);\n if (!existing) {\n if (normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" has no route to replace.`,\n [feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: feature.id,\n });\n continue;\n }\n if (!normalizedContribution.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" is contributed by both features \"${existing.featureId}\" and \"${feature.id}\". Set override: true to replace it explicitly.`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.override) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route \"${normalizedContribution.path}\" has competing overrides from features \"${existing.featureId}\" and \"${feature.id}\".`,\n [existing.featureId, feature.id],\n );\n }\n if (existing.parent !== normalizedContribution.parent) {\n throw new RouteCompositionError(\n normalizedContribution.path,\n `Route override for \"${normalizedContribution.path}\" from feature \"${feature.id}\" cannot change anchor from \"${existing.parent}\" in feature \"${existing.featureId}\" to \"${normalizedContribution.parent}\".`,\n [existing.featureId, feature.id],\n );\n }\n routes.set(normalizedContribution.path, {\n ...normalizedContribution,\n featureId: feature.id,\n ownerFeatureId: existing.ownerFeatureId,\n });\n }\n }\n\n for (const layout of layouts) {\n validateNestedRoute(layout.path, layout.parent, layoutById, layout.featureId);\n }\n for (const route of routes.values()) {\n validateNestedRoute(route.path, route.parent, layoutById, route.featureId);\n }\n return [...routes.values()];\n}\n"],"names":["anchorPaths","LayoutCompositionError","RouteCompositionError","normalizeRoutePath","isShellAnchor","parent","Object","hasOwn","layoutParentPath","layouts","get","path","resolvedShellAnchor","current","visited","Set","has","undefined","add","next","validateNestedRoute","featureId","protectedAnchor","protectedAnchorForRootPath","anchor","anchorPath","parentPath","startsWith","parentKind","anchorPathValues","values","protectedAnchorPaths","entries","filter","sort","leftPath","rightPath","length","find","validateLayoutParents","byId","Map","layout","id","existing","set","visiting","visit","delete","composeLayouts","features","flatMap","feature","map","layoutById","composeRoutes","providedLayouts","layoutPaths","routes","contribution","normalizedContribution","layoutAtPath","message","override","ownerFeatureId","route"],"mappings":"AAOA,SAAQA,WAAW,QAAO,2BAA2B;AACrD,SAAQC,sBAAsB,EAAEC,qBAAqB,QAAO,cAAc;AAC1E,SAAQC,kBAAkB,QAAO,4BAA4B;AAW7D,SAASC,cAAcC,MAAqB;IAC1C,OAAOC,OAAOC,MAAM,CAACP,aAAaK;AACpC;AAEA,SAASG,iBACPH,MAAqB,EACrBI,OAA4C;IAE5C,IAAIL,cAAcC,SAAS,OAAOL,WAAW,CAACK,OAAO;IACrD,OAAOI,QAAQC,GAAG,CAACL,SAASM;AAC9B;AAEA,SAASC,oBACPP,MAAqB,EACrBI,OAA4C;IAE5C,IAAII,UAAUR;IACd,MAAMS,UAAU,IAAIC;IACpB,MAAO,CAACX,cAAcS,SAAU;QAC9B,IAAIC,QAAQE,GAAG,CAACH,UAAU,OAAOI;QACjCH,QAAQI,GAAG,CAACL;QACZ,MAAMM,OAAOV,QAAQC,GAAG,CAACG,UAAUR;QACnC,IAAIc,SAASF,WAAW,OAAOA;QAC/BJ,UAAUM;IACZ;IACA,OAAON;AACT;AAEA,SAASO,oBACPT,IAAY,EACZN,MAAqB,EACrBI,OAA4C,EAC5CY,SAAiB;IAEjB,IAAIT,oBAAoBP,QAAQI,aAAa,QAAQ;QACnD,MAAMa,kBAAkBC,2BAA2BZ;QACnD,IAAIW,iBAAiB;YACnB,MAAM,CAACE,QAAQC,WAAW,GAAGH;YAC7B,MAAM,IAAIpB,sBACRS,MACA,CAAC,OAAO,EAAEA,KAAK,cAAc,EAAEU,UAAU,iDAAiD,EAAEG,OAAO,GAAG,EAAEC,WAAW,eAAe,EAAED,OAAO,EAAE,CAAC,EAC9I;gBAACH;aAAU;QAEf;IACF;IACA,MAAMK,aAAalB,iBAAiBH,QAAQI;IAC5C,IAAI,CAACiB,YAAY;QACf,IAAItB,cAAcC,SAAS;QAC3B,MAAM,IAAIH,sBACRS,MACA,CAAC,OAAO,EAAEA,KAAK,cAAc,EAAEU,UAAU,iCAAiC,EAAEhB,OAAO,EAAE,CAAC,EACtF;YAACgB;SAAU;IAEf;IACA,IAAIK,eAAe,OAAOf,SAASe,cAAc,CAACf,KAAKgB,UAAU,CAAC,GAAGD,WAAW,CAAC,CAAC,GAAG;QACnF,MAAME,aAAaxB,cAAcC,UAAU,WAAW;QACtD,MAAM,IAAIH,sBACRS,MACA,CAAC,OAAO,EAAEA,KAAK,uBAAuB,EAAEiB,WAAW,EAAE,EAAEvB,OAAO,GAAG,EAAEqB,WAAW,EAAE,CAAC,EACjF;YAACL;SAAU;IAEf;AACF;AAEA,MAAMQ,mBAAwC,IAAId,IAAIT,OAAOwB,MAAM,CAAC9B;AACpE,MAAM+B,uBAAuBzB,OAAO0B,OAAO,CAAChC,aACzCiC,MAAM,CAAC,CAAC,CAACT,OAAO,GAAKA,WAAW,QAChCU,IAAI,CAAC,CAAC,GAAGC,SAAS,EAAE,GAAGC,UAAU,GAAKA,UAAUC,MAAM,GAAGF,SAASE,MAAM;AAI3E,SAASd,2BACPZ,IAAY;IAEZ,OAAOoB,qBAAqBO,IAAI,CAC9B,CAAC,GAAGb,WAAW,GAAKd,SAASc,cAAcd,KAAKgB,UAAU,CAAC,GAAGF,WAAW,CAAC,CAAC;AAE/E;AAEA,SAASc,sBAAsB9B,OAAkC;IAC/D,MAAM+B,OAAO,IAAIC;IACjB,KAAK,MAAMC,UAAUjC,QAAS;QAC5B,IAAIL,cAAcsC,OAAOC,EAAE,GAAG;YAC5B,MAAM,IAAI1C,uBACRyC,OAAOC,EAAE,EACT,CAAC,WAAW,EAAED,OAAOC,EAAE,CAAC,cAAc,EAAED,OAAOrB,SAAS,CAAC,2BAA2B,CAAC,EACrF;gBAACqB,OAAOrB,SAAS;aAAC;QAEtB;QACA,IAAIQ,iBAAiBb,GAAG,CAAC0B,OAAO/B,IAAI,GAAG;YACrC,MAAM,IAAIV,uBACRyC,OAAOC,EAAE,EACT,CAAC,QAAQ,EAAED,OAAOC,EAAE,CAAC,cAAc,EAAED,OAAOrB,SAAS,CAAC,gBAAgB,EAAEqB,OAAO/B,IAAI,CAAC,sCAAsC,CAAC,EAC3H;gBAAC+B,OAAOrB,SAAS;aAAC;QAEtB;QACA,MAAMuB,WAAWJ,KAAK9B,GAAG,CAACgC,OAAOC,EAAE;QACnC,IAAIC,UAAU;YACZ,MAAM,IAAI3C,uBACRyC,OAAOC,EAAE,EACT,CAAC,WAAW,EAAED,OAAOC,EAAE,CAAC,mCAAmC,EAAEC,SAASvB,SAAS,CAAC,OAAO,EAAEqB,OAAOrB,SAAS,CAAC,EAAE,CAAC,EAC7G;gBAACuB,SAASvB,SAAS;gBAAEqB,OAAOrB,SAAS;aAAC;QAE1C;QACAmB,KAAKK,GAAG,CAACH,OAAOC,EAAE,EAAED;IACtB;IAEA,MAAMI,WAAW,IAAI/B;IACrB,MAAMD,UAAU,IAAIC;IACpB,SAASgC,MAAML,MAAsB;QACnC,IAAI5B,QAAQE,GAAG,CAAC0B,OAAOC,EAAE,KAAKvC,cAAcsC,OAAOrC,MAAM,GAAG;QAC5D,IAAIyC,SAAS9B,GAAG,CAAC0B,OAAOC,EAAE,GAAG;YAC3B,MAAM,IAAI1C,uBACRyC,OAAOC,EAAE,EACT,CAAC,QAAQ,EAAED,OAAOC,EAAE,CAAC,cAAc,EAAED,OAAOrB,SAAS,CAAC,gCAAgC,CAAC,EACvF;gBAACqB,OAAOrB,SAAS;aAAC;QAEtB;QACA,MAAMhB,SAASmC,KAAK9B,GAAG,CAACgC,OAAOrC,MAAM;QACrC,IAAI,CAACA,QAAQ;YACX,MAAM,IAAIJ,uBACRyC,OAAOC,EAAE,EACT,CAAC,QAAQ,EAAED,OAAOC,EAAE,CAAC,cAAc,EAAED,OAAOrB,SAAS,CAAC,iCAAiC,EAAEqB,OAAOrC,MAAM,CAAC,EAAE,CAAC,EAC1G;gBAACqC,OAAOrB,SAAS;aAAC;QAEtB;QACAyB,SAAS5B,GAAG,CAACwB,OAAOC,EAAE;QACtBI,MAAM1C;QACNyC,SAASE,MAAM,CAACN,OAAOC,EAAE;QACzB7B,QAAQI,GAAG,CAACwB,OAAOC,EAAE;IACvB;IAEA,KAAK,MAAMD,UAAUjC,QAASsC,MAAML;IACpC,OAAOF;AACT;AAEA,OAAO,SAASS,eAAeC,QAAkC;IAC/D,MAAMzC,UAAUyC,SAASC,OAAO,CAAC,CAACC,UAChC,AAACA,CAAAA,QAAQ3C,OAAO,IAAI,EAAE,AAAD,EAAG4C,GAAG,CAAC,CAACX,SAAY,CAAA;gBACvC,GAAGA,MAAM;gBACT/B,MAAMR,mBAAmBuC,OAAO/B,IAAI;gBACpCU,WAAW+B,QAAQT,EAAE;YACvB,CAAA;IAEF,MAAMW,aAAaf,sBAAsB9B;IACzC,KAAK,MAAMiC,UAAUjC,QAAS;QAC5BW,oBAAoBsB,OAAO/B,IAAI,EAAE+B,OAAOrC,MAAM,EAAEiD,YAAYZ,OAAOrB,SAAS;IAC9E;IACA,OAAOZ;AACT;AAEA,OAAO,SAAS8C,cACdL,QAAkC,EAClCM,eAA2C;IAE3C,MAAM/C,UAAU+C,kBAAkB;WAAIA;KAAgB,GAAGP,eAAeC;IACxE,MAAMI,aAAaf,sBAAsB9B;IACzC,MAAMgD,cAAc,IAAIhB;IACxB,KAAK,MAAMC,UAAUjC,QAAS;QAC5B,MAAMmC,WAAWa,YAAY/C,GAAG,CAACgC,OAAO/B,IAAI;QAC5C,IAAIiC,UAAU;YACZ,MAAM,IAAI1C,sBACRwC,OAAO/B,IAAI,EACX,CAAC,OAAO,EAAE+B,OAAO/B,IAAI,CAAC,mCAAmC,EAAEiC,SAASvB,SAAS,CAAC,OAAO,EAAEqB,OAAOrB,SAAS,CAAC,EAAE,CAAC,EAC3G;gBAACuB,SAASvB,SAAS;gBAAEqB,OAAOrB,SAAS;aAAC;QAE1C;QACAoC,YAAYZ,GAAG,CAACH,OAAO/B,IAAI,EAAE+B;IAC/B;IAEA,MAAMgB,SAAS,IAAIjB;IACnB,KAAK,MAAMW,WAAWF,SAAU;QAC9B,KAAK,MAAMS,gBAAgBP,QAAQM,MAAM,IAAI,EAAE,CAAE;YAC/C,MAAME,yBAAyB;gBAC7B,GAAGD,YAAY;gBACfhD,MAAMR,mBAAmBwD,aAAahD,IAAI;YAC5C;YACA,MAAMkD,eAAeJ,YAAY/C,GAAG,CAACkD,uBAAuBjD,IAAI;YAChE,IAAIkD,cAAc;gBAChB,MAAMC,UAAUF,uBAAuBG,QAAQ,GAC3C,CAAC,oBAAoB,EAAEH,uBAAuBjD,IAAI,CAAC,gBAAgB,EAAEyC,QAAQT,EAAE,CAAC,yBAAyB,EAAEkB,aAAalB,EAAE,CAAC,0BAA0B,EAAEkB,aAAaxC,SAAS,CAAC,EAAE,CAAC,GACjL,CAAC,OAAO,EAAEuC,uBAAuBjD,IAAI,CAAC,gBAAgB,EAAEyC,QAAQT,EAAE,CAAC,yBAAyB,EAAEkB,aAAalB,EAAE,CAAC,0BAA0B,EAAEkB,aAAaxC,SAAS,CAAC,iCAAiC,CAAC;gBACvM,MAAM,IAAInB,sBAAsB0D,uBAAuBjD,IAAI,EAAEmD,SAAS;oBACpED,aAAaxC,SAAS;oBACtB+B,QAAQT,EAAE;iBACX;YACH;YACA,MAAMC,WAAWc,OAAOhD,GAAG,CAACkD,uBAAuBjD,IAAI;YACvD,IAAI,CAACiC,UAAU;gBACb,IAAIgB,uBAAuBG,QAAQ,EAAE;oBACnC,MAAM,IAAI7D,sBACR0D,uBAAuBjD,IAAI,EAC3B,CAAC,oBAAoB,EAAEiD,uBAAuBjD,IAAI,CAAC,gBAAgB,EAAEyC,QAAQT,EAAE,CAAC,0BAA0B,CAAC,EAC3G;wBAACS,QAAQT,EAAE;qBAAC;gBAEhB;gBACAe,OAAOb,GAAG,CAACe,uBAAuBjD,IAAI,EAAE;oBACtC,GAAGiD,sBAAsB;oBACzBvC,WAAW+B,QAAQT,EAAE;oBACrBqB,gBAAgBZ,QAAQT,EAAE;gBAC5B;gBACA;YACF;YACA,IAAI,CAACiB,uBAAuBG,QAAQ,EAAE;gBACpC,MAAM,IAAI7D,sBACR0D,uBAAuBjD,IAAI,EAC3B,CAAC,OAAO,EAAEiD,uBAAuBjD,IAAI,CAAC,mCAAmC,EAAEiC,SAASvB,SAAS,CAAC,OAAO,EAAE+B,QAAQT,EAAE,CAAC,+CAA+C,CAAC,EAClK;oBAACC,SAASvB,SAAS;oBAAE+B,QAAQT,EAAE;iBAAC;YAEpC;YACA,IAAIC,SAASmB,QAAQ,EAAE;gBACrB,MAAM,IAAI7D,sBACR0D,uBAAuBjD,IAAI,EAC3B,CAAC,OAAO,EAAEiD,uBAAuBjD,IAAI,CAAC,yCAAyC,EAAEiC,SAASvB,SAAS,CAAC,OAAO,EAAE+B,QAAQT,EAAE,CAAC,EAAE,CAAC,EAC3H;oBAACC,SAASvB,SAAS;oBAAE+B,QAAQT,EAAE;iBAAC;YAEpC;YACA,IAAIC,SAASvC,MAAM,KAAKuD,uBAAuBvD,MAAM,EAAE;gBACrD,MAAM,IAAIH,sBACR0D,uBAAuBjD,IAAI,EAC3B,CAAC,oBAAoB,EAAEiD,uBAAuBjD,IAAI,CAAC,gBAAgB,EAAEyC,QAAQT,EAAE,CAAC,6BAA6B,EAAEC,SAASvC,MAAM,CAAC,cAAc,EAAEuC,SAASvB,SAAS,CAAC,MAAM,EAAEuC,uBAAuBvD,MAAM,CAAC,EAAE,CAAC,EAC3M;oBAACuC,SAASvB,SAAS;oBAAE+B,QAAQT,EAAE;iBAAC;YAEpC;YACAe,OAAOb,GAAG,CAACe,uBAAuBjD,IAAI,EAAE;gBACtC,GAAGiD,sBAAsB;gBACzBvC,WAAW+B,QAAQT,EAAE;gBACrBqB,gBAAgBpB,SAASoB,cAAc;YACzC;QACF;IACF;IAEA,KAAK,MAAMtB,UAAUjC,QAAS;QAC5BW,oBAAoBsB,OAAO/B,IAAI,EAAE+B,OAAOrC,MAAM,EAAEiD,YAAYZ,OAAOrB,SAAS;IAC9E;IACA,KAAK,MAAM4C,SAASP,OAAO5B,MAAM,GAAI;QACnCV,oBAAoB6C,MAAMtD,IAAI,EAAEsD,MAAM5D,MAAM,EAAEiD,YAAYW,MAAM5C,SAAS;IAC3E;IACA,OAAO;WAAIqC,OAAO5B,MAAM;KAAG;AAC7B"}
@@ -6,6 +6,10 @@ export declare class RouteCompositionError extends CompositionError {
6
6
  readonly path: string;
7
7
  constructor(path: string, message: string, featureIds: readonly string[]);
8
8
  }
9
+ export declare class LayoutCompositionError extends CompositionError {
10
+ readonly id: string;
11
+ constructor(id: string, message: string, featureIds: readonly string[]);
12
+ }
9
13
  export declare class ProviderCompositionError extends CompositionError {
10
14
  readonly id: string;
11
15
  constructor(id: string, message: string, featureIds: readonly string[]);
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/compose/errors.ts"],"names":[],"mappings":"AAAA,uBAAe,gBAAiB,SAAQ,KAAK;IAC3C,SAAgB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9C,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAKnF;AAED,qBAAa,qBAAsB,SAAQ,gBAAgB;IACzD,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIzE;AAED,qBAAa,wBAAyB,SAAQ,gBAAgB;IAC5D,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,mBAAoB,SAAQ,gBAAgB;IACvD,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,wBAAyB,SAAQ,gBAAgB;IAC5D,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,sBAAuB,SAAQ,gBAAgB;IAC1D,SAAgB,GAAG,EAAE,MAAM,CAAC;gBAEhB,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIxE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/compose/errors.ts"],"names":[],"mappings":"AAAA,uBAAe,gBAAiB,SAAQ,KAAK;IAC3C,SAAgB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9C,SAAS,aAAa,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAKnF;AAED,qBAAa,qBAAsB,SAAQ,gBAAgB;IACzD,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIzE;AAED,qBAAa,sBAAuB,SAAQ,gBAAgB;IAC1D,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,wBAAyB,SAAQ,gBAAgB;IAC5D,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,mBAAoB,SAAQ,gBAAgB;IACvD,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,wBAAyB,SAAQ,gBAAgB;IAC5D,SAAgB,EAAE,EAAE,MAAM,CAAC;gBAEf,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIvE;AAED,qBAAa,sBAAuB,SAAQ,gBAAgB;IAC1D,SAAgB,GAAG,EAAE,MAAM,CAAC;gBAEhB,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE;CAIxE"}
@@ -11,6 +11,12 @@ export class RouteCompositionError extends CompositionError {
11
11
  this.path = path;
12
12
  }
13
13
  }
14
+ export class LayoutCompositionError extends CompositionError {
15
+ constructor(id, message, featureIds){
16
+ super('LayoutCompositionError', message, featureIds);
17
+ this.id = id;
18
+ }
19
+ }
14
20
  export class ProviderCompositionError extends CompositionError {
15
21
  constructor(id, message, featureIds){
16
22
  super('ProviderCompositionError', message, featureIds);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/errors.ts"],"sourcesContent":["abstract class CompositionError extends Error {\n public readonly featureIds: readonly string[];\n\n protected constructor(name: string, message: string, featureIds: readonly string[]) {\n super(message);\n this.name = name;\n this.featureIds = featureIds;\n }\n}\n\nexport class RouteCompositionError extends CompositionError {\n public readonly path: string;\n\n constructor(path: string, message: string, featureIds: readonly string[]) {\n super('RouteCompositionError', message, featureIds);\n this.path = path;\n }\n}\n\nexport class ProviderCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('ProviderCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class NavCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('NavCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class SettingsCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('SettingsCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class ConfigCompositionError extends CompositionError {\n public readonly key: string;\n\n constructor(key: string, message: string, featureIds: readonly string[]) {\n super('ConfigCompositionError', message, featureIds);\n this.key = key;\n }\n}\n"],"names":["CompositionError","Error","name","message","featureIds","RouteCompositionError","path","ProviderCompositionError","id","NavCompositionError","SettingsCompositionError","ConfigCompositionError","key"],"mappings":"AAAA,IAAA,AAAeA,mBAAf,MAAeA,yBAAyBC;IAGtC,YAAsBC,IAAY,EAAEC,OAAe,EAAEC,UAA6B,CAAE;QAClF,KAAK,CAACD;QACN,IAAI,CAACD,IAAI,GAAGA;QACZ,IAAI,CAACE,UAAU,GAAGA;IACpB;AACF;AAEA,OAAO,MAAMC,8BAA8BL;IAGzC,YAAYM,IAAY,EAAEH,OAAe,EAAEC,UAA6B,CAAE;QACxE,KAAK,CAAC,yBAAyBD,SAASC;QACxC,IAAI,CAACE,IAAI,GAAGA;IACd;AACF;AAEA,OAAO,MAAMC,iCAAiCP;IAG5C,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,4BAA4BD,SAASC;QAC3C,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAMC,4BAA4BT;IAGvC,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,uBAAuBD,SAASC;QACtC,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAME,iCAAiCV;IAG5C,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,4BAA4BD,SAASC;QAC3C,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAMG,+BAA+BX;IAG1C,YAAYY,GAAW,EAAET,OAAe,EAAEC,UAA6B,CAAE;QACvE,KAAK,CAAC,0BAA0BD,SAASC;QACzC,IAAI,CAACQ,GAAG,GAAGA;IACb;AACF"}
1
+ {"version":3,"sources":["../../src/compose/errors.ts"],"sourcesContent":["abstract class CompositionError extends Error {\n public readonly featureIds: readonly string[];\n\n protected constructor(name: string, message: string, featureIds: readonly string[]) {\n super(message);\n this.name = name;\n this.featureIds = featureIds;\n }\n}\n\nexport class RouteCompositionError extends CompositionError {\n public readonly path: string;\n\n constructor(path: string, message: string, featureIds: readonly string[]) {\n super('RouteCompositionError', message, featureIds);\n this.path = path;\n }\n}\n\nexport class LayoutCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('LayoutCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class ProviderCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('ProviderCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class NavCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('NavCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class SettingsCompositionError extends CompositionError {\n public readonly id: string;\n\n constructor(id: string, message: string, featureIds: readonly string[]) {\n super('SettingsCompositionError', message, featureIds);\n this.id = id;\n }\n}\n\nexport class ConfigCompositionError extends CompositionError {\n public readonly key: string;\n\n constructor(key: string, message: string, featureIds: readonly string[]) {\n super('ConfigCompositionError', message, featureIds);\n this.key = key;\n }\n}\n"],"names":["CompositionError","Error","name","message","featureIds","RouteCompositionError","path","LayoutCompositionError","id","ProviderCompositionError","NavCompositionError","SettingsCompositionError","ConfigCompositionError","key"],"mappings":"AAAA,IAAA,AAAeA,mBAAf,MAAeA,yBAAyBC;IAGtC,YAAsBC,IAAY,EAAEC,OAAe,EAAEC,UAA6B,CAAE;QAClF,KAAK,CAACD;QACN,IAAI,CAACD,IAAI,GAAGA;QACZ,IAAI,CAACE,UAAU,GAAGA;IACpB;AACF;AAEA,OAAO,MAAMC,8BAA8BL;IAGzC,YAAYM,IAAY,EAAEH,OAAe,EAAEC,UAA6B,CAAE;QACxE,KAAK,CAAC,yBAAyBD,SAASC;QACxC,IAAI,CAACE,IAAI,GAAGA;IACd;AACF;AAEA,OAAO,MAAMC,+BAA+BP;IAG1C,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,0BAA0BD,SAASC;QACzC,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAMC,iCAAiCT;IAG5C,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,4BAA4BD,SAASC;QAC3C,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAME,4BAA4BV;IAGvC,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,uBAAuBD,SAASC;QACtC,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAMG,iCAAiCX;IAG5C,YAAYQ,EAAU,EAAEL,OAAe,EAAEC,UAA6B,CAAE;QACtE,KAAK,CAAC,4BAA4BD,SAASC;QAC3C,IAAI,CAACI,EAAE,GAAGA;IACZ;AACF;AAEA,OAAO,MAAMI,+BAA+BZ;IAG1C,YAAYa,GAAW,EAAEV,OAAe,EAAEC,UAA6B,CAAE;QACvE,KAAK,CAAC,0BAA0BD,SAASC;QACzC,IAAI,CAACS,GAAG,GAAGA;IACb;AACF"}
@@ -1,11 +1,18 @@
1
- import type { ClientFeature } from '#contract.js';
1
+ import type { ClientFeature, RouteParentId } from '#contract.js';
2
2
  interface RouteReference {
3
3
  path: string;
4
4
  featureId?: string;
5
5
  ownerFeatureId?: string;
6
+ parent?: RouteParentId;
6
7
  }
7
8
  type RouteReferences = Iterable<string | RouteReference>;
8
- export declare function validateNavigation(features: readonly ClientFeature[], routeReferences: RouteReferences): void;
9
+ interface LayoutReference {
10
+ id: string;
11
+ path: string;
12
+ featureId?: string;
13
+ parent?: RouteParentId;
14
+ }
15
+ export declare function validateNavigation(features: readonly ClientFeature[], routeReferences: RouteReferences, layoutReferences?: readonly LayoutReference[]): void;
9
16
  export declare function validateSettingsSections(features: readonly ClientFeature[], routeReferences: RouteReferences): void;
10
17
  export {};
11
18
  //# sourceMappingURL=validate-registries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAIhD,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;AAkBzD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAgCN;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAiCN"}
1
+ {"version":3,"file":"validate-registries.d.ts","sourceRoot":"","sources":["../../src/compose/validate-registries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,aAAa,EAAe,aAAa,EAAC,MAAM,cAAc,CAAC;AAI5E,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,KAAK,eAAe,GAAG,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;AAEzD,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AA0CD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,EAChC,gBAAgB,GAAE,SAAS,eAAe,EAAO,GAChD,IAAI,CAuEN;AA4BD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAClC,eAAe,EAAE,eAAe,GAC/B,IAAI,CAiCN"}
@@ -11,11 +11,50 @@ function routeOwners(routeReferences) {
11
11
  function hasExplicitCoordinator(feature) {
12
12
  return feature.coordinator === feature.id;
13
13
  }
14
- export function validateNavigation(features, routeReferences) {
15
- const routes = routeOwners(routeReferences);
14
+ function validateRoleMetadata(entry, feature) {
15
+ const rawEntry = entry;
16
+ const hasMinimumRole = Object.hasOwn(rawEntry, 'minimumRole');
17
+ if (entry.scope !== 'layout') {
18
+ if (hasMinimumRole) {
19
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" has minimum role metadata but is not layout-scoped.`, [
20
+ feature.id
21
+ ]);
22
+ }
23
+ return;
24
+ }
25
+ if (hasMinimumRole) {
26
+ if (typeof rawEntry.minimumRole !== 'string' || rawEntry.minimumRole.trim() === '') {
27
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" has invalid minimum role metadata. Expected a non-empty string.`, [
28
+ feature.id
29
+ ]);
30
+ }
31
+ }
32
+ }
33
+ export function validateNavigation(features, routeReferences, layoutReferences = []) {
34
+ const routeReferencesWithLayouts = [
35
+ ...routeReferences
36
+ ];
37
+ for (const layout of layoutReferences){
38
+ routeReferencesWithLayouts.push({
39
+ path: layout.path,
40
+ ...layout.featureId ? {
41
+ featureId: layout.featureId,
42
+ ownerFeatureId: layout.featureId
43
+ } : {},
44
+ ...layout.parent ? {
45
+ parent: layout.parent
46
+ } : {}
47
+ });
48
+ }
49
+ const routes = routeOwners(routeReferencesWithLayouts);
50
+ const layouts = new Map(layoutReferences.map((layout)=>[
51
+ layout.id,
52
+ layout
53
+ ]));
16
54
  const entries = new Map();
17
55
  for (const feature of features){
18
56
  for (const entry of feature.navigation ?? []){
57
+ validateRoleMetadata(entry, feature);
19
58
  const existingFeatureId = entries.get(entry.id);
20
59
  if (existingFeatureId) {
21
60
  throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" is contributed by both features "${existingFeatureId}" and "${feature.id}".`, [
@@ -30,6 +69,29 @@ export function validateNavigation(features, routeReferences) {
30
69
  feature.id
31
70
  ]);
32
71
  }
72
+ if (entry.scope === 'layout') {
73
+ if (typeof entry.layout !== 'string' || entry.layout.trim() === '') {
74
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" has invalid layout metadata. Expected a non-empty layout id.`, [
75
+ feature.id
76
+ ]);
77
+ }
78
+ const layout = layouts.get(entry.layout);
79
+ if (!layout) {
80
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" targets missing layout "${entry.layout}".`, [
81
+ feature.id
82
+ ]);
83
+ }
84
+ const route = routeReferencesForPath(routeReferencesWithLayouts, target);
85
+ const isLayoutRoot = route?.path === layout.path;
86
+ const isLayoutDescendant = routeDescendsFromLayout(route, entry.layout, layouts);
87
+ if (!isLayoutRoot && !isLayoutDescendant) {
88
+ throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" targets route "${target}" outside layout "${entry.layout}".`, [
89
+ feature.id
90
+ ]);
91
+ }
92
+ entries.set(entry.id, feature.id);
93
+ continue;
94
+ }
33
95
  if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {
34
96
  throw new NavCompositionError(entry.id, `Navigation entry "${entry.id}" in feature "${feature.id}" targets route "${target}" owned by feature "${routeOwner}". Declare coordinator: "${feature.id}" to own this cross-feature contribution.`, [
35
97
  routeOwner,
@@ -40,6 +102,25 @@ export function validateNavigation(features, routeReferences) {
40
102
  }
41
103
  }
42
104
  }
105
+ function routeReferencesForPath(routeReferences, path) {
106
+ for (const route of routeReferences){
107
+ const reference = typeof route === 'string' ? {
108
+ path: route
109
+ } : route;
110
+ if (normalizeRoutePath(reference.path) === path) return reference;
111
+ }
112
+ return undefined;
113
+ }
114
+ function routeDescendsFromLayout(route, layoutId, layouts) {
115
+ const visited = new Set();
116
+ let parent = route?.parent;
117
+ while(parent && !visited.has(parent)){
118
+ if (parent === layoutId) return true;
119
+ visited.add(parent);
120
+ parent = layouts.get(parent)?.parent;
121
+ }
122
+ return false;
123
+ }
43
124
  export function validateSettingsSections(features, routeReferences) {
44
125
  const routes = routeOwners(routeReferences);
45
126
  const sections = new Map();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature} from '#contract.js';\nimport {NavCompositionError, SettingsCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\ninterface RouteReference {\n path: string;\n featureId?: string;\n ownerFeatureId?: string;\n}\n\ntype RouteReferences = Iterable<string | RouteReference>;\n\nfunction routeOwners(routeReferences: RouteReferences): Map<string, string | undefined> {\n const owners = new Map<string, string | undefined>();\n for (const route of routeReferences) {\n const path = typeof route === 'string' ? route : route.path;\n owners.set(\n normalizeRoutePath(path),\n typeof route === 'string' ? undefined : (route.ownerFeatureId ?? route.featureId),\n );\n }\n return owners;\n}\n\nfunction hasExplicitCoordinator(feature: ClientFeature): boolean {\n return feature.coordinator === feature.id;\n}\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n): void {\n const routes = routeOwners(routeReferences);\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n const existingFeatureId = entries.get(entry.id);\n if (existingFeatureId) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const target = normalizeRoutePath(entry.to);\n const routeOwner = routes.get(target);\n if (routeOwner === undefined && !routes.has(target)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing route \"${target}\".`,\n [feature.id],\n );\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n }\n }\n}\n\nexport function validateSettingsSections(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n): void {\n const routes = routeOwners(routeReferences);\n const sections = new Map<string, string>();\n for (const feature of features) {\n for (const section of feature.settingsSections ?? []) {\n const existingFeatureId = sections.get(section.id);\n if (existingFeatureId) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const path = `/workspaces/$wid/settings/${section.pathSegment}`;\n const normalizedPath = normalizeRoutePath(path);\n const routeOwner = routes.get(normalizedPath);\n if (routeOwner === undefined && !routes.has(normalizedPath)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" requires route \"${path}\".`,\n [feature.id],\n );\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" targets route \"${path}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n sections.set(section.id, feature.id);\n }\n }\n}\n"],"names":["NavCompositionError","SettingsCompositionError","normalizeRoutePath","routeOwners","routeReferences","owners","Map","route","path","set","undefined","ownerFeatureId","featureId","hasExplicitCoordinator","feature","coordinator","id","validateNavigation","features","routes","entries","entry","navigation","existingFeatureId","get","target","to","routeOwner","has","validateSettingsSections","sections","section","settingsSections","pathSegment","normalizedPath"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/compose/validate-registries.ts"],"sourcesContent":["import type {ClientFeature, NavTabEntry, RouteParentId} from '#contract.js';\nimport {NavCompositionError, SettingsCompositionError} from './errors.js';\nimport {normalizeRoutePath} from './normalize-route-path.js';\n\ninterface RouteReference {\n path: string;\n featureId?: string;\n ownerFeatureId?: string;\n parent?: RouteParentId;\n}\n\ntype RouteReferences = Iterable<string | RouteReference>;\n\ninterface LayoutReference {\n id: string;\n path: string;\n featureId?: string;\n parent?: RouteParentId;\n}\n\nfunction routeOwners(routeReferences: RouteReferences): Map<string, string | undefined> {\n const owners = new Map<string, string | undefined>();\n for (const route of routeReferences) {\n const path = typeof route === 'string' ? route : route.path;\n owners.set(\n normalizeRoutePath(path),\n typeof route === 'string' ? undefined : (route.ownerFeatureId ?? route.featureId),\n );\n }\n return owners;\n}\n\nfunction hasExplicitCoordinator(feature: ClientFeature): boolean {\n return feature.coordinator === feature.id;\n}\n\nfunction validateRoleMetadata(entry: NavTabEntry, feature: ClientFeature): void {\n const rawEntry = entry as NavTabEntry & {minimumRole?: unknown};\n const hasMinimumRole = Object.hasOwn(rawEntry, 'minimumRole');\n if (entry.scope !== 'layout') {\n if (hasMinimumRole) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has minimum role metadata but is not layout-scoped.`,\n [feature.id],\n );\n }\n return;\n }\n if (hasMinimumRole) {\n if (typeof rawEntry.minimumRole !== 'string' || rawEntry.minimumRole.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid minimum role metadata. Expected a non-empty string.`,\n [feature.id],\n );\n }\n }\n}\n\nexport function validateNavigation(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n layoutReferences: readonly LayoutReference[] = [],\n): void {\n const routeReferencesWithLayouts: Array<string | RouteReference> = [...routeReferences];\n for (const layout of layoutReferences) {\n routeReferencesWithLayouts.push({\n path: layout.path,\n ...(layout.featureId ? {featureId: layout.featureId, ownerFeatureId: layout.featureId} : {}),\n ...(layout.parent ? {parent: layout.parent} : {}),\n });\n }\n const routes = routeOwners(routeReferencesWithLayouts);\n const layouts = new Map(layoutReferences.map((layout) => [layout.id, layout]));\n const entries = new Map<string, string>();\n for (const feature of features) {\n for (const entry of feature.navigation ?? []) {\n validateRoleMetadata(entry, feature);\n const existingFeatureId = entries.get(entry.id);\n if (existingFeatureId) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const target = normalizeRoutePath(entry.to);\n const routeOwner = routes.get(target);\n if (routeOwner === undefined && !routes.has(target)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing route \"${target}\".`,\n [feature.id],\n );\n }\n if (entry.scope === 'layout') {\n if (typeof entry.layout !== 'string' || entry.layout.trim() === '') {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" has invalid layout metadata. Expected a non-empty layout id.`,\n [feature.id],\n );\n }\n const layout = layouts.get(entry.layout);\n if (!layout) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets missing layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n const route = routeReferencesForPath(routeReferencesWithLayouts, target);\n const isLayoutRoot = route?.path === layout.path;\n const isLayoutDescendant = routeDescendsFromLayout(route, entry.layout, layouts);\n if (!isLayoutRoot && !isLayoutDescendant) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" outside layout \"${entry.layout}\".`,\n [feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n continue;\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new NavCompositionError(\n entry.id,\n `Navigation entry \"${entry.id}\" in feature \"${feature.id}\" targets route \"${target}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n entries.set(entry.id, feature.id);\n }\n }\n}\n\nfunction routeReferencesForPath(\n routeReferences: RouteReferences,\n path: string,\n): RouteReference | undefined {\n for (const route of routeReferences) {\n const reference = typeof route === 'string' ? {path: route} : route;\n if (normalizeRoutePath(reference.path) === path) return reference;\n }\n return undefined;\n}\n\nfunction routeDescendsFromLayout(\n route: RouteReference | undefined,\n layoutId: string,\n layouts: ReadonlyMap<string, LayoutReference>,\n): boolean {\n const visited = new Set<string>();\n let parent = route?.parent;\n while (parent && !visited.has(parent)) {\n if (parent === layoutId) return true;\n visited.add(parent);\n parent = layouts.get(parent)?.parent;\n }\n return false;\n}\n\nexport function validateSettingsSections(\n features: readonly ClientFeature[],\n routeReferences: RouteReferences,\n): void {\n const routes = routeOwners(routeReferences);\n const sections = new Map<string, string>();\n for (const feature of features) {\n for (const section of feature.settingsSections ?? []) {\n const existingFeatureId = sections.get(section.id);\n if (existingFeatureId) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" is contributed by both features \"${existingFeatureId}\" and \"${feature.id}\".`,\n [existingFeatureId, feature.id],\n );\n }\n const path = `/workspaces/$wid/settings/${section.pathSegment}`;\n const normalizedPath = normalizeRoutePath(path);\n const routeOwner = routes.get(normalizedPath);\n if (routeOwner === undefined && !routes.has(normalizedPath)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" requires route \"${path}\".`,\n [feature.id],\n );\n }\n if (routeOwner && routeOwner !== feature.id && !hasExplicitCoordinator(feature)) {\n throw new SettingsCompositionError(\n section.id,\n `Settings section \"${section.id}\" in feature \"${feature.id}\" targets route \"${path}\" owned by feature \"${routeOwner}\". Declare coordinator: \"${feature.id}\" to own this cross-feature contribution.`,\n [routeOwner, feature.id],\n );\n }\n sections.set(section.id, feature.id);\n }\n }\n}\n"],"names":["NavCompositionError","SettingsCompositionError","normalizeRoutePath","routeOwners","routeReferences","owners","Map","route","path","set","undefined","ownerFeatureId","featureId","hasExplicitCoordinator","feature","coordinator","id","validateRoleMetadata","entry","rawEntry","hasMinimumRole","Object","hasOwn","scope","minimumRole","trim","validateNavigation","features","layoutReferences","routeReferencesWithLayouts","layout","push","parent","routes","layouts","map","entries","navigation","existingFeatureId","get","target","to","routeOwner","has","routeReferencesForPath","isLayoutRoot","isLayoutDescendant","routeDescendsFromLayout","reference","layoutId","visited","Set","add","validateSettingsSections","sections","section","settingsSections","pathSegment","normalizedPath"],"mappings":"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"}
@@ -2,24 +2,42 @@ import type { IconName } from '@shipfox/react-ui/icon';
2
2
  import type { ComponentType, PropsWithChildren } from 'react';
3
3
  import type { z } from 'zod';
4
4
  export type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';
5
+ /** A fixed shell anchor or the stable id of a feature-owned layout. */
6
+ export type RouteParentId = AnchorId | (string & {});
5
7
  export interface RouteContribution {
6
8
  path: string;
7
- parent: AnchorId;
9
+ parent: RouteParentId;
8
10
  override?: boolean;
9
11
  impl: string;
10
12
  }
13
+ export interface LayoutContribution {
14
+ id: string;
15
+ path: string;
16
+ parent: RouteParentId;
17
+ impl: string;
18
+ }
11
19
  export interface FeatureProvider {
12
20
  id: string;
13
21
  Component: ComponentType<PropsWithChildren>;
14
22
  }
15
- export interface NavTabEntry {
23
+ interface NavTabEntryBase {
16
24
  id: string;
17
- scope: 'workspace' | 'project';
18
25
  label: string;
19
26
  to: string;
20
27
  exact?: boolean;
21
28
  order?: number;
22
29
  }
30
+ export type NavTabEntry = (NavTabEntryBase & {
31
+ scope: 'workspace' | 'project';
32
+ layout?: never;
33
+ }) | (NavTabEntryBase & {
34
+ scope: 'layout';
35
+ layout: string;
36
+ minimumRole?: string;
37
+ });
38
+ export type LayoutNavigationEntry = Extract<NavTabEntry, {
39
+ scope: 'layout';
40
+ }>;
23
41
  export interface SettingsSectionEntry {
24
42
  id: string;
25
43
  pathSegment: string;
@@ -35,6 +53,7 @@ export interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {
35
53
  * feature.
36
54
  */
37
55
  coordinator?: string;
56
+ layouts?: readonly LayoutContribution[];
38
57
  routes?: readonly RouteContribution[];
39
58
  providers?: readonly FeatureProvider[];
40
59
  navigation?: readonly NavTabEntry[];
@@ -42,4 +61,5 @@ export interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {
42
61
  configShape?: S;
43
62
  }
44
63
  export declare function defineClientFeature<const T extends ClientFeature>(feature: T): T;
64
+ export {};
45
65
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;IACpE,EAAE,EAAE,MAAM,CAAC;IACX;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEhF"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAE3B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,iBAAiB,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAE1F,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CAC7C;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,WAAW,GACnB,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAA;CAAC,CAAC,GACpE,CAAC,eAAe,GAAG;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC;AAEhF,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,WAAW,EAAE;IAAC,KAAK,EAAE,QAAQ,CAAA;CAAC,CAAC,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;IACpE,EAAE,EAAE,MAAM,CAAC;IACX;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACxC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACnD,WAAW,CAAC,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,CAAC,SAAS,aAAa,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CAEhF"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import type {IconName} from '@shipfox/react-ui/icon';\nimport type {ComponentType, PropsWithChildren} from 'react';\nimport type {z} from 'zod';\n\nexport type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';\n\nexport interface RouteContribution {\n path: string;\n parent: AnchorId;\n override?: boolean;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\nexport interface NavTabEntry {\n id: string;\n scope: 'workspace' | 'project';\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport interface SettingsSectionEntry {\n id: string;\n pathSegment: string;\n label: string;\n icon: IconName;\n order?: number;\n}\n\nexport interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {\n id: string;\n /**\n * Set this to the feature id when the feature intentionally coordinates a\n * navigation or settings contribution whose route belongs to another\n * feature.\n */\n coordinator?: string;\n routes?: readonly RouteContribution[];\n providers?: readonly FeatureProvider[];\n navigation?: readonly NavTabEntry[];\n settingsSections?: readonly SettingsSectionEntry[];\n configShape?: S;\n}\n\nexport function defineClientFeature<const T extends ClientFeature>(feature: T): T {\n return feature;\n}\n"],"names":["defineClientFeature","feature"],"mappings":"AAkDA,OAAO,SAASA,oBAAmDC,OAAU;IAC3E,OAAOA;AACT"}
1
+ {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import type {IconName} from '@shipfox/react-ui/icon';\nimport type {ComponentType, PropsWithChildren} from 'react';\nimport type {z} from 'zod';\n\nexport type AnchorId = 'root' | 'workspaceLayout' | 'projectLayout' | 'workspaceSettings';\n\n/** A fixed shell anchor or the stable id of a feature-owned layout. */\nexport type RouteParentId = AnchorId | (string & {});\n\nexport interface RouteContribution {\n path: string;\n parent: RouteParentId;\n override?: boolean;\n impl: string;\n}\n\nexport interface LayoutContribution {\n id: string;\n path: string;\n parent: RouteParentId;\n impl: string;\n}\n\nexport interface FeatureProvider {\n id: string;\n Component: ComponentType<PropsWithChildren>;\n}\n\ninterface NavTabEntryBase {\n id: string;\n label: string;\n to: string;\n exact?: boolean;\n order?: number;\n}\n\nexport type NavTabEntry =\n | (NavTabEntryBase & {scope: 'workspace' | 'project'; layout?: never})\n | (NavTabEntryBase & {scope: 'layout'; layout: string; minimumRole?: string});\n\nexport type LayoutNavigationEntry = Extract<NavTabEntry, {scope: 'layout'}>;\n\nexport interface SettingsSectionEntry {\n id: string;\n pathSegment: string;\n label: string;\n icon: IconName;\n order?: number;\n}\n\nexport interface ClientFeature<S extends z.ZodRawShape = z.ZodRawShape> {\n id: string;\n /**\n * Set this to the feature id when the feature intentionally coordinates a\n * navigation or settings contribution whose route belongs to another\n * feature.\n */\n coordinator?: string;\n layouts?: readonly LayoutContribution[];\n routes?: readonly RouteContribution[];\n providers?: readonly FeatureProvider[];\n navigation?: readonly NavTabEntry[];\n settingsSections?: readonly SettingsSectionEntry[];\n configShape?: S;\n}\n\nexport function defineClientFeature<const T extends ClientFeature>(feature: T): T {\n return feature;\n}\n"],"names":["defineClientFeature","feature"],"mappings":"AAkEA,OAAO,SAASA,oBAAmDC,OAAU;IAC3E,OAAOA;AACT"}
@@ -1,3 +1,4 @@
1
+ import type { RouteParentId } from '#contract.js';
1
2
  declare const anchorPaths: {
2
3
  readonly root: "/";
3
4
  readonly workspaceLayout: "/workspaces/$wid";
@@ -6,4 +7,5 @@ declare const anchorPaths: {
6
7
  };
7
8
  export { anchorPaths };
8
9
  export declare function routePathForAnchor(anchor: keyof typeof anchorPaths, fullPath: string): string;
10
+ export declare function routePathForParent(parent: RouteParentId, fullPath: string, layoutPaths?: ReadonlyMap<string, string>): string;
9
11
  //# sourceMappingURL=anchor-paths.d.ts.map