@real-router/core 0.83.1 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Router-BOyV6_me.js +2 -0
- package/dist/cjs/Router-BOyV6_me.js.map +1 -0
- package/dist/cjs/api/getRoutesApi.d.ts.map +1 -1
- package/dist/cjs/api.js +2 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/types/constants.d.ts +2 -1
- package/dist/cjs/types/constants.d.ts.map +1 -1
- package/dist/cjs/validation.js +2 -1
- package/dist/esm/Router-A9Vko7z1.mjs +2 -0
- package/dist/esm/Router-A9Vko7z1.mjs.map +1 -0
- package/dist/esm/api/getRoutesApi.d.mts.map +1 -1
- package/dist/esm/api.mjs +1 -1
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/constants.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/types/constants.d.mts +2 -1
- package/dist/esm/types/constants.d.mts.map +1 -1
- package/package.json +2 -2
- package/dist/cjs/Router-DDJ1AZzR.js +0 -2
- package/dist/cjs/Router-DDJ1AZzR.js.map +0 -1
- package/dist/cjs/Router.js +0 -2
- package/dist/cjs/Router.js.map +0 -1
- package/dist/cjs/RouterError.js +0 -2
- package/dist/cjs/RouterError.js.map +0 -1
- package/dist/cjs/api/cloneRouter.js +0 -2
- package/dist/cjs/api/cloneRouter.js.map +0 -1
- package/dist/cjs/api/getDependenciesApi.js +0 -2
- package/dist/cjs/api/getDependenciesApi.js.map +0 -1
- package/dist/cjs/api/getLifecycleApi.js +0 -2
- package/dist/cjs/api/getLifecycleApi.js.map +0 -1
- package/dist/cjs/api/getPluginApi.js +0 -2
- package/dist/cjs/api/getPluginApi.js.map +0 -1
- package/dist/cjs/api/getRoutesApi.js +0 -2
- package/dist/cjs/api/getRoutesApi.js.map +0 -1
- package/dist/cjs/api/helpers.js +0 -2
- package/dist/cjs/api/helpers.js.map +0 -1
- package/dist/cjs/channels/defaults.js +0 -2
- package/dist/cjs/channels/defaults.js.map +0 -1
- package/dist/cjs/channels/guard.js +0 -2
- package/dist/cjs/channels/guard.js.map +0 -1
- package/dist/cjs/channels/modeGate.js +0 -2
- package/dist/cjs/channels/modeGate.js.map +0 -1
- package/dist/cjs/constants.js +0 -2
- package/dist/cjs/constants.js.map +0 -1
- package/dist/cjs/createRouter.js +0 -2
- package/dist/cjs/createRouter.js.map +0 -1
- package/dist/cjs/engine/builder/buildTree.js +0 -2
- package/dist/cjs/engine/builder/buildTree.js.map +0 -1
- package/dist/cjs/engine/builder/computeCaches.js +0 -2
- package/dist/cjs/engine/builder/computeCaches.js.map +0 -1
- package/dist/cjs/engine/builder/createRouteTree.js +0 -2
- package/dist/cjs/engine/builder/createRouteTree.js.map +0 -1
- package/dist/cjs/engine/createMatcher.js +0 -2
- package/dist/cjs/engine/createMatcher.js.map +0 -1
- package/dist/cjs/engine/operations/routeTreeToDefinitions.js +0 -2
- package/dist/cjs/engine/operations/routeTreeToDefinitions.js.map +0 -1
- package/dist/cjs/engine/path-matcher/SegmentMatcher.js +0 -2
- package/dist/cjs/engine/path-matcher/SegmentMatcher.js.map +0 -1
- package/dist/cjs/engine/path-matcher/buildParamMeta.js +0 -2
- package/dist/cjs/engine/path-matcher/buildParamMeta.js.map +0 -1
- package/dist/cjs/engine/path-matcher/encoding.js +0 -2
- package/dist/cjs/engine/path-matcher/encoding.js.map +0 -1
- package/dist/cjs/engine/path-matcher/parseSegment.js +0 -2
- package/dist/cjs/engine/path-matcher/parseSegment.js.map +0 -1
- package/dist/cjs/engine/path-matcher/pathUtils.js +0 -2
- package/dist/cjs/engine/path-matcher/pathUtils.js.map +0 -1
- package/dist/cjs/engine/path-matcher/percentEncoding.js +0 -2
- package/dist/cjs/engine/path-matcher/percentEncoding.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/buildParts.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/buildParts.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/context.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/context.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/errors.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/errors.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/index.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/index.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/trie.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/trie.js.map +0 -1
- package/dist/cjs/engine/path-matcher/registration/trieNodes.js +0 -2
- package/dist/cjs/engine/path-matcher/registration/trieNodes.js.map +0 -1
- package/dist/cjs/engine/search-params/decode.js +0 -2
- package/dist/cjs/engine/search-params/decode.js.map +0 -1
- package/dist/cjs/engine/search-params/encode.js +0 -2
- package/dist/cjs/engine/search-params/encode.js.map +0 -1
- package/dist/cjs/engine/search-params/searchParams.js +0 -2
- package/dist/cjs/engine/search-params/searchParams.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/array.js +0 -2
- package/dist/cjs/engine/search-params/strategies/array.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/boolean.js +0 -2
- package/dist/cjs/engine/search-params/strategies/boolean.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/index.js +0 -2
- package/dist/cjs/engine/search-params/strategies/index.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/null.js +0 -2
- package/dist/cjs/engine/search-params/strategies/null.js.map +0 -1
- package/dist/cjs/engine/search-params/strategies/number.js +0 -2
- package/dist/cjs/engine/search-params/strategies/number.js.map +0 -1
- package/dist/cjs/engine/search-params/utils.js +0 -2
- package/dist/cjs/engine/search-params/utils.js.map +0 -1
- package/dist/cjs/engine/validation/route-batch.js +0 -2
- package/dist/cjs/engine/validation/route-batch.js.map +0 -1
- package/dist/cjs/engine/validation/routes.js +0 -2
- package/dist/cjs/engine/validation/routes.js.map +0 -1
- package/dist/cjs/getNavigator.js +0 -2
- package/dist/cjs/getNavigator.js.map +0 -1
- package/dist/cjs/guards.js +0 -2
- package/dist/cjs/guards.js.map +0 -1
- package/dist/cjs/helpers.js +0 -2
- package/dist/cjs/helpers.js.map +0 -1
- package/dist/cjs/internals.js +0 -2
- package/dist/cjs/internals.js.map +0 -1
- package/dist/cjs/limits.js +0 -2
- package/dist/cjs/limits.js.map +0 -1
- package/dist/cjs/namespaces/DependenciesNamespace/dependenciesStore.js +0 -2
- package/dist/cjs/namespaces/DependenciesNamespace/dependenciesStore.js.map +0 -1
- package/dist/cjs/namespaces/EventBusNamespace/EventBusNamespace.js +0 -2
- package/dist/cjs/namespaces/EventBusNamespace/EventBusNamespace.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/InFlightNavigation.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/NavigationNamespace.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/completeTransition.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/completeTransition.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/errorHandling.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/executeNavigation.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/guardPhase.js.map +0 -1
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js +0 -2
- package/dist/cjs/namespaces/NavigationNamespace/transition/navigateToNotFound.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/OptionsNamespace.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/OptionsNamespace.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/helpers.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/helpers.js.map +0 -1
- package/dist/cjs/namespaces/OptionsNamespace/validators.js +0 -2
- package/dist/cjs/namespaces/OptionsNamespace/validators.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/PluginsNamespace.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/PluginsNamespace.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/PluginsNamespace/validators.js +0 -2
- package/dist/cjs/namespaces/PluginsNamespace/validators.js.map +0 -1
- package/dist/cjs/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.js +0 -2
- package/dist/cjs/namespaces/RouteLifecycleNamespace/RouteLifecycleNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.js +0 -2
- package/dist/cjs/namespaces/RouterLifecycleNamespace/RouterLifecycleNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RouterLifecycleNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/RouterLifecycleNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/RoutesNamespace.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/constants.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/constants.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/forwardChain.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/forwardChain.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/helpers.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/routeGuards.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/routeGuards.js.map +0 -1
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js +0 -2
- package/dist/cjs/namespaces/RoutesNamespace/routesStore.js.map +0 -1
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js +0 -2
- package/dist/cjs/namespaces/StateNamespace/StateNamespace.js.map +0 -1
- package/dist/cjs/pipeline/buildURL.js +0 -2
- package/dist/cjs/pipeline/buildURL.js.map +0 -1
- package/dist/cjs/pipeline/canonicalize.js +0 -2
- package/dist/cjs/pipeline/canonicalize.js.map +0 -1
- package/dist/cjs/pipeline/materialize.js +0 -2
- package/dist/cjs/pipeline/materialize.js.map +0 -1
- package/dist/cjs/routerFSM.js +0 -2
- package/dist/cjs/routerFSM.js.map +0 -1
- package/dist/cjs/transitionPath.js +0 -2
- package/dist/cjs/transitionPath.js.map +0 -1
- package/dist/cjs/utils/event-emitter/EventEmitter.js +0 -2
- package/dist/cjs/utils/event-emitter/EventEmitter.js.map +0 -1
- package/dist/cjs/utils/fsm/fsm.js +0 -2
- package/dist/cjs/utils/fsm/fsm.js.map +0 -1
- package/dist/cjs/utils/logger/RouterLogger.js +0 -2
- package/dist/cjs/utils/logger/RouterLogger.js.map +0 -1
- package/dist/cjs/utils/logger/constants.js +0 -2
- package/dist/cjs/utils/logger/constants.js.map +0 -1
- package/dist/cjs/wiring/wireNamespaces.js +0 -2
- package/dist/cjs/wiring/wireNamespaces.js.map +0 -1
- package/dist/esm/Router-C7jMPCyc.mjs +0 -2
- package/dist/esm/Router-C7jMPCyc.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../../../src/namespaces/RoutesNamespace/constants.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/constants.ts\n\n/**\n * Default route name for the root node.\n */\nexport const DEFAULT_ROUTE_NAME = \"\";\n\n/**\n * Keys that belong to a route's structural/config surface — everything NOT in\n * this set is a plugin-defined **custom field** (e.g. lifecycle hooks,\n * `preload`, `searchSchema`), stored in `RoutesStore.routeCustomFields`.\n *\n * Single source of truth for the custom-field split, shared by route\n * registration (`add`/`replace`) and `update` so both classify patch keys\n * identically. `name`/`path`/`children` define route identity and are not\n * patchable via `update`; the remaining six are the structural/guard config.\n */\nexport const STANDARD_ROUTE_KEYS: ReadonlySet<string> = new Set([\n \"name\",\n \"path\",\n \"children\",\n \"canActivate\",\n \"canDeactivate\",\n \"forwardTo\",\n \"encodeParams\",\n \"decodeParams\",\n \"defaultParams\",\n]);\n"],"mappings":"AAiBA,MAAa,EAA2C,IAAI,IAAI,CAC9D,OACA,OACA,WACA,cACA,gBACA,YACA,eACA,eACA,eACF,CAAC"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e,t,n=100){let r=new Set,i=[e],a=e;for(;t[a];){let e=t[a];if(r.has(e)){let t=i.indexOf(e),n=[...i.slice(t),e];throw Error(`Circular forwardTo: ${n.join(` → `)}`)}if(r.add(a),i.push(e),a=e,i.length>n)throw Error(`forwardTo chain exceeds maximum depth (${n}): ${i.join(` → `)}`)}return a}exports.resolveForwardChain=e;
|
|
2
|
-
//# sourceMappingURL=forwardChain.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"forwardChain.js","names":[],"sources":["../../../../src/namespaces/RoutesNamespace/forwardChain.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/forwardChain.ts\n\nexport function resolveForwardChain(\n startRoute: string,\n forwardMap: Record<string, string>,\n maxDepth = 100,\n): string {\n const visited = new Set<string>();\n const chain: string[] = [startRoute];\n let current = startRoute;\n\n while (forwardMap[current]) {\n const next = forwardMap[current];\n\n if (visited.has(next)) {\n const cycleStart = chain.indexOf(next);\n const cycle = [...chain.slice(cycleStart), next];\n\n throw new Error(`Circular forwardTo: ${cycle.join(\" → \")}`);\n }\n\n visited.add(current);\n chain.push(next);\n current = next;\n\n if (chain.length > maxDepth) {\n throw new Error(\n `forwardTo chain exceeds maximum depth (${maxDepth}): ${chain.join(\" → \")}`,\n );\n }\n }\n\n return current;\n}\n"],"mappings":"AAEA,SAAgB,EACd,EACA,EACA,EAAW,IACH,CACR,IAAM,EAAU,IAAI,IACd,EAAkB,CAAC,CAAU,EAC/B,EAAU,EAEd,KAAO,EAAW,IAAU,CAC1B,IAAM,EAAO,EAAW,GAExB,GAAI,EAAQ,IAAI,CAAI,EAAG,CACrB,IAAM,EAAa,EAAM,QAAQ,CAAI,EAC/B,EAAQ,CAAC,GAAG,EAAM,MAAM,CAAU,EAAG,CAAI,EAE/C,MAAU,MAAM,uBAAuB,EAAM,KAAK,KAAK,GAAG,CAC5D,CAMA,GAJA,EAAQ,IAAI,CAAO,EACnB,EAAM,KAAK,CAAI,EACf,EAAU,EAEN,EAAM,OAAS,EACjB,MAAU,MACR,0CAA0C,EAAS,KAAK,EAAM,KAAK,KAAK,GAC1E,CAEJ,CAEA,OAAO,CACT"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|
-
//# sourceMappingURL=helpers.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
function e(e,t,n,r){if(t){let n=t===e,i=t.startsWith(`${e}.`);if(n||i){let i=n?``:` (current: "${t}")`;return r.warn(`router.removeRoute`,`Cannot remove route "${e}" — it is currently active${i}. Navigate away first.`),!1}}return n&&r.warn(`router.removeRoute`,`Route "${e}" removed while navigation is in progress. This may cause unexpected behavior.`),!0}function t(e,t){return e?(t.error(`router.clearRoutes`,`Cannot clear routes while navigation is in progress. Wait for navigation to complete.`),!1):!0}exports.validateClearRoutes=t,exports.validateRemoveRoute=e;
|
|
2
|
-
//# sourceMappingURL=routeGuards.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routeGuards.js","names":[],"sources":["../../../../src/namespaces/RoutesNamespace/routeGuards.ts"],"sourcesContent":["import type { RouterLogger } from \"../../types\";\n\n/**\n * Validates removeRoute constraints.\n * Returns false if removal should be blocked (route is active).\n * Logs warnings for edge cases.\n *\n * @param name - Route name to remove\n * @param currentStateName - Current active route name (or undefined)\n * @param isNavigating - Whether navigation is in progress\n * @param logger - Per-router logger instance (from `getInternals(router).logger`)\n * @returns true if removal can proceed, false if blocked\n */\nexport function validateRemoveRoute(\n name: string,\n currentStateName: string | undefined,\n isNavigating: boolean,\n logger: RouterLogger,\n): boolean {\n if (currentStateName) {\n const isExactMatch = currentStateName === name;\n const isParentOfCurrent = currentStateName.startsWith(`${name}.`);\n\n if (isExactMatch || isParentOfCurrent) {\n const suffix = isExactMatch ? \"\" : ` (current: \"${currentStateName}\")`;\n\n logger.warn(\n \"router.removeRoute\",\n `Cannot remove route \"${name}\" — it is currently active${suffix}. Navigate away first.`,\n );\n\n return false;\n }\n }\n\n if (isNavigating) {\n logger.warn(\n \"router.removeRoute\",\n `Route \"${name}\" removed while navigation is in progress. This may cause unexpected behavior.`,\n );\n }\n\n return true;\n}\n\n/**\n * Validates clearRoutes operation.\n * Returns false if operation should be blocked (navigation in progress).\n *\n * @param isNavigating - Whether navigation is in progress\n * @param logger - Per-router logger instance (from `getInternals(router).logger`)\n * @returns true if clearRoutes can proceed, false if blocked\n */\nexport function validateClearRoutes(\n isNavigating: boolean,\n logger: RouterLogger,\n): boolean {\n if (isNavigating) {\n logger.error(\n \"router.clearRoutes\",\n \"Cannot clear routes while navigation is in progress. Wait for navigation to complete.\",\n );\n\n return false;\n }\n\n return true;\n}\n"],"mappings":"AAaA,SAAgB,EACd,EACA,EACA,EACA,EACS,CACT,GAAI,EAAkB,CACpB,IAAM,EAAe,IAAqB,EACpC,EAAoB,EAAiB,WAAW,GAAG,EAAK,EAAE,EAEhE,GAAI,GAAgB,EAAmB,CACrC,IAAM,EAAS,EAAe,GAAK,eAAe,EAAiB,IAOnE,OALA,EAAO,KACL,qBACA,wBAAwB,EAAK,4BAA4B,EAAO,uBAClE,EAEO,EACT,CACF,CASA,OAPI,GACF,EAAO,KACL,qBACA,UAAU,EAAK,+EACjB,EAGK,EACT,CAUA,SAAgB,EACd,EACA,EACS,CAUT,OATI,GACF,EAAO,MACL,qBACA,uFACF,EAEO,IAGF,EACT"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|
-
//# sourceMappingURL=routesStore.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routesStore.js","names":["createRouteTree","createMatcher","createEmptyConfig","anyForwardConfigured","resolveForwardChain","STANDARD_ROUTE_KEYS","sanitizeRoute","queryParamsOf","routeTreeToDefinitions"],"sources":["../../../../src/namespaces/RoutesNamespace/routesStore.ts"],"sourcesContent":["// packages/core/src/namespaces/RoutesNamespace/routesStore.ts\n\nimport { DEFAULT_ROUTE_NAME, STANDARD_ROUTE_KEYS } from \"./constants\";\nimport { resolveForwardChain } from \"./forwardChain\";\nimport {\n anyForwardConfigured,\n assertRouteDefaultChannelsFor,\n assignConfigEntries,\n createEmptyConfig,\n queryParamsOf,\n sanitizeRoute,\n} from \"./helpers\";\nimport { assertChannelCorrect } from \"../../channels\";\nimport {\n createMatcher,\n createRouteTree,\n routeTreeToDefinitions,\n} from \"../../engine\";\n\nimport type { RouteConfig, RoutesDependencies } from \"./types\";\nimport type {\n CreateMatcherOptions,\n Matcher,\n RouteDefinition,\n RouteTree,\n} from \"../../engine\";\nimport type {\n DefaultDependencies,\n ForwardToCallback,\n GuardFn,\n Params,\n ParamsSearch,\n SearchParams,\n RouteConfigUpdate,\n RouterLogger,\n GuardFnFactory,\n Route,\n} from \"../../types\";\nimport type { RouteLifecycleNamespace } from \"../RouteLifecycleNamespace\";\n\n// =============================================================================\n// Interfaces\n// =============================================================================\n\nexport interface RoutesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n /**\n * DERIVED VIEW, not stored state: reconstructed from `tree` on every access\n * via `routeTreeToDefinitions` (the lossless inverse cloneRouter already\n * relies on — the `~` absolute marker is restored, child order is the\n * definition order). The tree is the single source of truth, so a third\n * retained copy of the route table (~30 B/route) is not kept. Every reader\n * is a cold CRUD/plugin-registration path; the derive is O(N).\n *\n * The returned array is a FRESH snapshot each time — mutating it never\n * affects the store (pass an explicitly-mutated snapshot to\n * `commitTreeChanges` instead, as `remove` does).\n */\n readonly definitions: RouteDefinition[];\n readonly config: RouteConfig;\n tree: RouteTree;\n matcher: Matcher;\n /**\n * Per-route-name cache of URL (path) param names, derived from `matcher` and\n * read by `RoutesNamespace.getUrlParams` (powers `areStatesEqual` /\n * `isActiveRoute`). Cleared on every `matcher` rebuild so comparisons never\n * stay frozen to a route's pre-mutation param shape (#723).\n */\n readonly urlParamsCache: Map<string, string[]>;\n /**\n * Per-route-name cache of declared query param names (`?a&b` across the\n * route's segments), read by `RoutesNamespace.getQueryParams` — THE registry\n * every channel mechanism classifies through and the URL build prints from\n * (#1556). Same lifecycle as `urlParamsCache`: cleared on every `matcher`\n * rebuild.\n */\n readonly queryParamsCache: Map<string, string[]>;\n resolvedForwardMap: Record<string, string>;\n\n /**\n * Does ANY route in the tree forward? Read by `isActiveRoute` before its\n * `forwardTo` arm's per-route gate, and worth its own field for a reason that\n * is measurable rather than aesthetic (#1595): the two maps behind that gate\n * are `Object.create(null)` dictionaries, which V8 keeps in dictionary mode,\n * and two lookups on them cost ~14 ns — paid by every route in the tree for a\n * feature only forwarding routes use, on every `<Link>` render across six\n * adapters. A tree with no `forwardTo` at all — the common case — answers with\n * one boolean load instead.\n *\n * ⚠ Maintained ONLY through {@link adoptForwardState}, together with\n * `resolvedForwardMap`. The two are views of the same config and a stale\n * `false` here silently switches the arm OFF, which is a correctness bug\n * wearing a performance change's clothes: a `<Link>` to a forwarding route\n * would render inactive again (the defect #1573 shipped the arm to fix).\n * Pinned across every route-CRUD path by `isActiveRoute-forward-arm` in\n * `tests/functional/routes/is-active-route.test.ts`.\n */\n hasAnyForward: boolean;\n routeCustomFields: Record<string, Record<string, unknown>>;\n rootPath: string;\n readonly matcherOptions: CreateMatcherOptions | undefined;\n depsStore: RoutesDependencies<Dependencies> | undefined;\n lifecycleNamespace: RouteLifecycleNamespace<Dependencies> | undefined;\n readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;\n}\n\n// =============================================================================\n// Tree operations\n// =============================================================================\n\nfunction rebuildTree(\n definitions: readonly RouteDefinition[],\n rootPath: string,\n matcherOptions: CreateMatcherOptions | undefined,\n): { tree: RouteTree; matcher: Matcher } {\n const tree = createRouteTree(DEFAULT_ROUTE_NAME, rootPath, definitions);\n const matcher = createMatcher(matcherOptions);\n\n matcher.registerTree(tree);\n\n return { tree, matcher };\n}\n\n/**\n * Rebuilds tree+matcher in place from `definitions` (defaults to the current\n * tree's own derived definitions — the same-table case, e.g. a rootPath\n * change).\n */\nexport function rebuildTreeInPlace<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n definitions: readonly RouteDefinition[] = store.definitions,\n): void {\n const result = rebuildTree(definitions, store.rootPath, store.matcherOptions);\n\n store.tree = result.tree;\n store.matcher = result.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n}\n\n/**\n * Prepare-then-commit root-path change.\n *\n * A root `?`-declaration declares the name on EVERY route at once, so a\n * `defaultParams` that was legal a moment ago can stop being legal without any\n * route changing — the one mutation where re-checking the WHOLE config is not\n * redundant. Built into locals first so a rejected root path leaves the store\n * exactly as it was, matching the atomicity `add` / `replace` promise.\n */\nexport function applyRootPath<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>, newRootPath: string): void {\n const prepared = rebuildTree(\n store.definitions,\n newRootPath,\n store.matcherOptions,\n );\n\n assertRouteDefaultChannelsFor(prepared.matcher, store.config, \"setRootPath\");\n\n store.rootPath = newRootPath;\n store.tree = prepared.tree;\n store.matcher = prepared.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n}\n\nexport function commitTreeChanges<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n definitions: readonly RouteDefinition[],\n): void {\n rebuildTreeInPlace(store, definitions);\n adoptForwardState(store, refreshForwardMap(store.config));\n}\n\n// =============================================================================\n// Store reset\n// =============================================================================\n\n/**\n * Clears all routes and resets config.\n * Does NOT clear lifecycle handlers or state — caller handles that.\n */\nexport function resetStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>): void {\n clearRouteData(store);\n rebuildTreeInPlace(store, []);\n}\n\n/**\n * Clears route data without rebuilding the tree.\n * Used by replace() to avoid double rebuild (clearRouteData + commitTreeChanges).\n * `definitions` needs no clearing — it is derived from the tree, which the\n * caller rebuilds (resetStore → empty, replace → the new artifacts).\n */\nexport function clearRouteData<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(store: RoutesStore<Dependencies>): void {\n Object.assign(store.config, createEmptyConfig());\n\n adoptForwardState(store, Object.create(null) as Record<string, string>);\n store.routeCustomFields = Object.create(null) as Record<\n string,\n Record<string, unknown>\n >;\n}\n\n// =============================================================================\n// Forward map\n// =============================================================================\n\n/**\n * The ONE way `resolvedForwardMap` and `hasAnyForward` move (#1595). They are two\n * views of the same forward config, so every site that re-derives one derives the\n * other here — a site that assigned only the map would leave a stale `false`\n * behind, and `isActiveRoute` would stop consulting its `forwardTo` arm.\n */\nfunction adoptForwardState<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n resolved: Record<string, string>,\n): void {\n store.resolvedForwardMap = resolved;\n store.hasAnyForward = anyForwardConfigured(store.config);\n}\n\nexport function refreshForwardMap(config: RouteConfig): Record<string, string> {\n const map = Object.create(null) as Record<string, string>;\n\n for (const fromRoute of Object.keys(config.forwardMap)) {\n map[fromRoute] = resolveForwardChain(fromRoute, config.forwardMap);\n }\n\n return map;\n}\n\n// =============================================================================\n// Route handler registration\n// =============================================================================\n\n/**\n * Throws if `forwardTo` is an async function (native or transpiled). Async\n * forwardTo callbacks break the synchronous matchPath/buildPath contract.\n * Runs inside `registerForwardTo` (the add/replace build path, before any store\n * mutation) AND inside `getRoutesApi`'s `updateForwardTo` (the update path), so\n * `update(name, { forwardTo: async })` is rejected at registration with the same\n * actionable error instead of deferring a generic TypeError to navigation (#967).\n */\nexport function assertForwardToNotAsync(\n forwardTo: unknown,\n fullName: string,\n): void {\n if (typeof forwardTo !== \"function\") {\n return;\n }\n\n const isNativeAsync =\n (forwardTo as { constructor: { name: string } }).constructor.name ===\n \"AsyncFunction\";\n const isTranspiledAsync = (forwardTo as { toString: () => string })\n .toString()\n .includes(\"__awaiter\");\n\n if (isNativeAsync || isTranspiledAsync) {\n throw new TypeError(\n `forwardTo callback cannot be async for route \"${fullName}\". ` +\n `Async functions break matchPath/buildPath.`,\n );\n }\n}\n\nfunction registerForwardTo<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n fullName: string,\n config: RouteConfig,\n logger: RouterLogger,\n): void {\n if (route.canActivate) {\n /* v8 ignore next -- @preserve: edge case, both string and function tested separately */\n const forwardTarget =\n typeof route.forwardTo === \"string\" ? route.forwardTo : \"[dynamic]\";\n\n logger.warn(\n \"real-router\",\n `Route \"${fullName}\" has both forwardTo and canActivate. ` +\n `canActivate will be ignored because forwardTo creates a redirect (industry standard). ` +\n `Move canActivate to the target route \"${forwardTarget}\".`,\n );\n }\n\n if (route.canDeactivate) {\n /* v8 ignore next -- @preserve: edge case, both string and function tested separately */\n const forwardTarget =\n typeof route.forwardTo === \"string\" ? route.forwardTo : \"[dynamic]\";\n\n logger.warn(\n \"real-router\",\n `Route \"${fullName}\" has both forwardTo and canDeactivate. ` +\n `canDeactivate will be ignored because forwardTo creates a redirect (industry standard). ` +\n `Move canDeactivate to the target route \"${forwardTarget}\".`,\n );\n }\n\n assertForwardToNotAsync(route.forwardTo, fullName);\n\n // forwardTo is guaranteed to exist at this point\n if (typeof route.forwardTo === \"string\") {\n config.forwardMap[fullName] = route.forwardTo;\n } else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n config.forwardFnMap[fullName] = route.forwardTo!;\n }\n}\n\nfunction registerSingleRouteHandlers<Dependencies extends DefaultDependencies>(\n route: Route<Dependencies>,\n fullName: string,\n config: RouteConfig,\n routeCustomFields: Record<string, Record<string, unknown>>,\n pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,\n pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,\n logger: RouterLogger,\n): void {\n const customFields = Object.fromEntries(\n Object.entries(route).filter(([key]) => !STANDARD_ROUTE_KEYS.has(key)),\n );\n\n if (Object.keys(customFields).length > 0) {\n routeCustomFields[fullName] = customFields;\n }\n\n // Guards are collected here and registered into the lifecycle later — by\n // `adoptRouteArtifacts` (add/replace) or `RoutesNamespace.flushPendingGuards`\n // (initial routes, the final step of the Router constructor — #1331) — so\n // the build stays a pure, side-effect-free preparation step.\n if (route.canActivate) {\n pendingCanActivate.set(fullName, route.canActivate);\n }\n\n if (route.canDeactivate) {\n pendingCanDeactivate.set(fullName, route.canDeactivate);\n }\n\n if (route.forwardTo) {\n registerForwardTo(route, fullName, config, logger);\n }\n\n if (route.decodeParams) {\n const decode = route.decodeParams;\n\n config.decoders[fullName] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if a user-provided decoder violates its `{ params, search }` return type\n decode(channels) ?? channels;\n }\n\n if (route.encodeParams) {\n const encode = route.encodeParams;\n\n config.encoders[fullName] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if a user-provided encoder violates its `{ params, search }` return type\n encode(channels) ?? channels;\n }\n\n if (route.defaultParams) {\n config.defaultParams[fullName] = route.defaultParams;\n }\n\n if (route.defaultSearch) {\n config.defaultSearch[fullName] = route.defaultSearch;\n }\n}\n\nfunction registerAllRouteHandlers<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n config: RouteConfig,\n routeCustomFields: Record<string, Record<string, unknown>>,\n pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>,\n pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>,\n logger: RouterLogger,\n parentName = \"\",\n): void {\n for (const route of routes) {\n const fullName = parentName ? `${parentName}.${route.name}` : route.name;\n\n registerSingleRouteHandlers(\n route,\n fullName,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n );\n\n if (route.children) {\n registerAllRouteHandlers(\n route.children,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n fullName,\n );\n }\n }\n}\n\n// =============================================================================\n// Prepare-then-commit (issue #698)\n//\n// add()/replace() build the complete new store state into LOCAL structures, and\n// only swap it into the store once every core-level error has surfaced from the\n// build itself (async/circular forwardTo throw in registerAllRouteHandlers /\n// refreshForwardMap; invalid path constraint throws in rebuildTree). The store\n// is mutated only by `adoptRouteArtifacts`, which compiles every prepared guard\n// factory BEFORE the swap (#956): a factory that throws on compile (or returns a\n// non-function) aborts there, with the store still untouched. So all error\n// classes — core-level build errors AND malformed guard factories — surface\n// before any mutation, leaving the existing routes intact (full atomicity). The\n// silent-corruption cases route-tree never throws on (duplicate name vs an\n// existing route, a name duplicated within the batch, missing parent) are caught\n// up front by `assertAddable`.\n// =============================================================================\n\n/**\n * The fully-built, ready-to-swap result of preparing a route mutation. Holds\n * everything `adoptRouteArtifacts` assigns into the store.\n */\ninterface RouteArtifacts<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n> {\n readonly config: RouteConfig;\n readonly routeCustomFields: Record<string, Record<string, unknown>>;\n readonly pendingCanActivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly pendingCanDeactivate: Map<string, GuardFnFactory<Dependencies>>;\n readonly tree: RouteTree;\n readonly matcher: Matcher;\n readonly resolvedForwardMap: Record<string, string>;\n}\n\n/** Null-proto shallow clone of a RouteConfig (preserves every sub-map's contents). */\nfunction cloneConfig(config: RouteConfig): RouteConfig {\n const clone = createEmptyConfig();\n\n assignConfigEntries(clone, config);\n\n return clone;\n}\n\n/**\n * Returns a new definitions array with `added` inserted, without mutating the\n * input. For a top-level add the existing definitions are shallow-copied and\n * `added` appended. For a parented add the spine down to the parent is cloned\n * (siblings/other branches are shared by reference) and `added` appended to the\n * parent's children. Caller guarantees the parent path exists (see assertAddable).\n */\nfunction insertAddedDefinitions(\n definitions: readonly RouteDefinition[],\n added: RouteDefinition[],\n parentSegments: readonly string[],\n): RouteDefinition[] {\n if (parentSegments.length === 0) {\n return [...definitions, ...added];\n }\n\n const [head, ...rest] = parentSegments;\n\n return definitions.map((def) => {\n if (def.name !== head) {\n return def;\n }\n\n const children = def.children ?? [];\n\n return {\n ...def,\n children:\n rest.length === 0\n ? [...children, ...added]\n : insertAddedDefinitions(children, added, rest),\n };\n });\n}\n\n/** Depth-first walk yielding each route's full dotted name (no side effects). */\nfunction walkRouteNames<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n onName: (fullName: string) => void,\n): void {\n for (const route of routes) {\n const fullName = parentName ? `${parentName}.${route.name}` : route.name;\n\n onName(fullName);\n\n if (route.children) {\n walkRouteNames(route.children, fullName, onName);\n }\n }\n}\n\n/**\n * Rejects a route name duplicated WITHIN a single batch — the silent-overwrite\n * case route-tree stays last-wins on (#953 for `add`, #968 for `replace`). Walks\n * the same depth-first dotted names, but tracks them in a local Set: a name seen\n * twice in one array means the caller's second route would silently shadow the\n * first (`matchPath` for the first route's path becomes unreachable). Mirrors\n * validation-plugin's batch-dup message (route-tree `checkBatchNameDuplicate`)\n * so the no-plugin error matches the with-plugin one. `methodName` is \"addRoute\"\n * for both add and replace — the plugin reports \"addRoute\" for replace batches\n * too, so this keeps with/without-plugin parity.\n */\nexport function assertNoDuplicateNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n methodName: string,\n): void {\n const seen = new Set<string>();\n\n walkRouteNames(routes, parentName, (fullName) => {\n if (seen.has(fullName)) {\n throw new Error(\n `[router.${methodName}] Duplicate route \"${fullName}\" in batch`,\n );\n }\n\n seen.add(fullName);\n });\n}\n\nconst INTERNAL_ROUTE_PREFIX = \"@@\";\n\n/**\n * Rejects a single (bare) route name that uses the reserved \"@@\" prefix —\n * internal / system names such as UNKNOWN_ROUTE (`\"@@router/UNKNOWN_ROUTE\"`).\n * Mutating such a name would let a real URL `matchPath` to a state with\n * `name === UNKNOWN_ROUTE`, silently conflating a genuine route with \"not\n * found\". This always-on guard protected all four mutators (#238) until the\n * validation-extraction (`d1ebff80`) demoted it to the opt-in\n * validation-plugin; only `add` was restored (#954), so `remove`/`update`\n * regained it via this helper (#1047). Mirrors validation-plugin's\n * `throwIfInternalRoute` message so the no-plugin error matches the with-plugin\n * one.\n */\nexport function assertNoInternalRouteName(\n name: string,\n methodName: string,\n): void {\n if (name.startsWith(INTERNAL_ROUTE_PREFIX)) {\n throw new Error(\n `[router.${methodName}] Route name \"${name}\" uses the reserved \"${INTERNAL_ROUTE_PREFIX}\" prefix. Routes with this prefix are internal and cannot be modified through the public API.`,\n );\n }\n}\n\n/**\n * Batch counterpart to {@link assertNoInternalRouteName}: rejects any route in\n * the batch (recursing children) whose BARE leaf name uses the reserved \"@@\"\n * prefix (the prefix is on the leaf, not the dotted fullName). Used by `add`\n * (#954) and `replace` (#1047).\n */\nexport function assertNoInternalNamesInBatch<\n Dependencies extends DefaultDependencies,\n>(routes: readonly Route<Dependencies>[], methodName: string): void {\n for (const route of routes) {\n assertNoInternalRouteName(route.name, methodName);\n\n if (route.children) {\n assertNoInternalNamesInBatch(route.children, methodName);\n }\n }\n}\n\n/**\n * Rejects two routes that share the same `path` at the same parent level WITHIN\n * a single `add` batch (#955). The matcher resolves a path collision last-wins,\n * so the earlier route stays addressable by name (`has` / `buildPath`) but is\n * unreachable by URL (`matchPath` returns the later route) — a silent shadow.\n * Paths only collide among siblings, so seen paths are tracked per parent\n * fullName. Mirrors validation-plugin's message (route-tree\n * `checkBatchPathDuplicate`) so the no-plugin error matches the with-plugin one.\n * Scoped to the batch (not the existing tree) per #955 — the in-batch case the\n * issue describes.\n */\nexport function assertNoDuplicatePathsInBatch<\n Dependencies extends DefaultDependencies,\n>(\n routes: readonly Route<Dependencies>[],\n parentName: string,\n methodName: string,\n): void {\n const seenByParent = new Map<string, Set<string>>();\n\n const walk = (\n siblings: readonly Route<Dependencies>[],\n parent: string,\n ): void => {\n for (const route of siblings) {\n const paths = seenByParent.get(parent);\n\n if (paths?.has(route.path)) {\n throw new Error(\n `[router.${methodName}] Path \"${route.path}\" is already defined`,\n );\n }\n\n if (paths) {\n paths.add(route.path);\n } else {\n seenByParent.set(parent, new Set([route.path]));\n }\n\n if (route.children) {\n walk(route.children, parent ? `${parent}.${route.name}` : route.name);\n }\n }\n };\n\n walk(routes, parentName);\n}\n\n/**\n * Up-front guard for `add` against the corruptions route-tree stays silent on: a\n * missing `parent`, a name that collides with an EXISTING route, a name\n * duplicated WITHIN the batch, a reserved \"@@\"-prefixed name (which would shadow\n * an internal/system route name), and a path duplicated among siblings WITHIN\n * the batch (any of which would otherwise be silently overwritten/shadowed).\n * Throws before any build.\n */\nexport function assertAddable<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n routes: readonly Route<Dependencies>[],\n parentName: string | undefined,\n): void {\n assertNoInternalNamesInBatch(routes, \"addRoute\");\n\n if (parentName !== undefined && !store.matcher.hasRoute(parentName)) {\n throw new Error(\n `[router.addRoute] Parent route \"${parentName}\" does not exist`,\n );\n }\n\n walkRouteNames(routes, parentName ?? \"\", (fullName) => {\n if (store.matcher.hasRoute(fullName)) {\n throw new Error(`[router.addRoute] Route \"${fullName}\" already exists`);\n }\n });\n\n assertNoDuplicateNamesInBatch(routes, parentName ?? \"\", \"addRoute\");\n assertNoDuplicatePathsInBatch(routes, parentName ?? \"\", \"addRoute\");\n}\n\n/**\n * Builds RouteArtifacts from a final definitions array and the routes whose\n * handlers (config + guards) populate `config`/`routeCustomFields`. Guards are\n * collected into the returned pending maps (depsStore is intentionally omitted\n * so nothing compiles or touches the lifecycle here). THROWS on async/circular\n * forwardTo and invalid path constraint — before the caller mutates the store.\n *\n * Takes a single args object: the positional list hit 8 parameters (S107) when\n * the per-router `logger` (#724) joined it, and named fields read better at the\n * two call sites anyway.\n */\nfunction buildArtifacts<Dependencies extends DefaultDependencies>({\n definitions,\n routesForHandlers,\n config,\n routeCustomFields,\n handlerParentName,\n rootPath,\n matcherOptions,\n logger,\n}: {\n definitions: readonly RouteDefinition[];\n routesForHandlers: readonly Route<Dependencies>[];\n config: RouteConfig;\n routeCustomFields: Record<string, Record<string, unknown>>;\n handlerParentName: string;\n rootPath: string;\n matcherOptions: CreateMatcherOptions | undefined;\n logger: RouterLogger;\n}): RouteArtifacts<Dependencies> {\n const pendingCanActivate = new Map<string, GuardFnFactory<Dependencies>>();\n const pendingCanDeactivate = new Map<string, GuardFnFactory<Dependencies>>();\n\n registerAllRouteHandlers(\n routesForHandlers,\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n logger,\n handlerParentName,\n );\n\n const resolvedForwardMap = refreshForwardMap(config);\n const { tree, matcher } = rebuildTree(definitions, rootPath, matcherOptions);\n\n return {\n config,\n routeCustomFields,\n pendingCanActivate,\n pendingCanDeactivate,\n tree,\n matcher,\n resolvedForwardMap,\n };\n}\n\n/** Builds the merged artifacts for an incremental `add` (existing ∪ new). */\nexport function buildAddArtifacts<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n routes: readonly Route<Dependencies>[],\n parentName: string | undefined,\n logger: RouterLogger,\n): RouteArtifacts<Dependencies> {\n const definitions = insertAddedDefinitions(\n store.definitions,\n routes.map((route) => sanitizeRoute(route)),\n parentName === undefined ? [] : parentName.split(\".\"),\n );\n\n return buildArtifacts({\n definitions,\n routesForHandlers: routes,\n config: cloneConfig(store.config),\n routeCustomFields: Object.assign(\n Object.create(null) as Record<string, Record<string, unknown>>,\n store.routeCustomFields,\n ),\n handlerParentName: parentName ?? \"\",\n rootPath: store.rootPath,\n matcherOptions: store.matcherOptions,\n logger,\n });\n}\n\n/** Builds the fresh artifacts for a full `replace` (standalone new set). */\nexport function buildReplaceArtifacts<Dependencies extends DefaultDependencies>(\n routes: readonly Route<Dependencies>[],\n rootPath: string,\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n): RouteArtifacts<Dependencies> {\n return buildArtifacts({\n definitions: routes.map((route) => sanitizeRoute(route)),\n routesForHandlers: routes,\n config: createEmptyConfig(),\n routeCustomFields: Object.create(null) as Record<\n string,\n Record<string, unknown>\n >,\n handlerParentName: \"\",\n rootPath,\n matcherOptions,\n logger,\n });\n}\n\n/**\n * Compiles every pending guard factory up front, returning\n * `[name, factory, compiledFn]` triples for installation. THROWS from `compile`\n * on the first factory that throws on compile or returns a non-function — the\n * pre-swap validation that makes `adoptRouteArtifacts` atomic for malformed\n * guards (#956). Compiling here (not at install) means a factory with\n * compile-time side effects runs exactly once.\n */\nfunction compilePendingGuards<Dependencies extends DefaultDependencies>(\n pending: Map<string, GuardFnFactory<Dependencies>>,\n compile: (\n handler: GuardFnFactory<Dependencies>,\n methodName: string,\n ) => GuardFn,\n methodName: string,\n): [string, GuardFnFactory<Dependencies>, GuardFn][] {\n const compiled: [string, GuardFnFactory<Dependencies>, GuardFn][] = [];\n\n for (const [name, factory] of pending) {\n compiled.push([name, factory, compile(factory, methodName)]);\n }\n\n return compiled;\n}\n\n/** Pre-compiled guard triples for {@link adoptRouteArtifacts} install. */\nexport interface CompiledArtifactGuards<\n Dependencies extends DefaultDependencies,\n> {\n activate: [string, GuardFnFactory<Dependencies>, GuardFn][];\n deactivate: [string, GuardFnFactory<Dependencies>, GuardFn][];\n}\n\n/**\n * Compiles an artifacts' pending guard factories up front (#956), THROWING on\n * the first factory that throws on compile or returns a non-function.\n *\n * `replaceRoutes` calls this in its PREPARE phase — **before**\n * `clearDefinitionGuards()` — and hands the result to `adoptRouteArtifacts`, so\n * a compile-throw aborts with BOTH the tree AND the old definition guards intact\n * (#1193, mirroring #1046's handler-limit hoist). `add` has no clear step, so\n * `adoptRouteArtifacts` compiles inline for it.\n */\nexport function compileArtifactGuards<Dependencies extends DefaultDependencies>(\n artifacts: RouteArtifacts<Dependencies>,\n deps: RoutesDependencies<Dependencies>,\n): CompiledArtifactGuards<Dependencies> {\n return {\n activate: compilePendingGuards(\n artifacts.pendingCanActivate,\n deps.compileGuard,\n \"canActivate\",\n ),\n deactivate: compilePendingGuards(\n artifacts.pendingCanDeactivate,\n deps.compileGuard,\n \"canDeactivate\",\n ),\n };\n}\n\n/**\n * Commits prepared artifacts into the store in place. Every pending guard\n * factory is compiled BEFORE the tree/config swap (#956): a factory that throws\n * on compile (or returns a non-function) aborts here with the store untouched,\n * so `add`/`replace` are atomic for malformed guards too — not just core build\n * errors. The tree/config assignments are pure and cannot throw; the\n * pre-compiled guards are then installed without re-compiling (the factory ran\n * once, at the pre-compile above). `depsStore` is always set on a wired router,\n * which is the only path that reaches `add`/`replace`.\n *\n * ⚠ **The config-time channel check (`assertRouteDefaultChannels`) is the\n * CALLER's PREPARE step, not this function's.** It used to run here, one line\n * before the swap, which is early enough for `add` and too late for `replace`:\n * `replace` erases the old definition guards BEFORE calling this, so a batch\n * this check refused left the tree intact and the guards gone — a previously\n * guarded route freely activatable. That is the #1193 fail-open shape verbatim,\n * which is why the guard COMPILE was hoisted into the callers; the channel\n * check now sits beside it, for the same reason. Keeping this function\n * throw-free is what makes its \"atomic swap\" contract true rather than nearly\n * true.\n */\nexport function adoptRouteArtifacts<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n artifacts: RouteArtifacts<Dependencies>,\n precompiled?: CompiledArtifactGuards<Dependencies>,\n): void {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- depsStore is set once the router is wired; add/replace only run on a wired router (constructor-time registration uses createRoutesStore)\n const deps = store.depsStore!;\n\n // Pre-swap compile: surfaces a malformed guard factory before any mutation.\n // `replace()` pre-compiles in its PREPARE phase (BEFORE clearDefinitionGuards)\n // and passes the result here, so a compile-throw never erases the old\n // definition guards (#1193); `add` has no clear step and compiles inline.\n const { activate: compiledActivate, deactivate: compiledDeactivate } =\n precompiled ?? compileArtifactGuards(artifacts, deps);\n\n // Atomic swap — pure assignments, cannot throw. (`definitions` is derived\n // from `tree`, so swapping the tree IS the definitions swap.)\n Object.assign(store.config, artifacts.config);\n store.routeCustomFields = artifacts.routeCustomFields;\n store.tree = artifacts.tree;\n store.matcher = artifacts.matcher;\n store.urlParamsCache.clear();\n store.queryParamsCache.clear();\n adoptForwardState(store, artifacts.resolvedForwardMap);\n\n // Install pre-compiled guards — no re-compile, no throw.\n for (const [name, factory, fn] of compiledActivate) {\n deps.addActivateGuard(name, factory, fn);\n }\n\n for (const [name, factory, fn] of compiledDeactivate) {\n deps.addDeactivateGuard(name, factory, fn);\n }\n}\n\n/**\n * COMMIT core for `update()` — the field-patch counterpart to\n * {@link adoptRouteArtifacts} (add/replace) / {@link commitTreeChanges} (remove)\n * / {@link resetStore} (clear), co-located here so all four route-CRUD commit\n * cores live in one file. Stays **NO_TREE_REBUILD**: an O(1) per-field patch\n * that never rebuilds the tree/matcher (so it must NOT funnel through\n * `adoptRouteArtifacts`).\n *\n * Prepare-then-commit (#951): every throwing step runs in PREPARE, before any\n * store write, so a rejected update leaves the route's prior config fully intact\n * — an async/cyclic `forwardTo` (#967), a guard factory that throws on compile\n * (#956 seam), a throwing custom-field getter, and the #961 handler-limit\n * pre-flight (#1046). Returns the structural fields for the caller's conditional\n * TREE_CHANGED emit, computed from the single destructure here so core invokes\n * each user getter once (#797 / #952 `null`-clears-definition-only preserved).\n */\nexport function commitRouteUpdate<Dependencies extends DefaultDependencies>(\n store: RoutesStore<Dependencies>,\n lifecycle: RouteLifecycleNamespace<Dependencies>,\n name: string,\n updates: RouteConfigUpdate<Dependencies>,\n): {\n forwardTo?: string | ForwardToCallback<Dependencies> | null | undefined;\n defaultParams?: Params | null | undefined;\n defaultSearch?: SearchParams | null | undefined;\n decodeParams?: ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n encodeParams?: ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n} {\n const {\n forwardTo,\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n canActivate,\n canDeactivate,\n } = updates;\n\n // ===== PREPARE — compute every change into LOCALS. Any throw here aborts\n // before a single store write, so the whole field set is applied\n // all-or-nothing (#951).\n\n // Channel check on the INCOMING value, in PREPARE: `update` does not rebuild\n // the tree (NO_TREE_REBUILD), so the route's declarations are the ones the\n // matcher already holds. Checked before any write, so a mis-channelled\n // `defaultParams` aborts the whole update rather than landing half-applied.\n if (defaultParams !== undefined && defaultParams !== null) {\n assertChannelCorrect(\n \"updateRoute\",\n name,\n defaultParams,\n queryParamsOf(store, name),\n \"this route's `defaultParams`\",\n \"Move it to `defaultSearch`\",\n );\n }\n\n const forwardToPlan =\n forwardTo === undefined\n ? undefined\n : prepareForwardTo(name, forwardTo, store.config);\n\n const nextCustomFields = prepareCustomFields(store, name, updates);\n\n // Guard factories are compiled NOW (a throwing factory surfaces in PREPARE);\n // the precompiled function is installed in COMMIT without re-invoking the\n // factory, so a factory side effect runs exactly once (reuses the #956\n // compile-then-install seam). Compiled after the other prepares so a throw\n // upstream skips invoking the factory at all.\n const activateFn =\n canActivate === undefined || canActivate === null\n ? undefined\n : lifecycle.compileGuardFactory(canActivate, \"canActivate\");\n const deactivateFn =\n canDeactivate === undefined || canDeactivate === null\n ? undefined\n : lifecycle.compileGuardFactory(canDeactivate, \"canDeactivate\");\n\n // Pre-flight the #961 handler-limit before the COMMIT writes, so an at-limit\n // update that adds a NEW guard slot aborts before forwardTo / scalar config\n // land (#1046, #951). A slot is new only when `name` does not already hold a\n // guard of that type — an overwrite does not count.\n lifecycle.preflightHandlerLimit(\n activateFn === undefined ? [] : [name],\n deactivateFn === undefined ? [] : [name],\n false,\n );\n\n // ===== COMMIT — pure writes from here; nothing below throws.\n // Custom (plugin-defined) fields. Consumers read these lazily via\n // getRouteConfig (lifecycle hooks, preload, searchSchema), so no TREE_CHANGED\n // is needed — the next read sees the new value; the caller's emit stays\n // structural-only by design (О-7).\n if (nextCustomFields !== undefined) {\n if (Object.keys(nextCustomFields).length > 0) {\n store.routeCustomFields[name] = nextCustomFields;\n } else {\n delete store.routeCustomFields[name];\n }\n }\n\n if (forwardToPlan !== undefined) {\n store.config.forwardMap = forwardToPlan.forwardMap;\n store.config.forwardFnMap = forwardToPlan.forwardFnMap;\n adoptForwardState(store, forwardToPlan.resolved);\n }\n\n commitScalarConfig(store, name, {\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n });\n\n // Install the guards from their PREPARE-phase precompiled functions; a `null`\n // clears the definition-origin guard only (#952). See commitGuardUpdate.\n commitGuardUpdate(lifecycle, \"activate\", name, canActivate, activateFn);\n commitGuardUpdate(lifecycle, \"deactivate\", name, canDeactivate, deactivateFn);\n\n return {\n forwardTo,\n defaultParams,\n defaultSearch,\n decodeParams,\n encodeParams,\n };\n}\n\n/**\n * PREPARE step for a `forwardTo` update (#951 atomicity): computes the new\n * forward maps and the resolved forward chain into LOCALS and returns them\n * WITHOUT touching the store. A throw here — an async `forwardTo` (#967) or a\n * cycle surfaced by `refreshForwardMap` — aborts `update()` before any field is\n * committed. The caller writes the returned bundle into the store in its COMMIT\n * phase. (Mirrors the build-then-swap shape of #698, but the swap is deferred to\n * the caller so it can be sequenced with the other prepared fields.)\n */\nfunction prepareForwardTo<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n name: string,\n forwardTo: string | ForwardToCallback<Dependencies> | null,\n config: RouteConfig,\n): {\n forwardMap: RouteConfig[\"forwardMap\"];\n forwardFnMap: RouteConfig[\"forwardFnMap\"];\n resolved: Record<string, string>;\n} {\n // #967: reject an async forwardTo at update time — parity with add/replace\n // (registerForwardTo runs the same check on the build path). A no-op for\n // string/null. Without this the async callback is stored silently and\n // surfaces later as a generic \"must return a string, got object\" TypeError\n // from #resolveDynamicForward at navigation. Runs first, before any clone.\n assertForwardToNotAsync(forwardTo, name);\n\n const forwardMap = Object.assign(\n Object.create(null) as RouteConfig[\"forwardMap\"],\n config.forwardMap,\n );\n const forwardFnMap = Object.assign(\n Object.create(null) as RouteConfig[\"forwardFnMap\"],\n config.forwardFnMap,\n );\n\n if (forwardTo === null) {\n delete forwardMap[name];\n delete forwardFnMap[name];\n } else if (typeof forwardTo === \"string\") {\n delete forwardFnMap[name];\n forwardMap[name] = forwardTo;\n } else {\n delete forwardMap[name];\n forwardFnMap[name] = forwardTo;\n }\n\n const resolved = refreshForwardMap({ ...config, forwardMap });\n\n return { forwardMap, forwardFnMap, resolved };\n}\n\n/**\n * PREPARE step for a route's plugin-defined **custom fields** (#951) — the\n * `update` counterpart to how `add`/`replace` register them\n * (`registerSingleRouteHandlers`). A custom field is any patch key not in\n * {@link STANDARD_ROUTE_KEYS}.\n *\n * Computes the merged record and RETURNS it for the caller to commit;\n * `undefined` means no custom-field key was present, so the caller leaves the\n * store untouched. Semantics mirror the scalar fields in\n * {@link commitScalarConfig}: shallow-merge by patch key, `null` removes a\n * single field, `undefined` is a no-op (leaves the field untouched). When the\n * merge empties the record, the caller drops the whole entry so `getRouteConfig`\n * returns `undefined` — symmetric with `add`, which only stores a record when at\n * least one custom field exists.\n *\n * Reading the custom-field getters HERE (in PREPARE, not at commit) is what lets\n * a throwing getter abort the whole update before any field is written. The\n * merged record is a **fresh object**, never mutated in place: `cloneRouter`\n * shares per-route custom-field records by reference (`Object.assign`), so\n * replacing the reference keeps a clone isolated from post-clone updates on the\n * source.\n */\nfunction prepareCustomFields<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n name: string,\n updates: RouteConfigUpdate<Dependencies>,\n): Record<string, unknown> | undefined {\n let next: Record<string, unknown> | undefined;\n\n // `Object.keys` (not `Object.entries`): a value is read only AFTER the\n // standard-key guard, so structural-field getters — already read once by\n // `update`'s destructuring — are not re-invoked. `Object.entries` would read\n // every value eagerly, double-invoking a `defaultParams`/`forwardTo` getter\n // and breaking the \"user getter called once\" invariant.\n // eslint-disable-next-line unicorn/prefer-object-iterable-methods -- see above\n for (const key of Object.keys(updates)) {\n if (STANDARD_ROUTE_KEYS.has(key)) {\n continue;\n }\n\n const value = (updates as Record<string, unknown>)[key];\n\n // `undefined` mirrors the structural path: leave the field untouched.\n if (value === undefined) {\n continue;\n }\n\n // Clone-on-first-write — keeps clones (which alias this record) isolated.\n next ??= { ...store.routeCustomFields[name] };\n\n if (value === null) {\n delete next[key];\n } else {\n next[key] = value;\n }\n }\n\n return next;\n}\n\n/**\n * Applies one nullable scalar-config update in place: `undefined` is a no-op\n * (field not in the patch), `null` deletes the entry, any other value sets it.\n */\nfunction commitScalarField<T>(\n map: Record<string, T>,\n name: string,\n value: T | null | undefined,\n): void {\n if (value === undefined) {\n return;\n }\n\n if (value === null) {\n delete map[name];\n } else {\n map[name] = value;\n }\n}\n\n/**\n * COMMIT step for the scalar config fields of an update (#951): writes\n * `defaultParams` / `defaultSearch` / `decodeParams` / `encodeParams` in place.\n * These assignments\n * are pure and never throw, so they run in the COMMIT phase after every throwing\n * field has been validated in PREPARE. `forwardTo` is handled separately — it\n * has its own throwing prepare step ({@link prepareForwardTo}).\n */\nfunction commitScalarConfig<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n store: RoutesStore<Dependencies>,\n name: string,\n updates: {\n defaultParams?: Params | null | undefined;\n defaultSearch?: SearchParams | null | undefined;\n decodeParams?:\n ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n encodeParams?:\n ((channels: ParamsSearch) => ParamsSearch) | null | undefined;\n },\n): void {\n commitScalarField(store.config.defaultParams, name, updates.defaultParams);\n commitScalarField(store.config.defaultSearch, name, updates.defaultSearch);\n\n if (updates.decodeParams !== undefined) {\n if (updates.decodeParams === null) {\n delete store.config.decoders[name];\n } else {\n const decoder = updates.decodeParams;\n\n store.config.decoders[name] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if user-provided decoder violates its `{ params, search }` return type\n decoder(channels) ?? channels;\n }\n }\n\n if (updates.encodeParams !== undefined) {\n if (updates.encodeParams === null) {\n delete store.config.encoders[name];\n } else {\n const encoder = updates.encodeParams;\n\n store.config.encoders[name] = (channels: ParamsSearch): ParamsSearch =>\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime fallback if user-provided encoder violates its `{ params, search }` return type\n encoder(channels) ?? channels;\n }\n }\n}\n\n/**\n * COMMIT step for one guard field of an update (#951). `undefined` is a no-op;\n * `null` clears the DEFINITION-origin guard only, preserving an external guard\n * (#952); a factory installs together with its PREPARE-phase `precompiledFn`\n * (no re-compile — #956 seam). Extracted from `update()` so its prepare/commit\n * orchestration stays within the cognitive-complexity budget.\n */\nfunction commitGuardUpdate<Dependencies extends DefaultDependencies>(\n lifecycle: RouteLifecycleNamespace<Dependencies>,\n kind: \"activate\" | \"deactivate\",\n name: string,\n value: GuardFnFactory<Dependencies> | null | undefined,\n precompiledFn: GuardFn | undefined,\n): void {\n if (value === undefined) {\n return;\n }\n\n if (kind === \"activate\") {\n if (value === null) {\n lifecycle.clearCanActivate(name, \"definition\");\n } else {\n lifecycle.addCanActivate(name, value, true, precompiledFn);\n }\n } else if (value === null) {\n lifecycle.clearCanDeactivate(name, \"definition\");\n } else {\n lifecycle.addCanDeactivate(name, value, true, precompiledFn);\n }\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\nexport function createRoutesStore<\n Dependencies extends DefaultDependencies = DefaultDependencies,\n>(\n routes: Route<Dependencies>[],\n matcherOptions: CreateMatcherOptions | undefined,\n logger: RouterLogger,\n): RoutesStore<Dependencies> {\n // Initial routes are a standalone set at rootPath \"\" — same build the\n // prepare-then-commit `replace` path uses. Guards land in the pending maps,\n // flushed by `flushPendingGuards()` at the end of the Router constructor\n // (#1331); `setDependencies` itself is a pure assignment.\n //\n // Reject the silent-corruption cases BEFORE building, giving the constructor\n // parity with `add()` / `replace()` — the third and last route-population\n // entry point (#1351): within-batch duplicate names (#953/#968) and reserved\n // \"@@\" names (#954). Without these the constructor silently last-wins a\n // duplicate-name sibling (the first route is dropped → its deep-link 404s)\n // while add/replace throw. `methodName` is \"addRoute\" so all three entry\n // points surface the identical bare-core error. (Duplicate PATHS are already\n // rejected downstream by the path-matcher backstop #1153, so they are not\n // re-checked here.)\n assertNoInternalNamesInBatch(routes, \"addRoute\");\n assertNoDuplicateNamesInBatch(routes, \"\", \"addRoute\");\n\n const artifacts = buildReplaceArtifacts(routes, \"\", matcherOptions, logger);\n\n const store: RoutesStore<Dependencies> = {\n // Deferred access: the getter runs only after `store` is initialized.\n get definitions() {\n return routeTreeToDefinitions(store.tree);\n },\n config: artifacts.config,\n tree: artifacts.tree,\n matcher: artifacts.matcher,\n urlParamsCache: new Map(),\n queryParamsCache: new Map(),\n resolvedForwardMap: artifacts.resolvedForwardMap,\n hasAnyForward: anyForwardConfigured(artifacts.config),\n routeCustomFields: artifacts.routeCustomFields,\n rootPath: \"\",\n matcherOptions,\n depsStore: undefined,\n lifecycleNamespace: undefined,\n pendingCanActivate: artifacts.pendingCanActivate,\n pendingCanDeactivate: artifacts.pendingCanDeactivate,\n };\n\n // Same config-time channel check the add/replace path runs, so the\n // constructor is not the one population entry point that accepts a config\n // whose own state the router would then reject on `start()`.\n assertRouteDefaultChannelsFor(store.matcher, store.config, \"addRoute\");\n\n return store;\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../../constants.js"),t=require("../../helpers.js"),n=require("../../pipeline/canonicalize.js"),r=require("../../pipeline/buildURL.js"),i=require("../../pipeline/materialize.js");var a=class{#e=void 0;#t=void 0;#n;get(){return this.#e}set(e){this.#t=this.#e,this.#e=e?t.freezeStateShell(e):void 0}getPrevious(){return this.#t}reset(){this.#e=void 0,this.#t=void 0}setDependencies(e){this.#n=e}makeState(t,a,o,s){let c=this.#n.port(),l=n.canonicalize(c,t,a??e.EMPTY_PARAMS,o,{resolveForward:!1});return i.materialize(l,{path:s??r.buildURL(l,c)})}areStatesEqual(e,n,r=!0){if(!e||!n)return!!e==!!n;if(e.name!==n.name)return!1;if(r){let r=this.#n.getUrlParams(e.name);for(let i of r)if(!t.areParamValuesEqual(e.params[i],n.params[i]))return!1;return!0}return o(e.params,n.params)&&o(e.search,n.search)}};function o(e,n){let r=Object.keys(e);if(r.length!==Object.keys(n).length)return!1;for(let i of r)if(!(i in n)||!t.areParamValuesEqual(e[i],n[i]))return!1;return!0}exports.StateNamespace=a;
|
|
2
|
-
//# sourceMappingURL=StateNamespace.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StateNamespace.js","names":["#frozenState","#previousState","freezeStateShell","#deps","canonicalize","EMPTY_PARAMS","materialize","buildURL","areParamValuesEqual"],"sources":["../../../../src/namespaces/StateNamespace/StateNamespace.ts"],"sourcesContent":["// packages/core/src/namespaces/StateNamespace/StateNamespace.ts\n\nimport { EMPTY_PARAMS } from \"../../constants\";\nimport { areParamValuesEqual, freezeStateShell } from \"../../helpers\";\nimport { buildURL, canonicalize, materialize } from \"../../pipeline\";\n\nimport type { StateNamespaceDependencies } from \"./types\";\nimport type { Params, SearchParams, State } from \"../../types\";\n\n/**\n * Independent namespace for managing router state storage and creation.\n *\n * Static methods handle validation (called by facade).\n * Instance methods handle state storage, freezing, and creation.\n */\nexport class StateNamespace {\n /**\n * Cached frozen state - avoids structuredClone on every getState() call.\n */\n #frozenState: State | undefined = undefined;\n\n /**\n * Previous state before the last setState call.\n */\n #previousState: State | undefined = undefined;\n\n /**\n * Dependencies injected from Router.\n */\n #deps!: StateNamespaceDependencies;\n\n // =========================================================================\n // Instance methods (trust input - already validated by facade)\n // =========================================================================\n\n /**\n * Returns the current router state.\n *\n * The returned state is deeply frozen (immutable) for safety.\n * Returns `undefined` if the router has not been started or has been stopped.\n */\n get<P extends Params = Params>(): State<P> | undefined {\n return this.#frozenState as State<P> | undefined; // NOSONAR -- generic narrowing needed for public API\n }\n\n /**\n * Sets the current router state.\n *\n * The state is deeply frozen before storage to ensure immutability.\n * The previous state is preserved and accessible via `getPrevious()`.\n *\n * @param state - Already validated by facade, or undefined to clear\n */\n set(state: State | undefined): void {\n // Preserve current state as previous before updating\n this.#previousState = this.#frozenState;\n\n // If state is already frozen (from makeState()), use it directly.\n // For external states, freeze in place without cloning.\n this.#frozenState = state ? freezeStateShell(state) : undefined;\n }\n\n /**\n * Returns the previous router state (before the last navigation).\n */\n getPrevious(): State | undefined {\n return this.#previousState;\n }\n\n reset(): void {\n this.#frozenState = undefined;\n this.#previousState = undefined;\n }\n\n // =========================================================================\n // Dependency Injection\n // =========================================================================\n\n /**\n * Sets dependencies for state creation methods.\n * Must be called before using makeState, areStatesEqual, etc.\n */\n setDependencies(deps: StateNamespaceDependencies): void {\n this.#deps = deps;\n }\n\n // =========================================================================\n // State Creation Methods\n // =========================================================================\n\n /**\n * Creates a state object for a route.\n *\n * `params` is frozen at creation so it is always immutable, even when\n * `skipFreeze=true` is passed to defer the outer `Object.freeze(state)` call.\n * This keeps params-freezing invariants independent of transition-pipeline\n * mutation (e.g. `completeTransition` attaching `state.transition`).\n *\n * **The LITERAL form of the pipeline** (nav-pipeline Phase 4). This method\n * used to carry its own copy of stage ③ (merge each channel's route default\n * UNDER the caller's value) and of the mode gate — a second, parallel\n * canonicalisation living outside `src/pipeline`. Two terminals for one rule\n * is not a style problem: #1584's existence precondition landed on the\n * pipeline's terminal and NOT on this one, because it was found by sweeping\n * `canonicalize`'s PORT consumers, and this method read its own dependency\n * bag. It now IS `canonicalize(…, { resolveForward: false })` — the same form\n * `buildPath` and `isActiveRoute`'s literal arm take, which is exactly this\n * method's documented contract: `forwardTo` is not resolved (`makeState(\"src\")`\n * stays on `\"src\"`) but the NAMED route's defaults are applied (forwardState\n * invariants #7/#8).\n *\n * ⚠ The literal form also applies `withholdFilledSlots` (a query default is\n * declined for a `?`-declared slot the caller filled in the PARAMS bag). That\n * is unreachable here rather than new behaviour: the only door to this method\n * is `PluginApi.makeState`, which runs the P1 channel guard first\n * (`getPluginApi.ts` → `throwOnMisChanneledKey`) on the SAME predicate — own\n * key, defined value, `?`-declared — so the bag that would trigger the\n * withholding is refused before it arrives. Verified by a 71-cell before/after\n * snapshot, not by reading.\n *\n * `context` is initialized as a fresh empty object — intentionally NOT frozen\n * so plugins can publish data via `claim.write(state, value)` after creation.\n */\n makeState<P extends Params = Params, S extends SearchParams = SearchParams>(\n name: string,\n params?: P,\n search?: S,\n path?: string,\n ): State<P, S> {\n // Stages ③ + the mode gate, from the ONE implementation (`canonicalize`) —\n // this method no longer carries its own. `resolveForward: false` is the\n // whole difference from `navigate`'s form, and it is this method's contract:\n // the route NAMED is the route answered about.\n const port = this.#deps.port();\n const canonical = canonicalize(port, name, params ?? EMPTY_PARAMS, search, {\n resolveForward: false,\n });\n\n // ⑤a only when the caller did not supply the URL. `buildURL` prints through\n // `port.buildPath` — the interceptable `ctx.buildPath` this method already\n // used — so the interceptor zone is unchanged, and the URL is built from the\n // SAME canonical intent the state is materialised from, which is what keeps\n // `state.path` in step with `state.search` for a caller that passes no path\n // (`canNavigateTo`, `isActiveRoute`).\n //\n // ⚠ No `skipFreeze` arm: the parameter died when Phase 2 moved the two\n // callers that used it (`canNavigateTo`, `isActiveRoute`) onto\n // `materialize({ skipFreeze: true })` directly, and the old body hid the\n // death because it forwarded `undefined` into a slot that needs no branch.\n // The public `PluginApi.makeState` type has four parameters and both call\n // sites pass four; unfreezing a state is the transition pipeline's business,\n // reached through `materialize`, not through this primitive. Coverage is\n // what surfaced it — the same way it caught `deps.makeState` and\n // `paramsMatchExcluding` when Phase 2 migrated their last consumers.\n return materialize<P, S>(canonical, {\n path: path ?? buildURL(canonical, port),\n });\n }\n\n // =========================================================================\n // State Comparison Methods\n // =========================================================================\n\n /**\n * Compares two states for equality.\n * By default, ignores query params (only compares URL params).\n */\n areStatesEqual(\n state1: State | undefined,\n state2: State | undefined,\n ignoreQueryParams = true,\n ): boolean {\n if (!state1 || !state2) {\n return !!state1 === !!state2;\n }\n\n if (state1.name !== state2.name) {\n return false;\n }\n\n if (ignoreQueryParams) {\n // URL (path) param names are cached at the routes layer and invalidated\n // on every tree mutation, so this stays correct after replace() (#723).\n const urlParams = this.#deps.getUrlParams(state1.name);\n\n for (const urlParam of urlParams) {\n if (\n !areParamValuesEqual(state1.params[urlParam], state2.params[urlParam])\n ) {\n return false;\n }\n }\n\n return true;\n }\n\n // Compare BOTH channels — path params and query (search). Query moved out\n // of `params` into `search` in M2 (#1548), so a full comparison must check\n // both. `search` is always present (makeState fills EMPTY_SEARCH).\n return (\n recordsShallowEqual(state1.params, state2.params) &&\n recordsShallowEqual(state1.search, state2.search)\n );\n }\n}\n\n/**\n * Shallow key/value equality of two param-like records (path params or query),\n * using {@link areParamValuesEqual} per key so array values compare by content.\n */\nfunction recordsShallowEqual(\n left: Readonly<Record<string, unknown>>,\n right: Readonly<Record<string, unknown>>,\n): boolean {\n const leftKeys = Object.keys(left);\n\n if (leftKeys.length !== Object.keys(right).length) {\n return false;\n }\n\n for (const key of leftKeys) {\n if (!(key in right) || !areParamValuesEqual(left[key], right[key])) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":"mMAeA,IAAa,EAAb,KAA4B,CAI1B,GAAkC,IAAA,GAKlC,GAAoC,IAAA,GAKpC,GAYA,KAAuD,CACrD,OAAO,KAAKA,EACd,CAUA,IAAI,EAAgC,CAElC,KAAKC,GAAiB,KAAKD,GAI3B,KAAKA,GAAe,EAAQE,EAAAA,iBAAiB,CAAK,EAAI,IAAA,EACxD,CAKA,aAAiC,CAC/B,OAAO,KAAKD,EACd,CAEA,OAAc,CACZ,KAAKD,GAAe,IAAA,GACpB,KAAKC,GAAiB,IAAA,EACxB,CAUA,gBAAgB,EAAwC,CACtD,KAAKE,GAAQ,CACf,CAuCA,UACE,EACA,EACA,EACA,EACa,CAKb,IAAM,EAAO,KAAKA,GAAM,KAAK,EACvB,EAAYC,EAAAA,aAAa,EAAM,EAAM,GAAUC,EAAAA,aAAc,EAAQ,CACzE,eAAgB,EAClB,CAAC,EAkBD,OAAOC,EAAAA,YAAkB,EAAW,CAClC,KAAM,GAAQC,EAAAA,SAAS,EAAW,CAAI,CACxC,CAAC,CACH,CAUA,eACE,EACA,EACA,EAAoB,GACX,CACT,GAAI,CAAC,GAAU,CAAC,EACd,MAAO,CAAC,CAAC,GAAW,CAAC,CAAC,EAGxB,GAAI,EAAO,OAAS,EAAO,KACzB,MAAO,GAGT,GAAI,EAAmB,CAGrB,IAAM,EAAY,KAAKJ,GAAM,aAAa,EAAO,IAAI,EAErD,IAAK,IAAM,KAAY,EACrB,GACE,CAACK,EAAAA,oBAAoB,EAAO,OAAO,GAAW,EAAO,OAAO,EAAS,EAErE,MAAO,GAIX,MAAO,EACT,CAKA,OACE,EAAoB,EAAO,OAAQ,EAAO,MAAM,GAChD,EAAoB,EAAO,OAAQ,EAAO,MAAM,CAEpD,CACF,EAMA,SAAS,EACP,EACA,EACS,CACT,IAAM,EAAW,OAAO,KAAK,CAAI,EAEjC,GAAI,EAAS,SAAW,OAAO,KAAK,CAAK,CAAC,CAAC,OACzC,MAAO,GAGT,IAAK,IAAM,KAAO,EAChB,GAAI,EAAE,KAAO,IAAU,CAACA,EAAAA,oBAAoB,EAAK,GAAM,EAAM,EAAI,EAC/D,MAAO,GAIX,MAAO,EACT"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"buildURL.js","names":[],"sources":["../../../src/pipeline/buildURL.ts"],"sourcesContent":["// packages/core/src/pipeline/buildURL.ts\n\nimport type { RouteResolver } from \"./port\";\nimport type { Canonical } from \"./types\";\n\n/**\n * Stage ⑤a — the URL of a canonical intent. Accepts nothing but a\n * {@link Canonical}, so \"print a URL out of un-defaulted channels\" cannot be\n * expressed: the query string is printed from `canonical.query` alone, never from a\n * `search ?? params` fallback.\n *\n * The actual URL assembly stays in the engine and is reached through the port —\n * specifically through the interceptable `ctx.buildPath`, which is a permanent\n * decision and not a milestone-1 stopgap: reaching for the engine's\n * `matcher.buildPath` would silently stop running `persistent-params`'\n * `buildPath` interceptor on the navigate path (see {@link RouteResolver}).\n */\nexport function buildURL(canonical: Canonical, port: RouteResolver): string {\n return port.buildPath(canonical.name, canonical.path, canonical.query);\n}\n"],"mappings":"AAiBA,SAAgB,EAAS,EAAsB,EAA6B,CAC1E,OAAO,EAAK,UAAU,EAAU,KAAM,EAAU,KAAM,EAAU,KAAK,CACvE"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../channels/defaults.js"),t=require("../constants.js"),n=require("../channels/modeGate.js"),r=require("../helpers.js");function i(e,t,n,r,i){let a=e.pathNames(t);if(a!==void 0)for(let e of Object.keys(n))!r.includes(e)&&!a.includes(e)&&i(t,e)}function a(a,o,s,c,l){let u=l?.resolveForward===!1?{name:o,params:s,search:c}:a.resolveForward(o,s,c),d=u.name,f=r.normalizeParams(u.params),p=l?.diagnoseUndeclared===!0?a.reportUndeclaredParamKey:void 0;p&&i(a,d,f,a.queryNames(d),p);let m=a.defaultParams(d),h=a.defaultSearch(d);if((u.search===void 0||u.search===t.EMPTY_SEARCH)&&m===void 0&&h===void 0)return{name:d,path:f,query:t.EMPTY_SEARCH};let g=a.queryNames(d),_=a.reportDroppedQueryKey,v=r.mergeWithDefault(l?.resolveForward===!1?e.withholdFilledSlots(h,f,g):h,u.search,t.EMPTY_SEARCH);return{name:d,path:r.mergeWithDefault(m,f,t.EMPTY_PARAMS,!0),query:a.admitsUndeclaredQuery()?v:n.admittedSearch(v,g,e=>{_!==void 0&&a.pathNames(d)!==void 0&&_(d,e)})}}exports.canonicalize=a;
|
|
2
|
-
//# sourceMappingURL=canonicalize.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canonicalize.js","names":["normalizeParams","EMPTY_SEARCH","mergeWithDefault","withholdFilledSlots","EMPTY_PARAMS","admittedSearch"],"sources":["../../../src/pipeline/canonicalize.ts"],"sourcesContent":["// packages/core/src/pipeline/canonicalize.ts\n\nimport { admittedSearch, withholdFilledSlots } from \"../channels\";\nimport { EMPTY_PARAMS, EMPTY_SEARCH } from \"../constants\";\nimport { mergeWithDefault, normalizeParams } from \"../helpers\";\n\nimport type { RouteResolver } from \"./port\";\nimport type { Canonical } from \"./types\";\nimport type { Params, SearchParams } from \"../types\";\n\n/**\n * Options for {@link canonicalize}. Both flags are opt-in, and both are read as\n * a ROLE rather than inferred from the shape of the call — the reasons differ\n * per flag and are recorded on each.\n */\nexport interface CanonicalizeOptions {\n /**\n * Run stage ① (`forwardTo` resolution through the interceptor seam)?\n *\n * Defaults to `true`. `false` is the LITERAL form: the intent canonicalises\n * against the route the caller NAMED, without following its `forwardTo` chain\n * and without entering the seam. The entry points that ask a question about a\n * literal route rather than producing a destination take it — `buildPath`\n * (A.5: `buildPath(\"src\")` stays `/src`, deliberately asymmetric with\n * `navigate`), `isActiveRoute`'s literal arm, and `makeState`.\n *\n * ⚠ The literal form also skips the seam's channel CHECK. The seam does not\n * SEPARATE channels — stage ② was deleted (`ba0f6b18b`), so the resolving form\n * REFUSES a mis-channelled bag while the literal form simply does not look.\n * Either way nothing is moved: a caller who rides a declared query key in the\n * `params` bag keeps it there, and the URL build prints from the query channel\n * alone. That is what makes channel-correctness the producer's contract.\n */\n resolveForward?: boolean;\n\n /**\n * Run the opt-in undeclared-key diagnostic (#1579)? Defaults to `false`.\n *\n * Explicit rather than inferred from `resolveForward`, and the difference is\n * not cosmetic: `canNavigateTo` DOES resolve `forwardTo` (so it shares the\n * form with `navigate`) yet is a PREDICATE that runs on every `<Link>` render.\n * Keying the diagnostic on the form warned from it — measured, not reasoned —\n * which is exactly the per-render flood the channel guard avoids by not\n * instrumenting predicates at all (RFC rev. 29 §5). Only the points that\n * COMMIT or hand back a state a developer will keep opt in.\n */\n diagnoseUndeclared?: boolean;\n}\n\n/**\n * The undeclared-key diagnostic (#1579 — the params half of #1553), lifted out of\n * {@link canonicalize} so the fast path (#1589) fits the cognitive-complexity\n * budget beside it.\n *\n * BOTH diagnostics presuppose that the route EXISTS — they answer \"does route X\n * declare this key?\", and for a route that is not a route the honest answer is\n * that the question does not apply (#1584). `queryNames` / `pathNames` answer\n * `[]` for a real route with no declarations, so `[]` cannot say \"no such route\";\n * `pathNames` carries the `undefined` arm that can. Reporting a nonexistent route\n * blamed the params for a typo in the ROUTE name — the most misleading direction\n * available — and burnt a de-dup slot per key, silencing the genuine warning if\n * that name later became real. The committing producers still refuse the\n * navigation on their own (`undefined` from `buildNavigationState`,\n * `ROUTE_NOT_FOUND` from `navigate`); only the diagnostic was wrong.\n *\n * Called only when the sink is present, so bare core never reaches the\n * `pathNames` lookup or the bag walk.\n */\nfunction diagnoseUndeclaredKeys(\n port: RouteResolver,\n resolvedName: string,\n pathBag: Params,\n declaredQuery: readonly string[],\n report: (routeName: string, key: string) => void,\n): void {\n const declaredPath = port.pathNames(resolvedName);\n\n if (declaredPath === undefined) {\n return;\n }\n\n for (const key of Object.keys(pathBag)) {\n if (!declaredQuery.includes(key) && !declaredPath.includes(key)) {\n report(resolvedName, key);\n }\n }\n}\n\n/**\n * THE single producer of {@link Canonical}: one pass over stage ① (resolve the\n * `forwardTo` chain) and stage ③ (merge each channel's route default UNDER the\n * caller's value). There is no separating stage ② — channels arrive correct by\n * the producer contract, and the port's `resolveForward` is wired to the seam\n * that REFUSES a mis-channelled bag rather than repairing one.\n *\n * Ordering is forced by the data, not by discipline: ③ needs the RESOLVED name\n * (target defaults cannot be read before `forwardTo` resolves), so ① always\n * precedes it.\n *\n * `undefined` is absence on both sides of the merge (`mergeWithDefault`,\n * #1550/#1551) — an explicitly-`undefined` caller value leaves the default in\n * place, and a default carrying `undefined` behaves like no entry.\n *\n * Channels are frozen here, at merge time — NOT in `materialize`. The two\n * freezes are different things: `materialize`'s `skipFreeze` governs the state\n * object (the navigate path defers it so `completeTransition` can attach\n * `transition`), while `params` / `search` must be immutable the moment a guard\n * can see them. `mergeWithDefault` also copies before freezing, so the caller's\n * own bag is never frozen out from under it.\n *\n * ⚠ The option bags at the call sites are INLINE LITERALS on purpose (#1589).\n * Hoisting them to shared frozen module constants was tried and measured worse:\n * `buildPath` and `isActiveRoute` slowed 6.6–10.5 % while sites that pass no\n * options moved 2 %. The literal in a small hot function is not an allocation at\n * all — V8 inlines the function, escape analysis removes the object, and the flag\n * folds to a constant. A shared frozen object replaces that with a property read\n * off the heap. Do not \"optimise\" these back.\n */\nexport function canonicalize(\n port: RouteResolver,\n name: string,\n params: Params,\n search?: SearchParams,\n opts?: CanonicalizeOptions,\n): Canonical {\n // ① — forwardTo resolution + source-route default layering, through the\n // interceptor seam (plugins inject here). The literal form skips it entirely:\n // no chain, no seam, no channel check — the caller's bags stand as given.\n const forwarded =\n opts?.resolveForward === false\n ? { name, params, search }\n : port.resolveForward(name, params, search);\n const resolvedName = forwarded.name;\n\n // Path-channel entry guard: drops `undefined`-valued keys and collapses an\n // empty bag onto the EMPTY_PARAMS singleton (#1027), so the zero-params hot\n // path allocates nothing downstream.\n const pathBag = normalizeParams(forwarded.params);\n\n // The undeclared-key diagnostic (#1579 — the params half of #1553). A key the\n // route declares NOWHERE stays in `state.params` as app-level data, which is\n // correct and documented — but it never reaches the URL, so the state does not\n // round-trip through its own `state.path`. Core does not change that; it only\n // offers to SAY it.\n //\n // Three things keep this from costing anything it should not:\n // - gated on the sink being present, so bare core pays one `undefined` check\n // and never walks the bag (the port member is a GETTER that returns\n // `undefined` until `validation-plugin` installs the validator — a plain\n // closure would always be truthy and the gate would be decorative);\n // - opted in EXPLICITLY by the committing producers, so every predicate stays\n // silent — including `canNavigateTo`, which resolves `forwardTo` and would\n // therefore be caught by a form-based test while still running on every\n // `<Link>` render (measured: it warned before the flag was made explicit);\n // - read from the CALLER's bag, before route defaults are merged in, so a\n // deliberate arbitrary `defaultParams` entry is not reported as a mistake.\n const reportUndeclared =\n opts?.diagnoseUndeclared === true\n ? port.reportUndeclaredParamKey\n : undefined;\n\n if (reportUndeclared) {\n // Read HERE rather than once at the top (#1589): the fast path below does not\n // use the `?`-declared names, only the fact that there are none, and hoisting\n // the read made every predicate pay a port hop for an array it discarded. The\n // diagnostic is opt-in and off on every predicate, so this read is now paid\n // only by the committing producers that asked for it.\n diagnoseUndeclaredKeys(\n port,\n resolvedName,\n pathBag,\n port.queryNames(resolvedName),\n reportUndeclared,\n );\n }\n\n // The route's OWN defaults. Each slot IS its channel — no split (#1549 routed\n // them by the route's declaration for one release; `ba0f6b18b` retired that\n // along with the rest of stage ②). `defaultParams` is the path channel,\n // `defaultSearch` the query channel, and the router never moves a key between\n // them: the two meet only when the URL is printed. A `defaultParams` naming a\n // `?`-declared key is refused at REGISTRATION (`assertRouteDefaultChannels`),\n // so nothing mis-channelled can reach this merge and there is nothing here to\n // repair. Splitting here used to be what made a config the router itself had\n // accepted survive its own always-on channel guard.\n // FAST PATH (#1589): nothing to merge and nothing to gate. A route with no\n // defaults on either slot, called without a query bag, cannot have a default\n // applied, cannot have a slot withheld, and cannot have a key dropped by the\n // mode gate — whatever the mode, `admittedSearch` would keep every key of a\n // query channel that is empty anyway. So the whole tail below is provably\n // identity, and this returns without it. Note what is NOT in that list: how\n // many names the route declares with `?`. See the gate's own comment.\n //\n // This is the `buildPath/warm-static` case, and it was the most diagnostic\n // number in the regression: a static route — no params, no query, no defaults —\n // paid the full pass and came out 2.6x slower than before the pipeline.\n //\n // ⚠ The channels are still FROZEN here (canonicalize invariant #4): `pathBag`\n // is `normalizeParams`' own fresh object, so it is frozen in place, and\n // `EMPTY_SEARCH` is the shared frozen singleton.\n //\n // ⚠ The query test accepts the EMPTY_SEARCH singleton as well as `undefined`,\n // and that is not cosmetic: `isActiveRoute` and the `forwardState` seam both\n // hand down the singleton rather than nothing, so a test for `undefined` alone\n // left the two render-path predicates — the whole point of the exercise — on\n // the slow path. A fresh `{}` is deliberately NOT accepted: telling an empty\n // literal from a non-empty one costs a key walk, and the two call sites that\n // used to mint one now pass the singleton instead.\n // ⚠ TWO facts, one from each side, and between them stage ③ and the mode gate\n // are provably identity (#1589):\n //\n // 1. the CALLER brought no query bag, so the mode gate has nothing to filter\n // and the query merge has nothing on its left;\n // 2. the ROUTE carries no default on either slot, so neither merge has\n // anything on its right.\n //\n // The merged query bag has exactly those two sources, so both being empty is\n // the whole condition. What is NOT in it: how many names the route declares\n // with `?`. That term WAS in the condition until #1589 — and it was redundant\n // against fact 1, because an empty bag has nothing to drop however many names\n // are declared. Established, not argued: the term survives all 3808 tests, and\n // a 33-probe × 3-mode matrix over a `?`-declaring route with no defaults is\n // byte-identical without it. Dropping it costs one port hop less per call\n // (`queryNames` is ~12 ns on its own — `getQueryParams` is a four-frame chain to\n // a cached Map, not a Map read) and widens the fast path to routes that declare\n // query params but carry no defaults.\n //\n // Which leaves the two defaults, and they are read ABOVE the gate on purpose:\n // they are the gate's own route half AND the slow path's first input, so the\n // fast path pays two hops and the slow path pays nothing extra. The alternative\n // — one `port.mergesNothing()` predicate here, defaults re-read below — buys the\n // fast path one more hop (measured: `isActiveRoute-exact` 101 vs 111 ns) at the\n // cost of a FOURTH hop on the defaults path, which measured +6.5 % there. Both\n // were built and measured; the symmetric one wins because it regresses nothing.\n const defaultPath = port.defaultParams(resolvedName);\n const defaultQuery = port.defaultSearch(resolvedName);\n\n if (\n (forwarded.search === undefined || forwarded.search === EMPTY_SEARCH) &&\n defaultPath === undefined &&\n defaultQuery === undefined\n ) {\n // Annotated rather than asserted: the literal's inferred `query` type is the\n // empty singleton's `Record<string, never>`, too narrow for `Canonical` to\n // overlap, and an inline `as SearchParams` is redundant to the receiver.\n const fastPath: { name: string; path: Params; query: SearchParams } = {\n name: resolvedName,\n path: pathBag,\n query: EMPTY_SEARCH,\n };\n\n return fastPath as Canonical;\n }\n\n // Below the gate: the SLOW path is the only consumer of the declared names, so\n // the read moved here from the top of the function (#1589) — hoisting it made\n // every predicate pay a port hop for an array it discarded. The two consumers\n // left on this side — the default split and the mode gate — share this one\n // local, so they still cannot disagree about which registry decided the channel\n // (#1556); the diagnostic further up reads its own, through the same accessor,\n // so the one-registry invariant is unchanged.\n const declaredQuery = port.queryNames(resolvedName);\n\n // ③ — route defaults UNDER the routed value, each channel independent. Read\n // per channel (not as one `{ params, search }` bag from a combined `defaults()`\n // accessor) so the merge itself allocates nothing on the zero-defaults hot\n // path — the `Canonical` literal below is this function's only allocation.\n // (The pipeline's second one is `materialize`'s options bag, at the call site:\n // two object literals per navigation over the pre-pipeline form.)\n // In the LITERAL form no seam runs, so nothing has enforced #1570's rule that\n // a default is never applied to a slot the caller already filled — in EITHER\n // bag. Apply it here: the query default and a caller's params-twin land in\n // DIFFERENT channels, where no merge ranks them, and the default would win by\n // construction. `buildPath(\"x\", { page: \"9\" })` on `defaultSearch { page: \"5\" }`\n // would print `?page=5` — the caller's value silently replaced by the default,\n // which is the §1.1 inversion this whole split exists to remove. Nothing is\n // rerouted: the caller's key stays in the bag they chose (and, being in the\n // path channel, is simply not printed — that IS the single-bag retirement),\n // only the default is withheld.\n // ⚠ Scoped to `declaredQuery`, and the scope is load-bearing: only a DECLARED\n // query name can HAVE a params-bag twin. Withholding on a key the route\n // declares nowhere (`/u` + `defaultSearch { theme }`), or on one that owns a\n // path slot beside its query twin (`/items/:id?id`, the #843/#1549 carve-out),\n // takes a default no caller was competing for — and left `buildPath` the only\n // producer out of agreement, printing an href this very route's `matchPath`\n // rewrote on the spot. That is the #1552/#1578 class, re-opened.\n // The mode gate's sink, resolved ONCE — the read IS the gate. The router\n // implements this member as a GETTER returning `undefined` while no validator\n // is installed (`wiring/wireNamespaces.ts`), so in bare core `dropSink` is\n // genuinely absent and the drop path below skips the `pathNames` existence\n // lookup entirely. Hoisting also keeps the getter from being re-invoked per\n // dropped key. (It used to be wired as a plain closure — always truthy — so\n // the check read as taken and bare core paid that lookup with no sink behind\n // it; both sinks report their absence honestly now.)\n //\n // Read BELOW the fast path (#1589): a route with nothing to gate cannot drop a\n // key, so it has no use for the sink and should not pay the getter.\n const dropSink = port.reportDroppedQueryKey;\n\n const queryDefaults =\n opts?.resolveForward === false\n ? withholdFilledSlots(defaultQuery, pathBag, declaredQuery)\n : defaultQuery;\n\n const query = mergeWithDefault(queryDefaults, forwarded.search, EMPTY_SEARCH);\n\n return {\n name: resolvedName,\n // `valueIsOwned` (#1589): `pathBag` is `normalizeParams`' own fresh object —\n // never its input — so the merge freezes it in place instead of copying a bag\n // that was already copied one line above. Only the PATH channel may say this;\n // `forwarded.search` above comes from the caller or the seam.\n path: mergeWithDefault(defaultPath, pathBag, EMPTY_PARAMS, true),\n // The mode gate (#1575), applied AFTER the default merge so a `defaultSearch`\n // for an undeclared key is dropped with it — under `default`/`strict` that\n // config is dead by the same rule, not a back door around it. Runs on the\n // merged bag rather than the caller's, because that is the bag ⑤a prints\n // from, and the invariant is about those two agreeing.\n query: port.admitsUndeclaredQuery()\n ? query\n : admittedSearch(query as SearchParams, declaredQuery, (key) => {\n // Same existence precondition as the params-bag diagnostic above\n // (#1584): the DROP is always-on and correct either way, but saying\n // \"key `q` is not declared on route `nope`\" about a route that does\n // not exist blames the query for a route-name typo. Found by sweeping\n // this file's port consumers after fixing the sibling — the two\n // diagnostics read the same `[]`-means-nothing answer.\n // The sink is checked FIRST: it is the cheap half, and it is the one\n // that is absent in bare core, so the `pathNames` lookup stays off the\n // path of a router with no validator installed.\n if (\n dropSink !== undefined &&\n port.pathNames(resolvedName) !== undefined\n ) {\n dropSink(resolvedName, key);\n }\n }),\n // The one and only cast to the brand in the codebase — reviewed once, here.\n } as Canonical;\n}\n"],"mappings":"wIAoEA,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CACN,IAAM,EAAe,EAAK,UAAU,CAAY,EAE5C,OAAiB,IAAA,GAIrB,IAAK,IAAM,KAAO,OAAO,KAAK,CAAO,EAC/B,CAAC,EAAc,SAAS,CAAG,GAAK,CAAC,EAAa,SAAS,CAAG,GAC5D,EAAO,EAAc,CAAG,CAG9B,CAgCA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACW,CAIX,IAAM,EACJ,GAAM,iBAAmB,GACrB,CAAE,OAAM,SAAQ,QAAO,EACvB,EAAK,eAAe,EAAM,EAAQ,CAAM,EACxC,EAAe,EAAU,KAKzB,EAAUA,EAAAA,gBAAgB,EAAU,MAAM,EAmB1C,EACJ,GAAM,qBAAuB,GACzB,EAAK,yBACL,IAAA,GAEF,GAMF,EACE,EACA,EACA,EACA,EAAK,WAAW,CAAY,EAC5B,CACF,EA6DF,IAAM,EAAc,EAAK,cAAc,CAAY,EAC7C,EAAe,EAAK,cAAc,CAAY,EAEpD,IACG,EAAU,SAAW,IAAA,IAAa,EAAU,SAAWC,EAAAA,eACxD,IAAgB,IAAA,IAChB,IAAiB,IAAA,GAWjB,MAAO,CALL,KAAM,EACN,KAAM,EACN,MAAOA,EAAAA,YAGK,EAUhB,IAAM,EAAgB,EAAK,WAAW,CAAY,EAoC5C,EAAW,EAAK,sBAOhB,EAAQC,EAAAA,iBAJZ,GAAM,iBAAmB,GACrBC,EAAAA,oBAAoB,EAAc,EAAS,CAAa,EACxD,EAEwC,EAAU,OAAQF,EAAAA,YAAY,EAE5E,MAAO,CACL,KAAM,EAKN,KAAMC,EAAAA,iBAAiB,EAAa,EAASE,EAAAA,aAAc,EAAI,EAM/D,MAAO,EAAK,sBAAsB,EAC9B,EACAC,EAAAA,eAAe,EAAuB,EAAgB,GAAQ,CAW1D,IAAa,IAAA,IACb,EAAK,UAAU,CAAY,IAAM,IAAA,IAEjC,EAAS,EAAc,CAAG,CAE9B,CAAC,CAEP,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("../constants.js"),t=require("../helpers.js");function n(n,r){let i={name:n.name,params:n.path,search:n.query,path:r.path,context:{},...!r.skipFreeze&&{transition:e.DEFAULT_TRANSITION}};return Object.freeze(i.params),r.skipFreeze?i:t.freezeStateShell(i)}exports.materialize=n;
|
|
2
|
-
//# sourceMappingURL=materialize.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"materialize.js","names":["DEFAULT_TRANSITION","freezeStateShell"],"sources":["../../../src/pipeline/materialize.ts"],"sourcesContent":["// packages/core/src/pipeline/materialize.ts\n\nimport { DEFAULT_TRANSITION } from \"../constants\";\nimport { freezeStateShell } from \"../helpers\";\n\nimport type { Canonical } from \"./types\";\nimport type { Params, SearchParams, State } from \"../types\";\n\nexport interface MaterializeOptions {\n /**\n * The already-built URL (stage ⑤a). Required: the entry points that could\n * want a lazily-built path (`canNavigateTo`, `isActiveRoute`) settled the\n * question in Phase 2 by calling `buildURL` themselves, so `materialize`\n * never grew the port argument the milestone-1 docs left open.\n */\n path: string;\n /**\n * Defer `Object.freeze` of the STATE OBJECT — the navigate path passes `true`\n * so `completeTransition` can attach `transition`. It does NOT affect the\n * channels: `params` / `search` are frozen at merge time in `canonicalize`.\n */\n skipFreeze?: boolean;\n}\n\n/**\n * Stage ⑤b — the State of a canonical intent, and THE shape of a router State.\n * Accepts nothing but a {@link Canonical}.\n *\n * The shape used to live in `helpers.createStateObject`, shared with\n * `StateNamespace.makeState`; Phase 4 folded that producer onto the pipeline and\n * left the helper with a single caller and a docstring whose whole justification\n * (\"one constructor for both producers\") had expired. It is inlined here — ⑤b IS\n * \"make the state object\", so the shape belongs to the primitive that owns the\n * stage rather than to a helper one import away.\n *\n * `context` is a fresh empty object, intentionally NOT frozen — plugins publish\n * into it via `claim.write(state, value)` after creation.\n *\n * Deliberately does NOT call `makeState`: that would re-run stage ③ (idempotent\n * but a wasted pass) and rebuild the path itself, defeating ⑤a. Since Phase 4 it\n * could not, anyway — `makeState` is `canonicalize`'s literal form and would\n * recurse.\n */\nexport function materialize<\n P extends Params = Params,\n S extends SearchParams = SearchParams,\n>(canonical: Canonical, opts: MaterializeOptions): State<P, S> {\n // `Canonical` is deliberately NOT generic: it is one opaque intent shape, and\n // parameterising it would push the caller's `P` through the port and the merge\n // helpers for no gain. The parameter belongs to the FUNCTION, exactly as on\n // `makeState<P, S>` — a public entry point (`matchPath<P>`) instantiates it and\n // hands the result straight back to the consumer, so the chain\n // `matchPath<P>` → `materialize<P>` → `State<P>` has to carry the caller's type\n // (measured: without it `materialize` collapses the chain to `State<Params>`\n // and a consumer's `State<MyParams>` assignment fails TS2322).\n const state = {\n name: canonical.name,\n params: canonical.path as P,\n search: canonical.query as S,\n path: opts.path,\n context: {},\n ...(!opts.skipFreeze && { transition: DEFAULT_TRANSITION }),\n } as State<P, S>;\n\n // The path channel is frozen HERE, at the publication boundary — `materialize`\n // is the one place a `Canonical` becomes something user code can hold (#1598).\n // BEFORE the `skipFreeze` branch on purpose: that flag defers the state SHELL\n // (the navigate path attaches `transition` and lets plugins write `context`\n // after the fact), never the channels, so guards see frozen bags either way.\n //\n // `params` ONLY, and that asymmetry is measured rather than stylistic:\n // `canonical.query` is already frozen on every path — the fast path hands over\n // the `EMPTY_SEARCH` singleton, the slow one gets it back frozen from\n // `admittedSearch` — and re-freezing a frozen object is not free (~8 ns), so\n // freezing both regressed `isActiveRoute-exact` by 9.8 % while freezing one\n // wins 5-12 % on every producer that never publishes.\n Object.freeze(state.params);\n\n return opts.skipFreeze ? state : freezeStateShell(state);\n}\n"],"mappings":"8DA2CA,SAAgB,EAGd,EAAsB,EAAuC,CAS7D,IAAM,EAAQ,CACZ,KAAM,EAAU,KAChB,OAAQ,EAAU,KAClB,OAAQ,EAAU,MAClB,KAAM,EAAK,KACX,QAAS,CAAC,EACV,GAAI,CAAC,EAAK,YAAc,CAAE,WAAYA,EAAAA,kBAAmB,CAC3D,EAgBA,OAFA,OAAO,OAAO,EAAM,MAAM,EAEnB,EAAK,WAAa,EAAQC,EAAAA,iBAAiB,CAAK,CACzD"}
|
package/dist/cjs/routerFSM.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./utils/fsm/fsm.js"),t={IDLE:`IDLE`,STARTING:`STARTING`,READY:`READY`,TRANSITION_STARTED:`TRANSITION_STARTED`,LEAVE_APPROVED:`LEAVE_APPROVED`,DISPOSED:`DISPOSED`},n={START:`START`,STARTED:`STARTED`,NAVIGATE:`NAVIGATE`,LEAVE_APPROVE:`LEAVE_APPROVE`,COMPLETE:`COMPLETE`,FAIL:`FAIL`,CANCEL:`CANCEL`,STOP:`STOP`,DISPOSE:`DISPOSE`},r={initial:t.IDLE,context:null,transitions:{[t.IDLE]:{[n.START]:t.STARTING,[n.DISPOSE]:t.DISPOSED},[t.STARTING]:{[n.STARTED]:t.READY,[n.FAIL]:t.IDLE,[n.STOP]:t.IDLE,[n.DISPOSE]:t.DISPOSED},[t.READY]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.FAIL]:t.READY,[n.STOP]:t.IDLE,[n.DISPOSE]:t.DISPOSED},[t.TRANSITION_STARTED]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.LEAVE_APPROVE]:t.LEAVE_APPROVED,[n.CANCEL]:t.READY,[n.FAIL]:t.READY,[n.DISPOSE]:t.DISPOSED},[t.LEAVE_APPROVED]:{[n.NAVIGATE]:t.TRANSITION_STARTED,[n.COMPLETE]:t.READY,[n.CANCEL]:t.READY,[n.FAIL]:t.READY,[n.DISPOSE]:t.DISPOSED},[t.DISPOSED]:{}}};function i(){return new e.FSM(r)}exports.createRouterFSM=i,exports.routerEvents=n,exports.routerStates=t;
|
|
2
|
-
//# sourceMappingURL=routerFSM.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routerFSM.js","names":["FSM"],"sources":["../../src/routerFSM.ts"],"sourcesContent":["// packages/core/src/routerFSM.ts\n\nimport { FSM } from \"./utils/fsm\";\n\nimport type { NavigationOptions, State } from \"./types\";\nimport type { FSMConfig } from \"./utils/fsm\";\n\n/**\n * Router FSM states.\n *\n * - IDLE: Router not started or stopped\n * - STARTING: Router is initializing\n * - READY: Router is ready for navigation\n * - TRANSITION_STARTED: Navigation in progress (before deactivation guards)\n * - LEAVE_APPROVED: Deactivation guards passed, activation guards pending\n * - DISPOSED: Router has been disposed (R2+)\n */\nexport const routerStates = {\n IDLE: \"IDLE\",\n STARTING: \"STARTING\",\n READY: \"READY\",\n TRANSITION_STARTED: \"TRANSITION_STARTED\",\n LEAVE_APPROVED: \"LEAVE_APPROVED\",\n DISPOSED: \"DISPOSED\",\n} as const;\n\nexport type RouterState = (typeof routerStates)[keyof typeof routerStates];\n\n/**\n * Router FSM events.\n *\n * - START: Begin router initialization\n * - STARTED: Router initialization complete\n * - NAVIGATE: Begin navigation\n * - COMPLETE: Navigation completed successfully\n * - FAIL: Navigation or initialization failed\n * - CANCEL: Navigation cancelled\n * - STOP: Stop router\n * - DISPOSE: Dispose router (R2+)\n */\nexport const routerEvents = {\n START: \"START\",\n STARTED: \"STARTED\",\n NAVIGATE: \"NAVIGATE\",\n LEAVE_APPROVE: \"LEAVE_APPROVE\",\n COMPLETE: \"COMPLETE\",\n FAIL: \"FAIL\",\n CANCEL: \"CANCEL\",\n STOP: \"STOP\",\n DISPOSE: \"DISPOSE\",\n} as const;\n\nexport type RouterEvent = (typeof routerEvents)[keyof typeof routerEvents];\n\n/**\n * Per-event payloads for the router FSM (#1169 commit-gate). The three hot\n * navigation transitions carry their transition states so the FSM action\n * dispatched by `send()` emits the matching transition event — i.e. events are\n * literal consequences of FSM transitions (no `forceState` + manual emit). See\n * `EventBusNamespace.#setupFSMActions`.\n */\nexport interface RouterPayloads {\n NAVIGATE: { toState: State; fromState?: State | undefined };\n LEAVE_APPROVE: { toState: State; fromState?: State | undefined };\n COMPLETE: {\n toState: State;\n fromState?: State | undefined;\n opts?: NavigationOptions | undefined;\n };\n}\n\n/**\n * Router FSM configuration.\n *\n * Transitions:\n * - IDLE → STARTING (START), DISPOSED (DISPOSE)\n * - STARTING → READY (STARTED), IDLE (FAIL, STOP), DISPOSED (DISPOSE)\n * - READY → TRANSITION_STARTED (NAVIGATE), READY (FAIL, self-loop for early validation errors), IDLE (STOP), DISPOSED (DISPOSE)\n * - TRANSITION_STARTED → LEAVE_APPROVED (LEAVE_APPROVE), TRANSITION_STARTED (NAVIGATE, self-loop), READY (CANCEL, FAIL), DISPOSED (DISPOSE)\n * - LEAVE_APPROVED → READY (COMPLETE, CANCEL, FAIL), TRANSITION_STARTED (NAVIGATE), DISPOSED (DISPOSE)\n * - DISPOSED → (no transitions)\n *\n * DISPOSE is wired from every non-DISPOSED state so `router.dispose()` always\n * settles the FSM at DISPOSED. The facade orchestrates cleanup through IDLE\n * for healthy flows; the direct transitions guarantee the FSM is not left\n * stuck if cleanup is skipped (e.g. dispose mid-STARTING when the start\n * pipeline threw before STARTED/FAIL).\n */\nconst routerFSMConfig: FSMConfig<RouterState, RouterEvent, null> = {\n initial: routerStates.IDLE,\n context: null,\n transitions: {\n [routerStates.IDLE]: {\n [routerEvents.START]: routerStates.STARTING,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.STARTING]: {\n [routerEvents.STARTED]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.IDLE,\n [routerEvents.STOP]: routerStates.IDLE,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.READY]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.STOP]: routerStates.IDLE,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.TRANSITION_STARTED]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.LEAVE_APPROVE]: routerStates.LEAVE_APPROVED,\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.LEAVE_APPROVED]: {\n [routerEvents.NAVIGATE]: routerStates.TRANSITION_STARTED,\n [routerEvents.COMPLETE]: routerStates.READY,\n [routerEvents.CANCEL]: routerStates.READY,\n [routerEvents.FAIL]: routerStates.READY,\n [routerEvents.DISPOSE]: routerStates.DISPOSED,\n },\n [routerStates.DISPOSED]: {},\n },\n};\n\n/**\n * Factory function to create a router FSM instance.\n *\n * @returns FSM instance with initial state \"IDLE\"\n */\nexport function createRouterFSM(): FSM<\n RouterState,\n RouterEvent,\n null,\n RouterPayloads\n> {\n return new FSM<RouterState, RouterEvent, null, RouterPayloads>(\n routerFSMConfig,\n );\n}\n"],"mappings":"sCAiBa,EAAe,CAC1B,KAAM,OACN,SAAU,WACV,MAAO,QACP,mBAAoB,qBACpB,eAAgB,iBAChB,SAAU,UACZ,EAgBa,EAAe,CAC1B,MAAO,QACP,QAAS,UACT,SAAU,WACV,cAAe,gBACf,SAAU,WACV,KAAM,OACN,OAAQ,SACR,KAAM,OACN,QAAS,SACX,EAsCM,EAA6D,CACjE,QAAS,EAAa,KACtB,QAAS,KACT,YAAa,EACV,EAAa,MAAO,EAClB,EAAa,OAAQ,EAAa,UAClC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,UAAW,EACtB,EAAa,SAAU,EAAa,OACpC,EAAa,MAAO,EAAa,MACjC,EAAa,MAAO,EAAa,MACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,OAAQ,EACnB,EAAa,UAAW,EAAa,oBACrC,EAAa,MAAO,EAAa,OACjC,EAAa,MAAO,EAAa,MACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,oBAAqB,EAChC,EAAa,UAAW,EAAa,oBACrC,EAAa,eAAgB,EAAa,gBAC1C,EAAa,QAAS,EAAa,OACnC,EAAa,MAAO,EAAa,OACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,gBAAiB,EAC5B,EAAa,UAAW,EAAa,oBACrC,EAAa,UAAW,EAAa,OACrC,EAAa,QAAS,EAAa,OACnC,EAAa,MAAO,EAAa,OACjC,EAAa,SAAU,EAAa,QACvC,GACC,EAAa,UAAW,CAAC,CAC5B,CACF,EAOA,SAAgB,GAKd,CACA,OAAO,IAAIA,EAAAA,IACT,CACF,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=[];Object.freeze(e);function t(e){let t=e.split(`.`),n=t.length,r=[t[0]],i=t[0].length;for(let a=1;a<n-1;a++)i+=1+t[a].length,r.push(e.slice(0,i));return r.push(e),r}const n=new Set([`string`,`number`,`boolean`]);function r(e){return n.has(typeof e)}function i(e,t,n,i){let a=t[e];if(!a||typeof a!=`object`)return!0;for(let e of Object.keys(a)){let t=n.params[e],a=i.params[e];if(r(t)&&r(a)&&String(t)!==String(a))return!1}return!0}function a(e,t,n,r,a,o){for(let s=0;s<o;s++){let o=r[s];if(o!==a[s]||!i(o,e,t,n))return s}return o}const o=new Map;function s(e){let t=o.get(e);if(t)return t;let n=c(e);return Object.freeze(n),o.set(e,n),n}function c(e){if(!e)return[``];let n=e.indexOf(`.`);if(n===-1)return[e];let r=e.indexOf(`.`,n+1);if(r===-1)return[e.slice(0,n),e];let i=e.indexOf(`.`,r+1);return i===-1?[e.slice(0,n),e.slice(0,r),e]:e.indexOf(`.`,i+1)===-1?[e.slice(0,n),e.slice(0,r),e.slice(0,i),e]:t(e)}let l,u,d=null,f,p,m=null;function h(t,n,r){if(!n)return{intersection:``,toActivate:s(t.name),toDeactivate:e};let i=r(t.name),o=r(n.name);if(!i&&!o)return{intersection:``,toActivate:s(t.name),toDeactivate:s(n.name)};let c=s(t.name),l=s(n.name),u=Math.min(l.length,c.length),d=a(i??o,t,n,c,l,u),f;if(d>=l.length)f=e;else if(d===0&&l.length===1)f=l;else{f=[];for(let e=l.length-1;e>=d;e--)f.push(l[e])}let p=d===0?c:c.slice(d);return{intersection:d>0?l[d-1]:``,toDeactivate:f,toActivate:p}}function g(e,t,n){if(d!==null&&e===l&&t===u)return d;if(m!==null&&e===f&&t===p)return m;let r=h(e,t,n);return f=l,p=u,m=d,l=e,u=t,d=r,r}exports.getTransitionPath=g,exports.nameToIDs=s;
|
|
2
|
-
//# sourceMappingURL=transitionPath.js.map
|