@real-router/core 0.82.0 → 0.82.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Router-DunclyU6.js +2 -0
- package/dist/cjs/Router-DunclyU6.js.map +1 -0
- package/dist/cjs/Router.d.ts +13 -0
- package/dist/cjs/Router.d.ts.map +1 -1
- package/dist/cjs/Router.js +1 -1
- package/dist/cjs/Router.js.map +1 -1
- package/dist/cjs/api.js +1 -2
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.js +1 -2
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js +2 -0
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js.map +1 -0
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js.map +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/constants.js +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/constants.js.map +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js.map +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js +2 -0
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js.map +1 -0
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js.map +1 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js +2 -0
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js.map +1 -0
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js.map +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js.map +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.d.ts +19 -0
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.d.ts.map +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js +1 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js.map +1 -1
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js +1 -1
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js.map +1 -1
- package/dist/cjs/pipeline/canonicalize.js +1 -1
- package/dist/cjs/pipeline/canonicalize.js.map +1 -1
- package/dist/cjs/pipeline/materialize.js +1 -1
- package/dist/cjs/pipeline/materialize.js.map +1 -1
- package/dist/cjs/pipeline/port.d.ts +8 -0
- package/dist/cjs/pipeline/port.d.ts.map +1 -1
- package/dist/cjs/validation.js +1 -2
- package/dist/esm/Router-yitiDzP1.mjs +2 -0
- package/dist/esm/Router-yitiDzP1.mjs.map +1 -0
- package/dist/esm/Router.d.mts +13 -0
- package/dist/esm/Router.d.mts.map +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/namespaces/RoutesNamespace/routesStore.d.mts +19 -0
- package/dist/esm/namespaces/RoutesNamespace/routesStore.d.mts.map +1 -1
- package/dist/esm/pipeline/port.d.mts +8 -0
- package/dist/esm/pipeline/port.d.mts.map +1 -1
- package/package.json +1 -1
- package/dist/cjs/Router-CCmcEi-S.js +0 -2
- package/dist/cjs/Router-CCmcEi-S.js.map +0 -1
- package/dist/esm/Router-DEpNuwBW.mjs +0 -2
- package/dist/esm/Router-DEpNuwBW.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RoutesNamespace.js","names":["#store","#deps","createRoutesStore","getTransitionPath","constants","canonicalize","#getBuildPathOptions","matchSourceTrailingSlash","materialize","EMPTY_SEARCH","#resolveDynamicForward","#layerChainDefaults","#collectStaticChain","EMPTY_PARAMS","#matchesActiveState","urlParamsOf","queryParamsOf","#matchesActiveStateUnsafe","paramsMatch","mergeDefined","#cachedBuildPathOpts","#cachedOptionsSource"],"sources":["../../../../src/namespaces/RoutesNamespace/RoutesNamespace.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/RoutesNamespace.ts\n\nimport { DEFAULT_ROUTE_NAME } from \"./constants\";\nimport {\n matchSourceTrailingSlash,\n paramsMatch,\n queryParamsOf,\n urlParamsOf,\n} from \"./helpers\";\nimport { createRoutesStore, applyRootPath, resetStore } from \"./routesStore\";\nimport { assertChannelCorrect } from \"../../channels\";\nimport { constants, EMPTY_PARAMS, EMPTY_SEARCH } from \"../../constants\";\nimport { mergeDefined } from \"../../helpers\";\nimport { canonicalize, materialize } from \"../../pipeline\";\nimport { getTransitionPath } from \"../../transitionPath\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RoutesDependencies } from \"./types\";\nimport type {\n CreateMatcherOptions,\n RouteParams,\n RouteTreeState,\n} from \"../../engine\";\nimport type { RouteResolver } from \"../../pipeline\";\nimport type { RouteMetaLookup } from \"../../transitionPath\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n AnyOptions,\n Params,\n RouterLogger,\n SearchParams,\n State,\n Route,\n} from \"../../types\";\nimport type { RouteLifecycleNamespace } from \"../RouteLifecycleNamespace\";\n\nfunction createRouteState<P extends RouteParams = RouteParams>(\n matchResult: {\n readonly segments: readonly { fullName: string }[];\n readonly params: Readonly<Record<string, unknown>>;\n readonly search: Readonly<Record<string, unknown>>;\n readonly meta: Readonly<Record<string, Record<string, \"url\" | \"query\">>>;\n },\n name?: string,\n): RouteTreeState<P> {\n // The matcher yields ≥1 segment for every successful match, each carrying the\n // cumulative route name as `fullName`, so the last element is always present.\n // (Formerly `buildNameFromSegments` with a `?? \"\"` fallback — that branch was\n // unreachable defensive cruft propped up by a white-box test; inlined here.)\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- matcher invariant: a successful match is never empty\n const resolvedName = name ?? matchResult.segments.at(-1)!.fullName;\n\n return {\n name: resolvedName,\n params: matchResult.params as P,\n search: matchResult.search,\n meta: matchResult.meta,\n };\n}\n\ninterface CachedBuildPathOpts {\n readonly trailingSlash?: \"always\" | \"never\" | undefined;\n readonly queryParamsMode?: \"default\" | \"strict\" | \"loose\" | undefined;\n}\n\n/**\n * Independent namespace for managing routes.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle storage and business logic.\n */\nexport class RoutesNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly #store: RoutesStore<Dependencies>;\n #cachedBuildPathOpts: CachedBuildPathOpts | undefined;\n // Source `options` reference captured on the first #getBuildPathOptions call;\n // used only by the dev-build immutability assertion below (#957).\n #cachedOptionsSource: AnyOptions | undefined;\n\n get #deps(): RoutesDependencies<Dependencies> {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return this.#store.depsStore!;\n }\n\n constructor(\n // No `= []` default: the sole caller (Router's ctor) always passes its own\n // already-defaulted `routes` — a namespace-level default would be dead code\n // and a default-before-required-params smell (S1788).\n routes: Route<Dependencies>[],\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n ) {\n this.#store = createRoutesStore(routes, matcherOptions, logger);\n }\n\n /**\n * Creates a predicate function to check if a route node should be updated.\n * Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.\n */\n static shouldUpdateNode(\n nodeName: string,\n getMeta: RouteMetaLookup,\n ): (toState: State, fromState?: State) => boolean {\n return (toState: State, fromState?: State): boolean => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!(toState && typeof toState === \"object\" && \"name\" in toState)) {\n throw new TypeError(\n \"[router.shouldUpdateNode] toState must be valid State object\",\n );\n }\n\n if (toState.transition.reload) {\n return true;\n }\n\n // Root node (DEFAULT_ROUTE_NAME === \"\") has no route-level identity — it\n // represents \"any route\". It must update on every transition so that\n // consumers subscribed via useRouteNode(\"\") (including RouteView at\n // the top of the tree) see every change. This matches the documented\n // contract in adapter docs: `useRouteNode(\"\")` — Root — ALL route\n // changes. See #519 for the missed transitions it was suffering from\n // (users → users.user had intersection=\"users\", leaving the root node\n // un-updated under a flat <Match segment=\"users.user\" exact> pattern).\n if (nodeName === DEFAULT_ROUTE_NAME) {\n return true;\n }\n\n const { intersection, toActivate, toDeactivate } = getTransitionPath(\n toState,\n fromState,\n getMeta,\n );\n\n if (nodeName === intersection) {\n return true;\n }\n\n if (toActivate.includes(nodeName)) {\n return true;\n }\n\n return toDeactivate.includes(nodeName);\n };\n }\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n /**\n * Sets dependencies. Pure assignment — no side effects (#1331).\n *\n * The pending canActivate/canDeactivate factories from initial routes are\n * flushed separately by {@link flushPendingGuards}, called once wiring is\n * complete, so the order of the wire-* calls is unconstrained.\n */\n setDependencies(deps: RoutesDependencies<Dependencies>): void {\n this.#store.depsStore = deps;\n }\n\n /**\n * Registers the pending guard factories collected from initial route\n * definitions. Deferred out of {@link setDependencies} (#1331) so it runs on\n * a fully-built, fully-bound router: a guard factory that calls any\n * `router.*` method sees a ready instance instead of a half-assembled one.\n *\n * Invoked as the last step of the Router constructor. Idempotent after the\n * first call (the pending maps are cleared). Runtime `add()`/`replace()`\n * compile guards in their own PREPARE phase and never populate these maps.\n */\n flushPendingGuards(): void {\n const deps = this.#deps;\n\n for (const [routeName, handler] of this.#store.pendingCanActivate) {\n deps.addActivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanActivate.clear();\n\n for (const [routeName, handler] of this.#store.pendingCanDeactivate) {\n deps.addDeactivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanDeactivate.clear();\n }\n\n /**\n * Sets the lifecycle namespace reference.\n */\n setLifecycleNamespace(\n namespace: RouteLifecycleNamespace<Dependencies> | undefined,\n ): void {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.#store.lifecycleNamespace = namespace!;\n }\n\n // =========================================================================\n // Route tree operations\n // =========================================================================\n\n setRootPath(newRootPath: string): void {\n applyRootPath(this.#store, newRootPath);\n }\n\n hasRoute(name: string): boolean {\n return this.#store.matcher.hasRoute(name);\n }\n\n clearRoutes(): void {\n resetStore(this.#store);\n }\n\n // =========================================================================\n // Path operations\n // =========================================================================\n\n /**\n * Builds a URL path for a route.\n * Note: Argument validation is done by facade (Router.ts) via validateBuildPathArgs.\n *\n * @param route - Route name\n * @param params - Route parameters\n * @param search - Query-channel params (RFC-4 M2 / #1548)\n * @param options - Router options\n */\n buildPath(\n route: string,\n params: Params,\n search?: SearchParams,\n options?: AnyOptions,\n ): string {\n if (route === constants.UNKNOWN_ROUTE) {\n return typeof params.path === \"string\" ? params.path : \"\";\n }\n\n // Stage ③ (each channel's route default under the caller's value — the slot\n // IS the channel, #1549 as `ba0f6b18b` left it) plus the mode gate (#1575),\n // one pass through the pipeline (nav-pipeline Phase 2, step 2-1). The LITERAL\n // form: `buildPath` does not follow `forwardTo` (A.5 — `buildPath(\"src\")`\n // stays `/src`, a deliberate asymmetry with `navigate`), so stage ① is\n // skipped and the seam is never entered.\n // ⚠ The v1 single-bag form is retired here, and skipping the seam is not what\n // retires it: the seam stopped MOVING a mis-channelled key when stage ② was\n // deleted (`ba0f6b18b`) — it refuses one now, on the resolving form. A caller\n // who rides a declared query key in the `params` bag simply keeps it there,\n // and the query string is printed from `canonical.query` alone. Before this\n // step the\n // matcher's `search ?? params` fallback printed it out of the path bag, so\n // `buildPath` disagreed with `navigate` on the same intent — an undeclared\n // key in `loose` (`/t?foo=1` vs `/t`), the `/coll/:id?id` collision\n // (`/items/V?id=V` vs `/items/V`), and a route's arbitrary `defaultParams`\n // (`/s?theme=d` vs `/s`). All three now agree.\n const canonical = canonicalize(this.#deps.port, route, params, search, {\n resolveForward: false,\n });\n\n // Stage ⑤a stays LOCAL to this method rather than going through `buildURL`,\n // and this is structural, not a preference: `buildURL` prints via\n // `port.buildPath`, which IS the interceptable `ctx.buildPath` wrapping this\n // very method (`Router.ts:349-359`) — routing through it would recurse. The\n // interceptor zone therefore stays exactly where it is (#1231:\n // `persistent-params` injects here), one layer above.\n // The route codec sees BOTH channels — `encodeParams({ params, search })` →\n // `{ params, search }` (§4) — so an encoder can shape the query as well as\n // the path.\n if (typeof this.#store.config.encoders[route] === \"function\") {\n const encoded = this.#store.config.encoders[route]({\n // BOTH channels spread, and the symmetry is the point: `canonical.*` is\n // frozen at merge time, so handing a channel through verbatim turns a\n // codec that edits its argument in place — legal before this entry point\n // joined the pipeline, and still legal for `params` — into a silent no-op\n // (sloppy mode) or a `TypeError` (ESM). Copying `params` alone left the\n // two halves of one documented hook behaving differently.\n params: { ...canonical.path },\n search: { ...canonical.query },\n });\n\n return this.#store.matcher.buildPath(\n route,\n encoded.params,\n encoded.search,\n this.#getBuildPathOptions(options),\n );\n }\n\n return this.#store.matcher.buildPath(\n route,\n canonical.path,\n canonical.query,\n this.#getBuildPathOptions(options),\n );\n }\n\n /**\n * Matches a URL path to a route in the tree.\n * Note: Argument validation is done by facade (Router.ts) via validateMatchPathArgs.\n */\n matchPath<P extends Params = Params>(\n path: string,\n options?: AnyOptions,\n ): State<P> | undefined {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Router.ts always passes options\n const opts = options!;\n\n const matchResult = this.#store.matcher.match(path);\n\n if (!matchResult) {\n return undefined;\n }\n\n const routeState = createRouteState(matchResult);\n const { name, params } = routeState;\n // The matcher always carries a search bag (a frozen `{}` when empty) but types\n // its values as `unknown`; narrow it to the query channel once here so the\n // codec / forwardState / rebuild uses it without per-site casts.\n const search = routeState.search as SearchParams;\n\n // Two-channel decode (RFC-4 M2 / #1548, §4): the route codec sees BOTH the\n // path params AND the parsed query — `decodeParams({ params, search })` →\n // `{ params, search }` — restoring v1's reach (v1 ran the whole path+query bag\n // through the decoder). Runs here, inside match, BEFORE any search-schema\n // plugin validation (the v1 order: engine codec → plugin). With no decoder the\n // channels pass through untouched.\n const decoder = this.#store.config.decoders[name];\n let decoded: { params: Params; search: SearchParams };\n\n if (typeof decoder === \"function\") {\n decoded = decoder({ params, search });\n\n // The ONE boundary on this path where a value core is about to build a\n // state from came out of USER code (#1582). The matcher's own output above\n // needs no check — it is router-produced and plain by construction — but a\n // decoder may hand back anything: an array becomes `params {\"0\":\"a\"}`, a\n // string becomes `{\"0\":\"o\",\"1\":\"o\",…}`, a `Map` or a prototyped object\n // becomes `{}`, and every one of those commits silently. Opt-in, like every\n // DX check: `null` in bare core, where the behaviour is unchanged either\n // way. Labelled for THIS entry point — the pre-pipeline call sat inside a\n // `forwardState` dep wrapper and blamed that method for a `matchPath` fault.\n this.#deps\n .getValidator()\n ?.routes.validateStateBuilderArgs(name, decoded.params, \"matchPath\");\n\n // The channel half of the same boundary — always on, unlike the shape\n // check above. A decoder that moves a declared `?key` into the params bag\n // used to be repaired by the seam and shipped as if it had been written\n // correctly; now it is refused. Checked HERE as well as at the seam so the\n // message names the decoder rather than the chain it later flows through —\n // same single assertion, applied where the fault was actually authored.\n assertChannelCorrect(\n \"matchPath\",\n name,\n decoded.params,\n this.getQueryParams(name),\n \"the `params` returned by this route's `decodeParams`\",\n );\n } else {\n decoded = { params, search };\n }\n\n // Stages ① + ③ + the mode gate, one pass through the pipeline (nav-pipeline\n // Phase 2, step 2-2). `canonicalize` reaches the SAME `forwardState` seam\n // this method used to call directly — `port.resolveForward` is\n // `ctx.forwardState` — so a search-schema interceptor still validates the\n // query on the URL→State path here (the `routeSearch` argument is defined,\n // marking this as a re-parse, not a navigate), and the seam still checks the\n // channels on the way through. What the pipeline replaces is the\n // hand-rolled composition that followed: the route's own default split\n // (#1549), the default merge, and the mode gate (#1575) now happen once,\n // inside `canonicalize`, from the same read-model `navigate` uses.\n // ⚠ Stage ② is GONE from this path, as it is from every other: the seam no\n // longer repairs a mis-channelled bag, it refuses one. A `forwardState`\n // interceptor injecting a declared query key into `result.params` used to\n // land in `state.search` here exactly as on `navigate`; both now throw.\n const canonical = canonicalize(\n this.#deps.port,\n name,\n decoded.params,\n decoded.search,\n );\n const routeName = canonical.name;\n\n // The canonical channels, ready for BOTH halves of the state: `canonical.path`\n // is path-only (the seam REFUSED any declared `?key` a plugin injection or a\n // decoder left in the params bag, and stage ③ layered the route's own\n // defaults under it, each slot in its own channel — #1549),\n // `canonical.query` is the full canonical query with the mode gate already\n // applied (#1575). Both are read from ONE object, so the\n // rebuilt `state.path` and the committed `state.search` cannot derive from\n // differently-merged bags (INVARIANTS makeState #6).\n const routeParams = canonical.path;\n const forwardedSearch = canonical.query;\n\n let builtPath = path;\n\n if (opts.rewritePathOnMatch) {\n // Two-channel encode for the URL rebuild (RFC-4 M2 / #1548, §4): path slots\n // from `routeParams` (canonical, path-only), the query string from\n // `forwardedSearch` (the full canonical query) — never the raw matched\n // query — so the rebuilt `state.path` stays in step with `state.search`.\n const encoded =\n typeof this.#store.config.encoders[routeName] === \"function\"\n ? this.#store.config.encoders[routeName]({\n params: routeParams,\n search: forwardedSearch,\n })\n : { params: routeParams, search: forwardedSearch };\n\n const ts = opts.trailingSlash;\n\n try {\n // Explicit two-channel build (RFC-4 M2 / #1548): path slots from\n // `encoded.params`, the query string from `encoded.search`. The channels\n // stay separate, so a `/coll/:id?id` collision keeps its path slot and\n // query twin independent — no single-bag reunification, no\n // search-wins-for-declared-query fixup.\n builtPath = this.#store.matcher.buildPath(\n routeName,\n encoded.params,\n encoded.search,\n {\n trailingSlash: ts === \"never\" || ts === \"always\" ? ts : undefined,\n queryParamsMode: opts.queryParamsMode,\n },\n );\n\n if (ts === \"preserve\") {\n builtPath = matchSourceTrailingSlash(path, builtPath);\n }\n } catch {\n // The match already succeeded (route found, params decoded); only the\n // post-match path rewrite threw — e.g. a custom encoder handed buildPath\n // a query value its codec cannot serialise. Keep the source path\n // un-rewritten rather than discard a valid match (#1157). Opposite of the\n // parse side (#737): there a throw means \"URL not understood\" → unmatched;\n // here the URL WAS matched and only re-canonicalisation failed.\n builtPath = path;\n }\n }\n\n // Stage ⑤b. `materialize` over `makeState` because the intent is ALREADY\n // canonical: `makeState` would re-run the default merge (idempotent, but a\n // wasted pass) and would rebuild the path when none is handed to it, which is\n // exactly the work ⑤a above just did. The generic rides through\n // (`matchPath<P>` → `materialize<P>` → `State<P>`), so a consumer's typed\n // params survive the migration.\n return materialize<P>(canonical, { path: builtPath });\n }\n\n /**\n * Applies `forwardTo` and returns the resolved name with the CHAIN's defaults\n * layered under the caller's channels.\n *\n * Order, per channel: every forwarding HOP's defaults (the earliest hop wins),\n * then the caller's value on top. The TARGET route's own defaults are\n * deliberately NOT part of it — the body says why they cannot be, and the\n * summary used to list them as step 3 while the very next comment denied it.\n */\n forwardState<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n name: string,\n params: P,\n search?: S,\n ): { name: string; params: P; search: S } {\n // TARGET-route defaults are NOT applied here for EITHER channel — they are\n // merged strictly BELOW the user channels at the pipeline's single terminal\n // (`canonicalize`, reached alike by `makeState`, `matchPath` and\n // `buildPath`). Folding a\n // target default into this result would ride ABOVE a user params-twin at the\n // terminal merge, inverting the priority — a\n // `navigate(x, { page: 2 })` on a `?page` route with `defaultSearch{page:1}`\n // would wrongly commit `page=1`. So both channels pass through as the user\n // gave them: `search` stays `resolvedSearch`, `params` stays the raw bag.\n //\n // The ONE default forwardState still merges is the forwardTo CHAIN's own\n // defaults (`#layerChainDefaults` in the forward branches): when `a` forwards\n // to `b`, `a`'s `defaultParams` fill in before the redirect and flow to `b`.\n // This canNOT move to the terminal — `canonicalize` sees only the RESOLVED\n // target `b` and cannot reconstruct source `a`'s defaults. Each of a hop's\n // slots keeps its own channel (`defaultParams` the path, `defaultSearch` the\n // query) whatever the target declares; `ba0f6b18b` retired #1570's routing by\n // the target's declaration, and a hop default naming a key the TARGET\n // declares with `?` is refused at the seam instead. Frozen empty search\n // singleton when absent.\n const resolvedSearch = (search ?? EMPTY_SEARCH) as S;\n\n if (Object.hasOwn(this.#store.config.forwardFnMap, name)) {\n const dynamicForward = this.#store.config.forwardFnMap[name];\n const { target, chain } = this.#resolveDynamicForward(\n name,\n dynamicForward,\n params,\n );\n\n return this.#layerChainDefaults(target, chain, params, resolvedSearch);\n }\n\n const staticForward = this.#store.resolvedForwardMap[name] ?? name;\n\n if (\n staticForward !== name &&\n Object.hasOwn(this.#store.config.forwardFnMap, staticForward)\n ) {\n const targetDynamicForward =\n this.#store.config.forwardFnMap[staticForward];\n const { target, chain } = this.#resolveDynamicForward(\n staticForward,\n targetDynamicForward,\n params,\n );\n\n // The static prefix stops AT `staticForward` (it has no static forward of\n // its own), and the dynamic walk starts THERE — so the two halves\n // concatenate without repeating a hop.\n return this.#layerChainDefaults(\n target,\n [...this.#collectStaticChain(name), ...chain],\n params,\n resolvedSearch,\n );\n }\n\n if (staticForward !== name) {\n return this.#layerChainDefaults(\n staticForward,\n this.#collectStaticChain(name),\n params,\n resolvedSearch,\n );\n }\n\n return {\n name,\n params,\n search: resolvedSearch,\n };\n }\n\n /**\n * Builds a RouteTreeState from an already-resolved route name and params.\n *\n * ⚠ Its ONE caller — `getPluginApi().buildNavigationState` — uses it as an\n * EXISTENCE probe and discards the object (`if (!ctx.buildStateResolved(…))\n * return;`). Since Phase 2 that entry point materialises its state from the\n * `Canonical`, so what this returns is built and dropped; what the caller\n * needs is the `undefined` arm, placed BEFORE `buildURL` because the matcher\n * throws on an unknown route while that entry point must answer `undefined`.\n * Whether this should therefore collapse into a `hasRoute`-shaped predicate is\n * an open question, not a settled design — the same dead-surface shape\n * coverage surfaced for `skipFreeze` in Phase 4.\n */\n buildStateResolved(\n resolvedName: string,\n resolvedParams: Params,\n ): RouteTreeState | undefined {\n const segments = this.#store.matcher.getSegmentsByName(resolvedName);\n\n if (!segments) {\n return undefined;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const meta = this.#store.matcher.getMetaByName(resolvedName)!;\n\n return createRouteState(\n // `resolvedParams` is path-only by the PRODUCER's contract, not by a repair\n // upstream: the seam refuses a declared `?key` in the params bag rather\n // than moving one out (stage ② is gone — `ba0f6b18b`). The `search: {}`\n // placeholder threads nowhere; the caller discards this object.\n { segments, params: resolvedParams, search: {}, meta },\n resolvedName,\n );\n }\n\n // =========================================================================\n // Query operations\n // =========================================================================\n\n /**\n * Checks if a route is currently active.\n *\n * Two arms, `literal || destination` (#1573). The literal arm is the whole\n * predicate below, unchanged. The destination arm repeats THAT SAME predicate\n * on the full output of stage ① — the resolved terminal name together with\n * the forwarding chain's defaults, each hop's slot in its own channel — so a\n * `<Link to=\"alias\">` reads active on the page it actually navigates to.\n *\n * It is a FALLBACK, never a pre-resolution: resolving before comparing would\n * send a section link (`users` forwarding to `users.list`) to the leaf and\n * darken it while a sibling descendant (`users.profile`) is active.\n *\n * It repeats the predicate on ①'s OUTPUT rather than substituting the name,\n * because the chain's `defaultParams` live on the forwarding SOURCE and are\n * layered by `forwardState` (#1566/#1570) — never by the forward map — and a\n * dynamic `forwardTo` is not in that map at all. Name substitution therefore\n * fixes neither, and it also carries no `search`, which is the channel a hop's\n * own `defaultSearch` lands in.\n */\n isActiveRoute(\n name: string,\n // Singletons, not fresh literals (#1589). `= {}` minted TWO throwaway objects\n // on every call — i.e. on every `<Link>` on every re-render across six\n // adapters — and the `search` one was worse than an allocation: being a fresh\n // `{}` it is neither `undefined` nor `EMPTY_SEARCH`, so it defeated\n // `canonicalize`'s own empty-query fast path and this predicate ran the full\n // merge + mode-gate tail every time. Measured: the flame graph showed\n // `withholdFilledSlots` and the drop-sink getter live under a route with\n // nothing to withhold and nothing to drop.\n params: Params = EMPTY_PARAMS,\n searchArg: SearchParams = EMPTY_SEARCH,\n strictEquality = false,\n ignoreQueryParams = true,\n ): boolean {\n if (\n this.#matchesActiveState(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n )\n ) {\n return true;\n }\n\n // O(1) gate: only a route that actually forwards can have a second arm.\n // Every `<Link>` in six adapters runs this predicate on every render, so a\n // non-forwarding route must not pay for the arm at all.\n if (\n !Object.hasOwn(this.#store.config.forwardMap, name) &&\n !Object.hasOwn(this.#store.config.forwardFnMap, name)\n ) {\n return false;\n }\n\n let forwarded;\n\n try {\n // The NAMESPACE primitive, not the interceptable seam: a predicate on the\n // render path must not run the plugin interceptor chain once per `<Link>`.\n forwarded = this.forwardState(name, params, searchArg);\n } catch (error) {\n // A dynamic `forwardTo` is user code and may throw. A predicate answers,\n // it never throws from inside a render — same policy as `canNavigateTo`\n // on a throwing guard (#959): honest `false` plus an operational log.\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Dynamic forwardTo of route \"${name}\" threw while resolving the active-link destination; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n\n // No `forwarded.name === name` guard: the O(1) gate above already proved the\n // route forwards, and a self-returning dynamic callback throws on the cycle\n // check before `forwardState` can hand one back — so the terminal name is\n // always a different route (measured; the branch was unreachable).\n return this.#matchesActiveState(\n forwarded.name,\n forwarded.params,\n forwarded.search,\n strictEquality,\n ignoreQueryParams,\n );\n }\n\n getMetaForState(\n name: string,\n ): Record<string, Record<string, \"url\" | \"query\">> | undefined {\n return this.#store.matcher.hasRoute(name)\n ? this.#store.matcher.getMetaByName(name)\n : undefined;\n }\n\n getUrlParams(name: string): string[] {\n return urlParamsOf(this.#store, name);\n }\n\n /**\n * Declared query param names of a route (`?a&b` across its segments,\n * ancestors included) that are NOT also path params — the query-channel twin\n * of {@link getUrlParams}. THE registry (#1556): the always-on channel guard,\n * the literal form's default withholding and the mode gate all classify\n * through it, and the URL build prints from it. (It powered the `defaultParams`\n * channel ROUTING too, until `ba0f6b18b` retired that — the slot is the channel\n * now, so nothing is routed anywhere.) A colliding name (`/items/:id?id` —\n * legal under M2, the channels\n * coexist) is path-owned for routing purposes: excluding it here keeps the\n * path slot's value in `state.params` and the rebuild's #843 precedence\n * intact. Same cache lifecycle: cleared on every matcher rebuild.\n *\n * Reads the matcher's `declaredQueryParams` — the SAME registry the\n * query-string build uses — rather than walking `matchSegments` (#1556). The\n * segment walk missed the ROOT node's `?`-declarations (`setRootPath(\"?a&b\")`,\n * how persistent-params declares its keys), because the root is captured\n * separately at `registerTree` and never appears in `matchSegments`. That\n * made a root-declared key print as query but classify as a path param: it\n * landed in `state.params`, vanished from `state.path` on the intent side,\n * and no `isActiveRoute` spelling matched a link to the active page. One\n * registry classifies and prints, so the two cannot drift again.\n */\n getQueryParams(name: string): string[] {\n return queryParamsOf(this.#store, name);\n }\n\n getStore(): RoutesStore<Dependencies> {\n return this.#store;\n }\n\n /**\n * The pipeline's read-model, for entry points that live on the FACADE rather\n * than in a namespace (`canNavigateTo`). The port is created during wiring,\n * after `registerInternals` has already run, so the facade cannot hold it —\n * it reaches it through the namespace that does, exactly as the resolver\n * itself reaches the store through {@link getStore}.\n */\n getPort(): RouteResolver {\n return this.#deps.port;\n }\n\n /**\n * The literal arm of {@link isActiveRoute} — unchanged by #1573.\n *\n * Safe boundary (#1577): both branches below READ the caller's bags — since\n * nav-pipeline Phase 2 step 2-5 through `canonicalize` in its literal form,\n * which walks `params` key by key, and then again in the descendant branch's\n * channel-by-channel `paramsMatch` — so an accessor-backed key, a `Proxy` or a\n * framework's reactive object throws HERE, on the render path. (It used to be\n * the exact branch's own channel split and the descendant branch's spread into\n * one bag; step 2-5 removed both, the exposure is unchanged.) The predicate's\n * stated policy is that it answers and never throws from inside a render (see\n * the `forwardState` wrap below), and #1573 implemented that for the\n * destination arm only. One boundary around the whole walk rather than a\n * `try` per read —\n * the same shape `isParams` took for the same class of hostile input (#1052).\n */\n #matchesActiveState(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n try {\n return this.#matchesActiveStateUnsafe(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n );\n } catch (error) {\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Reading the arguments for route \"${name}\" threw while resolving the active-link state; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n }\n\n #matchesActiveStateUnsafe(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n // Note: empty string check is handled by Router.ts facade\n const activeState = this.#deps.getState();\n\n if (!activeState) {\n return false;\n }\n\n const activeName = activeState.name;\n\n // Fast path: check if routes are related before expensive operations\n if (\n activeName !== name &&\n !activeName.startsWith(`${name}.`) &&\n !name.startsWith(`${activeName}.`)\n ) {\n return false;\n }\n\n // The comparison target, built by the SAME pipeline every producer uses\n // (nav-pipeline Phase 2, step 2-5) — in the LITERAL form: this predicate asks\n // about the route it was NAMED. `forwardTo` is resolved by the caller's\n // second arm (`isActiveRoute`), through the NON-interceptable namespace\n // primitive, and `{ resolveForward: false }` is what keeps it that way here:\n // the literal form never touches the port, so a plugin's interceptor chain\n // does not run once per `<Link>` per render.\n //\n // ⚠ Stage ② is gone from this point, and by now from everywhere: step 2-5\n // dropped this method's OWN channel split, and the seam that the other\n // points reach through the port stopped repairing bags too. The observable\n // change here is unchanged by that: a declared query key handed in the\n // `params` bag is no longer moved to the query channel before comparison,\n // so a v1 single-bag call stops matching. Channel-correctness is the\n // caller's contract, exactly as for `navigate` (which throws on that shape)\n // and `buildPath` (which prints without it).\n const canonical = canonicalize(this.#deps.port, name, params, searchArg, {\n resolveForward: false,\n });\n\n // Exact match case. Path \"\" skips the URL build — `areStatesEqual` compares\n // channels and never reads the URL, which is also why `materialize` needs no\n // port argument here (the fork milestone 1 left open, settled in step 2-3).\n if (strictEquality || activeName === name) {\n // `skipFreeze` (#1589): the state exists for the length of one comparison.\n // `areStatesEqual` reads `.name` / `.params` / `.search` and nothing else,\n // so freezing it — and attaching the `transition` the freeze implies —\n // buys a guarantee no one can observe. The CHANNELS are still frozen;\n // that happens in `canonicalize`, and it is the part that matters\n // (canonicalize invariant #4). Measured at 926 µs, ~5 % of this benchmark.\n return this.#deps.areStatesEqual(\n materialize(canonical, { path: \"\", skipFreeze: true }),\n activeState,\n ignoreQueryParams,\n );\n }\n\n // The fast path above lets through three relations: exact (handled in\n // the previous block), `activeName` descendant of `name`, and `name`\n // descendant of `activeName`. Only the first two count as \"active\" —\n // a link pointing DEEPER than the current state is a navigation option,\n // not an active state. Reject the descendant-of-active case explicitly.\n if (!activeName.startsWith(`${name}.`)) {\n return false;\n }\n\n // Hierarchical check: activeState is a descendant of target (name). Compared\n // CHANNEL BY CHANNEL (step 2-5) instead of over one recombined bag: the\n // canonical target already carries the route's defaults, each merged under\n // the caller's value in the channel its own SLOT names (#1549), so the\n // separate `paramsMatchExcluding` passes over `defaultParams` /\n // `defaultSearch` are no longer needed — a default that survived into\n // `canonical` is exactly a default the caller did not override.\n if (!paramsMatch(canonical.path, activeState.params)) {\n return false;\n }\n\n // The query channel obeys `ignoreQueryParams`, the same flag the exact arm\n // hands to `areStatesEqual`. The recombined-bag form could not: it folded\n // query into the path bag before matching, so an ancestor link compared its\n // query even when the caller asked to ignore it — the two arms disagreed\n // about the flag.\n if (\n !ignoreQueryParams &&\n !paramsMatch(canonical.query as Params, activeState.search as Params)\n ) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Every node on a `forwardTo` chain that FORWARDS, in walk order — the\n * terminal is excluded (its own defaults belong to the state builder, #1549).\n * `forwardMap` is proven acyclic at registration (`refreshForwardMap` runs\n * `resolveForwardChain`, which throws on a cycle), so the walk terminates.\n */\n #collectStaticChain(name: string): string[] {\n const chain: string[] = [];\n let current = name;\n\n while (Object.hasOwn(this.#store.config.forwardMap, current)) {\n chain.push(current);\n current = this.#store.config.forwardMap[current];\n }\n\n return chain;\n }\n\n /**\n * Layers the defaults of every forwarding hop UNDER the caller's channels.\n * Folding in walk order makes an EARLIER hop win over a later one (each merge\n * puts the next hop's defaults below what is already accumulated), and the\n * caller wins over all of them. Only the entered route was consulted before,\n * so a default declared on an intermediate hop never reached the target and\n * a required slot was left empty (#1566).\n *\n * Each slot IS its channel: a hop's `defaultParams` is the path channel and\n * its `defaultSearch` the query channel, whatever the resolved target\n * declares. A hop's `defaultParams` naming a key the TARGET declares with `?`\n * is refused at the `forwardState` seam, where the target is finally known.\n *\n * (#1570 routed this fold by the target's declaration for one release;\n * `ba0f6b18b` retired that with the rest of stage ②. The body below records\n * why — it is not repeated here, and the retired rule is not restated in the\n * present tense above it.)\n */\n #layerChainDefaults<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n target: string,\n chain: readonly string[],\n params: P,\n search: S,\n ): { name: string; params: P; search: S } {\n let hopDefaults: Params | undefined;\n let hopSearchDefaults: SearchParams | undefined;\n\n for (const routeName of chain) {\n // `undefined` is absence on both sides (#1550 / #1551): a source default\n // carrying `undefined` must not ride out of `forwardState` as an own key.\n hopDefaults = mergeDefined(\n this.#store.config.defaultParams[routeName] as Params | undefined,\n hopDefaults,\n );\n // A hop's `defaultSearch` was read by NOBODY until #1549: this fold only\n // took `defaultParams`, so the slot was silently inert on a forwarding\n // node while working on a terminal — the mirror of the terminal's own\n // defect, where `defaultParams` worked on a hop and broke on the route\n // itself. Each slot has to mean the same thing in both positions.\n hopSearchDefaults = mergeDefined(\n this.#store.config.defaultSearch[routeName] as SearchParams | undefined,\n hopSearchDefaults,\n );\n }\n\n // Each slot IS its channel — no split, here or anywhere else. A hop's\n // `defaultParams` is the path channel and its `defaultSearch` the query\n // channel, whatever the resolved target declares.\n //\n // This used to route the fold by the TARGET's declaration, on the argument\n // that a hop \"can only spell a default in `defaultParams`\". That argument\n // was already false — the fold reads `defaultSearch` two lines above — and\n // the routing was doing real damage: a hop author could not tell which\n // channel their own config would end up in without reading a target that a\n // `forwardTo` CALLBACK may not even determine until navigation time. Now\n // they can: the slot they wrote is the channel they get.\n //\n // A hop whose `defaultParams` names a key the TARGET declares with `?` is\n // still caught — not here, but at the `forwardState` seam, where the target\n // is finally known and the check can name both routes. Registration cannot\n // see it (a dynamic `forwardTo` has no target yet), which is exactly why\n // that check lives at resolution rather than being guessed here.\n // No cross-channel withholding any more, and its removal is the point.\n // #1570 needed it because the split put a caller's params-twin and the\n // query half of the same default in DIFFERENT bags, where no merge ranks\n // them — so the default had to be withheld by hand or it won. Nothing is\n // split now: a key in the caller's `params` and a key in the chain's\n // `search` are different channels holding different names, because a caller\n // who puts a declared query name in `params` is refused outright. Each\n // channel merges against its own default and nothing has to look sideways.\n\n return {\n name: target,\n // The caller's params stay ABOVE the path half, and their `undefined`\n // keys are stripped exactly as before — this merge runs whether or not\n // the chain contributed anything.\n params: mergeDefined(hopDefaults as P | undefined, params),\n // `mergeDefined`, not a spread: an explicit `undefined` from the caller is\n // ABSENCE (#1550 / #1551), so it must not delete the hop default. A spread\n // copied the `undefined` key over and killed it — asymmetric with a\n // route-level `defaultSearch`, where the rule already held.\n search: mergeDefined(hopSearchDefaults, search) as S,\n };\n }\n\n #getBuildPathOptions(options?: AnyOptions): CachedBuildPathOpts {\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return rebuilds the identical buildPath options (deterministic) and re-caches them. (ConditionalExpression stays live: `→false` always rebuilds but a real consumer test pins the cached identity.)\n if (this.#cachedBuildPathOpts) {\n /* v8 ignore next 5 -- @preserve: dev assertion guarding a future caller that passes per-call varying options; the sole caller (Router.buildPath, always via this.#options.get()) passes the same immutable, deep-frozen per-instance options, so this branch is unreachable through the public API by construction (#957) */\n if (options !== this.#cachedOptionsSource) {\n this.#deps.logger.warn(\n \"router.buildPath\",\n \"`options` differs from the cached source reference; router options are immutable per router instance, so the first-cached buildPath options are reused (#957).\",\n );\n }\n\n return this.#cachedBuildPathOpts;\n }\n\n this.#cachedOptionsSource = options;\n\n const ts = options?.trailingSlash;\n\n this.#cachedBuildPathOpts = Object.freeze({\n trailingSlash: ts === \"never\" || ts === \"always\" ? ts : undefined,\n queryParamsMode: options?.queryParamsMode,\n });\n\n return this.#cachedBuildPathOpts;\n }\n\n #resolveDynamicForward(\n startName: string,\n startFn: ForwardToCallback<Dependencies>,\n params: Params,\n ): { target: string; chain: string[] } {\n const visited = new Set<string>([startName]);\n // Every node that forwards, in walk order — `startName` does by definition.\n // The terminal is never pushed, so the caller can layer hop defaults without\n // pulling in the target's (#1566/#1549).\n const chain: string[] = [startName];\n\n let current = startFn(this.#deps.getDependency, params);\n let depth = 0;\n const MAX_DEPTH = 100;\n\n if (typeof current !== \"string\") {\n throw new TypeError(\n `forwardTo callback must return a string, got ${typeof current}`,\n );\n }\n\n while (depth < MAX_DEPTH) {\n if (this.#store.matcher.getSegmentsByName(current) === undefined) {\n throw new Error(`Route \"${current}\" does not exist`);\n }\n\n if (visited.has(current)) {\n const cycle = [...visited, current].join(\" → \");\n\n throw new Error(`Circular forwardTo: ${cycle}`);\n }\n\n visited.add(current);\n\n if (Object.hasOwn(this.#store.config.forwardFnMap, current)) {\n const fn = this.#store.config.forwardFnMap[\n current\n ] as ForwardToCallback<Dependencies>;\n\n chain.push(current);\n current = fn(this.#deps.getDependency, params);\n\n depth++;\n continue;\n }\n\n const staticForward = this.#store.config.forwardMap[current];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (staticForward !== undefined) {\n chain.push(current);\n current = staticForward;\n depth++;\n continue;\n }\n\n return { target: current, chain };\n }\n\n throw new Error(`forwardTo exceeds maximum depth of ${MAX_DEPTH}`);\n }\n}\n"],"mappings":"6RAqCA,SAAS,EACP,EAMA,EACmB,CAQnB,MAAO,CACL,KAHmB,GAAQ,EAAY,SAAS,GAAG,EAAE,CAAC,CAAE,SAIxD,OAAQ,EAAY,OACpB,OAAQ,EAAY,OACpB,KAAM,EAAY,IACpB,CACF,CAaA,IAAa,EAAb,KAEE,CACA,GACA,GAGA,GAEA,GAAIC,IAA0C,CAE5C,OAAO,KAAKD,GAAO,SACrB,CAEA,YAIE,EACA,EACA,EACA,CACA,KAAKA,GAASE,EAAAA,kBAAkB,EAAQ,EAAgB,CAAM,CAChE,CAMA,OAAO,iBACL,EACA,EACgD,CAChD,OAAQ,EAAgB,IAA+B,CAErD,GAAI,EAAE,GAAW,OAAO,GAAY,UAAY,SAAU,GACxD,MAAU,UACR,8DACF,EAeF,GAZI,EAAQ,WAAW,QAYnB,IAAA,GACF,MAAO,GAGT,GAAM,CAAE,eAAc,aAAY,gBAAiBC,EAAAA,kBACjD,EACA,EACA,CACF,EAUA,OARI,IAAa,GAIb,EAAW,SAAS,CAAQ,EACvB,GAGF,EAAa,SAAS,CAAQ,CACvC,CACF,CAaA,gBAAgB,EAA8C,CAC5D,KAAKH,GAAO,UAAY,CAC1B,CAYA,oBAA2B,CACzB,IAAM,EAAO,KAAKC,GAElB,IAAK,GAAM,CAAC,EAAW,KAAY,KAAKD,GAAO,mBAC7C,EAAK,iBAAiB,EAAW,CAAO,EAG1C,KAAKA,GAAO,mBAAmB,MAAM,EAErC,IAAK,GAAM,CAAC,EAAW,KAAY,KAAKA,GAAO,qBAC7C,EAAK,mBAAmB,EAAW,CAAO,EAG5C,KAAKA,GAAO,qBAAqB,MAAM,CACzC,CAKA,sBACE,EACM,CAEN,KAAKA,GAAO,mBAAqB,CACnC,CAMA,YAAY,EAA2B,CACrC,EAAA,cAAc,KAAKA,GAAQ,CAAW,CACxC,CAEA,SAAS,EAAuB,CAC9B,OAAO,KAAKA,GAAO,QAAQ,SAAS,CAAI,CAC1C,CAEA,aAAoB,CAClB,EAAA,WAAW,KAAKA,EAAM,CACxB,CAeA,UACE,EACA,EACA,EACA,EACQ,CACR,GAAI,IAAUI,EAAAA,UAAU,cACtB,OAAO,OAAO,EAAO,MAAS,SAAW,EAAO,KAAO,GAoBzD,IAAM,EAAYC,EAAAA,aAAa,KAAKJ,GAAM,KAAM,EAAO,EAAQ,EAAQ,CACrE,eAAgB,EAClB,CAAC,EAWD,GAAI,OAAO,KAAKD,GAAO,OAAO,SAAS,IAAW,WAAY,CAC5D,IAAM,EAAU,KAAKA,GAAO,OAAO,SAAS,EAAM,CAAC,CAOjD,OAAQ,CAAE,GAAG,EAAU,IAAK,EAC5B,OAAQ,CAAE,GAAG,EAAU,KAAM,CAC/B,CAAC,EAED,OAAO,KAAKA,GAAO,QAAQ,UACzB,EACA,EAAQ,OACR,EAAQ,OACR,KAAKM,GAAqB,CAAO,CACnC,CACF,CAEA,OAAO,KAAKN,GAAO,QAAQ,UACzB,EACA,EAAU,KACV,EAAU,MACV,KAAKM,GAAqB,CAAO,CACnC,CACF,CAMA,UACE,EACA,EACsB,CAEtB,IAAM,EAAO,EAEP,EAAc,KAAKN,GAAO,QAAQ,MAAM,CAAI,EAElD,GAAI,CAAC,EACH,OAGF,IAAM,EAAa,EAAiB,CAAW,EACzC,CAAE,OAAM,UAAW,EAInB,EAAS,EAAW,OAQpB,EAAU,KAAKA,GAAO,OAAO,SAAS,GACxC,EAEA,OAAO,GAAY,YACrB,EAAU,EAAQ,CAAE,SAAQ,QAAO,CAAC,EAWpC,KAAKC,GACF,aAAa,CAAC,EACb,OAAO,yBAAyB,EAAM,EAAQ,OAAQ,WAAW,EAQrE,EAAA,qBACE,YACA,EACA,EAAQ,OACR,KAAK,eAAe,CAAI,EACxB,sDACF,GAEA,EAAU,CAAE,SAAQ,QAAO,EAiB7B,IAAM,EAAYI,EAAAA,aAChB,KAAKJ,GAAM,KACX,EACA,EAAQ,OACR,EAAQ,MACV,EACM,EAAY,EAAU,KAUtB,EAAc,EAAU,KACxB,EAAkB,EAAU,MAE9B,EAAY,EAEhB,GAAI,EAAK,mBAAoB,CAK3B,IAAM,EACJ,OAAO,KAAKD,GAAO,OAAO,SAAS,IAAe,WAC9C,KAAKA,GAAO,OAAO,SAAS,EAAU,CAAC,CACrC,OAAQ,EACR,OAAQ,CACV,CAAC,EACD,CAAE,OAAQ,EAAa,OAAQ,CAAgB,EAE/C,EAAK,EAAK,cAEhB,GAAI,CAMF,EAAY,KAAKA,GAAO,QAAQ,UAC9B,EACA,EAAQ,OACR,EAAQ,OACR,CACE,cAAe,IAAO,SAAW,IAAO,SAAW,EAAK,IAAA,GACxD,gBAAiB,EAAK,eACxB,CACF,EAEI,IAAO,aACT,EAAYO,EAAAA,yBAAyB,EAAM,CAAS,EAExD,MAAQ,CAON,EAAY,CACd,CACF,CAQA,OAAOC,EAAAA,YAAe,EAAW,CAAE,KAAM,CAAU,CAAC,CACtD,CAWA,aAIE,EACA,EACA,EACwC,CAqBxC,IAAM,EAAkB,GAAUC,EAAAA,aAElC,GAAI,OAAO,OAAO,KAAKT,GAAO,OAAO,aAAc,CAAI,EAAG,CACxD,IAAM,EAAiB,KAAKA,GAAO,OAAO,aAAa,GACjD,CAAE,SAAQ,SAAU,KAAKU,GAC7B,EACA,EACA,CACF,EAEA,OAAO,KAAKC,GAAoB,EAAQ,EAAO,EAAQ,CAAc,CACvE,CAEA,IAAM,EAAgB,KAAKX,GAAO,mBAAmB,IAAS,EAE9D,GACE,IAAkB,GAClB,OAAO,OAAO,KAAKA,GAAO,OAAO,aAAc,CAAa,EAC5D,CACA,IAAM,EACJ,KAAKA,GAAO,OAAO,aAAa,GAC5B,CAAE,SAAQ,SAAU,KAAKU,GAC7B,EACA,EACA,CACF,EAKA,OAAO,KAAKC,GACV,EACA,CAAC,GAAG,KAAKC,GAAoB,CAAI,EAAG,GAAG,CAAK,EAC5C,EACA,CACF,CACF,CAWA,OATI,IAAkB,EASf,CACL,OACA,SACA,OAAQ,CACV,EAZS,KAAKD,GACV,EACA,KAAKC,GAAoB,CAAI,EAC7B,EACA,CACF,CAQJ,CAeA,mBACE,EACA,EAC4B,CAC5B,IAAM,EAAW,KAAKZ,GAAO,QAAQ,kBAAkB,CAAY,EAE9D,KAOL,OAAO,EAKL,CAAE,WAAU,OAAQ,EAAgB,OAAQ,CAAC,EAAG,KAPrC,KAAKA,GAAO,QAAQ,cAAc,CAOM,CAAE,EACrD,CACF,CACF,CA0BA,cACE,EASA,EAAiBa,EAAAA,aACjB,EAA0BJ,EAAAA,aAC1B,EAAiB,GACjB,EAAoB,GACX,CACT,GACE,KAAKK,GACH,EACA,EACA,EACA,EACA,CACF,EAEA,MAAO,GAMT,GACE,CAAC,OAAO,OAAO,KAAKd,GAAO,OAAO,WAAY,CAAI,GAClD,CAAC,OAAO,OAAO,KAAKA,GAAO,OAAO,aAAc,CAAI,EAEpD,MAAO,GAGT,IAAI,EAEJ,GAAI,CAGF,EAAY,KAAK,aAAa,EAAM,EAAQ,CAAS,CACvD,OAAS,EAAO,CAUd,OANA,KAAKC,GAAM,OAAO,KAChB,uBACA,+BAA+B,EAAK,qFACpC,CACF,EAEO,EACT,CAMA,OAAO,KAAKa,GACV,EAAU,KACV,EAAU,OACV,EAAU,OACV,EACA,CACF,CACF,CAEA,gBACE,EAC6D,CAC7D,OAAO,KAAKd,GAAO,QAAQ,SAAS,CAAI,EACpC,KAAKA,GAAO,QAAQ,cAAc,CAAI,EACtC,IAAA,EACN,CAEA,aAAa,EAAwB,CACnC,OAAOe,EAAAA,YAAY,KAAKf,GAAQ,CAAI,CACtC,CAyBA,eAAe,EAAwB,CACrC,OAAOgB,EAAAA,cAAc,KAAKhB,GAAQ,CAAI,CACxC,CAEA,UAAsC,CACpC,OAAO,KAAKA,EACd,CASA,SAAyB,CACvB,OAAO,KAAKC,GAAM,IACpB,CAkBA,GACE,EACA,EACA,EACA,EACA,EACS,CACT,GAAI,CACF,OAAO,KAAKgB,GACV,EACA,EACA,EACA,EACA,CACF,CACF,OAAS,EAAO,CAOd,OANA,KAAKhB,GAAM,OAAO,KAChB,uBACA,oCAAoC,EAAK,+EACzC,CACF,EAEO,EACT,CACF,CAEA,GACE,EACA,EACA,EACA,EACA,EACS,CAET,IAAM,EAAc,KAAKA,GAAM,SAAS,EAExC,GAAI,CAAC,EACH,MAAO,GAGT,IAAM,EAAa,EAAY,KAG/B,GACE,IAAe,GACf,CAAC,EAAW,WAAW,GAAG,EAAK,EAAE,GACjC,CAAC,EAAK,WAAW,GAAG,EAAW,EAAE,EAEjC,MAAO,GAmBT,IAAM,EAAYI,EAAAA,aAAa,KAAKJ,GAAM,KAAM,EAAM,EAAQ,EAAW,CACvE,eAAgB,EAClB,CAAC,EAmDD,OA9CI,GAAkB,IAAe,EAO5B,KAAKA,GAAM,eAChBO,EAAAA,YAAY,EAAW,CAAE,KAAM,GAAI,WAAY,EAAK,CAAC,EACrD,EACA,CACF,EA4BF,EApBI,CAAC,EAAW,WAAW,GAAG,EAAK,EAAE,GAWjC,CAACU,EAAAA,YAAY,EAAU,KAAM,EAAY,MAAM,GAUjD,CAAC,GACD,CAACA,EAAAA,YAAY,EAAU,MAAiB,EAAY,MAAgB,EAMxE,CAQA,GAAoB,EAAwB,CAC1C,IAAM,EAAkB,CAAC,EACrB,EAAU,EAEd,KAAO,OAAO,OAAO,KAAKlB,GAAO,OAAO,WAAY,CAAO,GACzD,EAAM,KAAK,CAAO,EAClB,EAAU,KAAKA,GAAO,OAAO,WAAW,GAG1C,OAAO,CACT,CAoBA,GAIE,EACA,EACA,EACA,EACwC,CACxC,IAAI,EACA,EAEJ,IAAK,IAAM,KAAa,EAGtB,EAAcmB,EAAAA,aACZ,KAAKnB,GAAO,OAAO,cAAc,GACjC,CACF,EAMA,EAAoBmB,EAAAA,aAClB,KAAKnB,GAAO,OAAO,cAAc,GACjC,CACF,EA6BF,MAAO,CACL,KAAM,EAIN,OAAQmB,EAAAA,aAAa,EAA8B,CAAM,EAKzD,OAAQA,EAAAA,aAAa,EAAmB,CAAM,CAChD,CACF,CAEA,GAAqB,EAA2C,CAE9D,GAAI,KAAKC,GASP,OAPI,IAAY,KAAKC,IACnB,KAAKpB,GAAM,OAAO,KAChB,mBACA,gKACF,EAGK,KAAKmB,GAGd,KAAKC,GAAuB,EAE5B,IAAM,EAAK,GAAS,cAOpB,MALA,MAAKD,GAAuB,OAAO,OAAO,CACxC,cAAe,IAAO,SAAW,IAAO,SAAW,EAAK,IAAA,GACxD,gBAAiB,GAAS,eAC5B,CAAC,EAEM,KAAKA,EACd,CAEA,GACE,EACA,EACA,EACqC,CACrC,IAAM,EAAU,IAAI,IAAY,CAAC,CAAS,CAAC,EAIrC,EAAkB,CAAC,CAAS,EAE9B,EAAU,EAAQ,KAAKnB,GAAM,cAAe,CAAM,EAClD,EAAQ,EAGZ,GAAI,OAAO,GAAY,SACrB,MAAU,UACR,gDAAgD,OAAO,GACzD,EAGF,KAAO,EAAQ,KAAW,CACxB,GAAI,KAAKD,GAAO,QAAQ,kBAAkB,CAAO,IAAM,IAAA,GACrD,MAAU,MAAM,UAAU,EAAQ,iBAAiB,EAGrD,GAAI,EAAQ,IAAI,CAAO,EAAG,CACxB,IAAM,EAAQ,CAAC,GAAG,EAAS,CAAO,CAAC,CAAC,KAAK,KAAK,EAE9C,MAAU,MAAM,uBAAuB,GAAO,CAChD,CAIA,GAFA,EAAQ,IAAI,CAAO,EAEf,OAAO,OAAO,KAAKA,GAAO,OAAO,aAAc,CAAO,EAAG,CAC3D,IAAM,EAAK,KAAKA,GAAO,OAAO,aAC5B,GAGF,EAAM,KAAK,CAAO,EAClB,EAAU,EAAG,KAAKC,GAAM,cAAe,CAAM,EAE7C,IACA,QACF,CAEA,IAAM,EAAgB,KAAKD,GAAO,OAAO,WAAW,GAGpD,GAAI,IAAkB,IAAA,GAAW,CAC/B,EAAM,KAAK,CAAO,EAClB,EAAU,EACV,IACA,QACF,CAEA,MAAO,CAAE,OAAQ,EAAS,OAAM,CAClC,CAEA,MAAU,MAAM,wCAAiD,CACnE,CACF"}
|
|
1
|
+
{"version":3,"file":"RoutesNamespace.js","names":["#store","#deps","createRoutesStore","getTransitionPath","constants","canonicalize","#getBuildPathOptions","matchSourceTrailingSlash","materialize","EMPTY_SEARCH","#resolveDynamicForward","#layerChainDefaults","#collectStaticChain","EMPTY_PARAMS","#matchesActiveState","urlParamsOf","queryParamsOf","#matchesActiveStateUnsafe","paramsMatch","mergeDefined","#cachedBuildPathOpts","#cachedOptionsSource"],"sources":["../../../../src/namespaces/RoutesNamespace/RoutesNamespace.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/RoutesNamespace.ts\n\nimport { DEFAULT_ROUTE_NAME } from \"./constants\";\nimport {\n matchSourceTrailingSlash,\n paramsMatch,\n queryParamsOf,\n urlParamsOf,\n} from \"./helpers\";\nimport { createRoutesStore, applyRootPath, resetStore } from \"./routesStore\";\nimport { assertChannelCorrect } from \"../../channels\";\nimport { constants, EMPTY_PARAMS, EMPTY_SEARCH } from \"../../constants\";\nimport { mergeDefined } from \"../../helpers\";\nimport { canonicalize, materialize } from \"../../pipeline\";\nimport { getTransitionPath } from \"../../transitionPath\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RoutesDependencies } from \"./types\";\nimport type {\n CreateMatcherOptions,\n RouteParams,\n RouteTreeState,\n} from \"../../engine\";\nimport type { RouteResolver } from \"../../pipeline\";\nimport type { RouteMetaLookup } from \"../../transitionPath\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n AnyOptions,\n Params,\n RouterLogger,\n SearchParams,\n State,\n Route,\n} from \"../../types\";\nimport type { RouteLifecycleNamespace } from \"../RouteLifecycleNamespace\";\n\nfunction createRouteState<P extends RouteParams = RouteParams>(\n matchResult: {\n readonly segments: readonly { fullName: string }[];\n readonly params: Readonly<Record<string, unknown>>;\n readonly search: Readonly<Record<string, unknown>>;\n readonly meta: Readonly<Record<string, Record<string, \"url\" | \"query\">>>;\n },\n name?: string,\n): RouteTreeState<P> {\n // The matcher yields ≥1 segment for every successful match, each carrying the\n // cumulative route name as `fullName`, so the last element is always present.\n // (Formerly `buildNameFromSegments` with a `?? \"\"` fallback — that branch was\n // unreachable defensive cruft propped up by a white-box test; inlined here.)\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- matcher invariant: a successful match is never empty\n const resolvedName = name ?? matchResult.segments.at(-1)!.fullName;\n\n return {\n name: resolvedName,\n params: matchResult.params as P,\n search: matchResult.search,\n meta: matchResult.meta,\n };\n}\n\ninterface CachedBuildPathOpts {\n readonly trailingSlash?: \"always\" | \"never\" | undefined;\n readonly queryParamsMode?: \"default\" | \"strict\" | \"loose\" | undefined;\n}\n\n/**\n * Independent namespace for managing routes.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle storage and business logic.\n */\nexport class RoutesNamespace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly #store: RoutesStore<Dependencies>;\n #cachedBuildPathOpts: CachedBuildPathOpts | undefined;\n // Source `options` reference captured on the first #getBuildPathOptions call;\n // used only by the dev-build immutability assertion below (#957).\n #cachedOptionsSource: AnyOptions | undefined;\n\n get #deps(): RoutesDependencies<Dependencies> {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n return this.#store.depsStore!;\n }\n\n constructor(\n // No `= []` default: the sole caller (Router's ctor) always passes its own\n // already-defaulted `routes` — a namespace-level default would be dead code\n // and a default-before-required-params smell (S1788).\n routes: Route<Dependencies>[],\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n ) {\n this.#store = createRoutesStore(routes, matcherOptions, logger);\n }\n\n /**\n * Creates a predicate function to check if a route node should be updated.\n * Note: Argument validation is done by facade (Router.ts) via validateShouldUpdateNodeArgs.\n */\n static shouldUpdateNode(\n nodeName: string,\n getMeta: RouteMetaLookup,\n ): (toState: State, fromState?: State) => boolean {\n return (toState: State, fromState?: State): boolean => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!(toState && typeof toState === \"object\" && \"name\" in toState)) {\n throw new TypeError(\n \"[router.shouldUpdateNode] toState must be valid State object\",\n );\n }\n\n if (toState.transition.reload) {\n return true;\n }\n\n // Root node (DEFAULT_ROUTE_NAME === \"\") has no route-level identity — it\n // represents \"any route\". It must update on every transition so that\n // consumers subscribed via useRouteNode(\"\") (including RouteView at\n // the top of the tree) see every change. This matches the documented\n // contract in adapter docs: `useRouteNode(\"\")` — Root — ALL route\n // changes. See #519 for the missed transitions it was suffering from\n // (users → users.user had intersection=\"users\", leaving the root node\n // un-updated under a flat <Match segment=\"users.user\" exact> pattern).\n if (nodeName === DEFAULT_ROUTE_NAME) {\n return true;\n }\n\n const { intersection, toActivate, toDeactivate } = getTransitionPath(\n toState,\n fromState,\n getMeta,\n );\n\n if (nodeName === intersection) {\n return true;\n }\n\n if (toActivate.includes(nodeName)) {\n return true;\n }\n\n return toDeactivate.includes(nodeName);\n };\n }\n\n // =========================================================================\n // Dependency injection\n // =========================================================================\n\n /**\n * Sets dependencies. Pure assignment — no side effects (#1331).\n *\n * The pending canActivate/canDeactivate factories from initial routes are\n * flushed separately by {@link flushPendingGuards}, called once wiring is\n * complete, so the order of the wire-* calls is unconstrained.\n */\n setDependencies(deps: RoutesDependencies<Dependencies>): void {\n this.#store.depsStore = deps;\n }\n\n /**\n * Registers the pending guard factories collected from initial route\n * definitions. Deferred out of {@link setDependencies} (#1331) so it runs on\n * a fully-built, fully-bound router: a guard factory that calls any\n * `router.*` method sees a ready instance instead of a half-assembled one.\n *\n * Invoked as the last step of the Router constructor. Idempotent after the\n * first call (the pending maps are cleared). Runtime `add()`/`replace()`\n * compile guards in their own PREPARE phase and never populate these maps.\n */\n flushPendingGuards(): void {\n const deps = this.#deps;\n\n for (const [routeName, handler] of this.#store.pendingCanActivate) {\n deps.addActivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanActivate.clear();\n\n for (const [routeName, handler] of this.#store.pendingCanDeactivate) {\n deps.addDeactivateGuard(routeName, handler);\n }\n\n this.#store.pendingCanDeactivate.clear();\n }\n\n /**\n * Sets the lifecycle namespace reference.\n */\n setLifecycleNamespace(\n namespace: RouteLifecycleNamespace<Dependencies> | undefined,\n ): void {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n this.#store.lifecycleNamespace = namespace!;\n }\n\n // =========================================================================\n // Route tree operations\n // =========================================================================\n\n setRootPath(newRootPath: string): void {\n applyRootPath(this.#store, newRootPath);\n }\n\n hasRoute(name: string): boolean {\n return this.#store.matcher.hasRoute(name);\n }\n\n clearRoutes(): void {\n resetStore(this.#store);\n }\n\n // =========================================================================\n // Path operations\n // =========================================================================\n\n /**\n * Builds a URL path for a route.\n * Note: Argument validation is done by facade (Router.ts) via validateBuildPathArgs.\n *\n * @param route - Route name\n * @param params - Route parameters\n * @param search - Query-channel params (RFC-4 M2 / #1548)\n * @param options - Router options\n */\n buildPath(\n route: string,\n params: Params,\n search?: SearchParams,\n options?: AnyOptions,\n ): string {\n if (route === constants.UNKNOWN_ROUTE) {\n return typeof params.path === \"string\" ? params.path : \"\";\n }\n\n // Stage ③ (each channel's route default under the caller's value — the slot\n // IS the channel, #1549 as `ba0f6b18b` left it) plus the mode gate (#1575),\n // one pass through the pipeline (nav-pipeline Phase 2, step 2-1). The LITERAL\n // form: `buildPath` does not follow `forwardTo` (A.5 — `buildPath(\"src\")`\n // stays `/src`, a deliberate asymmetry with `navigate`), so stage ① is\n // skipped and the seam is never entered.\n // ⚠ The v1 single-bag form is retired here, and skipping the seam is not what\n // retires it: the seam stopped MOVING a mis-channelled key when stage ② was\n // deleted (`ba0f6b18b`) — it refuses one now, on the resolving form. A caller\n // who rides a declared query key in the `params` bag simply keeps it there,\n // and the query string is printed from `canonical.query` alone. Before this\n // step the\n // matcher's `search ?? params` fallback printed it out of the path bag, so\n // `buildPath` disagreed with `navigate` on the same intent — an undeclared\n // key in `loose` (`/t?foo=1` vs `/t`), the `/coll/:id?id` collision\n // (`/items/V?id=V` vs `/items/V`), and a route's arbitrary `defaultParams`\n // (`/s?theme=d` vs `/s`). All three now agree.\n const canonical = canonicalize(this.#deps.port, route, params, search, {\n resolveForward: false,\n });\n\n // Stage ⑤a stays LOCAL to this method rather than going through `buildURL`,\n // and this is structural, not a preference: `buildURL` prints via\n // `port.buildPath`, which IS the interceptable `ctx.buildPath` wrapping this\n // very method (`Router.ts:349-359`) — routing through it would recurse. The\n // interceptor zone therefore stays exactly where it is (#1231:\n // `persistent-params` injects here), one layer above.\n // The route codec sees BOTH channels — `encodeParams({ params, search })` →\n // `{ params, search }` (§4) — so an encoder can shape the query as well as\n // the path.\n if (typeof this.#store.config.encoders[route] === \"function\") {\n const encoded = this.#store.config.encoders[route]({\n // BOTH channels spread, and the symmetry is the point: `canonical.*` is\n // frozen at merge time, so handing a channel through verbatim turns a\n // codec that edits its argument in place — legal before this entry point\n // joined the pipeline, and still legal for `params` — into a silent no-op\n // (sloppy mode) or a `TypeError` (ESM). Copying `params` alone left the\n // two halves of one documented hook behaving differently.\n params: { ...canonical.path },\n search: { ...canonical.query },\n });\n\n return this.#store.matcher.buildPath(\n route,\n encoded.params,\n encoded.search,\n this.#getBuildPathOptions(options),\n );\n }\n\n return this.#store.matcher.buildPath(\n route,\n canonical.path,\n canonical.query,\n this.#getBuildPathOptions(options),\n );\n }\n\n /**\n * Matches a URL path to a route in the tree.\n * Note: Argument validation is done by facade (Router.ts) via validateMatchPathArgs.\n */\n matchPath<P extends Params = Params>(\n path: string,\n options?: AnyOptions,\n ): State<P> | undefined {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Router.ts always passes options\n const opts = options!;\n\n const matchResult = this.#store.matcher.match(path);\n\n if (!matchResult) {\n return undefined;\n }\n\n const routeState = createRouteState(matchResult);\n const { name, params } = routeState;\n // The matcher always carries a search bag (a frozen `{}` when empty) but types\n // its values as `unknown`; narrow it to the query channel once here so the\n // codec / forwardState / rebuild uses it without per-site casts.\n const search = routeState.search as SearchParams;\n\n // Two-channel decode (RFC-4 M2 / #1548, §4): the route codec sees BOTH the\n // path params AND the parsed query — `decodeParams({ params, search })` →\n // `{ params, search }` — restoring v1's reach (v1 ran the whole path+query bag\n // through the decoder). Runs here, inside match, BEFORE any search-schema\n // plugin validation (the v1 order: engine codec → plugin). With no decoder the\n // channels pass through untouched.\n const decoder = this.#store.config.decoders[name];\n let decoded: { params: Params; search: SearchParams };\n\n if (typeof decoder === \"function\") {\n decoded = decoder({ params, search });\n\n // The ONE boundary on this path where a value core is about to build a\n // state from came out of USER code (#1582). The matcher's own output above\n // needs no check — it is router-produced and plain by construction — but a\n // decoder may hand back anything: an array becomes `params {\"0\":\"a\"}`, a\n // string becomes `{\"0\":\"o\",\"1\":\"o\",…}`, a `Map` or a prototyped object\n // becomes `{}`, and every one of those commits silently. Opt-in, like every\n // DX check: `null` in bare core, where the behaviour is unchanged either\n // way. Labelled for THIS entry point — the pre-pipeline call sat inside a\n // `forwardState` dep wrapper and blamed that method for a `matchPath` fault.\n this.#deps\n .getValidator()\n ?.routes.validateStateBuilderArgs(name, decoded.params, \"matchPath\");\n\n // The channel half of the same boundary — always on, unlike the shape\n // check above. A decoder that moves a declared `?key` into the params bag\n // used to be repaired by the seam and shipped as if it had been written\n // correctly; now it is refused. Checked HERE as well as at the seam so the\n // message names the decoder rather than the chain it later flows through —\n // same single assertion, applied where the fault was actually authored.\n assertChannelCorrect(\n \"matchPath\",\n name,\n decoded.params,\n this.getQueryParams(name),\n \"the `params` returned by this route's `decodeParams`\",\n );\n } else {\n decoded = { params, search };\n }\n\n // Stages ① + ③ + the mode gate, one pass through the pipeline (nav-pipeline\n // Phase 2, step 2-2). `canonicalize` reaches the SAME `forwardState` seam\n // this method used to call directly — `port.resolveForward` is\n // `ctx.forwardState` — so a search-schema interceptor still validates the\n // query on the URL→State path here (the `routeSearch` argument is defined,\n // marking this as a re-parse, not a navigate), and the seam still checks the\n // channels on the way through. What the pipeline replaces is the\n // hand-rolled composition that followed: the route's own default split\n // (#1549), the default merge, and the mode gate (#1575) now happen once,\n // inside `canonicalize`, from the same read-model `navigate` uses.\n // ⚠ Stage ② is GONE from this path, as it is from every other: the seam no\n // longer repairs a mis-channelled bag, it refuses one. A `forwardState`\n // interceptor injecting a declared query key into `result.params` used to\n // land in `state.search` here exactly as on `navigate`; both now throw.\n const canonical = canonicalize(\n this.#deps.port,\n name,\n decoded.params,\n decoded.search,\n );\n const routeName = canonical.name;\n\n // The canonical channels, ready for BOTH halves of the state: `canonical.path`\n // is path-only (the seam REFUSED any declared `?key` a plugin injection or a\n // decoder left in the params bag, and stage ③ layered the route's own\n // defaults under it, each slot in its own channel — #1549),\n // `canonical.query` is the full canonical query with the mode gate already\n // applied (#1575). Both are read from ONE object, so the\n // rebuilt `state.path` and the committed `state.search` cannot derive from\n // differently-merged bags (INVARIANTS makeState #6).\n const routeParams = canonical.path;\n const forwardedSearch = canonical.query;\n\n let builtPath = path;\n\n if (opts.rewritePathOnMatch) {\n // Two-channel encode for the URL rebuild (RFC-4 M2 / #1548, §4): path slots\n // from `routeParams` (canonical, path-only), the query string from\n // `forwardedSearch` (the full canonical query) — never the raw matched\n // query — so the rebuilt `state.path` stays in step with `state.search`.\n const encoded =\n typeof this.#store.config.encoders[routeName] === \"function\"\n ? this.#store.config.encoders[routeName]({\n params: routeParams,\n search: forwardedSearch,\n })\n : { params: routeParams, search: forwardedSearch };\n\n const ts = opts.trailingSlash;\n\n try {\n // Explicit two-channel build (RFC-4 M2 / #1548): path slots from\n // `encoded.params`, the query string from `encoded.search`. The channels\n // stay separate, so a `/coll/:id?id` collision keeps its path slot and\n // query twin independent — no single-bag reunification, no\n // search-wins-for-declared-query fixup.\n builtPath = this.#store.matcher.buildPath(\n routeName,\n encoded.params,\n encoded.search,\n {\n trailingSlash: ts === \"never\" || ts === \"always\" ? ts : undefined,\n queryParamsMode: opts.queryParamsMode,\n },\n );\n\n if (ts === \"preserve\") {\n builtPath = matchSourceTrailingSlash(path, builtPath);\n }\n } catch {\n // The match already succeeded (route found, params decoded); only the\n // post-match path rewrite threw — e.g. a custom encoder handed buildPath\n // a query value its codec cannot serialise. Keep the source path\n // un-rewritten rather than discard a valid match (#1157). Opposite of the\n // parse side (#737): there a throw means \"URL not understood\" → unmatched;\n // here the URL WAS matched and only re-canonicalisation failed.\n builtPath = path;\n }\n }\n\n // Stage ⑤b. `materialize` over `makeState` because the intent is ALREADY\n // canonical: `makeState` would re-run the default merge (idempotent, but a\n // wasted pass) and would rebuild the path when none is handed to it, which is\n // exactly the work ⑤a above just did. The generic rides through\n // (`matchPath<P>` → `materialize<P>` → `State<P>`), so a consumer's typed\n // params survive the migration.\n return materialize<P>(canonical, { path: builtPath });\n }\n\n /**\n * Applies `forwardTo` and returns the resolved name with the CHAIN's defaults\n * layered under the caller's channels.\n *\n * Order, per channel: every forwarding HOP's defaults (the earliest hop wins),\n * then the caller's value on top. The TARGET route's own defaults are\n * deliberately NOT part of it — the body says why they cannot be, and the\n * summary used to list them as step 3 while the very next comment denied it.\n */\n forwardState<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n name: string,\n params: P,\n search?: S,\n ): { name: string; params: P; search: S } {\n // TARGET-route defaults are NOT applied here for EITHER channel — they are\n // merged strictly BELOW the user channels at the pipeline's single terminal\n // (`canonicalize`, reached alike by `makeState`, `matchPath` and\n // `buildPath`). Folding a\n // target default into this result would ride ABOVE a user params-twin at the\n // terminal merge, inverting the priority — a\n // `navigate(x, { page: 2 })` on a `?page` route with `defaultSearch{page:1}`\n // would wrongly commit `page=1`. So both channels pass through as the user\n // gave them: `search` stays `resolvedSearch`, `params` stays the raw bag.\n //\n // The ONE default forwardState still merges is the forwardTo CHAIN's own\n // defaults (`#layerChainDefaults` in the forward branches): when `a` forwards\n // to `b`, `a`'s `defaultParams` fill in before the redirect and flow to `b`.\n // This canNOT move to the terminal — `canonicalize` sees only the RESOLVED\n // target `b` and cannot reconstruct source `a`'s defaults. Each of a hop's\n // slots keeps its own channel (`defaultParams` the path, `defaultSearch` the\n // query) whatever the target declares; `ba0f6b18b` retired #1570's routing by\n // the target's declaration, and a hop default naming a key the TARGET\n // declares with `?` is refused at the seam instead. Frozen empty search\n // singleton when absent.\n const resolvedSearch = (search ?? EMPTY_SEARCH) as S;\n\n if (Object.hasOwn(this.#store.config.forwardFnMap, name)) {\n const dynamicForward = this.#store.config.forwardFnMap[name];\n const { target, chain } = this.#resolveDynamicForward(\n name,\n dynamicForward,\n params,\n );\n\n return this.#layerChainDefaults(target, chain, params, resolvedSearch);\n }\n\n const staticForward = this.#store.resolvedForwardMap[name] ?? name;\n\n if (\n staticForward !== name &&\n Object.hasOwn(this.#store.config.forwardFnMap, staticForward)\n ) {\n const targetDynamicForward =\n this.#store.config.forwardFnMap[staticForward];\n const { target, chain } = this.#resolveDynamicForward(\n staticForward,\n targetDynamicForward,\n params,\n );\n\n // The static prefix stops AT `staticForward` (it has no static forward of\n // its own), and the dynamic walk starts THERE — so the two halves\n // concatenate without repeating a hop.\n return this.#layerChainDefaults(\n target,\n [...this.#collectStaticChain(name), ...chain],\n params,\n resolvedSearch,\n );\n }\n\n if (staticForward !== name) {\n return this.#layerChainDefaults(\n staticForward,\n this.#collectStaticChain(name),\n params,\n resolvedSearch,\n );\n }\n\n return {\n name,\n params,\n search: resolvedSearch,\n };\n }\n\n /**\n * Builds a RouteTreeState from an already-resolved route name and params.\n *\n * ⚠ Its ONE caller — `getPluginApi().buildNavigationState` — uses it as an\n * EXISTENCE probe and discards the object (`if (!ctx.buildStateResolved(…))\n * return;`). Since Phase 2 that entry point materialises its state from the\n * `Canonical`, so what this returns is built and dropped; what the caller\n * needs is the `undefined` arm, placed BEFORE `buildURL` because the matcher\n * throws on an unknown route while that entry point must answer `undefined`.\n * Whether this should therefore collapse into a `hasRoute`-shaped predicate is\n * an open question, not a settled design — the same dead-surface shape\n * coverage surfaced for `skipFreeze` in Phase 4.\n */\n buildStateResolved(\n resolvedName: string,\n resolvedParams: Params,\n ): RouteTreeState | undefined {\n const segments = this.#store.matcher.getSegmentsByName(resolvedName);\n\n if (!segments) {\n return undefined;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const meta = this.#store.matcher.getMetaByName(resolvedName)!;\n\n return createRouteState(\n // `resolvedParams` is path-only by the PRODUCER's contract, not by a repair\n // upstream: the seam refuses a declared `?key` in the params bag rather\n // than moving one out (stage ② is gone — `ba0f6b18b`). The `search: {}`\n // placeholder threads nowhere; the caller discards this object.\n { segments, params: resolvedParams, search: {}, meta },\n resolvedName,\n );\n }\n\n // =========================================================================\n // Query operations\n // =========================================================================\n\n /**\n * Checks if a route is currently active.\n *\n * Two arms, `literal || destination` (#1573). The literal arm is the whole\n * predicate below, unchanged. The destination arm repeats THAT SAME predicate\n * on the full output of stage ① — the resolved terminal name together with\n * the forwarding chain's defaults, each hop's slot in its own channel — so a\n * `<Link to=\"alias\">` reads active on the page it actually navigates to.\n *\n * It is a FALLBACK, never a pre-resolution: resolving before comparing would\n * send a section link (`users` forwarding to `users.list`) to the leaf and\n * darken it while a sibling descendant (`users.profile`) is active.\n *\n * It repeats the predicate on ①'s OUTPUT rather than substituting the name,\n * because the chain's `defaultParams` live on the forwarding SOURCE and are\n * layered by `forwardState` (#1566/#1570) — never by the forward map — and a\n * dynamic `forwardTo` is not in that map at all. Name substitution therefore\n * fixes neither, and it also carries no `search`, which is the channel a hop's\n * own `defaultSearch` lands in.\n */\n isActiveRoute(\n name: string,\n // Singletons, not fresh literals (#1589). `= {}` minted TWO throwaway objects\n // on every call — i.e. on every `<Link>` on every re-render across six\n // adapters — and the `search` one was worse than an allocation: being a fresh\n // `{}` it is neither `undefined` nor `EMPTY_SEARCH`, so it defeated\n // `canonicalize`'s own empty-query fast path and this predicate ran the full\n // merge + mode-gate tail every time. Measured: the flame graph showed\n // `withholdFilledSlots` and the drop-sink getter live under a route with\n // nothing to withhold and nothing to drop.\n params: Params = EMPTY_PARAMS,\n searchArg: SearchParams = EMPTY_SEARCH,\n strictEquality = false,\n ignoreQueryParams = true,\n ): boolean {\n if (\n this.#matchesActiveState(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n )\n ) {\n return true;\n }\n\n // O(1) gate: only a route that actually forwards can have a second arm.\n // Every `<Link>` in six adapters runs this predicate on every render, so a\n // non-forwarding route must not pay for the arm at all.\n //\n // TREE-WIDE first, per-route second (#1595). The two maps below are\n // `Object.create(null)` dictionaries — V8 keeps those in dictionary mode\n // whatever their size, empty ones included — and the pair of lookups measured\n // ~14 ns, i.e. 1.75x this predicate's pre-pipeline cost on the shape that\n // reaches here: an INACTIVE link, which is most links on a page. The cost is\n // not the `Object.hasOwn` form (replacing it with a plain property read\n // measured identical) but touching the dictionaries at all, so the fix is to\n // not touch them when no route in the tree forwards. `hasAnyForward` is\n // maintained beside `resolvedForwardMap`, never separately.\n if (\n !this.#store.hasAnyForward ||\n (!Object.hasOwn(this.#store.config.forwardMap, name) &&\n !Object.hasOwn(this.#store.config.forwardFnMap, name))\n ) {\n return false;\n }\n\n let forwarded;\n\n try {\n // The NAMESPACE primitive, not the interceptable seam: a predicate on the\n // render path must not run the plugin interceptor chain once per `<Link>`.\n forwarded = this.forwardState(name, params, searchArg);\n } catch (error) {\n // A dynamic `forwardTo` is user code and may throw. A predicate answers,\n // it never throws from inside a render — same policy as `canNavigateTo`\n // on a throwing guard (#959): honest `false` plus an operational log.\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Dynamic forwardTo of route \"${name}\" threw while resolving the active-link destination; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n\n // No `forwarded.name === name` guard: the O(1) gate above already proved the\n // route forwards, and a self-returning dynamic callback throws on the cycle\n // check before `forwardState` can hand one back — so the terminal name is\n // always a different route (measured; the branch was unreachable).\n return this.#matchesActiveState(\n forwarded.name,\n forwarded.params,\n forwarded.search,\n strictEquality,\n ignoreQueryParams,\n );\n }\n\n getMetaForState(\n name: string,\n ): Record<string, Record<string, \"url\" | \"query\">> | undefined {\n return this.#store.matcher.hasRoute(name)\n ? this.#store.matcher.getMetaByName(name)\n : undefined;\n }\n\n getUrlParams(name: string): string[] {\n return urlParamsOf(this.#store, name);\n }\n\n /**\n * Declared query param names of a route (`?a&b` across its segments,\n * ancestors included) that are NOT also path params — the query-channel twin\n * of {@link getUrlParams}. THE registry (#1556): the always-on channel guard,\n * the literal form's default withholding and the mode gate all classify\n * through it, and the URL build prints from it. (It powered the `defaultParams`\n * channel ROUTING too, until `ba0f6b18b` retired that — the slot is the channel\n * now, so nothing is routed anywhere.) A colliding name (`/items/:id?id` —\n * legal under M2, the channels\n * coexist) is path-owned for routing purposes: excluding it here keeps the\n * path slot's value in `state.params` and the rebuild's #843 precedence\n * intact. Same cache lifecycle: cleared on every matcher rebuild.\n *\n * Reads the matcher's `declaredQueryParams` — the SAME registry the\n * query-string build uses — rather than walking `matchSegments` (#1556). The\n * segment walk missed the ROOT node's `?`-declarations (`setRootPath(\"?a&b\")`,\n * how persistent-params declares its keys), because the root is captured\n * separately at `registerTree` and never appears in `matchSegments`. That\n * made a root-declared key print as query but classify as a path param: it\n * landed in `state.params`, vanished from `state.path` on the intent side,\n * and no `isActiveRoute` spelling matched a link to the active page. One\n * registry classifies and prints, so the two cannot drift again.\n */\n getQueryParams(name: string): string[] {\n return queryParamsOf(this.#store, name);\n }\n\n getStore(): RoutesStore<Dependencies> {\n return this.#store;\n }\n\n /**\n * The pipeline's read-model, for entry points that live on the FACADE rather\n * than in a namespace (`canNavigateTo`). The port is created during wiring,\n * after `registerInternals` has already run, so the facade cannot hold it —\n * it reaches it through the namespace that does, exactly as the resolver\n * itself reaches the store through {@link getStore}.\n */\n getPort(): RouteResolver {\n return this.#deps.port;\n }\n\n /**\n * The literal arm of {@link isActiveRoute} — unchanged by #1573.\n *\n * Safe boundary (#1577): both branches below READ the caller's bags — since\n * nav-pipeline Phase 2 step 2-5 through `canonicalize` in its literal form,\n * which walks `params` key by key, and then again in the descendant branch's\n * channel-by-channel `paramsMatch` — so an accessor-backed key, a `Proxy` or a\n * framework's reactive object throws HERE, on the render path. (It used to be\n * the exact branch's own channel split and the descendant branch's spread into\n * one bag; step 2-5 removed both, the exposure is unchanged.) The predicate's\n * stated policy is that it answers and never throws from inside a render (see\n * the `forwardState` wrap below), and #1573 implemented that for the\n * destination arm only. One boundary around the whole walk rather than a\n * `try` per read —\n * the same shape `isParams` took for the same class of hostile input (#1052).\n */\n #matchesActiveState(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n try {\n return this.#matchesActiveStateUnsafe(\n name,\n params,\n searchArg,\n strictEquality,\n ignoreQueryParams,\n );\n } catch (error) {\n this.#deps.logger.warn(\n \"router.isActiveRoute\",\n `Reading the arguments for route \"${name}\" threw while resolving the active-link state; treating the link as inactive.`,\n error,\n );\n\n return false;\n }\n }\n\n #matchesActiveStateUnsafe(\n name: string,\n params: Params,\n searchArg: SearchParams,\n strictEquality: boolean,\n ignoreQueryParams: boolean,\n ): boolean {\n // Note: empty string check is handled by Router.ts facade\n const activeState = this.#deps.getState();\n\n if (!activeState) {\n return false;\n }\n\n const activeName = activeState.name;\n\n // Fast path: check if routes are related before expensive operations\n if (\n activeName !== name &&\n !activeName.startsWith(`${name}.`) &&\n !name.startsWith(`${activeName}.`)\n ) {\n return false;\n }\n\n // The comparison target, built by the SAME pipeline every producer uses\n // (nav-pipeline Phase 2, step 2-5) — in the LITERAL form: this predicate asks\n // about the route it was NAMED. `forwardTo` is resolved by the caller's\n // second arm (`isActiveRoute`), through the NON-interceptable namespace\n // primitive, and `{ resolveForward: false }` is what keeps it that way here:\n // the literal form never touches the port, so a plugin's interceptor chain\n // does not run once per `<Link>` per render.\n //\n // ⚠ Stage ② is gone from this point, and by now from everywhere: step 2-5\n // dropped this method's OWN channel split, and the seam that the other\n // points reach through the port stopped repairing bags too. The observable\n // change here is unchanged by that: a declared query key handed in the\n // `params` bag is no longer moved to the query channel before comparison,\n // so a v1 single-bag call stops matching. Channel-correctness is the\n // caller's contract, exactly as for `navigate` (which throws on that shape)\n // and `buildPath` (which prints without it).\n const canonical = canonicalize(this.#deps.port, name, params, searchArg, {\n resolveForward: false,\n });\n\n // Exact match case. Path \"\" skips the URL build — `areStatesEqual` compares\n // channels and never reads the URL, which is also why `materialize` needs no\n // port argument here (the fork milestone 1 left open, settled in step 2-3).\n if (strictEquality || activeName === name) {\n // `skipFreeze` (#1589): the state exists for the length of one comparison.\n // `areStatesEqual` reads `.name` / `.params` / `.search` and nothing else,\n // so freezing it — and attaching the `transition` the freeze implies —\n // buys a guarantee no one can observe. The CHANNELS are still frozen;\n // that happens in `canonicalize`, and it is the part that matters\n // (canonicalize invariant #4). Measured at 926 µs, ~5 % of this benchmark.\n return this.#deps.areStatesEqual(\n materialize(canonical, { path: \"\", skipFreeze: true }),\n activeState,\n ignoreQueryParams,\n );\n }\n\n // The fast path above lets through three relations: exact (handled in\n // the previous block), `activeName` descendant of `name`, and `name`\n // descendant of `activeName`. Only the first two count as \"active\" —\n // a link pointing DEEPER than the current state is a navigation option,\n // not an active state. Reject the descendant-of-active case explicitly.\n if (!activeName.startsWith(`${name}.`)) {\n return false;\n }\n\n // Hierarchical check: activeState is a descendant of target (name). Compared\n // CHANNEL BY CHANNEL (step 2-5) instead of over one recombined bag: the\n // canonical target already carries the route's defaults, each merged under\n // the caller's value in the channel its own SLOT names (#1549), so the\n // separate `paramsMatchExcluding` passes over `defaultParams` /\n // `defaultSearch` are no longer needed — a default that survived into\n // `canonical` is exactly a default the caller did not override.\n if (!paramsMatch(canonical.path, activeState.params)) {\n return false;\n }\n\n // The query channel obeys `ignoreQueryParams`, the same flag the exact arm\n // hands to `areStatesEqual`. The recombined-bag form could not: it folded\n // query into the path bag before matching, so an ancestor link compared its\n // query even when the caller asked to ignore it — the two arms disagreed\n // about the flag.\n if (\n !ignoreQueryParams &&\n !paramsMatch(canonical.query as Params, activeState.search as Params)\n ) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Every node on a `forwardTo` chain that FORWARDS, in walk order — the\n * terminal is excluded (its own defaults belong to the state builder, #1549).\n * `forwardMap` is proven acyclic at registration (`refreshForwardMap` runs\n * `resolveForwardChain`, which throws on a cycle), so the walk terminates.\n */\n #collectStaticChain(name: string): string[] {\n const chain: string[] = [];\n let current = name;\n\n while (Object.hasOwn(this.#store.config.forwardMap, current)) {\n chain.push(current);\n current = this.#store.config.forwardMap[current];\n }\n\n return chain;\n }\n\n /**\n * Layers the defaults of every forwarding hop UNDER the caller's channels.\n * Folding in walk order makes an EARLIER hop win over a later one (each merge\n * puts the next hop's defaults below what is already accumulated), and the\n * caller wins over all of them. Only the entered route was consulted before,\n * so a default declared on an intermediate hop never reached the target and\n * a required slot was left empty (#1566).\n *\n * Each slot IS its channel: a hop's `defaultParams` is the path channel and\n * its `defaultSearch` the query channel, whatever the resolved target\n * declares. A hop's `defaultParams` naming a key the TARGET declares with `?`\n * is refused at the `forwardState` seam, where the target is finally known.\n *\n * (#1570 routed this fold by the target's declaration for one release;\n * `ba0f6b18b` retired that with the rest of stage ②. The body below records\n * why — it is not repeated here, and the retired rule is not restated in the\n * present tense above it.)\n */\n #layerChainDefaults<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n >(\n target: string,\n chain: readonly string[],\n params: P,\n search: S,\n ): { name: string; params: P; search: S } {\n let hopDefaults: Params | undefined;\n let hopSearchDefaults: SearchParams | undefined;\n\n for (const routeName of chain) {\n // `undefined` is absence on both sides (#1550 / #1551): a source default\n // carrying `undefined` must not ride out of `forwardState` as an own key.\n hopDefaults = mergeDefined(\n this.#store.config.defaultParams[routeName] as Params | undefined,\n hopDefaults,\n );\n // A hop's `defaultSearch` was read by NOBODY until #1549: this fold only\n // took `defaultParams`, so the slot was silently inert on a forwarding\n // node while working on a terminal — the mirror of the terminal's own\n // defect, where `defaultParams` worked on a hop and broke on the route\n // itself. Each slot has to mean the same thing in both positions.\n hopSearchDefaults = mergeDefined(\n this.#store.config.defaultSearch[routeName] as SearchParams | undefined,\n hopSearchDefaults,\n );\n }\n\n // Each slot IS its channel — no split, here or anywhere else. A hop's\n // `defaultParams` is the path channel and its `defaultSearch` the query\n // channel, whatever the resolved target declares.\n //\n // This used to route the fold by the TARGET's declaration, on the argument\n // that a hop \"can only spell a default in `defaultParams`\". That argument\n // was already false — the fold reads `defaultSearch` two lines above — and\n // the routing was doing real damage: a hop author could not tell which\n // channel their own config would end up in without reading a target that a\n // `forwardTo` CALLBACK may not even determine until navigation time. Now\n // they can: the slot they wrote is the channel they get.\n //\n // A hop whose `defaultParams` names a key the TARGET declares with `?` is\n // still caught — not here, but at the `forwardState` seam, where the target\n // is finally known and the check can name both routes. Registration cannot\n // see it (a dynamic `forwardTo` has no target yet), which is exactly why\n // that check lives at resolution rather than being guessed here.\n // No cross-channel withholding any more, and its removal is the point.\n // #1570 needed it because the split put a caller's params-twin and the\n // query half of the same default in DIFFERENT bags, where no merge ranks\n // them — so the default had to be withheld by hand or it won. Nothing is\n // split now: a key in the caller's `params` and a key in the chain's\n // `search` are different channels holding different names, because a caller\n // who puts a declared query name in `params` is refused outright. Each\n // channel merges against its own default and nothing has to look sideways.\n\n return {\n name: target,\n // The caller's params stay ABOVE the path half, and their `undefined`\n // keys are stripped exactly as before — this merge runs whether or not\n // the chain contributed anything.\n params: mergeDefined(hopDefaults as P | undefined, params),\n // `mergeDefined`, not a spread: an explicit `undefined` from the caller is\n // ABSENCE (#1550 / #1551), so it must not delete the hop default. A spread\n // copied the `undefined` key over and killed it — asymmetric with a\n // route-level `defaultSearch`, where the rule already held.\n search: mergeDefined(hopSearchDefaults, search) as S,\n };\n }\n\n #getBuildPathOptions(options?: AnyOptions): CachedBuildPathOpts {\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return rebuilds the identical buildPath options (deterministic) and re-caches them. (ConditionalExpression stays live: `→false` always rebuilds but a real consumer test pins the cached identity.)\n if (this.#cachedBuildPathOpts) {\n /* v8 ignore next 5 -- @preserve: dev assertion guarding a future caller that passes per-call varying options; the sole caller (Router.buildPath, always via this.#options.get()) passes the same immutable, deep-frozen per-instance options, so this branch is unreachable through the public API by construction (#957) */\n if (options !== this.#cachedOptionsSource) {\n this.#deps.logger.warn(\n \"router.buildPath\",\n \"`options` differs from the cached source reference; router options are immutable per router instance, so the first-cached buildPath options are reused (#957).\",\n );\n }\n\n return this.#cachedBuildPathOpts;\n }\n\n this.#cachedOptionsSource = options;\n\n const ts = options?.trailingSlash;\n\n this.#cachedBuildPathOpts = Object.freeze({\n trailingSlash: ts === \"never\" || ts === \"always\" ? ts : undefined,\n queryParamsMode: options?.queryParamsMode,\n });\n\n return this.#cachedBuildPathOpts;\n }\n\n #resolveDynamicForward(\n startName: string,\n startFn: ForwardToCallback<Dependencies>,\n params: Params,\n ): { target: string; chain: string[] } {\n const visited = new Set<string>([startName]);\n // Every node that forwards, in walk order — `startName` does by definition.\n // The terminal is never pushed, so the caller can layer hop defaults without\n // pulling in the target's (#1566/#1549).\n const chain: string[] = [startName];\n\n let current = startFn(this.#deps.getDependency, params);\n let depth = 0;\n const MAX_DEPTH = 100;\n\n if (typeof current !== \"string\") {\n throw new TypeError(\n `forwardTo callback must return a string, got ${typeof current}`,\n );\n }\n\n while (depth < MAX_DEPTH) {\n if (this.#store.matcher.getSegmentsByName(current) === undefined) {\n throw new Error(`Route \"${current}\" does not exist`);\n }\n\n if (visited.has(current)) {\n const cycle = [...visited, current].join(\" → \");\n\n throw new Error(`Circular forwardTo: ${cycle}`);\n }\n\n visited.add(current);\n\n if (Object.hasOwn(this.#store.config.forwardFnMap, current)) {\n const fn = this.#store.config.forwardFnMap[\n current\n ] as ForwardToCallback<Dependencies>;\n\n chain.push(current);\n current = fn(this.#deps.getDependency, params);\n\n depth++;\n continue;\n }\n\n const staticForward = this.#store.config.forwardMap[current];\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (staticForward !== undefined) {\n chain.push(current);\n current = staticForward;\n depth++;\n continue;\n }\n\n return { target: current, chain };\n }\n\n throw new Error(`forwardTo exceeds maximum depth of ${MAX_DEPTH}`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../channels/defaults.js"),t=require("../../helpers.js");function n(){return{decoders:Object.create(null),encoders:Object.create(null),defaultParams:Object.create(null),defaultSearch:Object.create(null),forwardMap:Object.create(null),forwardFnMap:Object.create(null)}}function r(e,t){for(let n of Object.keys(t))Object.assign(e[n],t[n])}function
|
|
1
|
+
const e=require("../../channels/defaults.js"),t=require("../../helpers.js");function n(){return{decoders:Object.create(null),encoders:Object.create(null),defaultParams:Object.create(null),defaultSearch:Object.create(null),forwardMap:Object.create(null),forwardFnMap:Object.create(null)}}function r(e){return Object.keys(e.forwardMap).length>0||Object.keys(e.forwardFnMap).length>0}function i(e,t){for(let n of Object.keys(t))Object.assign(e[n],t[n])}function a(e,n){for(let r in e)if(!t.areParamValuesEqual(e[r],n[r]))return!1;return!0}function o(e){let t={name:e.name,path:e.path};return e.children&&(t.children=e.children.map(e=>o(e))),t}function s(e,t,n=``){for(let r=0;r<e.length;r++){let i=e[r],a=n?`${n}.${i.name}`:i.name;if(a===t)return e.splice(r,1),!0;if(i.children&&t.startsWith(`${a}.`)&&s(i.children,t,a))return!0}return!1}function c(e,t){for(let n of Object.keys(e))t(n)&&delete e[n]}function l(e,t){let n=t.search(/[?#]/),r=n===-1?t:t.slice(0,n);if(r===`/`||r.endsWith(`/`))return t;let i=e.search(/[?#]/),a=i===-1?e:e.slice(0,i);return a.length>1&&a.endsWith(`/`)?`${r}/${n===-1?``:t.slice(n)}`:t}function u(e){let t=[];for(let n of e)for(let e of n.paramMeta.urlParams)t.push(e);return t}function d(e,t,n){let r=n.get(t);if(r!==void 0)return r;let i=e.getSegmentsByName(t),a=i?u(i):[];return n.set(t,a),a}function f(e,t){return d(e.matcher,t,e.urlParamsCache)}function p(e,t,n,r){let i=r.get(t);if(i!==void 0)return i;let a=e.getDeclaredQueryParams(t),o=[];if(a){let r=d(e,t,n);o=a.filter(e=>!r.includes(e))}return r.set(t,o),o}function m(e,t){return p(e.matcher,t,e.urlParamsCache,e.queryParamsCache)}function h(t,n,r){let i=new Map,a=new Map;e.assertRouteDefaultChannels(n.defaultParams,e=>p(t,e,i,a),r)}exports.anyForwardConfigured=r,exports.assertRouteDefaultChannelsFor=h,exports.assignConfigEntries=i,exports.clearConfigEntries=c,exports.collectUrlParamsArray=u,exports.createEmptyConfig=n,exports.matchSourceTrailingSlash=l,exports.paramsMatch=a,exports.queryParamsFor=p,exports.queryParamsOf=m,exports.removeFromDefinitions=s,exports.sanitizeRoute=o,exports.urlParamsFor=d,exports.urlParamsOf=f;
|
|
2
2
|
//# sourceMappingURL=helpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":["areParamValuesEqual"],"sources":["../../../../src/namespaces/RoutesNamespace/helpers.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/helpers.ts\n\nimport { assertRouteDefaultChannels } from \"../../channels\";\nimport { areParamValuesEqual } from \"../../helpers\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RouteConfig } from \"./types\";\nimport type { Matcher, RouteDefinition, RouteTree } from \"../../engine\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n Params,\n ParamsSearch,\n SearchParams,\n Route,\n} from \"../../types\";\n\n/**\n * Creates an empty RouteConfig.\n */\nexport function createEmptyConfig(): RouteConfig {\n return {\n decoders: Object.create(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n encoders: Object.create(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n defaultParams: Object.create(null) as Record<string, Params>,\n defaultSearch: Object.create(null) as Record<string, SearchParams>,\n forwardMap: Object.create(null) as Record<string, string>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n forwardFnMap: Object.create(null) as Record<string, ForwardToCallback<any>>,\n };\n}\n\n/**\n * Copies every {@link RouteConfig} sub-map's entries from `source` into\n * `target` (shallow per map — entries are shared by reference). Driven by\n * `Object.keys(source)` instead of one `Object.assign` per field, so a newly\n * added config sub-field is carried over automatically with nothing to forget\n * at each copy site (#965). Both configs are produced by\n * {@link createEmptyConfig}, so every key in `source` also exists on `target`,\n * and every value is a record object — the invariant this enumeration relies on.\n */\nexport function assignConfigEntries(\n target: RouteConfig,\n source: RouteConfig,\n): void {\n for (const key of Object.keys(source) as (keyof RouteConfig)[]) {\n Object.assign(target[key], source[key]);\n }\n}\n\n// ============================================================================\n// Route Tree Helpers\n// ============================================================================\n\n/**\n * Checks if all params from source exist with same values in target.\n * Small function body allows V8 inlining.\n */\nexport function paramsMatch(source: Params, target: Params): boolean {\n for (const key in source) {\n // Provenance-tolerant per value (#1554) — the hierarchical isActiveRoute\n // branch compares a caller bag against the COMMITTED state, whose values\n // may have come from the URL parser (`?tab=2` → `2`) while the caller wrote\n // strings. Same predicate as the exact branch (`areStatesEqual`), so both\n // branches answer identically for one location.\n if (!areParamValuesEqual(source[key], target[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Sanitizes a route by keeping only essential properties.\n */\nexport function sanitizeRoute<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n): RouteDefinition {\n const sanitized: RouteDefinition = {\n name: route.name,\n path: route.path,\n };\n\n if (route.children) {\n sanitized.children = route.children.map((child) => sanitizeRoute(child));\n }\n\n return sanitized;\n}\n\n/**\n * Recursively removes a route from definitions array.\n */\nexport function removeFromDefinitions(\n definitions: RouteDefinition[],\n routeName: string,\n parentPrefix = \"\",\n): boolean {\n for (let i = 0; i < definitions.length; i++) {\n const route = definitions[i];\n const fullName = parentPrefix\n ? `${parentPrefix}.${route.name}`\n : route.name;\n\n if (fullName === routeName) {\n definitions.splice(i, 1);\n\n return true;\n }\n\n if (\n route.children &&\n routeName.startsWith(`${fullName}.`) &&\n removeFromDefinitions(route.children, routeName, fullName)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Clears configuration entries that match the predicate.\n */\nexport function clearConfigEntries<T>(\n config: Record<string, T>,\n matcher: (key: string) => boolean,\n): void {\n for (const key of Object.keys(config)) {\n if (matcher(key)) {\n delete config[key];\n }\n }\n}\n\n/**\n * Used by matchPath() when trailingSlash is \"preserve\": the matcher's\n * buildPath() with an unset trailingSlash mode strips trailing slashes,\n * but \"preserve\" means the source path's trailing-slash choice wins.\n * If the source had a trailing slash, re-attach it to the rewritten path.\n * The reverse case (rewritten has trailing, source does not) is not\n * reachable with the current matcher — it never adds a trailing slash\n * with undefined mode.\n */\nexport function matchSourceTrailingSlash(\n sourcePath: string,\n rewrittenPath: string,\n): string {\n const queryIndex = rewrittenPath.search(/[?#]/);\n const pathPart =\n queryIndex === -1 ? rewrittenPath : rewrittenPath.slice(0, queryIndex);\n\n // Stryker disable next-line LogicalOperator: equivalent — buildPath strips trailing slashes, so the rewritten path never ends with \"/\" unless it IS \"/\" (already caught by the `=== \"/\"` operand). `endsWith(\"/\")` is unreachable-true, so `||` ≡ `&&`.\n if (pathPart === \"/\" || pathPart.endsWith(\"/\")) {\n return rewrittenPath;\n }\n\n const sourceQueryIndex = sourcePath.search(/[?#]/);\n const sourcePathPart =\n sourceQueryIndex === -1\n ? sourcePath\n : sourcePath.slice(0, sourceQueryIndex);\n\n if (!(sourcePathPart.length > 1 && sourcePathPart.endsWith(\"/\"))) {\n return rewrittenPath;\n }\n\n const querySuffix = queryIndex === -1 ? \"\" : rewrittenPath.slice(queryIndex);\n\n return `${pathPart}/${querySuffix}`;\n}\n\n// =============================================================================\n// The query-channel registry — ONE derivation, shared by every reader (#1556)\n// =============================================================================\n\n/** Flattens the path-slot names declared across a route's matched segments. */\nexport function collectUrlParamsArray(\n segments: readonly RouteTree[],\n): string[] {\n const params: string[] = [];\n\n for (const segment of segments) {\n for (const param of segment.paramMeta.urlParams) {\n params.push(param);\n }\n }\n\n return params;\n}\n\n/**\n * The route's PATH slot names, cached per route name.\n *\n * Store-level rather than a namespace method so the config-time channel check\n * reads the SAME registry the URL build prints from. A second derivation is\n * exactly the drift #1556 removed.\n */\nexport function urlParamsFor(\n matcher: Matcher,\n name: string,\n cache: Map<string, string[]>,\n): string[] {\n const cached = cache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const segments = matcher.getSegmentsByName(name);\n const result = segments\n ? collectUrlParamsArray(segments as readonly RouteTree[])\n : [];\n\n cache.set(name, result);\n\n return result;\n}\n\n/** Store-bound {@link urlParamsFor}. */\nexport function urlParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): string[] {\n return urlParamsFor(store.matcher, name, store.urlParamsCache);\n}\n\n/**\n * The route's declared `?query` names minus its path slots — the registry that\n * both classifies and PRINTS (#1556), with the `/items/:id?id` carve-out\n * (#843 / #1549) falling out of the subtraction rather than being re-decided.\n */\nexport function queryParamsFor(\n matcher: Matcher,\n name: string,\n urlCache: Map<string, string[]>,\n queryCache: Map<string, string[]>,\n): string[] {\n const cached = queryCache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const declared = matcher.getDeclaredQueryParams(name);\n let result: string[] = [];\n\n if (declared) {\n const urlParams = urlParamsFor(matcher, name, urlCache);\n\n result = declared.filter((param: string) => !urlParams.includes(param));\n }\n\n queryCache.set(name, result);\n\n return result;\n}\n\n/** Store-bound {@link queryParamsFor}. */\nexport function queryParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): string[] {\n return queryParamsFor(\n store.matcher,\n name,\n store.urlParamsCache,\n store.queryParamsCache,\n );\n}\n\n/**\n * Store-layer adapter for {@link assertRouteDefaultChannels}: supplies the\n * declared-query accessor the pure rule takes as data.\n *\n * The caches are LOCAL to the attempt, not the store's, and that is the whole\n * reason this adapter exists rather than the four entry points each building the\n * closure. Every caller runs on PREPARED artifacts, before any swap: validating\n * against the store's caches would answer about a tree the rejected batch has\n * not installed — and the guard would then be checking the wrong config while\n * claiming to protect the right one.\n */\nexport function assertRouteDefaultChannelsFor(\n matcher: Matcher,\n config: RouteConfig,\n method: string,\n): void {\n const urlCache = new Map<string, string[]>();\n const queryCache = new Map<string, string[]>();\n\n assertRouteDefaultChannels(\n config.defaultParams,\n (name) => queryParamsFor(matcher, name, urlCache, queryCache),\n method,\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"helpers.js","names":["areParamValuesEqual"],"sources":["../../../../src/namespaces/RoutesNamespace/helpers.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/helpers.ts\n\nimport { assertRouteDefaultChannels } from \"../../channels\";\nimport { areParamValuesEqual } from \"../../helpers\";\n\nimport type { RoutesStore } from \"./routesStore\";\nimport type { RouteConfig } from \"./types\";\nimport type { Matcher, RouteDefinition, RouteTree } from \"../../engine\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n Params,\n ParamsSearch,\n SearchParams,\n Route,\n} from \"../../types\";\n\n/**\n * Creates an empty RouteConfig.\n */\nexport function createEmptyConfig(): RouteConfig {\n return {\n decoders: Object.create(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n encoders: Object.create(null) as Record<\n string,\n (channels: ParamsSearch) => ParamsSearch\n >,\n defaultParams: Object.create(null) as Record<string, Params>,\n defaultSearch: Object.create(null) as Record<string, SearchParams>,\n forwardMap: Object.create(null) as Record<string, string>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n forwardFnMap: Object.create(null) as Record<string, ForwardToCallback<any>>,\n };\n}\n\n/**\n * Does ANY route in this config forward? The tree-wide answer behind\n * `RoutesStore.hasAnyForward` — see that field for why one boolean is worth\n * deriving (#1595).\n *\n * Deliberately NOT a field on {@link RouteConfig}: {@link assignConfigEntries}\n * enumerates that interface's values and assumes every one is a record object,\n * so a boolean there would be copied as if it had entries.\n */\nexport function anyForwardConfigured(config: RouteConfig): boolean {\n return (\n Object.keys(config.forwardMap).length > 0 ||\n Object.keys(config.forwardFnMap).length > 0\n );\n}\n\n/**\n * Copies every {@link RouteConfig} sub-map's entries from `source` into\n * `target` (shallow per map — entries are shared by reference). Driven by\n * `Object.keys(source)` instead of one `Object.assign` per field, so a newly\n * added config sub-field is carried over automatically with nothing to forget\n * at each copy site (#965). Both configs are produced by\n * {@link createEmptyConfig}, so every key in `source` also exists on `target`,\n * and every value is a record object — the invariant this enumeration relies on.\n */\nexport function assignConfigEntries(\n target: RouteConfig,\n source: RouteConfig,\n): void {\n for (const key of Object.keys(source) as (keyof RouteConfig)[]) {\n Object.assign(target[key], source[key]);\n }\n}\n\n// ============================================================================\n// Route Tree Helpers\n// ============================================================================\n\n/**\n * Checks if all params from source exist with same values in target.\n * Small function body allows V8 inlining.\n */\nexport function paramsMatch(source: Params, target: Params): boolean {\n for (const key in source) {\n // Provenance-tolerant per value (#1554) — the hierarchical isActiveRoute\n // branch compares a caller bag against the COMMITTED state, whose values\n // may have come from the URL parser (`?tab=2` → `2`) while the caller wrote\n // strings. Same predicate as the exact branch (`areStatesEqual`), so both\n // branches answer identically for one location.\n if (!areParamValuesEqual(source[key], target[key])) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Sanitizes a route by keeping only essential properties.\n */\nexport function sanitizeRoute<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n): RouteDefinition {\n const sanitized: RouteDefinition = {\n name: route.name,\n path: route.path,\n };\n\n if (route.children) {\n sanitized.children = route.children.map((child) => sanitizeRoute(child));\n }\n\n return sanitized;\n}\n\n/**\n * Recursively removes a route from definitions array.\n */\nexport function removeFromDefinitions(\n definitions: RouteDefinition[],\n routeName: string,\n parentPrefix = \"\",\n): boolean {\n for (let i = 0; i < definitions.length; i++) {\n const route = definitions[i];\n const fullName = parentPrefix\n ? `${parentPrefix}.${route.name}`\n : route.name;\n\n if (fullName === routeName) {\n definitions.splice(i, 1);\n\n return true;\n }\n\n if (\n route.children &&\n routeName.startsWith(`${fullName}.`) &&\n removeFromDefinitions(route.children, routeName, fullName)\n ) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Clears configuration entries that match the predicate.\n */\nexport function clearConfigEntries<T>(\n config: Record<string, T>,\n matcher: (key: string) => boolean,\n): void {\n for (const key of Object.keys(config)) {\n if (matcher(key)) {\n delete config[key];\n }\n }\n}\n\n/**\n * Used by matchPath() when trailingSlash is \"preserve\": the matcher's\n * buildPath() with an unset trailingSlash mode strips trailing slashes,\n * but \"preserve\" means the source path's trailing-slash choice wins.\n * If the source had a trailing slash, re-attach it to the rewritten path.\n * The reverse case (rewritten has trailing, source does not) is not\n * reachable with the current matcher — it never adds a trailing slash\n * with undefined mode.\n */\nexport function matchSourceTrailingSlash(\n sourcePath: string,\n rewrittenPath: string,\n): string {\n const queryIndex = rewrittenPath.search(/[?#]/);\n const pathPart =\n queryIndex === -1 ? rewrittenPath : rewrittenPath.slice(0, queryIndex);\n\n // Stryker disable next-line LogicalOperator: equivalent — buildPath strips trailing slashes, so the rewritten path never ends with \"/\" unless it IS \"/\" (already caught by the `=== \"/\"` operand). `endsWith(\"/\")` is unreachable-true, so `||` ≡ `&&`.\n if (pathPart === \"/\" || pathPart.endsWith(\"/\")) {\n return rewrittenPath;\n }\n\n const sourceQueryIndex = sourcePath.search(/[?#]/);\n const sourcePathPart =\n sourceQueryIndex === -1\n ? sourcePath\n : sourcePath.slice(0, sourceQueryIndex);\n\n if (!(sourcePathPart.length > 1 && sourcePathPart.endsWith(\"/\"))) {\n return rewrittenPath;\n }\n\n const querySuffix = queryIndex === -1 ? \"\" : rewrittenPath.slice(queryIndex);\n\n return `${pathPart}/${querySuffix}`;\n}\n\n// =============================================================================\n// The query-channel registry — ONE derivation, shared by every reader (#1556)\n// =============================================================================\n\n/** Flattens the path-slot names declared across a route's matched segments. */\nexport function collectUrlParamsArray(\n segments: readonly RouteTree[],\n): string[] {\n const params: string[] = [];\n\n for (const segment of segments) {\n for (const param of segment.paramMeta.urlParams) {\n params.push(param);\n }\n }\n\n return params;\n}\n\n/**\n * The route's PATH slot names, cached per route name.\n *\n * Store-level rather than a namespace method so the config-time channel check\n * reads the SAME registry the URL build prints from. A second derivation is\n * exactly the drift #1556 removed.\n */\nexport function urlParamsFor(\n matcher: Matcher,\n name: string,\n cache: Map<string, string[]>,\n): string[] {\n const cached = cache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const segments = matcher.getSegmentsByName(name);\n const result = segments\n ? collectUrlParamsArray(segments as readonly RouteTree[])\n : [];\n\n cache.set(name, result);\n\n return result;\n}\n\n/** Store-bound {@link urlParamsFor}. */\nexport function urlParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): string[] {\n return urlParamsFor(store.matcher, name, store.urlParamsCache);\n}\n\n/**\n * The route's declared `?query` names minus its path slots — the registry that\n * both classifies and PRINTS (#1556), with the `/items/:id?id` carve-out\n * (#843 / #1549) falling out of the subtraction rather than being re-decided.\n */\nexport function queryParamsFor(\n matcher: Matcher,\n name: string,\n urlCache: Map<string, string[]>,\n queryCache: Map<string, string[]>,\n): string[] {\n const cached = queryCache.get(name);\n\n // Stryker disable next-line BlockStatement: equivalent — cache short-circuit; emptying the early-return recomputes the identical value (deterministic per route name) and re-caches it. (ConditionalExpression stays live: `→true` returns undefined on a cache miss = killed.)\n if (cached !== undefined) {\n return cached;\n }\n\n const declared = matcher.getDeclaredQueryParams(name);\n let result: string[] = [];\n\n if (declared) {\n const urlParams = urlParamsFor(matcher, name, urlCache);\n\n result = declared.filter((param: string) => !urlParams.includes(param));\n }\n\n queryCache.set(name, result);\n\n return result;\n}\n\n/** Store-bound {@link queryParamsFor}. */\nexport function queryParamsOf<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n name: string,\n): string[] {\n return queryParamsFor(\n store.matcher,\n name,\n store.urlParamsCache,\n store.queryParamsCache,\n );\n}\n\n/**\n * Store-layer adapter for {@link assertRouteDefaultChannels}: supplies the\n * declared-query accessor the pure rule takes as data.\n *\n * The caches are LOCAL to the attempt, not the store's, and that is the whole\n * reason this adapter exists rather than the four entry points each building the\n * closure. Every caller runs on PREPARED artifacts, before any swap: validating\n * against the store's caches would answer about a tree the rejected batch has\n * not installed — and the guard would then be checking the wrong config while\n * claiming to protect the right one.\n */\nexport function assertRouteDefaultChannelsFor(\n matcher: Matcher,\n config: RouteConfig,\n method: string,\n): void {\n const urlCache = new Map<string, string[]>();\n const queryCache = new Map<string, string[]>();\n\n assertRouteDefaultChannels(\n config.defaultParams,\n (name) => queryParamsFor(matcher, name, urlCache, queryCache),\n method,\n );\n}\n"],"mappings":"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"}
|
|
@@ -37,6 +37,25 @@ interface RoutesStore<Dependencies extends DefaultDependencies = DefaultDependen
|
|
|
37
37
|
*/
|
|
38
38
|
readonly queryParamsCache: Map<string, string[]>;
|
|
39
39
|
resolvedForwardMap: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Does ANY route in the tree forward? Read by `isActiveRoute` before its
|
|
42
|
+
* `forwardTo` arm's per-route gate, and worth its own field for a reason that
|
|
43
|
+
* is measurable rather than aesthetic (#1595): the two maps behind that gate
|
|
44
|
+
* are `Object.create(null)` dictionaries, which V8 keeps in dictionary mode,
|
|
45
|
+
* and two lookups on them cost ~14 ns — paid by every route in the tree for a
|
|
46
|
+
* feature only forwarding routes use, on every `<Link>` render across six
|
|
47
|
+
* adapters. A tree with no `forwardTo` at all — the common case — answers with
|
|
48
|
+
* one boolean load instead.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ Maintained ONLY through {@link adoptForwardState}, together with
|
|
51
|
+
* `resolvedForwardMap`. The two are views of the same config and a stale
|
|
52
|
+
* `false` here silently switches the arm OFF, which is a correctness bug
|
|
53
|
+
* wearing a performance change's clothes: a `<Link>` to a forwarding route
|
|
54
|
+
* would render inactive again (the defect #1573 shipped the arm to fix).
|
|
55
|
+
* Pinned across every route-CRUD path by `isActiveRoute-forward-arm` in
|
|
56
|
+
* `tests/functional/routes/is-active-route.test.ts`.
|
|
57
|
+
*/
|
|
58
|
+
hasAnyForward: boolean;
|
|
40
59
|
routeCustomFields: Record<string, Record<string, unknown>>;
|
|
41
60
|
rootPath: string;
|
|
42
61
|
readonly matcherOptions: CreateMatcherOptions | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routesStore.d.ts","names":[],"sources":["../../../../src/namespaces/RoutesNamespace/routesStore.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"routesStore.d.ts","names":[],"sources":["../../../../src/namespaces/RoutesNamespace/routesStore.ts"],"mappings":";;;;;;UA4CiB,YACf,qBAAqB,sBAAsB;;;;;;;;;;;;;WAclC,aAAa;WACb,QAAQ;EACjB,MAAM;EACN,SAAS;;;;;;;WAOA,gBAAgB;;;;;;;;WAQhB,kBAAkB;EAC3B,oBAAoB;;;;;;;;;;;;;;;;;;;EAoBpB;EACA,mBAAmB,eAAe;EAClC;WACS,gBAAgB;EACzB,WAAW,mBAAmB;EAC9B,oBAAoB,wBAAwB;WACnC,oBAAoB,YAAY,eAAe;WAC/C,sBAAsB,YAAY,eAAe"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../channels/guard.js"),t=require("../../engine/builder/createRouteTree.js"),n=require("../../engine/operations/routeTreeToDefinitions.js"),r=require("../../engine/createMatcher.js"),i=require("./constants.js"),a=require("./helpers.js"),o=require("./forwardChain.js");function s(e,n,i){let a=t.createRouteTree(``,n,e),o=r.createMatcher(i);return o.registerTree(a),{tree:a,matcher:o}}function c(e,t=e.definitions){let n=s(t,e.rootPath,e.matcherOptions);e.tree=n.tree,e.matcher=n.matcher,e.urlParamsCache.clear(),e.queryParamsCache.clear()}function l(e,t){let n=s(e.definitions,t,e.matcherOptions);a.assertRouteDefaultChannelsFor(n.matcher,e.config,`setRootPath`),e.rootPath=t,e.tree=n.tree,e.matcher=n.matcher,e.urlParamsCache.clear(),e.queryParamsCache.clear()}function u(e,t){c(e,t),
|
|
1
|
+
const e=require("../../channels/guard.js"),t=require("../../engine/builder/createRouteTree.js"),n=require("../../engine/operations/routeTreeToDefinitions.js"),r=require("../../engine/createMatcher.js"),i=require("./constants.js"),a=require("./helpers.js"),o=require("./forwardChain.js");function s(e,n,i){let a=t.createRouteTree(``,n,e),o=r.createMatcher(i);return o.registerTree(a),{tree:a,matcher:o}}function c(e,t=e.definitions){let n=s(t,e.rootPath,e.matcherOptions);e.tree=n.tree,e.matcher=n.matcher,e.urlParamsCache.clear(),e.queryParamsCache.clear()}function l(e,t){let n=s(e.definitions,t,e.matcherOptions);a.assertRouteDefaultChannelsFor(n.matcher,e.config,`setRootPath`),e.rootPath=t,e.tree=n.tree,e.matcher=n.matcher,e.urlParamsCache.clear(),e.queryParamsCache.clear()}function u(e,t){c(e,t),p(e,m(e.config))}function d(e){f(e),c(e,[])}function f(e){Object.assign(e.config,a.createEmptyConfig()),p(e,Object.create(null)),e.routeCustomFields=Object.create(null)}function p(e,t){e.resolvedForwardMap=t,e.hasAnyForward=a.anyForwardConfigured(e.config)}function m(e){let t=Object.create(null);for(let n of Object.keys(e.forwardMap))t[n]=o.resolveForwardChain(n,e.forwardMap);return t}function h(e,t){if(typeof e!=`function`)return;let n=e.constructor.name===`AsyncFunction`,r=e.toString().includes(`__awaiter`);if(n||r)throw TypeError(`forwardTo callback cannot be async for route "${t}". Async functions break matchPath/buildPath.`)}function g(e,t,n,r){if(e.canActivate){let n=typeof e.forwardTo==`string`?e.forwardTo:`[dynamic]`;r.warn(`real-router`,`Route "${t}" has both forwardTo and canActivate. canActivate will be ignored because forwardTo creates a redirect (industry standard). Move canActivate to the target route "${n}".`)}if(e.canDeactivate){let n=typeof e.forwardTo==`string`?e.forwardTo:`[dynamic]`;r.warn(`real-router`,`Route "${t}" has both forwardTo and canDeactivate. canDeactivate will be ignored because forwardTo creates a redirect (industry standard). Move canDeactivate to the target route "${n}".`)}h(e.forwardTo,t),typeof e.forwardTo==`string`?n.forwardMap[t]=e.forwardTo:n.forwardFnMap[t]=e.forwardTo}function _(e,t,n,r,a,o,s){let c=Object.fromEntries(Object.entries(e).filter(([e])=>!i.STANDARD_ROUTE_KEYS.has(e)));if(Object.keys(c).length>0&&(r[t]=c),e.canActivate&&a.set(t,e.canActivate),e.canDeactivate&&o.set(t,e.canDeactivate),e.forwardTo&&g(e,t,n,s),e.decodeParams){let r=e.decodeParams;n.decoders[t]=e=>r(e)??e}if(e.encodeParams){let r=e.encodeParams;n.encoders[t]=e=>r(e)??e}e.defaultParams&&(n.defaultParams[t]=e.defaultParams),e.defaultSearch&&(n.defaultSearch[t]=e.defaultSearch)}function v(e,t,n,r,i,a,o=``){for(let s of e){let e=o?`${o}.${s.name}`:s.name;_(s,e,t,n,r,i,a),s.children&&v(s.children,t,n,r,i,a,e)}}function y(e){let t=a.createEmptyConfig();return a.assignConfigEntries(t,e),t}function b(e,t,n){if(n.length===0)return[...e,...t];let[r,...i]=n;return e.map(e=>{if(e.name!==r)return e;let n=e.children??[];return{...e,children:i.length===0?[...n,...t]:b(n,t,i)}})}function x(e,t,n){for(let r of e){let e=t?`${t}.${r.name}`:r.name;n(e),r.children&&x(r.children,e,n)}}function S(e,t,n){let r=new Set;x(e,t,e=>{if(r.has(e))throw Error(`[router.${n}] Duplicate route "${e}" in batch`);r.add(e)})}function C(e,t){if(e.startsWith(`@@`))throw Error(`[router.${t}] Route name "${e}" uses the reserved "@@" prefix. Routes with this prefix are internal and cannot be modified through the public API.`)}function w(e,t){for(let n of e)C(n.name,t),n.children&&w(n.children,t)}function T(e,t,n){let r=new Map,i=(e,t)=>{for(let a of e){let e=r.get(t);if(e?.has(a.path))throw Error(`[router.${n}] Path "${a.path}" is already defined`);e?e.add(a.path):r.set(t,new Set([a.path])),a.children&&i(a.children,t?`${t}.${a.name}`:a.name)}};i(e,t)}function E(e,t,n){if(w(t,`addRoute`),n!==void 0&&!e.matcher.hasRoute(n))throw Error(`[router.addRoute] Parent route "${n}" does not exist`);x(t,n??``,t=>{if(e.matcher.hasRoute(t))throw Error(`[router.addRoute] Route "${t}" already exists`)}),S(t,n??``,`addRoute`),T(t,n??``,`addRoute`)}function D({definitions:e,routesForHandlers:t,config:n,routeCustomFields:r,handlerParentName:i,rootPath:a,matcherOptions:o,logger:c}){let l=new Map,u=new Map;v(t,n,r,l,u,c,i);let d=m(n),{tree:f,matcher:p}=s(e,a,o);return{config:n,routeCustomFields:r,pendingCanActivate:l,pendingCanDeactivate:u,tree:f,matcher:p,resolvedForwardMap:d}}function O(e,t,n,r){return D({definitions:b(e.definitions,t.map(e=>a.sanitizeRoute(e)),n===void 0?[]:n.split(`.`)),routesForHandlers:t,config:y(e.config),routeCustomFields:Object.assign(Object.create(null),e.routeCustomFields),handlerParentName:n??``,rootPath:e.rootPath,matcherOptions:e.matcherOptions,logger:r})}function k(e,t,n,r){return D({definitions:e.map(e=>a.sanitizeRoute(e)),routesForHandlers:e,config:a.createEmptyConfig(),routeCustomFields:Object.create(null),handlerParentName:``,rootPath:t,matcherOptions:n,logger:r})}function A(e,t,n){let r=[];for(let[i,a]of e)r.push([i,a,t(a,n)]);return r}function j(e,t){return{activate:A(e.pendingCanActivate,t.compileGuard,`canActivate`),deactivate:A(e.pendingCanDeactivate,t.compileGuard,`canDeactivate`)}}function M(e,t,n){let r=e.depsStore,{activate:i,deactivate:a}=n??j(t,r);Object.assign(e.config,t.config),e.routeCustomFields=t.routeCustomFields,e.tree=t.tree,e.matcher=t.matcher,e.urlParamsCache.clear(),e.queryParamsCache.clear(),p(e,t.resolvedForwardMap);for(let[e,t,n]of i)r.addActivateGuard(e,t,n);for(let[e,t,n]of a)r.addDeactivateGuard(e,t,n)}function N(t,n,r,i){let{forwardTo:o,defaultParams:s,defaultSearch:c,decodeParams:l,encodeParams:u,canActivate:d,canDeactivate:f}=i;s!=null&&e.assertChannelCorrect(`updateRoute`,r,s,a.queryParamsOf(t,r),"this route's `defaultParams`","Move it to `defaultSearch`");let m=o===void 0?void 0:P(r,o,t.config),h=F(t,r,i),g=d==null?void 0:n.compileGuardFactory(d,`canActivate`),_=f==null?void 0:n.compileGuardFactory(f,`canDeactivate`);return n.preflightHandlerLimit(g===void 0?[]:[r],_===void 0?[]:[r],!1),h!==void 0&&(Object.keys(h).length>0?t.routeCustomFields[r]=h:delete t.routeCustomFields[r]),m!==void 0&&(t.config.forwardMap=m.forwardMap,t.config.forwardFnMap=m.forwardFnMap,p(t,m.resolved)),L(t,r,{defaultParams:s,defaultSearch:c,decodeParams:l,encodeParams:u}),R(n,`activate`,r,d,g),R(n,`deactivate`,r,f,_),{forwardTo:o,defaultParams:s,defaultSearch:c,decodeParams:l,encodeParams:u}}function P(e,t,n){h(t,e);let r=Object.assign(Object.create(null),n.forwardMap),i=Object.assign(Object.create(null),n.forwardFnMap);return t===null?(delete r[e],delete i[e]):typeof t==`string`?(delete i[e],r[e]=t):(delete r[e],i[e]=t),{forwardMap:r,forwardFnMap:i,resolved:m({...n,forwardMap:r})}}function F(e,t,n){let r;for(let a of Object.keys(n)){if(i.STANDARD_ROUTE_KEYS.has(a))continue;let o=n[a];o!==void 0&&(r??={...e.routeCustomFields[t]},o===null?delete r[a]:r[a]=o)}return r}function I(e,t,n){n!==void 0&&(n===null?delete e[t]:e[t]=n)}function L(e,t,n){if(I(e.config.defaultParams,t,n.defaultParams),I(e.config.defaultSearch,t,n.defaultSearch),n.decodeParams!==void 0)if(n.decodeParams===null)delete e.config.decoders[t];else{let r=n.decodeParams;e.config.decoders[t]=e=>r(e)??e}if(n.encodeParams!==void 0)if(n.encodeParams===null)delete e.config.encoders[t];else{let r=n.encodeParams;e.config.encoders[t]=e=>r(e)??e}}function R(e,t,n,r,i){r!==void 0&&(t===`activate`?r===null?e.clearCanActivate(n,`definition`):e.addCanActivate(n,r,!0,i):r===null?e.clearCanDeactivate(n,`definition`):e.addCanDeactivate(n,r,!0,i))}function z(e,t,r){w(e,`addRoute`),S(e,``,`addRoute`);let i=k(e,``,t,r),o={get definitions(){return n.routeTreeToDefinitions(o.tree)},config:i.config,tree:i.tree,matcher:i.matcher,urlParamsCache:new Map,queryParamsCache:new Map,resolvedForwardMap:i.resolvedForwardMap,hasAnyForward:a.anyForwardConfigured(i.config),routeCustomFields:i.routeCustomFields,rootPath:``,matcherOptions:t,depsStore:void 0,lifecycleNamespace:void 0,pendingCanActivate:i.pendingCanActivate,pendingCanDeactivate:i.pendingCanDeactivate};return a.assertRouteDefaultChannelsFor(o.matcher,o.config,`addRoute`),o}exports.adoptRouteArtifacts=M,exports.applyRootPath=l,exports.assertAddable=E,exports.assertForwardToNotAsync=h,exports.assertNoDuplicateNamesInBatch=S,exports.assertNoDuplicatePathsInBatch=T,exports.assertNoInternalNamesInBatch=w,exports.assertNoInternalRouteName=C,exports.buildAddArtifacts=O,exports.buildReplaceArtifacts=k,exports.clearRouteData=f,exports.commitRouteUpdate=N,exports.commitTreeChanges=u,exports.compileArtifactGuards=j,exports.createRoutesStore=z,exports.rebuildTreeInPlace=c,exports.refreshForwardMap=m,exports.resetStore=d;
|
|
2
2
|
//# sourceMappingURL=routesStore.js.map
|