every-plugin 3.0.0-rc.7 → 3.0.0-rc.8

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.
@@ -3,7 +3,7 @@ const require_shared_deps_spec = require('../shared-deps-spec.cjs');
3
3
  let node_fs = require("node:fs");
4
4
 
5
5
  //#region src/runtime/mf-config.ts
6
- const PLUGIN_VERSION = "3.0.0-rc.7";
6
+ const PLUGIN_VERSION = "3.0.0-rc.8";
7
7
  const MF_CORE_SHARED_DEPS = require_shared_deps_spec.buildMfCoreSharedDependencies({ ownVersion: PLUGIN_VERSION });
8
8
 
9
9
  //#endregion
@@ -2,7 +2,7 @@ import { buildMfCoreSharedDependencies } from "../shared-deps-spec.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
 
4
4
  //#region src/runtime/mf-config.ts
5
- const PLUGIN_VERSION = "3.0.0-rc.7";
5
+ const PLUGIN_VERSION = "3.0.0-rc.8";
6
6
  const MF_CORE_SHARED_DEPS = buildMfCoreSharedDependencies({ ownVersion: PLUGIN_VERSION });
7
7
 
8
8
  //#endregion
@@ -1,3 +1,4 @@
1
+ const require_ui_manifest_contract = require('./contract.cjs');
1
2
  const require_mount_registry = require('./mount-registry.cjs');
2
3
  const require_descriptor = require('./descriptor.cjs');
3
4
  const require_nav = require('./nav.cjs');
@@ -94,6 +95,7 @@ async function constructTree(input) {
94
95
  const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));
95
96
  const resolved = [];
96
97
  for (const ref of refs) resolved.push(await input.resolve(ref));
98
+ const ordered = [...resolved.filter((plugin) => plugin.key === "ui"), ...resolved.filter((plugin) => plugin.key !== "ui")];
97
99
  const optionsByPlugin = /* @__PURE__ */ new Map();
98
100
  for (const plugin of resolved) {
99
101
  const optionsById = /* @__PURE__ */ new Map();
@@ -105,17 +107,26 @@ async function constructTree(input) {
105
107
  optionsByPlugin.set(plugin.key, optionsById);
106
108
  }
107
109
  const declarations = /* @__PURE__ */ new Map();
108
- for (const plugin of resolved) for (const record of plugin.manifest.routes) {
110
+ const mountByLayoutId = /* @__PURE__ */ new Map();
111
+ for (const plugin of ordered) for (const record of plugin.manifest.routes) {
109
112
  if (!record.mount) continue;
110
113
  const mountId = record.mount;
111
114
  if (!require_mount_registry.MOUNT_REGISTRY[mountId].implemented) throw new Error(`mount "${record.mount}" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by "${plugin.key}"`);
112
115
  const existing = declarations.get(mountId);
113
- if (existing) throw new Error(`mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`);
116
+ if (existing) {
117
+ if (existing.key === "ui" && plugin.key !== "ui") {
118
+ console.log(`[Compose] mount declaration "${existing.key}:${existing.record.id}" (${mountId}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed declaration skipped`);
119
+ mountByLayoutId.set(record.id, mountId);
120
+ continue;
121
+ }
122
+ throw new Error(`mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`);
123
+ }
114
124
  declarations.set(mountId, {
115
125
  key: plugin.key,
116
126
  record,
117
127
  options: optionsByPlugin.get(plugin.key).get(record.id)
118
128
  });
129
+ mountByLayoutId.set(record.id, mountId);
119
130
  }
120
131
  const rootRoute = toAnyRoute((0, _tanstack_react_router.createRootRoute)({
121
132
  ...input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {},
@@ -138,8 +149,6 @@ async function constructTree(input) {
138
149
  });
139
150
  mountRoutes.set(mountId, toAnyRoute(route));
140
151
  }
141
- const mountByLayoutId = /* @__PURE__ */ new Map();
142
- for (const [mountId, declaration] of declarations) mountByLayoutId.set(declaration.record.id, mountId);
143
152
  const nav = [];
144
153
  const mountCounts = {};
145
154
  const childrenByParent = /* @__PURE__ */ new Map();
@@ -150,7 +159,7 @@ async function constructTree(input) {
150
159
  list.push(child);
151
160
  childrenByParent.set(parent, list);
152
161
  };
153
- for (const plugin of resolved) {
162
+ for (const plugin of ordered) {
154
163
  const optionsById = optionsByPlugin.get(plugin.key);
155
164
  const fullPathById = /* @__PURE__ */ new Map();
156
165
  const mountById = /* @__PURE__ */ new Map();
@@ -185,12 +194,20 @@ async function constructTree(input) {
185
194
  throw new Error(`route "${record.id}" in "${plugin.key}" must live under a mount`);
186
195
  };
187
196
  const pending = plugin.manifest.routes.filter((record) => !record.mount);
197
+ const shadowedIds = /* @__PURE__ */ new Set();
188
198
  let progressed = true;
189
199
  while (pending.length > 0 && progressed) {
190
200
  progressed = false;
191
201
  for (let i = 0; i < pending.length; i += 1) {
192
202
  const record = pending[i];
193
- if (record.parentId && !byId.has(record.parentId) && !mountByLayoutId.has(record.parentId)) continue;
203
+ const parentId = record.parentId;
204
+ if (parentId) {
205
+ if (shadowedIds.has(parentId)) {
206
+ shadowedIds.add(record.id);
207
+ continue;
208
+ }
209
+ if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;
210
+ }
194
211
  pending.splice(i, 1);
195
212
  progressed = true;
196
213
  const options = optionsById.get(record.id);
@@ -200,7 +217,14 @@ async function constructTree(input) {
200
217
  if (!isLayoutRoute) {
201
218
  const siblings = claimedPathsByParent.get(parent.route) ?? /* @__PURE__ */ new Map();
202
219
  const claimant = siblings.get(routePath);
203
- if (claimant) throw new Error(`path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`);
220
+ if (claimant) {
221
+ if (plugin.key !== "ui" && claimant.startsWith(`${"ui"}:`)) {
222
+ console.log(`[Compose] route claim "${claimant}" (${routePath}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed route skipped`);
223
+ shadowedIds.add(record.id);
224
+ break;
225
+ }
226
+ throw new Error(`path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`);
227
+ }
204
228
  siblings.set(routePath, `${plugin.key}:${record.id}`);
205
229
  claimedPathsByParent.set(parent.route, siblings);
206
230
  }
@@ -1 +1 @@
1
- {"version":3,"file":"construct.cjs","names":["redirect","ROUTE_OPTION_KEYS","MOUNT_REGISTRY","createRootRoute","Outlet","createRoute","resolveMountSegment","compareNavItems","digestOf"],"sources":["../../../src/ui/manifest/construct.ts"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { createRootRoute, createRoute, Outlet, redirect } from \"@tanstack/react-router\";\nimport { digestOf } from \"./descriptor\";\nimport type { PluginManifest, RouteRecord } from \"./manifest-schema\";\nimport { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from \"./mount-registry\";\nimport { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from \"./nav\";\nimport { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from \"./route-config\";\n\n/**\n * Host-side route-graph construction (ADR 0008 §1): manifests + route\n * configs in → host-built tree out. The host constructs ITS OWN Route\n * objects from validated manifest data via public `createRoute` APIs; no\n * foreign route object is ever mutated. Gates are attached HOST-side from\n * the mount registry — plugins cannot ship a route that escapes its\n * mount's gate because plugins never construct route objects.\n *\n * Mounts are the ONLY cross-source parenting points: a root-level\n * pathless layout file (`_public.tsx`) declares the mount for the whole\n * composition (exactly one declaring source per mount); every other\n * source's routes live under the same `_mount/` directory prefix and\n * parent onto the shared mount route. This module executes inside the\n * core ui's module graph (production MF expose `./compose`, dev source\n * import) so constructed routes are minted by the same react/router\n * instance that renders them.\n */\n\n/**\n * TanStack's Route generics are intentionally invariant — routes built from\n * manifest data can't satisfy them structurally. One named boundary for\n * handing a constructed route to the tree-building APIs.\n */\nfunction toAnyRoute(route: unknown): AnyRoute {\n return route as AnyRoute;\n}\n\nexport interface GateUser {\n id: string;\n name?: string;\n /** Better Auth user role — \"admin\" gates the admin mount. */\n role?: string;\n}\n\nexport interface HostContext {\n user?: GateUser;\n session?: { user?: GateUser };\n}\n\ntype GateFn = (args: {\n context: HostContext;\n location: { pathname: string; searchStr: string };\n}) => void;\n\n/**\n * Gate redirects are absolute: gates attach to root-level mount routes, and\n * relatives would silently shift if a mount ever moved deeper in the tree.\n * The login surface is plugin-owned, never core-known.\n */\nconst LOGIN_TO: string = \"/login\";\nconst HOME_TO: string = \"/\";\n\n/**\n * Gate implementations by mount gate kind. Only `session` and `admin` are\n * implemented (ADR 0008 §3); unimplemented mounts are rejected during mount\n * declaration before any gate lookup, and gates are optional by type — a\n * mount without an entry (or `none`) renders ungated.\n */\nconst GATES: Partial<Record<MountDef[\"gate\"], GateFn>> = {\n session: ({ context, location }) => {\n const user = userOf(context);\n if (!user) {\n throw redirect({\n to: LOGIN_TO,\n search: { redirect: location.pathname + location.searchStr } as never,\n });\n }\n },\n admin: ({ context }) => {\n const user = userOf(context);\n if (!user) throw redirect({ to: LOGIN_TO });\n if (user.role !== \"admin\") throw redirect({ to: HOME_TO });\n },\n};\n\nconst userOf = (context: HostContext | undefined): GateUser | undefined =>\n context?.user ?? context?.session?.user;\n\nexport interface ConstructPluginRef {\n /** composition identity — plugin key; participates in the digest */\n key: string;\n /** deployment identity — the MF container name loadRemote targets */\n mfName: string;\n}\n\nexport interface ResolvedPlugin {\n key: string;\n manifest: PluginManifest;\n routeConfig: RouteConfigModule;\n}\n\nexport interface ConstructInput {\n name: string;\n plugins: ReadonlyArray<ConstructPluginRef>;\n resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;\n /** core ui `__root` options — component/error/notFound + head baseline */\n rootOptions?: RouteOptionsBundle;\n}\n\nexport interface ConstructedTree {\n rootRoute: AnyRoute;\n routeTree: AnyRoute;\n digest: string;\n manifests: PluginManifest[];\n nav: NavManifest;\n /** constructed route count per mount id */\n mountCounts: Record<string, number>;\n}\n\ninterface MountDeclaration {\n key: string;\n record: RouteRecord;\n options: RouteOptionsBundle;\n}\n\nfunction joinPath(a: string | undefined, b: string | undefined): string {\n const left = a && a !== \"/\" ? a.replace(/\\/+$/, \"\") : \"\";\n const right = b && b !== \"/\" ? b.replace(/^\\/+/, \"\") : \"\";\n if (!left) return right ? `/${right}` : \"/\";\n if (!right) return left;\n return `${left}/${right}`;\n}\n\nconst navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {\n const nav = options.staticData?.nav;\n if (nav && typeof nav === \"object\" && typeof (nav as NavDeclaration).label === \"string\") {\n return nav as NavDeclaration;\n }\n return undefined;\n};\n\n/** Layout routes (mount or pathless) pass their authored options through,\n * like every other route kind. On mount routes the registry gate composes\n * BEFORE the declaration's own beforeLoad so a source can never escape its\n * mount's gate (ADR 0008 §3), while the declaration's return value still\n * flows to children as route context. */\nfunction layoutBeforeLoad(\n gate: GateFn | undefined,\n declared: RouteOptionsBundle[\"beforeLoad\"],\n): RouteOptionsBundle[\"beforeLoad\"] {\n if (!gate) return declared;\n return (args) => {\n gate(args);\n return declared?.(args);\n };\n}\n\nconst AUTHORED_OPTION_KEYS = ROUTE_OPTION_KEYS.filter(\n (key) => key !== \"beforeLoad\" && key !== \"component\",\n);\n\nfunction authoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const authored: Partial<Record<keyof RouteOptionsBundle, unknown>> = {};\n for (const key of AUTHORED_OPTION_KEYS) {\n if (options[key] !== undefined) authored[key] = options[key];\n }\n return authored as Partial<RouteOptionsBundle>;\n}\n\nfunction rootAuthoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const { params: _params, ...authored } = authoredOptions(options);\n return authored;\n}\n\nexport async function constructTree(input: ConstructInput): Promise<ConstructedTree> {\n const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));\n const resolved: ResolvedPlugin[] = [];\n for (const ref of refs) {\n resolved.push(await input.resolve(ref));\n }\n\n const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();\n for (const plugin of resolved) {\n const optionsById = new Map<string, RouteOptionsBundle>();\n await Promise.all(\n plugin.manifest.routes.map(async (record) => {\n const loader = plugin.routeConfig.routeConfigLoaders[record.id];\n if (!loader) {\n throw new Error(`routeConfig for \"${plugin.key}\" has no loader for route \"${record.id}\"`);\n }\n optionsById.set(record.id, await loader());\n }),\n );\n optionsByPlugin.set(plugin.key, optionsById);\n }\n\n const declarations = new Map<MountId, MountDeclaration>();\n for (const plugin of resolved) {\n for (const record of plugin.manifest.routes) {\n if (!record.mount) continue;\n const mountId = record.mount as MountId;\n const def = MOUNT_REGISTRY[mountId];\n if (!def.implemented) {\n throw new Error(\n `mount \"${record.mount}\" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by \"${plugin.key}\"`,\n );\n }\n const existing = declarations.get(mountId);\n if (existing) {\n throw new Error(\n `mount \"${mountId}\" declared by multiple sources: \"${existing.key}\" (${existing.record.file}) and \"${plugin.key}\" (${record.file})`,\n );\n }\n declarations.set(mountId, {\n key: plugin.key,\n record,\n options: optionsByPlugin.get(plugin.key)!.get(record.id)!,\n });\n }\n }\n\n const rootRoute = toAnyRoute(\n createRootRoute({\n ...(input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {}),\n component: input.rootOptions?.component ?? Outlet,\n ...(input.rootOptions?.beforeLoad ? { beforeLoad: input.rootOptions.beforeLoad } : {}),\n }),\n );\n\n const mountRoutes = new Map<MountId, AnyRoute>();\n for (const mountId of Object.keys(MOUNT_REGISTRY) as MountId[]) {\n const declaration = declarations.get(mountId);\n if (!declaration) continue;\n const def = MOUNT_REGISTRY[mountId]!;\n const gate = GATES[def.gate];\n const options = declaration.options;\n const route = createRoute({\n id: `__mount_${mountId}`,\n getParentRoute: () => rootRoute,\n ...(gate || options.beforeLoad\n ? { beforeLoad: layoutBeforeLoad(gate, options.beforeLoad) }\n : {}),\n ...authoredOptions(options),\n component: options.component ?? Outlet,\n });\n mountRoutes.set(mountId, toAnyRoute(route));\n }\n\n const mountByLayoutId = new Map<string, MountId>();\n for (const [mountId, declaration] of declarations) {\n mountByLayoutId.set(declaration.record.id, mountId);\n }\n\n const nav: NavItem[] = [];\n const mountCounts: Record<string, number> = {};\n const childrenByParent = new Map<AnyRoute, AnyRoute[]>();\n /** cross-plugin path-collision guard: claimed paths per parent route */\n const claimedPathsByParent = new Map<AnyRoute, Map<string, string>>();\n const attach = (parent: AnyRoute, child: AnyRoute) => {\n const list = childrenByParent.get(parent) ?? [];\n list.push(child);\n childrenByParent.set(parent, list);\n };\n\n for (const plugin of resolved) {\n const optionsById = optionsByPlugin.get(plugin.key)!;\n const fullPathById = new Map<string, string>();\n const mountById = new Map<string, MountId>();\n const byId = new Map<string, { record: RouteRecord; route: AnyRoute }>();\n\n const parentFor = (\n record: RouteRecord,\n ): { route: AnyRoute; fullPath: string; mount: MountId } => {\n if (record.parentId) {\n const local = byId.get(record.parentId);\n if (local) {\n return {\n route: local.route,\n fullPath: fullPathById.get(record.parentId)!,\n mount: mountById.get(record.parentId)!,\n };\n }\n const mount = mountByLayoutId.get(record.parentId);\n if (mount) return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n throw new Error(\n `unresolvable parent \"${record.parentId}\" for \"${record.id}\" in \"${plugin.key}\" — mounts are the only cross-source parenting points`,\n );\n }\n const firstSegment = record.id.split(\"/\")[0] ?? \"\";\n if (firstSegment.startsWith(\"_\")) {\n const mount = resolveMountSegment(firstSegment.slice(1));\n if (mount && mountRoutes.has(mount))\n return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n if (mount) {\n throw new Error(\n `mount \"${mount}\" is not declared by any source (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(\n `unknown mount \"_${firstSegment.slice(1)}\" (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(`route \"${record.id}\" in \"${plugin.key}\" must live under a mount`);\n };\n\n const pending = plugin.manifest.routes.filter((record) => !record.mount);\n let progressed = true;\n while (pending.length > 0 && progressed) {\n progressed = false;\n for (let i = 0; i < pending.length; i += 1) {\n const record = pending[i]!;\n if (\n record.parentId &&\n !byId.has(record.parentId) &&\n !mountByLayoutId.has(record.parentId)\n ) {\n continue;\n }\n pending.splice(i, 1);\n progressed = true;\n\n const options = optionsById.get(record.id)!;\n const parent = parentFor(record);\n const routePath = record.path;\n const isLayoutRoute = record.type === \"layout\" || routePath === undefined;\n if (!isLayoutRoute) {\n const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();\n const claimant = siblings.get(routePath);\n if (claimant) {\n throw new Error(\n `path collision: \"${plugin.key}\" route \"${record.id}\" declares \"${routePath}\" which is already claimed by \"${claimant}\" under the same parent`,\n );\n }\n siblings.set(routePath, `${plugin.key}:${record.id}`);\n claimedPathsByParent.set(parent.route, siblings);\n }\n const fullPath = joinPath(parent.fullPath, routePath);\n let route: AnyRoute;\n if (isLayoutRoute) {\n route = createRoute({\n id: `${plugin.key}__${record.id}`,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n } else {\n route = createRoute({\n path: routePath,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n }\n\n byId.set(record.id, { record, route: toAnyRoute(route) });\n fullPathById.set(record.id, fullPath);\n mountById.set(record.id, parent.mount);\n attach(parent.route, toAnyRoute(route));\n mountCounts[parent.mount] = (mountCounts[parent.mount] ?? 0) + 1;\n\n const navDeclaration = navOf(options);\n if (navDeclaration && navDeclaration.hidden !== true) {\n nav.push({\n id: `${plugin.key}:${record.id}`,\n label: navDeclaration.label,\n ...(typeof navDeclaration.icon === \"string\" ? { icon: navDeclaration.icon } : {}),\n ...(typeof navDeclaration.group === \"string\" ? { group: navDeclaration.group } : {}),\n ...(typeof navDeclaration.order === \"number\" ? { order: navDeclaration.order } : {}),\n to: navDeclaration.to ?? fullPath,\n plugin: plugin.key,\n mount: parent.mount,\n });\n }\n break;\n }\n }\n if (pending.length > 0) {\n throw new Error(\n `unresolvable parentage in \"${plugin.key}\": ${pending.map((record) => record.id).join(\", \")}`,\n );\n }\n }\n\n for (const [parent, children] of childrenByParent) {\n (parent as { addChildren: (children: AnyRoute[]) => void }).addChildren(children);\n }\n rootRoute.addChildren([...mountRoutes.values()] as AnyRoute[]);\n nav.sort(compareNavItems);\n\n const digest = await digestOf({\n plugins: refs.map((ref) => ({ key: ref.key, mfName: ref.mfName })),\n manifests: resolved.map((plugin) => plugin.manifest),\n });\n\n return {\n rootRoute,\n routeTree: rootRoute,\n digest,\n manifests: resolved.map((plugin) => plugin.manifest),\n nav: { items: nav },\n mountCounts,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"construct.cjs","names":["redirect","ROUTE_OPTION_KEYS","MOUNT_REGISTRY","createRootRoute","Outlet","createRoute","resolveMountSegment","compareNavItems","digestOf"],"sources":["../../../src/ui/manifest/construct.ts"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { createRootRoute, createRoute, Outlet, redirect } from \"@tanstack/react-router\";\nimport { CORE_UI_PLUGIN_KEY } from \"./contract\";\nimport { digestOf } from \"./descriptor\";\nimport type { PluginManifest, RouteRecord } from \"./manifest-schema\";\nimport { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from \"./mount-registry\";\nimport { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from \"./nav\";\nimport { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from \"./route-config\";\n\n/**\n * Host-side route-graph construction (ADR 0008 §1): manifests + route\n * configs in → host-built tree out. The host constructs ITS OWN Route\n * objects from validated manifest data via public `createRoute` APIs; no\n * foreign route object is ever mutated. Gates are attached HOST-side from\n * the mount registry — plugins cannot ship a route that escapes its\n * mount's gate because plugins never construct route objects.\n *\n * Mounts are the ONLY cross-source parenting points: a root-level\n * pathless layout file (`_public.tsx`) declares the mount for the whole\n * composition (exactly one declaring source per mount); every other\n * source's routes live under the same `_mount/` directory prefix and\n * parent onto the shared mount route. This module executes inside the\n * core ui's module graph (production MF expose `./compose`, dev source\n * import) so constructed routes are minted by the same react/router\n * instance that renders them.\n */\n\n/**\n * TanStack's Route generics are intentionally invariant — routes built from\n * manifest data can't satisfy them structurally. One named boundary for\n * handing a constructed route to the tree-building APIs.\n */\nfunction toAnyRoute(route: unknown): AnyRoute {\n return route as AnyRoute;\n}\n\nexport interface GateUser {\n id: string;\n name?: string;\n /** Better Auth user role — \"admin\" gates the admin mount. */\n role?: string;\n}\n\nexport interface HostContext {\n user?: GateUser;\n session?: { user?: GateUser };\n}\n\ntype GateFn = (args: {\n context: HostContext;\n location: { pathname: string; searchStr: string };\n}) => void;\n\n/**\n * Gate redirects are absolute: gates attach to root-level mount routes, and\n * relatives would silently shift if a mount ever moved deeper in the tree.\n * The login surface is plugin-owned, never core-known.\n */\nconst LOGIN_TO: string = \"/login\";\nconst HOME_TO: string = \"/\";\n\n/**\n * Gate implementations by mount gate kind. Only `session` and `admin` are\n * implemented (ADR 0008 §3); unimplemented mounts are rejected during mount\n * declaration before any gate lookup, and gates are optional by type — a\n * mount without an entry (or `none`) renders ungated.\n */\nconst GATES: Partial<Record<MountDef[\"gate\"], GateFn>> = {\n session: ({ context, location }) => {\n const user = userOf(context);\n if (!user) {\n throw redirect({\n to: LOGIN_TO,\n search: { redirect: location.pathname + location.searchStr } as never,\n });\n }\n },\n admin: ({ context }) => {\n const user = userOf(context);\n if (!user) throw redirect({ to: LOGIN_TO });\n if (user.role !== \"admin\") throw redirect({ to: HOME_TO });\n },\n};\n\nconst userOf = (context: HostContext | undefined): GateUser | undefined =>\n context?.user ?? context?.session?.user;\n\nexport interface ConstructPluginRef {\n /** composition identity — plugin key; participates in the digest */\n key: string;\n /** deployment identity — the MF container name loadRemote targets */\n mfName: string;\n}\n\nexport interface ResolvedPlugin {\n key: string;\n manifest: PluginManifest;\n routeConfig: RouteConfigModule;\n}\n\nexport interface ConstructInput {\n name: string;\n plugins: ReadonlyArray<ConstructPluginRef>;\n resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;\n /** core ui `__root` options — component/error/notFound + head baseline */\n rootOptions?: RouteOptionsBundle;\n}\n\nexport interface ConstructedTree {\n rootRoute: AnyRoute;\n routeTree: AnyRoute;\n digest: string;\n manifests: PluginManifest[];\n nav: NavManifest;\n /** constructed route count per mount id */\n mountCounts: Record<string, number>;\n}\n\ninterface MountDeclaration {\n key: string;\n record: RouteRecord;\n options: RouteOptionsBundle;\n}\n\nfunction joinPath(a: string | undefined, b: string | undefined): string {\n const left = a && a !== \"/\" ? a.replace(/\\/+$/, \"\") : \"\";\n const right = b && b !== \"/\" ? b.replace(/^\\/+/, \"\") : \"\";\n if (!left) return right ? `/${right}` : \"/\";\n if (!right) return left;\n return `${left}/${right}`;\n}\n\nconst navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {\n const nav = options.staticData?.nav;\n if (nav && typeof nav === \"object\" && typeof (nav as NavDeclaration).label === \"string\") {\n return nav as NavDeclaration;\n }\n return undefined;\n};\n\n/** Layout routes (mount or pathless) pass their authored options through,\n * like every other route kind. On mount routes the registry gate composes\n * BEFORE the declaration's own beforeLoad so a source can never escape its\n * mount's gate (ADR 0008 §3), while the declaration's return value still\n * flows to children as route context. */\nfunction layoutBeforeLoad(\n gate: GateFn | undefined,\n declared: RouteOptionsBundle[\"beforeLoad\"],\n): RouteOptionsBundle[\"beforeLoad\"] {\n if (!gate) return declared;\n return (args) => {\n gate(args);\n return declared?.(args);\n };\n}\n\nconst AUTHORED_OPTION_KEYS = ROUTE_OPTION_KEYS.filter(\n (key) => key !== \"beforeLoad\" && key !== \"component\",\n);\n\nfunction authoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const authored: Partial<Record<keyof RouteOptionsBundle, unknown>> = {};\n for (const key of AUTHORED_OPTION_KEYS) {\n if (options[key] !== undefined) authored[key] = options[key];\n }\n return authored as Partial<RouteOptionsBundle>;\n}\n\nfunction rootAuthoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const { params: _params, ...authored } = authoredOptions(options);\n return authored;\n}\n\nexport async function constructTree(input: ConstructInput): Promise<ConstructedTree> {\n const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));\n const resolved: ResolvedPlugin[] = [];\n for (const ref of refs) {\n resolved.push(await input.resolve(ref));\n }\n\n const ordered = [\n ...resolved.filter((plugin) => plugin.key === CORE_UI_PLUGIN_KEY),\n ...resolved.filter((plugin) => plugin.key !== CORE_UI_PLUGIN_KEY),\n ];\n\n const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();\n for (const plugin of resolved) {\n const optionsById = new Map<string, RouteOptionsBundle>();\n await Promise.all(\n plugin.manifest.routes.map(async (record) => {\n const loader = plugin.routeConfig.routeConfigLoaders[record.id];\n if (!loader) {\n throw new Error(`routeConfig for \"${plugin.key}\" has no loader for route \"${record.id}\"`);\n }\n optionsById.set(record.id, await loader());\n }),\n );\n optionsByPlugin.set(plugin.key, optionsById);\n }\n\n const declarations = new Map<MountId, MountDeclaration>();\n const mountByLayoutId = new Map<string, MountId>();\n for (const plugin of ordered) {\n for (const record of plugin.manifest.routes) {\n if (!record.mount) continue;\n const mountId = record.mount as MountId;\n const def = MOUNT_REGISTRY[mountId];\n if (!def.implemented) {\n throw new Error(\n `mount \"${record.mount}\" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by \"${plugin.key}\"`,\n );\n }\n const existing = declarations.get(mountId);\n if (existing) {\n if (existing.key === CORE_UI_PLUGIN_KEY && plugin.key !== CORE_UI_PLUGIN_KEY) {\n console.log(\n `[Compose] mount declaration \"${existing.key}:${existing.record.id}\" (${mountId}) shadows \"${plugin.key}:${record.id}\" — core ui wins; shadowed declaration skipped`,\n );\n mountByLayoutId.set(record.id, mountId);\n continue;\n }\n throw new Error(\n `mount \"${mountId}\" declared by multiple sources: \"${existing.key}\" (${existing.record.file}) and \"${plugin.key}\" (${record.file})`,\n );\n }\n declarations.set(mountId, {\n key: plugin.key,\n record,\n options: optionsByPlugin.get(plugin.key)!.get(record.id)!,\n });\n mountByLayoutId.set(record.id, mountId);\n }\n }\n\n const rootRoute = toAnyRoute(\n createRootRoute({\n ...(input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {}),\n component: input.rootOptions?.component ?? Outlet,\n ...(input.rootOptions?.beforeLoad ? { beforeLoad: input.rootOptions.beforeLoad } : {}),\n }),\n );\n\n const mountRoutes = new Map<MountId, AnyRoute>();\n for (const mountId of Object.keys(MOUNT_REGISTRY) as MountId[]) {\n const declaration = declarations.get(mountId);\n if (!declaration) continue;\n const def = MOUNT_REGISTRY[mountId]!;\n const gate = GATES[def.gate];\n const options = declaration.options;\n const route = createRoute({\n id: `__mount_${mountId}`,\n getParentRoute: () => rootRoute,\n ...(gate || options.beforeLoad\n ? { beforeLoad: layoutBeforeLoad(gate, options.beforeLoad) }\n : {}),\n ...authoredOptions(options),\n component: options.component ?? Outlet,\n });\n mountRoutes.set(mountId, toAnyRoute(route));\n }\n\n const nav: NavItem[] = [];\n const mountCounts: Record<string, number> = {};\n const childrenByParent = new Map<AnyRoute, AnyRoute[]>();\n /** cross-plugin path-collision guard: claimed paths per parent route */\n const claimedPathsByParent = new Map<AnyRoute, Map<string, string>>();\n const attach = (parent: AnyRoute, child: AnyRoute) => {\n const list = childrenByParent.get(parent) ?? [];\n list.push(child);\n childrenByParent.set(parent, list);\n };\n\n for (const plugin of ordered) {\n const optionsById = optionsByPlugin.get(plugin.key)!;\n const fullPathById = new Map<string, string>();\n const mountById = new Map<string, MountId>();\n const byId = new Map<string, { record: RouteRecord; route: AnyRoute }>();\n\n const parentFor = (\n record: RouteRecord,\n ): { route: AnyRoute; fullPath: string; mount: MountId } => {\n if (record.parentId) {\n const local = byId.get(record.parentId);\n if (local) {\n return {\n route: local.route,\n fullPath: fullPathById.get(record.parentId)!,\n mount: mountById.get(record.parentId)!,\n };\n }\n const mount = mountByLayoutId.get(record.parentId);\n if (mount) return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n throw new Error(\n `unresolvable parent \"${record.parentId}\" for \"${record.id}\" in \"${plugin.key}\" — mounts are the only cross-source parenting points`,\n );\n }\n const firstSegment = record.id.split(\"/\")[0] ?? \"\";\n if (firstSegment.startsWith(\"_\")) {\n const mount = resolveMountSegment(firstSegment.slice(1));\n if (mount && mountRoutes.has(mount))\n return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n if (mount) {\n throw new Error(\n `mount \"${mount}\" is not declared by any source (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(\n `unknown mount \"_${firstSegment.slice(1)}\" (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(`route \"${record.id}\" in \"${plugin.key}\" must live under a mount`);\n };\n\n const pending = plugin.manifest.routes.filter((record) => !record.mount);\n const shadowedIds = new Set<string>();\n let progressed = true;\n while (pending.length > 0 && progressed) {\n progressed = false;\n for (let i = 0; i < pending.length; i += 1) {\n const record = pending[i]!;\n const parentId = record.parentId;\n if (parentId) {\n if (shadowedIds.has(parentId)) {\n shadowedIds.add(record.id);\n continue;\n }\n if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;\n }\n pending.splice(i, 1);\n progressed = true;\n\n const options = optionsById.get(record.id)!;\n const parent = parentFor(record);\n const routePath = record.path;\n const isLayoutRoute = record.type === \"layout\" || routePath === undefined;\n if (!isLayoutRoute) {\n const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();\n const claimant = siblings.get(routePath);\n if (claimant) {\n if (\n plugin.key !== CORE_UI_PLUGIN_KEY &&\n claimant.startsWith(`${CORE_UI_PLUGIN_KEY}:`)\n ) {\n console.log(\n `[Compose] route claim \"${claimant}\" (${routePath}) shadows \"${plugin.key}:${record.id}\" — core ui wins; shadowed route skipped`,\n );\n shadowedIds.add(record.id);\n break;\n }\n throw new Error(\n `path collision: \"${plugin.key}\" route \"${record.id}\" declares \"${routePath}\" which is already claimed by \"${claimant}\" under the same parent`,\n );\n }\n siblings.set(routePath, `${plugin.key}:${record.id}`);\n claimedPathsByParent.set(parent.route, siblings);\n }\n const fullPath = joinPath(parent.fullPath, routePath);\n let route: AnyRoute;\n if (isLayoutRoute) {\n route = createRoute({\n id: `${plugin.key}__${record.id}`,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n } else {\n route = createRoute({\n path: routePath,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n }\n\n byId.set(record.id, { record, route: toAnyRoute(route) });\n fullPathById.set(record.id, fullPath);\n mountById.set(record.id, parent.mount);\n attach(parent.route, toAnyRoute(route));\n mountCounts[parent.mount] = (mountCounts[parent.mount] ?? 0) + 1;\n\n const navDeclaration = navOf(options);\n if (navDeclaration && navDeclaration.hidden !== true) {\n nav.push({\n id: `${plugin.key}:${record.id}`,\n label: navDeclaration.label,\n ...(typeof navDeclaration.icon === \"string\" ? { icon: navDeclaration.icon } : {}),\n ...(typeof navDeclaration.group === \"string\" ? { group: navDeclaration.group } : {}),\n ...(typeof navDeclaration.order === \"number\" ? { order: navDeclaration.order } : {}),\n to: navDeclaration.to ?? fullPath,\n plugin: plugin.key,\n mount: parent.mount,\n });\n }\n break;\n }\n }\n if (pending.length > 0) {\n throw new Error(\n `unresolvable parentage in \"${plugin.key}\": ${pending.map((record) => record.id).join(\", \")}`,\n );\n }\n }\n\n for (const [parent, children] of childrenByParent) {\n (parent as { addChildren: (children: AnyRoute[]) => void }).addChildren(children);\n }\n rootRoute.addChildren([...mountRoutes.values()] as AnyRoute[]);\n nav.sort(compareNavItems);\n\n const digest = await digestOf({\n plugins: refs.map((ref) => ({ key: ref.key, mfName: ref.mfName })),\n manifests: resolved.map((plugin) => plugin.manifest),\n });\n\n return {\n rootRoute,\n routeTree: rootRoute,\n digest,\n manifests: resolved.map((plugin) => plugin.manifest),\n nav: { items: nav },\n mountCounts,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"construct.d.cts","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"mappings":";;;;;UAmCiB;EACf;EACA;;EAEA;;UAGe;EACf,OAAO;EACP;IAAY,OAAO;;;UA0CJ;;EAEf;;EAEA;;UAGe;EACf;EACA,UAAU;EACV,aAAa;;UAGE;EACf;EACA,SAAS,cAAc;EACvB,UAAU,KAAK,uBAAuB,QAAQ;;EAE9C,cAAc;;UAGC;EACf,WAAW;EACX,WAAW;EACX;EACA,WAAW;EACX,KAAK;;EAEL,aAAa;;iBA0DO,cAAc,OAAO,iBAAiB,QAAQ"}
1
+ {"version":3,"file":"construct.d.cts","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"mappings":";;;;;UAoCiB;EACf;EACA;;EAEA;;UAGe;EACf,OAAO;EACP;IAAY,OAAO;;;UA0CJ;;EAEf;;EAEA;;UAGe;EACf;EACA,UAAU;EACV,aAAa;;UAGE;EACf;EACA,SAAS,cAAc;EACvB,UAAU,KAAK,uBAAuB,QAAQ;;EAE9C,cAAc;;UAGC;EACf,WAAW;EACX,WAAW;EACX;EACA,WAAW;EACX,KAAK;;EAEL,aAAa;;iBA0DO,cAAc,OAAO,iBAAiB,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"construct.d.mts","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"mappings":";;;;;UAmCiB;EACf;EACA;;EAEA;;UAGe;EACf,OAAO;EACP;IAAY,OAAO;;;UA0CJ;;EAEf;;EAEA;;UAGe;EACf;EACA,UAAU;EACV,aAAa;;UAGE;EACf;EACA,SAAS,cAAc;EACvB,UAAU,KAAK,uBAAuB,QAAQ;;EAE9C,cAAc;;UAGC;EACf,WAAW;EACX,WAAW;EACX;EACA,WAAW;EACX,KAAK;;EAEL,aAAa;;iBA0DO,cAAc,OAAO,iBAAiB,QAAQ"}
1
+ {"version":3,"file":"construct.d.mts","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"mappings":";;;;;UAoCiB;EACf;EACA;;EAEA;;UAGe;EACf,OAAO;EACP;IAAY,OAAO;;;UA0CJ;;EAEf;;EAEA;;UAGe;EACf;EACA,UAAU;EACV,aAAa;;UAGE;EACf;EACA,SAAS,cAAc;EACvB,UAAU,KAAK,uBAAuB,QAAQ;;EAE9C,cAAc;;UAGC;EACf,WAAW;EACX,WAAW;EACX;EACA,WAAW;EACX,KAAK;;EAEL,aAAa;;iBA0DO,cAAc,OAAO,iBAAiB,QAAQ"}
@@ -1,3 +1,4 @@
1
+ import { CORE_UI_PLUGIN_KEY } from "./contract.mjs";
1
2
  import { MOUNT_REGISTRY, resolveMountSegment } from "./mount-registry.mjs";
2
3
  import { digestOf } from "./descriptor.mjs";
3
4
  import { compareNavItems } from "./nav.mjs";
@@ -94,6 +95,7 @@ async function constructTree(input) {
94
95
  const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));
95
96
  const resolved = [];
96
97
  for (const ref of refs) resolved.push(await input.resolve(ref));
98
+ const ordered = [...resolved.filter((plugin) => plugin.key === "ui"), ...resolved.filter((plugin) => plugin.key !== "ui")];
97
99
  const optionsByPlugin = /* @__PURE__ */ new Map();
98
100
  for (const plugin of resolved) {
99
101
  const optionsById = /* @__PURE__ */ new Map();
@@ -105,17 +107,26 @@ async function constructTree(input) {
105
107
  optionsByPlugin.set(plugin.key, optionsById);
106
108
  }
107
109
  const declarations = /* @__PURE__ */ new Map();
108
- for (const plugin of resolved) for (const record of plugin.manifest.routes) {
110
+ const mountByLayoutId = /* @__PURE__ */ new Map();
111
+ for (const plugin of ordered) for (const record of plugin.manifest.routes) {
109
112
  if (!record.mount) continue;
110
113
  const mountId = record.mount;
111
114
  if (!MOUNT_REGISTRY[mountId].implemented) throw new Error(`mount "${record.mount}" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by "${plugin.key}"`);
112
115
  const existing = declarations.get(mountId);
113
- if (existing) throw new Error(`mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`);
116
+ if (existing) {
117
+ if (existing.key === "ui" && plugin.key !== "ui") {
118
+ console.log(`[Compose] mount declaration "${existing.key}:${existing.record.id}" (${mountId}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed declaration skipped`);
119
+ mountByLayoutId.set(record.id, mountId);
120
+ continue;
121
+ }
122
+ throw new Error(`mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`);
123
+ }
114
124
  declarations.set(mountId, {
115
125
  key: plugin.key,
116
126
  record,
117
127
  options: optionsByPlugin.get(plugin.key).get(record.id)
118
128
  });
129
+ mountByLayoutId.set(record.id, mountId);
119
130
  }
120
131
  const rootRoute = toAnyRoute(createRootRoute({
121
132
  ...input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {},
@@ -138,8 +149,6 @@ async function constructTree(input) {
138
149
  });
139
150
  mountRoutes.set(mountId, toAnyRoute(route));
140
151
  }
141
- const mountByLayoutId = /* @__PURE__ */ new Map();
142
- for (const [mountId, declaration] of declarations) mountByLayoutId.set(declaration.record.id, mountId);
143
152
  const nav = [];
144
153
  const mountCounts = {};
145
154
  const childrenByParent = /* @__PURE__ */ new Map();
@@ -150,7 +159,7 @@ async function constructTree(input) {
150
159
  list.push(child);
151
160
  childrenByParent.set(parent, list);
152
161
  };
153
- for (const plugin of resolved) {
162
+ for (const plugin of ordered) {
154
163
  const optionsById = optionsByPlugin.get(plugin.key);
155
164
  const fullPathById = /* @__PURE__ */ new Map();
156
165
  const mountById = /* @__PURE__ */ new Map();
@@ -185,12 +194,20 @@ async function constructTree(input) {
185
194
  throw new Error(`route "${record.id}" in "${plugin.key}" must live under a mount`);
186
195
  };
187
196
  const pending = plugin.manifest.routes.filter((record) => !record.mount);
197
+ const shadowedIds = /* @__PURE__ */ new Set();
188
198
  let progressed = true;
189
199
  while (pending.length > 0 && progressed) {
190
200
  progressed = false;
191
201
  for (let i = 0; i < pending.length; i += 1) {
192
202
  const record = pending[i];
193
- if (record.parentId && !byId.has(record.parentId) && !mountByLayoutId.has(record.parentId)) continue;
203
+ const parentId = record.parentId;
204
+ if (parentId) {
205
+ if (shadowedIds.has(parentId)) {
206
+ shadowedIds.add(record.id);
207
+ continue;
208
+ }
209
+ if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;
210
+ }
194
211
  pending.splice(i, 1);
195
212
  progressed = true;
196
213
  const options = optionsById.get(record.id);
@@ -200,7 +217,14 @@ async function constructTree(input) {
200
217
  if (!isLayoutRoute) {
201
218
  const siblings = claimedPathsByParent.get(parent.route) ?? /* @__PURE__ */ new Map();
202
219
  const claimant = siblings.get(routePath);
203
- if (claimant) throw new Error(`path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`);
220
+ if (claimant) {
221
+ if (plugin.key !== "ui" && claimant.startsWith(`${"ui"}:`)) {
222
+ console.log(`[Compose] route claim "${claimant}" (${routePath}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed route skipped`);
223
+ shadowedIds.add(record.id);
224
+ break;
225
+ }
226
+ throw new Error(`path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`);
227
+ }
204
228
  siblings.set(routePath, `${plugin.key}:${record.id}`);
205
229
  claimedPathsByParent.set(parent.route, siblings);
206
230
  }
@@ -1 +1 @@
1
- {"version":3,"file":"construct.mjs","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { createRootRoute, createRoute, Outlet, redirect } from \"@tanstack/react-router\";\nimport { digestOf } from \"./descriptor\";\nimport type { PluginManifest, RouteRecord } from \"./manifest-schema\";\nimport { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from \"./mount-registry\";\nimport { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from \"./nav\";\nimport { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from \"./route-config\";\n\n/**\n * Host-side route-graph construction (ADR 0008 §1): manifests + route\n * configs in → host-built tree out. The host constructs ITS OWN Route\n * objects from validated manifest data via public `createRoute` APIs; no\n * foreign route object is ever mutated. Gates are attached HOST-side from\n * the mount registry — plugins cannot ship a route that escapes its\n * mount's gate because plugins never construct route objects.\n *\n * Mounts are the ONLY cross-source parenting points: a root-level\n * pathless layout file (`_public.tsx`) declares the mount for the whole\n * composition (exactly one declaring source per mount); every other\n * source's routes live under the same `_mount/` directory prefix and\n * parent onto the shared mount route. This module executes inside the\n * core ui's module graph (production MF expose `./compose`, dev source\n * import) so constructed routes are minted by the same react/router\n * instance that renders them.\n */\n\n/**\n * TanStack's Route generics are intentionally invariant — routes built from\n * manifest data can't satisfy them structurally. One named boundary for\n * handing a constructed route to the tree-building APIs.\n */\nfunction toAnyRoute(route: unknown): AnyRoute {\n return route as AnyRoute;\n}\n\nexport interface GateUser {\n id: string;\n name?: string;\n /** Better Auth user role — \"admin\" gates the admin mount. */\n role?: string;\n}\n\nexport interface HostContext {\n user?: GateUser;\n session?: { user?: GateUser };\n}\n\ntype GateFn = (args: {\n context: HostContext;\n location: { pathname: string; searchStr: string };\n}) => void;\n\n/**\n * Gate redirects are absolute: gates attach to root-level mount routes, and\n * relatives would silently shift if a mount ever moved deeper in the tree.\n * The login surface is plugin-owned, never core-known.\n */\nconst LOGIN_TO: string = \"/login\";\nconst HOME_TO: string = \"/\";\n\n/**\n * Gate implementations by mount gate kind. Only `session` and `admin` are\n * implemented (ADR 0008 §3); unimplemented mounts are rejected during mount\n * declaration before any gate lookup, and gates are optional by type — a\n * mount without an entry (or `none`) renders ungated.\n */\nconst GATES: Partial<Record<MountDef[\"gate\"], GateFn>> = {\n session: ({ context, location }) => {\n const user = userOf(context);\n if (!user) {\n throw redirect({\n to: LOGIN_TO,\n search: { redirect: location.pathname + location.searchStr } as never,\n });\n }\n },\n admin: ({ context }) => {\n const user = userOf(context);\n if (!user) throw redirect({ to: LOGIN_TO });\n if (user.role !== \"admin\") throw redirect({ to: HOME_TO });\n },\n};\n\nconst userOf = (context: HostContext | undefined): GateUser | undefined =>\n context?.user ?? context?.session?.user;\n\nexport interface ConstructPluginRef {\n /** composition identity — plugin key; participates in the digest */\n key: string;\n /** deployment identity — the MF container name loadRemote targets */\n mfName: string;\n}\n\nexport interface ResolvedPlugin {\n key: string;\n manifest: PluginManifest;\n routeConfig: RouteConfigModule;\n}\n\nexport interface ConstructInput {\n name: string;\n plugins: ReadonlyArray<ConstructPluginRef>;\n resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;\n /** core ui `__root` options — component/error/notFound + head baseline */\n rootOptions?: RouteOptionsBundle;\n}\n\nexport interface ConstructedTree {\n rootRoute: AnyRoute;\n routeTree: AnyRoute;\n digest: string;\n manifests: PluginManifest[];\n nav: NavManifest;\n /** constructed route count per mount id */\n mountCounts: Record<string, number>;\n}\n\ninterface MountDeclaration {\n key: string;\n record: RouteRecord;\n options: RouteOptionsBundle;\n}\n\nfunction joinPath(a: string | undefined, b: string | undefined): string {\n const left = a && a !== \"/\" ? a.replace(/\\/+$/, \"\") : \"\";\n const right = b && b !== \"/\" ? b.replace(/^\\/+/, \"\") : \"\";\n if (!left) return right ? `/${right}` : \"/\";\n if (!right) return left;\n return `${left}/${right}`;\n}\n\nconst navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {\n const nav = options.staticData?.nav;\n if (nav && typeof nav === \"object\" && typeof (nav as NavDeclaration).label === \"string\") {\n return nav as NavDeclaration;\n }\n return undefined;\n};\n\n/** Layout routes (mount or pathless) pass their authored options through,\n * like every other route kind. On mount routes the registry gate composes\n * BEFORE the declaration's own beforeLoad so a source can never escape its\n * mount's gate (ADR 0008 §3), while the declaration's return value still\n * flows to children as route context. */\nfunction layoutBeforeLoad(\n gate: GateFn | undefined,\n declared: RouteOptionsBundle[\"beforeLoad\"],\n): RouteOptionsBundle[\"beforeLoad\"] {\n if (!gate) return declared;\n return (args) => {\n gate(args);\n return declared?.(args);\n };\n}\n\nconst AUTHORED_OPTION_KEYS = ROUTE_OPTION_KEYS.filter(\n (key) => key !== \"beforeLoad\" && key !== \"component\",\n);\n\nfunction authoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const authored: Partial<Record<keyof RouteOptionsBundle, unknown>> = {};\n for (const key of AUTHORED_OPTION_KEYS) {\n if (options[key] !== undefined) authored[key] = options[key];\n }\n return authored as Partial<RouteOptionsBundle>;\n}\n\nfunction rootAuthoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const { params: _params, ...authored } = authoredOptions(options);\n return authored;\n}\n\nexport async function constructTree(input: ConstructInput): Promise<ConstructedTree> {\n const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));\n const resolved: ResolvedPlugin[] = [];\n for (const ref of refs) {\n resolved.push(await input.resolve(ref));\n }\n\n const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();\n for (const plugin of resolved) {\n const optionsById = new Map<string, RouteOptionsBundle>();\n await Promise.all(\n plugin.manifest.routes.map(async (record) => {\n const loader = plugin.routeConfig.routeConfigLoaders[record.id];\n if (!loader) {\n throw new Error(`routeConfig for \"${plugin.key}\" has no loader for route \"${record.id}\"`);\n }\n optionsById.set(record.id, await loader());\n }),\n );\n optionsByPlugin.set(plugin.key, optionsById);\n }\n\n const declarations = new Map<MountId, MountDeclaration>();\n for (const plugin of resolved) {\n for (const record of plugin.manifest.routes) {\n if (!record.mount) continue;\n const mountId = record.mount as MountId;\n const def = MOUNT_REGISTRY[mountId];\n if (!def.implemented) {\n throw new Error(\n `mount \"${record.mount}\" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by \"${plugin.key}\"`,\n );\n }\n const existing = declarations.get(mountId);\n if (existing) {\n throw new Error(\n `mount \"${mountId}\" declared by multiple sources: \"${existing.key}\" (${existing.record.file}) and \"${plugin.key}\" (${record.file})`,\n );\n }\n declarations.set(mountId, {\n key: plugin.key,\n record,\n options: optionsByPlugin.get(plugin.key)!.get(record.id)!,\n });\n }\n }\n\n const rootRoute = toAnyRoute(\n createRootRoute({\n ...(input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {}),\n component: input.rootOptions?.component ?? Outlet,\n ...(input.rootOptions?.beforeLoad ? { beforeLoad: input.rootOptions.beforeLoad } : {}),\n }),\n );\n\n const mountRoutes = new Map<MountId, AnyRoute>();\n for (const mountId of Object.keys(MOUNT_REGISTRY) as MountId[]) {\n const declaration = declarations.get(mountId);\n if (!declaration) continue;\n const def = MOUNT_REGISTRY[mountId]!;\n const gate = GATES[def.gate];\n const options = declaration.options;\n const route = createRoute({\n id: `__mount_${mountId}`,\n getParentRoute: () => rootRoute,\n ...(gate || options.beforeLoad\n ? { beforeLoad: layoutBeforeLoad(gate, options.beforeLoad) }\n : {}),\n ...authoredOptions(options),\n component: options.component ?? Outlet,\n });\n mountRoutes.set(mountId, toAnyRoute(route));\n }\n\n const mountByLayoutId = new Map<string, MountId>();\n for (const [mountId, declaration] of declarations) {\n mountByLayoutId.set(declaration.record.id, mountId);\n }\n\n const nav: NavItem[] = [];\n const mountCounts: Record<string, number> = {};\n const childrenByParent = new Map<AnyRoute, AnyRoute[]>();\n /** cross-plugin path-collision guard: claimed paths per parent route */\n const claimedPathsByParent = new Map<AnyRoute, Map<string, string>>();\n const attach = (parent: AnyRoute, child: AnyRoute) => {\n const list = childrenByParent.get(parent) ?? [];\n list.push(child);\n childrenByParent.set(parent, list);\n };\n\n for (const plugin of resolved) {\n const optionsById = optionsByPlugin.get(plugin.key)!;\n const fullPathById = new Map<string, string>();\n const mountById = new Map<string, MountId>();\n const byId = new Map<string, { record: RouteRecord; route: AnyRoute }>();\n\n const parentFor = (\n record: RouteRecord,\n ): { route: AnyRoute; fullPath: string; mount: MountId } => {\n if (record.parentId) {\n const local = byId.get(record.parentId);\n if (local) {\n return {\n route: local.route,\n fullPath: fullPathById.get(record.parentId)!,\n mount: mountById.get(record.parentId)!,\n };\n }\n const mount = mountByLayoutId.get(record.parentId);\n if (mount) return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n throw new Error(\n `unresolvable parent \"${record.parentId}\" for \"${record.id}\" in \"${plugin.key}\" — mounts are the only cross-source parenting points`,\n );\n }\n const firstSegment = record.id.split(\"/\")[0] ?? \"\";\n if (firstSegment.startsWith(\"_\")) {\n const mount = resolveMountSegment(firstSegment.slice(1));\n if (mount && mountRoutes.has(mount))\n return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n if (mount) {\n throw new Error(\n `mount \"${mount}\" is not declared by any source (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(\n `unknown mount \"_${firstSegment.slice(1)}\" (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(`route \"${record.id}\" in \"${plugin.key}\" must live under a mount`);\n };\n\n const pending = plugin.manifest.routes.filter((record) => !record.mount);\n let progressed = true;\n while (pending.length > 0 && progressed) {\n progressed = false;\n for (let i = 0; i < pending.length; i += 1) {\n const record = pending[i]!;\n if (\n record.parentId &&\n !byId.has(record.parentId) &&\n !mountByLayoutId.has(record.parentId)\n ) {\n continue;\n }\n pending.splice(i, 1);\n progressed = true;\n\n const options = optionsById.get(record.id)!;\n const parent = parentFor(record);\n const routePath = record.path;\n const isLayoutRoute = record.type === \"layout\" || routePath === undefined;\n if (!isLayoutRoute) {\n const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();\n const claimant = siblings.get(routePath);\n if (claimant) {\n throw new Error(\n `path collision: \"${plugin.key}\" route \"${record.id}\" declares \"${routePath}\" which is already claimed by \"${claimant}\" under the same parent`,\n );\n }\n siblings.set(routePath, `${plugin.key}:${record.id}`);\n claimedPathsByParent.set(parent.route, siblings);\n }\n const fullPath = joinPath(parent.fullPath, routePath);\n let route: AnyRoute;\n if (isLayoutRoute) {\n route = createRoute({\n id: `${plugin.key}__${record.id}`,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n } else {\n route = createRoute({\n path: routePath,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n }\n\n byId.set(record.id, { record, route: toAnyRoute(route) });\n fullPathById.set(record.id, fullPath);\n mountById.set(record.id, parent.mount);\n attach(parent.route, toAnyRoute(route));\n mountCounts[parent.mount] = (mountCounts[parent.mount] ?? 0) + 1;\n\n const navDeclaration = navOf(options);\n if (navDeclaration && navDeclaration.hidden !== true) {\n nav.push({\n id: `${plugin.key}:${record.id}`,\n label: navDeclaration.label,\n ...(typeof navDeclaration.icon === \"string\" ? { icon: navDeclaration.icon } : {}),\n ...(typeof navDeclaration.group === \"string\" ? { group: navDeclaration.group } : {}),\n ...(typeof navDeclaration.order === \"number\" ? { order: navDeclaration.order } : {}),\n to: navDeclaration.to ?? fullPath,\n plugin: plugin.key,\n mount: parent.mount,\n });\n }\n break;\n }\n }\n if (pending.length > 0) {\n throw new Error(\n `unresolvable parentage in \"${plugin.key}\": ${pending.map((record) => record.id).join(\", \")}`,\n );\n }\n }\n\n for (const [parent, children] of childrenByParent) {\n (parent as { addChildren: (children: AnyRoute[]) => void }).addChildren(children);\n }\n rootRoute.addChildren([...mountRoutes.values()] as AnyRoute[]);\n nav.sort(compareNavItems);\n\n const digest = await digestOf({\n plugins: refs.map((ref) => ({ key: ref.key, mfName: ref.mfName })),\n manifests: resolved.map((plugin) => plugin.manifest),\n });\n\n return {\n rootRoute,\n routeTree: rootRoute,\n digest,\n manifests: resolved.map((plugin) => plugin.manifest),\n nav: { items: nav },\n mountCounts,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAS,WAAW,OAA0B;CAC5C,OAAO;AACT;;;;;;AAwBA,MAAM,WAAmB;AACzB,MAAM,UAAkB;;;;;;;AAQxB,MAAM,QAAmD;CACvD,UAAU,EAAE,SAAS,eAAe;EAElC,IAAI,CADS,OAAO,OACZ,GACN,MAAM,SAAS;GACb,IAAI;GACJ,QAAQ,EAAE,UAAU,SAAS,WAAW,SAAS,UAAU;EAC7D,CAAC;CAEL;CACA,QAAQ,EAAE,cAAc;EACtB,MAAM,OAAO,OAAO,OAAO;EAC3B,IAAI,CAAC,MAAM,MAAM,SAAS,EAAE,IAAI,SAAS,CAAC;EAC1C,IAAI,KAAK,SAAS,SAAS,MAAM,SAAS,EAAE,IAAI,QAAQ,CAAC;CAC3D;AACF;AAEA,MAAM,UAAU,YACd,SAAS,QAAQ,SAAS,SAAS;AAuCrC,SAAS,SAAS,GAAuB,GAA+B;CACtE,MAAM,OAAO,KAAK,MAAM,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI;CACtD,MAAM,QAAQ,KAAK,MAAM,MAAM,EAAE,QAAQ,QAAQ,EAAE,IAAI;CACvD,IAAI,CAAC,MAAM,OAAO,QAAQ,IAAI,UAAU;CACxC,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,GAAG,KAAK,GAAG;AACpB;AAEA,MAAM,SAAS,YAA4D;CACzE,MAAM,MAAM,QAAQ,YAAY;CAChC,IAAI,OAAO,OAAO,QAAQ,YAAY,OAAQ,IAAuB,UAAU,UAC7E,OAAO;AAGX;;;;;;AAOA,SAAS,iBACP,MACA,UACkC;CAClC,IAAI,CAAC,MAAM,OAAO;CAClB,QAAQ,SAAS;EACf,KAAK,IAAI;EACT,OAAO,WAAW,IAAI;CACxB;AACF;AAEA,MAAM,uBAAuB,kBAAkB,QAC5C,QAAQ,QAAQ,gBAAgB,QAAQ,WAC3C;AAEA,SAAS,gBAAgB,SAA0D;CACjF,MAAM,WAA+D,CAAC;CACtE,KAAK,MAAM,OAAO,sBAChB,IAAI,QAAQ,SAAS,QAAW,SAAS,OAAO,QAAQ;CAE1D,OAAO;AACT;AAEA,SAAS,oBAAoB,SAA0D;CACrF,MAAM,EAAE,QAAQ,SAAS,GAAG,aAAa,gBAAgB,OAAO;CAChE,OAAO;AACT;AAEA,eAAsB,cAAc,OAAiD;CACnF,MAAM,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;CACzE,MAAM,WAA6B,CAAC;CACpC,KAAK,MAAM,OAAO,MAChB,SAAS,KAAK,MAAM,MAAM,QAAQ,GAAG,CAAC;CAGxC,MAAM,kCAAkB,IAAI,IAA6C;CACzE,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,8BAAc,IAAI,IAAgC;EACxD,MAAM,QAAQ,IACZ,OAAO,SAAS,OAAO,IAAI,OAAO,WAAW;GAC3C,MAAM,SAAS,OAAO,YAAY,mBAAmB,OAAO;GAC5D,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,oBAAoB,OAAO,IAAI,6BAA6B,OAAO,GAAG,EAAE;GAE1F,YAAY,IAAI,OAAO,IAAI,MAAM,OAAO,CAAC;EAC3C,CAAC,CACH;EACA,gBAAgB,IAAI,OAAO,KAAK,WAAW;CAC7C;CAEA,MAAM,+BAAe,IAAI,IAA+B;CACxD,KAAK,MAAM,UAAU,UACnB,KAAK,MAAM,UAAU,OAAO,SAAS,QAAQ;EAC3C,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,UAAU,OAAO;EAEvB,IAAI,CADQ,eAAe,QACnB,CAAC,aACP,MAAM,IAAI,MACR,UAAU,OAAO,MAAM,0EAA0E,OAAO,IAAI,EAC9G;EAEF,MAAM,WAAW,aAAa,IAAI,OAAO;EACzC,IAAI,UACF,MAAM,IAAI,MACR,UAAU,QAAQ,mCAAmC,SAAS,IAAI,KAAK,SAAS,OAAO,KAAK,SAAS,OAAO,IAAI,KAAK,OAAO,KAAK,EACnI;EAEF,aAAa,IAAI,SAAS;GACxB,KAAK,OAAO;GACZ;GACA,SAAS,gBAAgB,IAAI,OAAO,GAAG,CAAC,CAAE,IAAI,OAAO,EAAE;EACzD,CAAC;CACH;CAGF,MAAM,YAAY,WAChB,gBAAgB;EACd,GAAI,MAAM,cAAc,oBAAoB,MAAM,WAAW,IAAI,CAAC;EAClE,WAAW,MAAM,aAAa,aAAa;EAC3C,GAAI,MAAM,aAAa,aAAa,EAAE,YAAY,MAAM,YAAY,WAAW,IAAI,CAAC;CACtF,CAAC,CACH;CAEA,MAAM,8BAAc,IAAI,IAAuB;CAC/C,KAAK,MAAM,WAAW,OAAO,KAAK,cAAc,GAAgB;EAC9D,MAAM,cAAc,aAAa,IAAI,OAAO;EAC5C,IAAI,CAAC,aAAa;EAClB,MAAM,MAAM,eAAe;EAC3B,MAAM,OAAO,MAAM,IAAI;EACvB,MAAM,UAAU,YAAY;EAC5B,MAAM,QAAQ,YAAY;GACxB,IAAI,WAAW;GACf,sBAAsB;GACtB,GAAI,QAAQ,QAAQ,aAChB,EAAE,YAAY,iBAAiB,MAAM,QAAQ,UAAU,EAAE,IACzD,CAAC;GACL,GAAG,gBAAgB,OAAO;GAC1B,WAAW,QAAQ,aAAa;EAClC,CAAC;EACD,YAAY,IAAI,SAAS,WAAW,KAAK,CAAC;CAC5C;CAEA,MAAM,kCAAkB,IAAI,IAAqB;CACjD,KAAK,MAAM,CAAC,SAAS,gBAAgB,cACnC,gBAAgB,IAAI,YAAY,OAAO,IAAI,OAAO;CAGpD,MAAM,MAAiB,CAAC;CACxB,MAAM,cAAsC,CAAC;CAC7C,MAAM,mCAAmB,IAAI,IAA0B;;CAEvD,MAAM,uCAAuB,IAAI,IAAmC;CACpE,MAAM,UAAU,QAAkB,UAAoB;EACpD,MAAM,OAAO,iBAAiB,IAAI,MAAM,KAAK,CAAC;EAC9C,KAAK,KAAK,KAAK;EACf,iBAAiB,IAAI,QAAQ,IAAI;CACnC;CAEA,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,cAAc,gBAAgB,IAAI,OAAO,GAAG;EAClD,MAAM,+BAAe,IAAI,IAAoB;EAC7C,MAAM,4BAAY,IAAI,IAAqB;EAC3C,MAAM,uBAAO,IAAI,IAAsD;EAEvE,MAAM,aACJ,WAC0D;GAC1D,IAAI,OAAO,UAAU;IACnB,MAAM,QAAQ,KAAK,IAAI,OAAO,QAAQ;IACtC,IAAI,OACF,OAAO;KACL,OAAO,MAAM;KACb,UAAU,aAAa,IAAI,OAAO,QAAQ;KAC1C,OAAO,UAAU,IAAI,OAAO,QAAQ;IACtC;IAEF,MAAM,QAAQ,gBAAgB,IAAI,OAAO,QAAQ;IACjD,IAAI,OAAO,OAAO;KAAE,OAAO,YAAY,IAAI,KAAK;KAAI,UAAU;KAAI;IAAM;IACxE,MAAM,IAAI,MACR,wBAAwB,OAAO,SAAS,SAAS,OAAO,GAAG,QAAQ,OAAO,IAAI,sDAChF;GACF;GACA,MAAM,eAAe,OAAO,GAAG,MAAM,GAAG,CAAC,CAAC,MAAM;GAChD,IAAI,aAAa,WAAW,GAAG,GAAG;IAChC,MAAM,QAAQ,oBAAoB,aAAa,MAAM,CAAC,CAAC;IACvD,IAAI,SAAS,YAAY,IAAI,KAAK,GAChC,OAAO;KAAE,OAAO,YAAY,IAAI,KAAK;KAAI,UAAU;KAAI;IAAM;IAC/D,IAAI,OACF,MAAM,IAAI,MACR,UAAU,MAAM,0CAA0C,OAAO,GAAG,QAAQ,OAAO,IAAI,GACzF;IAEF,MAAM,IAAI,MACR,mBAAmB,aAAa,MAAM,CAAC,EAAE,YAAY,OAAO,GAAG,QAAQ,OAAO,IAAI,GACpF;GACF;GACA,MAAM,IAAI,MAAM,UAAU,OAAO,GAAG,QAAQ,OAAO,IAAI,0BAA0B;EACnF;EAEA,MAAM,UAAU,OAAO,SAAS,OAAO,QAAQ,WAAW,CAAC,OAAO,KAAK;EACvE,IAAI,aAAa;EACjB,OAAO,QAAQ,SAAS,KAAK,YAAY;GACvC,aAAa;GACb,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;IAC1C,MAAM,SAAS,QAAQ;IACvB,IACE,OAAO,YACP,CAAC,KAAK,IAAI,OAAO,QAAQ,KACzB,CAAC,gBAAgB,IAAI,OAAO,QAAQ,GAEpC;IAEF,QAAQ,OAAO,GAAG,CAAC;IACnB,aAAa;IAEb,MAAM,UAAU,YAAY,IAAI,OAAO,EAAE;IACzC,MAAM,SAAS,UAAU,MAAM;IAC/B,MAAM,YAAY,OAAO;IACzB,MAAM,gBAAgB,OAAO,SAAS,YAAY,cAAc;IAChE,IAAI,CAAC,eAAe;KAClB,MAAM,WAAW,qBAAqB,IAAI,OAAO,KAAK,qBAAK,IAAI,IAAoB;KACnF,MAAM,WAAW,SAAS,IAAI,SAAS;KACvC,IAAI,UACF,MAAM,IAAI,MACR,oBAAoB,OAAO,IAAI,WAAW,OAAO,GAAG,cAAc,UAAU,iCAAiC,SAAS,wBACxH;KAEF,SAAS,IAAI,WAAW,GAAG,OAAO,IAAI,GAAG,OAAO,IAAI;KACpD,qBAAqB,IAAI,OAAO,OAAO,QAAQ;IACjD;IACA,MAAM,WAAW,SAAS,OAAO,UAAU,SAAS;IACpD,IAAI;IACJ,IAAI,eACF,QAAQ,YAAY;KAClB,IAAI,GAAG,OAAO,IAAI,IAAI,OAAO;KAC7B,sBAAsB,OAAO;KAC7B,GAAG,gBAAgB,OAAO;KAC1B,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;KAC/D,WAAW,QAAQ,aAAa;IAClC,CAAC;SAED,QAAQ,YAAY;KAClB,MAAM;KACN,sBAAsB,OAAO;KAC7B,GAAG,gBAAgB,OAAO;KAC1B,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;KAC/D,WAAW,QAAQ,aAAa;IAClC,CAAC;IAGH,KAAK,IAAI,OAAO,IAAI;KAAE;KAAQ,OAAO,WAAW,KAAK;IAAE,CAAC;IACxD,aAAa,IAAI,OAAO,IAAI,QAAQ;IACpC,UAAU,IAAI,OAAO,IAAI,OAAO,KAAK;IACrC,OAAO,OAAO,OAAO,WAAW,KAAK,CAAC;IACtC,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK;IAE/D,MAAM,iBAAiB,MAAM,OAAO;IACpC,IAAI,kBAAkB,eAAe,WAAW,MAC9C,IAAI,KAAK;KACP,IAAI,GAAG,OAAO,IAAI,GAAG,OAAO;KAC5B,OAAO,eAAe;KACtB,GAAI,OAAO,eAAe,SAAS,WAAW,EAAE,MAAM,eAAe,KAAK,IAAI,CAAC;KAC/E,GAAI,OAAO,eAAe,UAAU,WAAW,EAAE,OAAO,eAAe,MAAM,IAAI,CAAC;KAClF,GAAI,OAAO,eAAe,UAAU,WAAW,EAAE,OAAO,eAAe,MAAM,IAAI,CAAC;KAClF,IAAI,eAAe,MAAM;KACzB,QAAQ,OAAO;KACf,OAAO,OAAO;IAChB,CAAC;IAEH;GACF;EACF;EACA,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,MACR,8BAA8B,OAAO,IAAI,KAAK,QAAQ,KAAK,WAAW,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,GAC5F;CAEJ;CAEA,KAAK,MAAM,CAAC,QAAQ,aAAa,kBAC/B,AAAC,OAA2D,YAAY,QAAQ;CAElF,UAAU,YAAY,CAAC,GAAG,YAAY,OAAO,CAAC,CAAe;CAC7D,IAAI,KAAK,eAAe;CAOxB,OAAO;EACL;EACA,WAAW;EACX,cARmB,SAAS;GAC5B,SAAS,KAAK,KAAK,SAAS;IAAE,KAAK,IAAI;IAAK,QAAQ,IAAI;GAAO,EAAE;GACjE,WAAW,SAAS,KAAK,WAAW,OAAO,QAAQ;EACrD,CAAC;EAMC,WAAW,SAAS,KAAK,WAAW,OAAO,QAAQ;EACnD,KAAK,EAAE,OAAO,IAAI;EAClB;CACF;AACF"}
1
+ {"version":3,"file":"construct.mjs","names":[],"sources":["../../../src/ui/manifest/construct.ts"],"sourcesContent":["import type { AnyRoute } from \"@tanstack/react-router\";\nimport { createRootRoute, createRoute, Outlet, redirect } from \"@tanstack/react-router\";\nimport { CORE_UI_PLUGIN_KEY } from \"./contract\";\nimport { digestOf } from \"./descriptor\";\nimport type { PluginManifest, RouteRecord } from \"./manifest-schema\";\nimport { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from \"./mount-registry\";\nimport { compareNavItems, type NavDeclaration, type NavItem, type NavManifest } from \"./nav\";\nimport { ROUTE_OPTION_KEYS, type RouteConfigModule, type RouteOptionsBundle } from \"./route-config\";\n\n/**\n * Host-side route-graph construction (ADR 0008 §1): manifests + route\n * configs in → host-built tree out. The host constructs ITS OWN Route\n * objects from validated manifest data via public `createRoute` APIs; no\n * foreign route object is ever mutated. Gates are attached HOST-side from\n * the mount registry — plugins cannot ship a route that escapes its\n * mount's gate because plugins never construct route objects.\n *\n * Mounts are the ONLY cross-source parenting points: a root-level\n * pathless layout file (`_public.tsx`) declares the mount for the whole\n * composition (exactly one declaring source per mount); every other\n * source's routes live under the same `_mount/` directory prefix and\n * parent onto the shared mount route. This module executes inside the\n * core ui's module graph (production MF expose `./compose`, dev source\n * import) so constructed routes are minted by the same react/router\n * instance that renders them.\n */\n\n/**\n * TanStack's Route generics are intentionally invariant — routes built from\n * manifest data can't satisfy them structurally. One named boundary for\n * handing a constructed route to the tree-building APIs.\n */\nfunction toAnyRoute(route: unknown): AnyRoute {\n return route as AnyRoute;\n}\n\nexport interface GateUser {\n id: string;\n name?: string;\n /** Better Auth user role — \"admin\" gates the admin mount. */\n role?: string;\n}\n\nexport interface HostContext {\n user?: GateUser;\n session?: { user?: GateUser };\n}\n\ntype GateFn = (args: {\n context: HostContext;\n location: { pathname: string; searchStr: string };\n}) => void;\n\n/**\n * Gate redirects are absolute: gates attach to root-level mount routes, and\n * relatives would silently shift if a mount ever moved deeper in the tree.\n * The login surface is plugin-owned, never core-known.\n */\nconst LOGIN_TO: string = \"/login\";\nconst HOME_TO: string = \"/\";\n\n/**\n * Gate implementations by mount gate kind. Only `session` and `admin` are\n * implemented (ADR 0008 §3); unimplemented mounts are rejected during mount\n * declaration before any gate lookup, and gates are optional by type — a\n * mount without an entry (or `none`) renders ungated.\n */\nconst GATES: Partial<Record<MountDef[\"gate\"], GateFn>> = {\n session: ({ context, location }) => {\n const user = userOf(context);\n if (!user) {\n throw redirect({\n to: LOGIN_TO,\n search: { redirect: location.pathname + location.searchStr } as never,\n });\n }\n },\n admin: ({ context }) => {\n const user = userOf(context);\n if (!user) throw redirect({ to: LOGIN_TO });\n if (user.role !== \"admin\") throw redirect({ to: HOME_TO });\n },\n};\n\nconst userOf = (context: HostContext | undefined): GateUser | undefined =>\n context?.user ?? context?.session?.user;\n\nexport interface ConstructPluginRef {\n /** composition identity — plugin key; participates in the digest */\n key: string;\n /** deployment identity — the MF container name loadRemote targets */\n mfName: string;\n}\n\nexport interface ResolvedPlugin {\n key: string;\n manifest: PluginManifest;\n routeConfig: RouteConfigModule;\n}\n\nexport interface ConstructInput {\n name: string;\n plugins: ReadonlyArray<ConstructPluginRef>;\n resolve: (ref: ConstructPluginRef) => Promise<ResolvedPlugin>;\n /** core ui `__root` options — component/error/notFound + head baseline */\n rootOptions?: RouteOptionsBundle;\n}\n\nexport interface ConstructedTree {\n rootRoute: AnyRoute;\n routeTree: AnyRoute;\n digest: string;\n manifests: PluginManifest[];\n nav: NavManifest;\n /** constructed route count per mount id */\n mountCounts: Record<string, number>;\n}\n\ninterface MountDeclaration {\n key: string;\n record: RouteRecord;\n options: RouteOptionsBundle;\n}\n\nfunction joinPath(a: string | undefined, b: string | undefined): string {\n const left = a && a !== \"/\" ? a.replace(/\\/+$/, \"\") : \"\";\n const right = b && b !== \"/\" ? b.replace(/^\\/+/, \"\") : \"\";\n if (!left) return right ? `/${right}` : \"/\";\n if (!right) return left;\n return `${left}/${right}`;\n}\n\nconst navOf = (options: RouteOptionsBundle): NavDeclaration | undefined => {\n const nav = options.staticData?.nav;\n if (nav && typeof nav === \"object\" && typeof (nav as NavDeclaration).label === \"string\") {\n return nav as NavDeclaration;\n }\n return undefined;\n};\n\n/** Layout routes (mount or pathless) pass their authored options through,\n * like every other route kind. On mount routes the registry gate composes\n * BEFORE the declaration's own beforeLoad so a source can never escape its\n * mount's gate (ADR 0008 §3), while the declaration's return value still\n * flows to children as route context. */\nfunction layoutBeforeLoad(\n gate: GateFn | undefined,\n declared: RouteOptionsBundle[\"beforeLoad\"],\n): RouteOptionsBundle[\"beforeLoad\"] {\n if (!gate) return declared;\n return (args) => {\n gate(args);\n return declared?.(args);\n };\n}\n\nconst AUTHORED_OPTION_KEYS = ROUTE_OPTION_KEYS.filter(\n (key) => key !== \"beforeLoad\" && key !== \"component\",\n);\n\nfunction authoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const authored: Partial<Record<keyof RouteOptionsBundle, unknown>> = {};\n for (const key of AUTHORED_OPTION_KEYS) {\n if (options[key] !== undefined) authored[key] = options[key];\n }\n return authored as Partial<RouteOptionsBundle>;\n}\n\nfunction rootAuthoredOptions(options: RouteOptionsBundle): Partial<RouteOptionsBundle> {\n const { params: _params, ...authored } = authoredOptions(options);\n return authored;\n}\n\nexport async function constructTree(input: ConstructInput): Promise<ConstructedTree> {\n const refs = [...input.plugins].sort((a, b) => a.key.localeCompare(b.key));\n const resolved: ResolvedPlugin[] = [];\n for (const ref of refs) {\n resolved.push(await input.resolve(ref));\n }\n\n const ordered = [\n ...resolved.filter((plugin) => plugin.key === CORE_UI_PLUGIN_KEY),\n ...resolved.filter((plugin) => plugin.key !== CORE_UI_PLUGIN_KEY),\n ];\n\n const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();\n for (const plugin of resolved) {\n const optionsById = new Map<string, RouteOptionsBundle>();\n await Promise.all(\n plugin.manifest.routes.map(async (record) => {\n const loader = plugin.routeConfig.routeConfigLoaders[record.id];\n if (!loader) {\n throw new Error(`routeConfig for \"${plugin.key}\" has no loader for route \"${record.id}\"`);\n }\n optionsById.set(record.id, await loader());\n }),\n );\n optionsByPlugin.set(plugin.key, optionsById);\n }\n\n const declarations = new Map<MountId, MountDeclaration>();\n const mountByLayoutId = new Map<string, MountId>();\n for (const plugin of ordered) {\n for (const record of plugin.manifest.routes) {\n if (!record.mount) continue;\n const mountId = record.mount as MountId;\n const def = MOUNT_REGISTRY[mountId];\n if (!def.implemented) {\n throw new Error(\n `mount \"${record.mount}\" is declared vocabulary but unimplemented (ADR 0008 §3) — declared by \"${plugin.key}\"`,\n );\n }\n const existing = declarations.get(mountId);\n if (existing) {\n if (existing.key === CORE_UI_PLUGIN_KEY && plugin.key !== CORE_UI_PLUGIN_KEY) {\n console.log(\n `[Compose] mount declaration \"${existing.key}:${existing.record.id}\" (${mountId}) shadows \"${plugin.key}:${record.id}\" — core ui wins; shadowed declaration skipped`,\n );\n mountByLayoutId.set(record.id, mountId);\n continue;\n }\n throw new Error(\n `mount \"${mountId}\" declared by multiple sources: \"${existing.key}\" (${existing.record.file}) and \"${plugin.key}\" (${record.file})`,\n );\n }\n declarations.set(mountId, {\n key: plugin.key,\n record,\n options: optionsByPlugin.get(plugin.key)!.get(record.id)!,\n });\n mountByLayoutId.set(record.id, mountId);\n }\n }\n\n const rootRoute = toAnyRoute(\n createRootRoute({\n ...(input.rootOptions ? rootAuthoredOptions(input.rootOptions) : {}),\n component: input.rootOptions?.component ?? Outlet,\n ...(input.rootOptions?.beforeLoad ? { beforeLoad: input.rootOptions.beforeLoad } : {}),\n }),\n );\n\n const mountRoutes = new Map<MountId, AnyRoute>();\n for (const mountId of Object.keys(MOUNT_REGISTRY) as MountId[]) {\n const declaration = declarations.get(mountId);\n if (!declaration) continue;\n const def = MOUNT_REGISTRY[mountId]!;\n const gate = GATES[def.gate];\n const options = declaration.options;\n const route = createRoute({\n id: `__mount_${mountId}`,\n getParentRoute: () => rootRoute,\n ...(gate || options.beforeLoad\n ? { beforeLoad: layoutBeforeLoad(gate, options.beforeLoad) }\n : {}),\n ...authoredOptions(options),\n component: options.component ?? Outlet,\n });\n mountRoutes.set(mountId, toAnyRoute(route));\n }\n\n const nav: NavItem[] = [];\n const mountCounts: Record<string, number> = {};\n const childrenByParent = new Map<AnyRoute, AnyRoute[]>();\n /** cross-plugin path-collision guard: claimed paths per parent route */\n const claimedPathsByParent = new Map<AnyRoute, Map<string, string>>();\n const attach = (parent: AnyRoute, child: AnyRoute) => {\n const list = childrenByParent.get(parent) ?? [];\n list.push(child);\n childrenByParent.set(parent, list);\n };\n\n for (const plugin of ordered) {\n const optionsById = optionsByPlugin.get(plugin.key)!;\n const fullPathById = new Map<string, string>();\n const mountById = new Map<string, MountId>();\n const byId = new Map<string, { record: RouteRecord; route: AnyRoute }>();\n\n const parentFor = (\n record: RouteRecord,\n ): { route: AnyRoute; fullPath: string; mount: MountId } => {\n if (record.parentId) {\n const local = byId.get(record.parentId);\n if (local) {\n return {\n route: local.route,\n fullPath: fullPathById.get(record.parentId)!,\n mount: mountById.get(record.parentId)!,\n };\n }\n const mount = mountByLayoutId.get(record.parentId);\n if (mount) return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n throw new Error(\n `unresolvable parent \"${record.parentId}\" for \"${record.id}\" in \"${plugin.key}\" — mounts are the only cross-source parenting points`,\n );\n }\n const firstSegment = record.id.split(\"/\")[0] ?? \"\";\n if (firstSegment.startsWith(\"_\")) {\n const mount = resolveMountSegment(firstSegment.slice(1));\n if (mount && mountRoutes.has(mount))\n return { route: mountRoutes.get(mount)!, fullPath: \"\", mount };\n if (mount) {\n throw new Error(\n `mount \"${mount}\" is not declared by any source (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(\n `unknown mount \"_${firstSegment.slice(1)}\" (route \"${record.id}\" in \"${plugin.key}\")`,\n );\n }\n throw new Error(`route \"${record.id}\" in \"${plugin.key}\" must live under a mount`);\n };\n\n const pending = plugin.manifest.routes.filter((record) => !record.mount);\n const shadowedIds = new Set<string>();\n let progressed = true;\n while (pending.length > 0 && progressed) {\n progressed = false;\n for (let i = 0; i < pending.length; i += 1) {\n const record = pending[i]!;\n const parentId = record.parentId;\n if (parentId) {\n if (shadowedIds.has(parentId)) {\n shadowedIds.add(record.id);\n continue;\n }\n if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;\n }\n pending.splice(i, 1);\n progressed = true;\n\n const options = optionsById.get(record.id)!;\n const parent = parentFor(record);\n const routePath = record.path;\n const isLayoutRoute = record.type === \"layout\" || routePath === undefined;\n if (!isLayoutRoute) {\n const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();\n const claimant = siblings.get(routePath);\n if (claimant) {\n if (\n plugin.key !== CORE_UI_PLUGIN_KEY &&\n claimant.startsWith(`${CORE_UI_PLUGIN_KEY}:`)\n ) {\n console.log(\n `[Compose] route claim \"${claimant}\" (${routePath}) shadows \"${plugin.key}:${record.id}\" — core ui wins; shadowed route skipped`,\n );\n shadowedIds.add(record.id);\n break;\n }\n throw new Error(\n `path collision: \"${plugin.key}\" route \"${record.id}\" declares \"${routePath}\" which is already claimed by \"${claimant}\" under the same parent`,\n );\n }\n siblings.set(routePath, `${plugin.key}:${record.id}`);\n claimedPathsByParent.set(parent.route, siblings);\n }\n const fullPath = joinPath(parent.fullPath, routePath);\n let route: AnyRoute;\n if (isLayoutRoute) {\n route = createRoute({\n id: `${plugin.key}__${record.id}`,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n } else {\n route = createRoute({\n path: routePath,\n getParentRoute: () => parent.route,\n ...authoredOptions(options),\n ...(options.beforeLoad ? { beforeLoad: options.beforeLoad } : {}),\n component: options.component ?? Outlet,\n });\n }\n\n byId.set(record.id, { record, route: toAnyRoute(route) });\n fullPathById.set(record.id, fullPath);\n mountById.set(record.id, parent.mount);\n attach(parent.route, toAnyRoute(route));\n mountCounts[parent.mount] = (mountCounts[parent.mount] ?? 0) + 1;\n\n const navDeclaration = navOf(options);\n if (navDeclaration && navDeclaration.hidden !== true) {\n nav.push({\n id: `${plugin.key}:${record.id}`,\n label: navDeclaration.label,\n ...(typeof navDeclaration.icon === \"string\" ? { icon: navDeclaration.icon } : {}),\n ...(typeof navDeclaration.group === \"string\" ? { group: navDeclaration.group } : {}),\n ...(typeof navDeclaration.order === \"number\" ? { order: navDeclaration.order } : {}),\n to: navDeclaration.to ?? fullPath,\n plugin: plugin.key,\n mount: parent.mount,\n });\n }\n break;\n }\n }\n if (pending.length > 0) {\n throw new Error(\n `unresolvable parentage in \"${plugin.key}\": ${pending.map((record) => record.id).join(\", \")}`,\n );\n }\n }\n\n for (const [parent, children] of childrenByParent) {\n (parent as { addChildren: (children: AnyRoute[]) => void }).addChildren(children);\n }\n rootRoute.addChildren([...mountRoutes.values()] as AnyRoute[]);\n nav.sort(compareNavItems);\n\n const digest = await digestOf({\n plugins: refs.map((ref) => ({ key: ref.key, mfName: ref.mfName })),\n manifests: resolved.map((plugin) => plugin.manifest),\n });\n\n return {\n rootRoute,\n routeTree: rootRoute,\n digest,\n manifests: resolved.map((plugin) => plugin.manifest),\n nav: { items: nav },\n mountCounts,\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_runtime = require('../../_virtual/_rolldown/runtime.cjs');
3
+ const require_ui_manifest_contract = require('./contract.cjs');
3
4
  const require_mount_registry = require('./mount-registry.cjs');
4
5
  const require_route_config = require('./route-config.cjs');
5
- const require_ui_manifest_contract = require('./contract.cjs');
6
6
  const require_manifest_schema = require('./manifest-schema.cjs');
7
7
  require('./index.cjs');
8
8
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
+ import { MANIFEST_FILENAME, ROUTE_CONFIG_FILENAME } from "./contract.mjs";
1
2
  import { MOUNT_REGISTRY, resolveMountSegment } from "./mount-registry.mjs";
2
3
  import { ROUTE_OPTION_KEYS } from "./route-config.mjs";
3
- import { MANIFEST_FILENAME, ROUTE_CONFIG_FILENAME } from "./contract.mjs";
4
4
  import { MANIFEST_VERSION, PluginManifestSchema } from "./manifest-schema.mjs";
5
5
  import "./index.mjs";
6
6
  import path from "node:path";
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_identity = require('../../identity.cjs');
3
+ const require_ui_manifest_contract = require('./contract.cjs');
3
4
  const require_mount_registry = require('./mount-registry.cjs');
4
5
  const require_descriptor = require('./descriptor.cjs');
5
6
  const require_nav = require('./nav.cjs');
6
7
  const require_construct = require('./construct.cjs');
7
- const require_ui_manifest_contract = require('./contract.cjs');
8
8
  const require_manifest_schema = require('./manifest-schema.cjs');
9
9
 
10
10
  exports.App = require_descriptor.App;
@@ -1,9 +1,9 @@
1
1
  import { containerName } from "../../identity.mjs";
2
+ import { CORE_UI_PLUGIN_KEY, MANIFEST_FILENAME, PLUGIN_UI_SHARED_EXPOSES, ROUTE_CONFIG_FILENAME, UI_EXPOSES, UI_REMOTE_ENTRY_FILENAME, UI_REMOTE_SERVER_ENTRY_FILENAME } from "./contract.mjs";
2
3
  import { MOUNTS, MOUNT_ALIASES, MOUNT_REGISTRY, MOUNT_REGISTRY_VERSION, resolveMountSegment } from "./mount-registry.mjs";
3
4
  import { App, Plugin, digestOf, resolveApp } from "./descriptor.mjs";
4
5
  import { compareNavItems } from "./nav.mjs";
5
6
  import { constructTree } from "./construct.mjs";
6
- import { CORE_UI_PLUGIN_KEY, MANIFEST_FILENAME, PLUGIN_UI_SHARED_EXPOSES, ROUTE_CONFIG_FILENAME, UI_EXPOSES, UI_REMOTE_ENTRY_FILENAME, UI_REMOTE_SERVER_ENTRY_FILENAME } from "./contract.mjs";
7
7
  import { ComposePayloadSchema, MANIFEST_VERSION, ManifestSchema, PluginManifestSchema, RouteRecordSchema, SUPPORTED_MANIFEST_VERSION, parsePluginManifest } from "./manifest-schema.mjs";
8
8
 
9
9
  export { App, CORE_UI_PLUGIN_KEY, ComposePayloadSchema, MANIFEST_FILENAME, MANIFEST_VERSION, MOUNTS, MOUNT_ALIASES, MOUNT_REGISTRY, MOUNT_REGISTRY_VERSION, ManifestSchema, PLUGIN_UI_SHARED_EXPOSES, Plugin, PluginManifestSchema, ROUTE_CONFIG_FILENAME, RouteRecordSchema, SUPPORTED_MANIFEST_VERSION, UI_EXPOSES, UI_REMOTE_ENTRY_FILENAME, UI_REMOTE_SERVER_ENTRY_FILENAME, compareNavItems, constructTree, digestOf, parsePluginManifest, resolveApp, resolveMountSegment, containerName as sanitizeContainerName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "every-plugin",
3
- "version": "3.0.0-rc.7",
3
+ "version": "3.0.0-rc.8",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,5 +1,6 @@
1
1
  import type { AnyRoute } from "@tanstack/react-router";
2
2
  import { createRootRoute, createRoute, Outlet, redirect } from "@tanstack/react-router";
3
+ import { CORE_UI_PLUGIN_KEY } from "./contract";
3
4
  import { digestOf } from "./descriptor";
4
5
  import type { PluginManifest, RouteRecord } from "./manifest-schema";
5
6
  import { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from "./mount-registry";
@@ -177,6 +178,11 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
177
178
  resolved.push(await input.resolve(ref));
178
179
  }
179
180
 
181
+ const ordered = [
182
+ ...resolved.filter((plugin) => plugin.key === CORE_UI_PLUGIN_KEY),
183
+ ...resolved.filter((plugin) => plugin.key !== CORE_UI_PLUGIN_KEY),
184
+ ];
185
+
180
186
  const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();
181
187
  for (const plugin of resolved) {
182
188
  const optionsById = new Map<string, RouteOptionsBundle>();
@@ -193,7 +199,8 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
193
199
  }
194
200
 
195
201
  const declarations = new Map<MountId, MountDeclaration>();
196
- for (const plugin of resolved) {
202
+ const mountByLayoutId = new Map<string, MountId>();
203
+ for (const plugin of ordered) {
197
204
  for (const record of plugin.manifest.routes) {
198
205
  if (!record.mount) continue;
199
206
  const mountId = record.mount as MountId;
@@ -205,6 +212,13 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
205
212
  }
206
213
  const existing = declarations.get(mountId);
207
214
  if (existing) {
215
+ if (existing.key === CORE_UI_PLUGIN_KEY && plugin.key !== CORE_UI_PLUGIN_KEY) {
216
+ console.log(
217
+ `[Compose] mount declaration "${existing.key}:${existing.record.id}" (${mountId}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed declaration skipped`,
218
+ );
219
+ mountByLayoutId.set(record.id, mountId);
220
+ continue;
221
+ }
208
222
  throw new Error(
209
223
  `mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`,
210
224
  );
@@ -214,6 +228,7 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
214
228
  record,
215
229
  options: optionsByPlugin.get(plugin.key)!.get(record.id)!,
216
230
  });
231
+ mountByLayoutId.set(record.id, mountId);
217
232
  }
218
233
  }
219
234
 
@@ -244,11 +259,6 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
244
259
  mountRoutes.set(mountId, toAnyRoute(route));
245
260
  }
246
261
 
247
- const mountByLayoutId = new Map<string, MountId>();
248
- for (const [mountId, declaration] of declarations) {
249
- mountByLayoutId.set(declaration.record.id, mountId);
250
- }
251
-
252
262
  const nav: NavItem[] = [];
253
263
  const mountCounts: Record<string, number> = {};
254
264
  const childrenByParent = new Map<AnyRoute, AnyRoute[]>();
@@ -260,7 +270,7 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
260
270
  childrenByParent.set(parent, list);
261
271
  };
262
272
 
263
- for (const plugin of resolved) {
273
+ for (const plugin of ordered) {
264
274
  const optionsById = optionsByPlugin.get(plugin.key)!;
265
275
  const fullPathById = new Map<string, string>();
266
276
  const mountById = new Map<string, MountId>();
@@ -302,17 +312,19 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
302
312
  };
303
313
 
304
314
  const pending = plugin.manifest.routes.filter((record) => !record.mount);
315
+ const shadowedIds = new Set<string>();
305
316
  let progressed = true;
306
317
  while (pending.length > 0 && progressed) {
307
318
  progressed = false;
308
319
  for (let i = 0; i < pending.length; i += 1) {
309
320
  const record = pending[i]!;
310
- if (
311
- record.parentId &&
312
- !byId.has(record.parentId) &&
313
- !mountByLayoutId.has(record.parentId)
314
- ) {
315
- continue;
321
+ const parentId = record.parentId;
322
+ if (parentId) {
323
+ if (shadowedIds.has(parentId)) {
324
+ shadowedIds.add(record.id);
325
+ continue;
326
+ }
327
+ if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;
316
328
  }
317
329
  pending.splice(i, 1);
318
330
  progressed = true;
@@ -325,6 +337,16 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
325
337
  const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();
326
338
  const claimant = siblings.get(routePath);
327
339
  if (claimant) {
340
+ if (
341
+ plugin.key !== CORE_UI_PLUGIN_KEY &&
342
+ claimant.startsWith(`${CORE_UI_PLUGIN_KEY}:`)
343
+ ) {
344
+ console.log(
345
+ `[Compose] route claim "${claimant}" (${routePath}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed route skipped`,
346
+ );
347
+ shadowedIds.add(record.id);
348
+ break;
349
+ }
328
350
  throw new Error(
329
351
  `path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`,
330
352
  );