@pyreon/router 0.5.7 → 0.6.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"216d545d-1","name":"loader.ts"},{"uid":"216d545d-3","name":"match.ts"},{"uid":"216d545d-5","name":"scroll.ts"},{"uid":"216d545d-7","name":"types.ts"},{"uid":"216d545d-9","name":"router.ts"},{"uid":"216d545d-11","name":"components.tsx"},{"uid":"216d545d-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"216d545d-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"216d545d-0"},"216d545d-3":{"renderedLength":12203,"gzipLength":3691,"brotliLength":0,"metaUid":"216d545d-2"},"216d545d-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"216d545d-4"},"216d545d-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"216d545d-6"},"216d545d-9":{"renderedLength":16575,"gzipLength":4649,"brotliLength":0,"metaUid":"216d545d-8"},"216d545d-11":{"renderedLength":6669,"gzipLength":2506,"brotliLength":0,"metaUid":"216d545d-10"},"216d545d-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"216d545d-12"}},"nodeMetas":{"216d545d-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"216d545d-1"},"imported":[{"uid":"216d545d-14"}],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-10"}]},"216d545d-2":{"id":"/src/match.ts","moduleParts":{"index.js":"216d545d-3"},"imported":[],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-8"}]},"216d545d-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"216d545d-5"},"imported":[],"importedBy":[{"uid":"216d545d-8"}]},"216d545d-6":{"id":"/src/types.ts","moduleParts":{"index.js":"216d545d-7"},"imported":[],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-8"}]},"216d545d-8":{"id":"/src/router.ts","moduleParts":{"index.js":"216d545d-9"},"imported":[{"uid":"216d545d-14"},{"uid":"216d545d-15"},{"uid":"216d545d-2"},{"uid":"216d545d-4"},{"uid":"216d545d-6"}],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-10"}]},"216d545d-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"216d545d-11"},"imported":[{"uid":"216d545d-14"},{"uid":"216d545d-0"},{"uid":"216d545d-8"}],"importedBy":[{"uid":"216d545d-12"}]},"216d545d-12":{"id":"/src/index.ts","moduleParts":{"index.js":"216d545d-13"},"imported":[{"uid":"216d545d-10"},{"uid":"216d545d-0"},{"uid":"216d545d-2"},{"uid":"216d545d-8"},{"uid":"216d545d-6"}],"importedBy":[],"isEntry":true},"216d545d-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"216d545d-10"},{"uid":"216d545d-0"},{"uid":"216d545d-8"}]},"216d545d-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"216d545d-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"d5fae101-1","name":"loader.ts"},{"uid":"d5fae101-3","name":"match.ts"},{"uid":"d5fae101-5","name":"scroll.ts"},{"uid":"d5fae101-7","name":"types.ts"},{"uid":"d5fae101-9","name":"router.ts"},{"uid":"d5fae101-11","name":"components.tsx"},{"uid":"d5fae101-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"d5fae101-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"d5fae101-0"},"d5fae101-3":{"renderedLength":12203,"gzipLength":3691,"brotliLength":0,"metaUid":"d5fae101-2"},"d5fae101-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"d5fae101-4"},"d5fae101-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"d5fae101-6"},"d5fae101-9":{"renderedLength":16575,"gzipLength":4649,"brotliLength":0,"metaUid":"d5fae101-8"},"d5fae101-11":{"renderedLength":6581,"gzipLength":2480,"brotliLength":0,"metaUid":"d5fae101-10"},"d5fae101-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"d5fae101-12"}},"nodeMetas":{"d5fae101-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"d5fae101-1"},"imported":[{"uid":"d5fae101-14"}],"importedBy":[{"uid":"d5fae101-12"},{"uid":"d5fae101-10"}]},"d5fae101-2":{"id":"/src/match.ts","moduleParts":{"index.js":"d5fae101-3"},"imported":[],"importedBy":[{"uid":"d5fae101-12"},{"uid":"d5fae101-8"}]},"d5fae101-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"d5fae101-5"},"imported":[],"importedBy":[{"uid":"d5fae101-8"}]},"d5fae101-6":{"id":"/src/types.ts","moduleParts":{"index.js":"d5fae101-7"},"imported":[],"importedBy":[{"uid":"d5fae101-12"},{"uid":"d5fae101-8"}]},"d5fae101-8":{"id":"/src/router.ts","moduleParts":{"index.js":"d5fae101-9"},"imported":[{"uid":"d5fae101-14"},{"uid":"d5fae101-15"},{"uid":"d5fae101-2"},{"uid":"d5fae101-4"},{"uid":"d5fae101-6"}],"importedBy":[{"uid":"d5fae101-12"},{"uid":"d5fae101-10"}]},"d5fae101-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"d5fae101-11"},"imported":[{"uid":"d5fae101-14"},{"uid":"d5fae101-0"},{"uid":"d5fae101-8"}],"importedBy":[{"uid":"d5fae101-12"}]},"d5fae101-12":{"id":"/src/index.ts","moduleParts":{"index.js":"d5fae101-13"},"imported":[{"uid":"d5fae101-10"},{"uid":"d5fae101-0"},{"uid":"d5fae101-2"},{"uid":"d5fae101-8"},{"uid":"d5fae101-6"}],"importedBy":[],"isEntry":true},"d5fae101-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"d5fae101-10"},{"uid":"d5fae101-0"},{"uid":"d5fae101-8"}]},"d5fae101-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"d5fae101-8"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, createRef, h, onUnmount, popContext, pushContext, useContext } from "@pyreon/core";
1
+ import { createContext, createRef, h, onUnmount, provide, useContext } from "@pyreon/core";
2
2
  import { computed, signal } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/loader.ts
@@ -1028,9 +1028,8 @@ function sanitizePath(path) {
1028
1028
  const _prefetched = /* @__PURE__ */ new WeakMap();
1029
1029
  const RouterProvider = (props) => {
1030
1030
  const router = props.router;
1031
- pushContext(new Map([[RouterContext.id, router]]));
1031
+ provide(RouterContext, router);
1032
1032
  onUnmount(() => {
1033
- popContext();
1034
1033
  router.destroy();
1035
1034
  setActiveRouter(null);
1036
1035
  });
@@ -1186,8 +1185,7 @@ function renderWithLoader(router, record, Comp, route) {
1186
1185
  * Uses Pyreon's context stack so useLoaderData() reads it during child setup.
1187
1186
  */
1188
1187
  function LoaderDataProvider(props) {
1189
- pushContext(new Map([[LoaderDataContext.id, props.data]]));
1190
- onUnmount(() => popContext());
1188
+ provide(LoaderDataContext, props.data);
1191
1189
  return props.children;
1192
1190
  }
1193
1191
  /** Evict oldest cache entries when the component cache exceeds maxCacheSize. */
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/loader.ts","../src/match.ts","../src/scroll.ts","../src/types.ts","../src/router.ts","../src/components.tsx"],"sourcesContent":["import type { Context } from \"@pyreon/core\"\nimport { createContext, useContext } from \"@pyreon/core\"\nimport type { RouterInstance } from \"./types\"\n\n/**\n * Context frame that holds the loader data for the currently rendered route record.\n * Pushed by RouterView's withLoaderData wrapper before invoking the route component.\n */\nexport const LoaderDataContext: Context<unknown> = createContext<unknown>(undefined)\n\n/**\n * Returns the data resolved by the current route's `loader` function.\n * Must be called inside a route component rendered by <RouterView />.\n *\n * @example\n * const routes = [{ path: \"/users\", component: Users, loader: fetchUsers }]\n *\n * function Users() {\n * const users = useLoaderData<User[]>()\n * return h(\"ul\", null, users.map(u => h(\"li\", null, u.name)))\n * }\n */\nexport function useLoaderData<T = unknown>(): T {\n return useContext(LoaderDataContext) as T\n}\n\n/**\n * SSR helper: pre-run all loaders for the given path before rendering.\n * Call this before `renderToString` so route components can read data via `useLoaderData()`.\n *\n * @example\n * const router = createRouter({ routes, url: req.url })\n * await prefetchLoaderData(router, req.url)\n * const html = await renderToString(h(App, { router }))\n */\nexport async function prefetchLoaderData(router: RouterInstance, path: string): Promise<void> {\n const route = router._resolve(path)\n const ac = new AbortController()\n router._abortController = ac\n await Promise.all(\n route.matched\n .filter((r) => r.loader)\n .map(async (r) => {\n const data = await r.loader?.({\n params: route.params,\n query: route.query,\n signal: ac.signal,\n })\n router._loaderData.set(r, data)\n }),\n )\n}\n\n/**\n * Serialize loader data to a JSON-safe plain object for embedding in SSR HTML.\n * Keys are route path patterns (stable across server and client).\n *\n * @example — SSR handler:\n * await prefetchLoaderData(router, req.url)\n * const { html, head } = await renderWithHead(h(App, null))\n * const page = `...${head}\n * <script>window.__PYREON_LOADER_DATA__=${JSON.stringify(serializeLoaderData(router))}</script>\n * ...${html}...`\n */\nexport function serializeLoaderData(router: RouterInstance): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [record, data] of router._loaderData) {\n result[record.path] = data\n }\n return result\n}\n\n/**\n * Hydrate loader data from a serialized object (e.g. `window.__PYREON_LOADER_DATA__`).\n * Populates the router's internal `_loaderData` map so the initial render uses\n * server-fetched data without re-running loaders on the client.\n *\n * Call this before `mount()`, after `createRouter()`.\n *\n * @example — client entry:\n * import { hydrateLoaderData } from \"@pyreon/router\"\n * const router = createRouter({ routes })\n * hydrateLoaderData(router, window.__PYREON_LOADER_DATA__ ?? {})\n * mount(h(App, null), document.getElementById(\"app\")!)\n */\nexport function hydrateLoaderData(\n router: RouterInstance,\n serialized: Record<string, unknown>,\n): void {\n if (!serialized || typeof serialized !== \"object\") return\n const route = router._resolve(router.currentRoute().path)\n for (const record of route.matched) {\n if (Object.hasOwn(serialized, record.path)) {\n router._loaderData.set(record, serialized[record.path])\n }\n }\n}\n","import type { ResolvedRoute, RouteMeta, RouteRecord } from \"./types\"\n\n// ─── Query string ─────────────────────────────────────────────────────────────\n\n/**\n * Parse a query string into key-value pairs. Duplicate keys are overwritten\n * (last value wins). Use `parseQueryMulti` to preserve duplicates as arrays.\n */\nexport function parseQuery(qs: string): Record<string, string> {\n if (!qs) return {}\n const result: Record<string, string> = {}\n for (const part of qs.split(\"&\")) {\n const eqIdx = part.indexOf(\"=\")\n if (eqIdx < 0) {\n const key = decodeURIComponent(part)\n if (key) result[key] = \"\"\n } else {\n const key = decodeURIComponent(part.slice(0, eqIdx))\n const val = decodeURIComponent(part.slice(eqIdx + 1))\n if (key) result[key] = val\n }\n }\n return result\n}\n\n/**\n * Parse a query string preserving duplicate keys as arrays.\n *\n * @example\n * parseQueryMulti(\"color=red&color=blue&size=lg\")\n * // → { color: [\"red\", \"blue\"], size: \"lg\" }\n */\nexport function parseQueryMulti(qs: string): Record<string, string | string[]> {\n if (!qs) return {}\n const result: Record<string, string | string[]> = {}\n for (const part of qs.split(\"&\")) {\n const eqIdx = part.indexOf(\"=\")\n let key: string\n let val: string\n if (eqIdx < 0) {\n key = decodeURIComponent(part)\n val = \"\"\n } else {\n key = decodeURIComponent(part.slice(0, eqIdx))\n val = decodeURIComponent(part.slice(eqIdx + 1))\n }\n if (!key) continue\n const existing = result[key]\n if (existing === undefined) {\n result[key] = val\n } else if (Array.isArray(existing)) {\n existing.push(val)\n } else {\n result[key] = [existing, val]\n }\n }\n return result\n}\n\nexport function stringifyQuery(query: Record<string, string>): string {\n const parts: string[] = []\n for (const [k, v] of Object.entries(query)) {\n parts.push(v ? `${encodeURIComponent(k)}=${encodeURIComponent(v)}` : encodeURIComponent(k))\n }\n return parts.length ? `?${parts.join(\"&\")}` : \"\"\n}\n\n// ─── Compiled route structures ───────────────────────────────────────────────\n\n/**\n * Pre-compiled segment info — computed once per route, reused on every match.\n * Avoids repeated split()/filter()/startsWith(\":\") on hot paths.\n */\ninterface CompiledSegment {\n /** Original segment string */\n raw: string\n /** true if this segment is a `:param` */\n isParam: boolean\n /** true if this segment is a `:param*` splat */\n isSplat: boolean\n /** true if this segment is a `:param?` optional */\n isOptional: boolean\n /** Param name (without leading `:` and trailing `*`/`?`) — empty for static segments */\n paramName: string\n}\n\ninterface CompiledRoute {\n route: RouteRecord\n /** true for wildcard patterns: `(.*)` or `*` */\n isWildcard: boolean\n /** Pre-split and classified segments */\n segments: CompiledSegment[]\n /** Number of segments */\n segmentCount: number\n /** true if route has no dynamic segments (pure static) */\n isStatic: boolean\n /** For static routes: the normalized path (e.g. \"/about\") for Map lookup */\n staticPath: string | null\n /** Compiled children (lazily populated) */\n children: CompiledRoute[] | null\n /** First static segment (for dispatch index), null if first segment is dynamic or route is wildcard */\n firstSegment: string | null\n}\n\n/**\n * A flattened route entry — pre-joins parent+child segments at compile time\n * so nested routes can be matched in a single pass without recursion.\n */\ninterface FlattenedRoute {\n /** All segments from root to leaf, concatenated */\n segments: CompiledSegment[]\n segmentCount: number\n /** The full matched chain from root to leaf (e.g. [adminLayout, usersPage]) */\n matchedChain: RouteRecord[]\n /** true if all segments are static */\n isStatic: boolean\n /** For static flattened routes: the full joined path */\n staticPath: string | null\n /** Pre-merged meta from all routes in the chain */\n meta: RouteMeta\n /** First static segment for dispatch index */\n firstSegment: string | null\n /** true if any segment is a splat */\n hasSplat: boolean\n /** true if this is a wildcard catch-all route (`*` or `(.*)`) */\n isWildcard: boolean\n /** true if any segment is optional (`:param?`) */\n hasOptional: boolean\n /** Minimum number of segments that must be present (excluding trailing optionals) */\n minSegments: number\n}\n\n/** WeakMap cache: compile each RouteRecord[] once */\nconst _compiledCache = new WeakMap<RouteRecord[], CompiledRoute[]>()\n\nfunction compileSegment(raw: string): CompiledSegment {\n if (raw.endsWith(\"*\") && raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: true, isOptional: false, paramName: raw.slice(1, -1) }\n }\n if (raw.endsWith(\"?\") && raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: false, isOptional: true, paramName: raw.slice(1, -1) }\n }\n if (raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: false, isOptional: false, paramName: raw.slice(1) }\n }\n return { raw, isParam: false, isSplat: false, isOptional: false, paramName: \"\" }\n}\n\nfunction compileRoute(route: RouteRecord): CompiledRoute {\n const pattern = route.path\n const isWildcard = pattern === \"(.*)\" || pattern === \"*\"\n\n if (isWildcard) {\n return {\n route,\n isWildcard: true,\n segments: [],\n segmentCount: 0,\n isStatic: false,\n staticPath: null,\n children: null,\n firstSegment: null,\n }\n }\n\n const segments = pattern.split(\"/\").filter(Boolean).map(compileSegment)\n const isStatic = segments.every((s) => !s.isParam)\n const staticPath = isStatic ? `/${segments.map((s) => s.raw).join(\"/\")}` : null\n const first = segments.length > 0 ? segments[0] : undefined\n const firstSegment = first && !first.isParam ? first.raw : null\n\n return {\n route,\n isWildcard: false,\n segments,\n segmentCount: segments.length,\n isStatic,\n staticPath,\n children: null,\n firstSegment,\n }\n}\n\n/** Expand alias paths into additional compiled entries sharing the original RouteRecord */\nfunction expandAliases(r: RouteRecord, c: CompiledRoute): CompiledRoute[] {\n if (!r.alias) return []\n const aliases = Array.isArray(r.alias) ? r.alias : [r.alias]\n return aliases.map((aliasPath) => {\n const { alias: _, ...withoutAlias } = r\n const ac = compileRoute({ ...withoutAlias, path: aliasPath })\n ac.children = c.children\n ac.route = r\n return ac\n })\n}\n\nfunction compileRoutes(routes: RouteRecord[]): CompiledRoute[] {\n const cached = _compiledCache.get(routes)\n if (cached) return cached\n\n const compiled: CompiledRoute[] = []\n for (const r of routes) {\n const c = compileRoute(r)\n if (r.children && r.children.length > 0) {\n c.children = compileRoutes(r.children)\n }\n compiled.push(c)\n compiled.push(...expandAliases(r, c))\n }\n _compiledCache.set(routes, compiled)\n return compiled\n}\n\n// ─── Route flattening ────────────────────────────────────────────────────────\n\n/** Extract first static segment from a segment list, or null if dynamic/empty */\nfunction getFirstSegment(segments: CompiledSegment[]): string | null {\n const first = segments[0]\n if (first && !first.isParam) return first.raw\n return null\n}\n\n/** Build a FlattenedRoute from segments + metadata */\nfunction makeFlatEntry(\n segments: CompiledSegment[],\n chain: RouteRecord[],\n meta: RouteMeta,\n isWildcard: boolean,\n): FlattenedRoute {\n const isStatic = !isWildcard && segments.every((s) => !s.isParam)\n const hasOptional = segments.some((s) => s.isOptional)\n // minSegments: count of segments up to and not including trailing optionals\n let minSegs = segments.length\n if (hasOptional) {\n while (minSegs > 0 && segments[minSegs - 1]?.isOptional) minSegs--\n }\n return {\n segments,\n segmentCount: segments.length,\n matchedChain: chain,\n isStatic,\n staticPath: isStatic ? `/${segments.map((s) => s.raw).join(\"/\")}` : null,\n meta,\n firstSegment: getFirstSegment(segments),\n hasSplat: segments.some((s) => s.isSplat),\n isWildcard,\n hasOptional,\n minSegments: minSegs,\n }\n}\n\n/**\n * Flatten nested routes into leaf entries with pre-joined segments.\n * This eliminates recursion during matching for the common case.\n */\nfunction flattenRoutes(compiled: CompiledRoute[]): FlattenedRoute[] {\n const result: FlattenedRoute[] = []\n flattenWalk(result, compiled, [], [], {})\n return result\n}\n\nfunction flattenWalk(\n result: FlattenedRoute[],\n routes: CompiledRoute[],\n parentSegments: CompiledSegment[],\n parentChain: RouteRecord[],\n parentMeta: RouteMeta,\n): void {\n for (const c of routes) {\n const chain = [...parentChain, c.route]\n const meta = c.route.meta ? { ...parentMeta, ...c.route.meta } : { ...parentMeta }\n flattenOne(result, c, parentSegments, chain, meta)\n }\n}\n\nfunction flattenOne(\n result: FlattenedRoute[],\n c: CompiledRoute,\n parentSegments: CompiledSegment[],\n chain: RouteRecord[],\n meta: RouteMeta,\n): void {\n if (c.isWildcard) {\n result.push(makeFlatEntry(parentSegments, chain, meta, true))\n if (c.children && c.children.length > 0) {\n flattenWalk(result, c.children, parentSegments, chain, meta)\n }\n return\n }\n\n const joined = [...parentSegments, ...c.segments]\n if (c.children && c.children.length > 0) {\n flattenWalk(result, c.children, joined, chain, meta)\n }\n result.push(makeFlatEntry(joined, chain, meta, false))\n}\n\n// ─── Combined index ─────────────────────────────────────────────────────────\n\ninterface RouteIndex {\n /** O(1) lookup for fully static paths (including nested) */\n staticMap: Map<string, FlattenedRoute>\n /** First-segment dispatch: maps first path segment → candidate routes */\n segmentMap: Map<string, FlattenedRoute[]>\n /** Routes whose first segment is dynamic (fallback) */\n dynamicFirst: FlattenedRoute[]\n /** Wildcard/catch-all routes */\n wildcards: FlattenedRoute[]\n}\n\nconst _indexCache = new WeakMap<RouteRecord[], RouteIndex>()\n\n/** Classify a single flattened route into the appropriate index bucket */\nfunction indexFlatRoute(\n f: FlattenedRoute,\n staticMap: Map<string, FlattenedRoute>,\n segmentMap: Map<string, FlattenedRoute[]>,\n dynamicFirst: FlattenedRoute[],\n wildcards: FlattenedRoute[],\n): void {\n // Static map: first static entry wins (preserves definition order)\n if (f.isStatic && f.staticPath && !staticMap.has(f.staticPath)) {\n staticMap.set(f.staticPath, f)\n }\n\n if (f.isWildcard) {\n wildcards.push(f)\n return\n }\n\n // Root route \"/\" has 0 segments — already in static map\n if (f.segmentCount === 0) return\n\n // First-segment dispatch\n if (f.firstSegment) {\n let bucket = segmentMap.get(f.firstSegment)\n if (!bucket) {\n bucket = []\n segmentMap.set(f.firstSegment, bucket)\n }\n bucket.push(f)\n } else {\n dynamicFirst.push(f)\n }\n}\n\nfunction buildRouteIndex(routes: RouteRecord[], compiled: CompiledRoute[]): RouteIndex {\n const cached = _indexCache.get(routes)\n if (cached) return cached\n\n const flattened = flattenRoutes(compiled)\n\n const staticMap = new Map<string, FlattenedRoute>()\n const segmentMap = new Map<string, FlattenedRoute[]>()\n const dynamicFirst: FlattenedRoute[] = []\n const wildcards: FlattenedRoute[] = []\n\n for (const f of flattened) {\n indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards)\n }\n\n const index: RouteIndex = { staticMap, segmentMap, dynamicFirst, wildcards }\n _indexCache.set(routes, index)\n return index\n}\n\n// ─── Fast path splitting ─────────────────────────────────────────────────────\n\n/** Split path into segments without allocating a filtered array */\nfunction splitPath(path: string): string[] {\n // Fast path for common cases\n if (path === \"/\") return []\n // Remove leading slash, split, no filter needed if path is clean\n const start = path.charCodeAt(0) === 47 /* / */ ? 1 : 0\n const end = path.length\n if (start >= end) return []\n\n const parts: string[] = []\n let segStart = start\n for (let i = start; i <= end; i++) {\n if (i === end || path.charCodeAt(i) === 47 /* / */) {\n if (i > segStart) {\n parts.push(path.substring(segStart, i))\n }\n segStart = i + 1\n }\n }\n return parts\n}\n\n/** Decode only if the segment contains a `%` character */\nfunction decodeSafe(s: string): string {\n return s.indexOf(\"%\") >= 0 ? decodeURIComponent(s) : s\n}\n\n// ─── Path matching (compiled) ────────────────────────────────────────────────\n\n/**\n * Match a single route pattern against a path segment.\n * Returns extracted params or null if no match.\n *\n * Supports:\n * - Exact segments: \"/about\"\n * - Param segments: \"/user/:id\"\n * - Wildcard: \"(.*)\" matches everything\n */\n/** Match a single pattern segment against a path segment, extracting params. Returns false on mismatch. */\nfunction matchPatternSegment(\n pp: string,\n pt: string | undefined,\n params: Record<string, string>,\n pathParts: string[],\n i: number,\n): \"splat\" | \"continue\" | \"fail\" {\n if (pp.endsWith(\"*\") && pp.startsWith(\":\")) {\n params[pp.slice(1, -1)] = pathParts.slice(i).map(decodeURIComponent).join(\"/\")\n return \"splat\"\n }\n if (pp.endsWith(\"?\") && pp.startsWith(\":\")) {\n if (pt !== undefined) params[pp.slice(1, -1)] = decodeURIComponent(pt)\n return \"continue\"\n }\n if (pt === undefined) return \"fail\"\n if (pp.startsWith(\":\")) {\n params[pp.slice(1)] = decodeURIComponent(pt)\n return \"continue\"\n }\n return pp === pt ? \"continue\" : \"fail\"\n}\n\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n if (pattern === \"(.*)\" || pattern === \"*\") return {}\n\n const patternParts = pattern.split(\"/\").filter(Boolean)\n const pathParts = path.split(\"/\").filter(Boolean)\n\n const params: Record<string, string> = {}\n for (let i = 0; i < patternParts.length; i++) {\n const result = matchPatternSegment(\n patternParts[i] as string,\n pathParts[i],\n params,\n pathParts,\n i,\n )\n if (result === \"splat\") return params\n if (result === \"fail\") return null\n }\n\n if (pathParts.length > patternParts.length) return null\n return params\n}\n\n// ─── Compiled matching helpers ────────────────────────────────────────────────\n\n/** Collect remaining path segments as a decoded splat value */\nfunction captureSplat(pathParts: string[], from: number, pathLen: number): string {\n const remaining: string[] = []\n for (let j = from; j < pathLen; j++) {\n const p = pathParts[j]\n if (p !== undefined) remaining.push(decodeSafe(p))\n }\n return remaining.join(\"/\")\n}\n\n// ─── Flattened route matching ─────────────────────────────────────────────────\n\n/** Check whether a flattened route's segment count is compatible with the path length */\nfunction isSegmentCountCompatible(f: FlattenedRoute, pathLen: number): boolean {\n if (f.segmentCount === pathLen) return true\n if (f.hasSplat && pathLen >= f.segmentCount) return true\n if (f.hasOptional && pathLen >= f.minSegments && pathLen <= f.segmentCount) return true\n return false\n}\n\n/** Try to match a flattened route against path parts */\nfunction matchFlattened(\n f: FlattenedRoute,\n pathParts: string[],\n pathLen: number,\n): Record<string, string> | null {\n if (!isSegmentCountCompatible(f, pathLen)) return null\n\n const params: Record<string, string> = {}\n const segments = f.segments\n const count = f.segmentCount\n for (let i = 0; i < count; i++) {\n const seg = segments[i]\n const pt = pathParts[i]\n if (!seg) return null\n if (seg.isSplat) {\n params[seg.paramName] = captureSplat(pathParts, i, pathLen)\n return params\n }\n if (pt === undefined) {\n if (!seg.isOptional) return null\n continue\n }\n if (seg.isParam) {\n params[seg.paramName] = decodeSafe(pt)\n } else if (seg.raw !== pt) {\n return null\n }\n }\n return params\n}\n\n/** Search a list of flattened candidates for a match */\nfunction searchCandidates(\n candidates: FlattenedRoute[],\n pathParts: string[],\n pathLen: number,\n): MatchResult | null {\n for (let i = 0; i < candidates.length; i++) {\n const f = candidates[i]\n if (!f) continue\n const params = matchFlattened(f, pathParts, pathLen)\n if (params) {\n return { params, matched: f.matchedChain }\n }\n }\n return null\n}\n\n// ─── Route resolution ─────────────────────────────────────────────────────────\n\ninterface MatchResult {\n params: Record<string, string>\n matched: RouteRecord[]\n}\n\n/**\n * Resolve a raw path (including query string and hash) against the route tree.\n * Uses flattened index for O(1) static lookup and first-segment dispatch.\n */\nexport function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute {\n const qIdx = rawPath.indexOf(\"?\")\n const pathAndHash = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath\n const queryPart = qIdx >= 0 ? rawPath.slice(qIdx + 1) : \"\"\n\n const hIdx = pathAndHash.indexOf(\"#\")\n const cleanPath = hIdx >= 0 ? pathAndHash.slice(0, hIdx) : pathAndHash\n const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : \"\"\n\n const query = parseQuery(queryPart)\n\n // Build index (cached after first call)\n const compiled = compileRoutes(routes)\n const index = buildRouteIndex(routes, compiled)\n\n // Fast path 1: O(1) static Map lookup (covers nested static too)\n const staticMatch = index.staticMap.get(cleanPath)\n if (staticMatch) {\n return {\n path: cleanPath,\n params: {},\n query,\n hash,\n matched: staticMatch.matchedChain,\n meta: staticMatch.meta,\n }\n }\n\n // Split path for segment-based matching\n const pathParts = splitPath(cleanPath)\n const pathLen = pathParts.length\n\n // Fast path 2: first-segment dispatch (O(1) bucket lookup + small scan)\n if (pathLen > 0) {\n const first = pathParts[0] as string\n const bucket = index.segmentMap.get(first)\n if (bucket) {\n const match = searchCandidates(bucket, pathParts, pathLen)\n if (match) {\n return {\n path: cleanPath,\n params: match.params,\n query,\n hash,\n matched: match.matched,\n meta: mergeMeta(match.matched),\n }\n }\n }\n }\n\n // Fallback: dynamic-first-segment routes\n const dynMatch = searchCandidates(index.dynamicFirst, pathParts, pathLen)\n if (dynMatch) {\n return {\n path: cleanPath,\n params: dynMatch.params,\n query,\n hash,\n matched: dynMatch.matched,\n meta: mergeMeta(dynMatch.matched),\n }\n }\n\n // Fallback: wildcard/catch-all routes\n const w = index.wildcards[0]\n if (w) {\n return {\n path: cleanPath,\n params: {},\n query,\n hash,\n matched: w.matchedChain,\n meta: w.meta,\n }\n }\n\n return { path: cleanPath, params: {}, query, hash, matched: [], meta: {} }\n}\n\n/** Merge meta from matched routes (leaf takes precedence) */\nfunction mergeMeta(matched: RouteRecord[]): RouteMeta {\n const meta: RouteMeta = {}\n for (const record of matched) {\n if (record.meta) Object.assign(meta, record.meta)\n }\n return meta\n}\n\n/** Build a path string from a named route's pattern and params */\nexport function buildPath(pattern: string, params: Record<string, string>): string {\n const built = pattern.replace(/\\/:([^/]+)\\?/g, (_match, key) => {\n const val = params[key]\n // Optional param — omit the entire segment if no value provided\n if (!val) return \"\"\n return `/${encodeURIComponent(val)}`\n })\n return built.replace(/:([^/]+)\\*?/g, (match, key) => {\n const val = params[key] ?? \"\"\n // Splat params contain slashes — don't encode them\n if (match.endsWith(\"*\")) return val.split(\"/\").map(encodeURIComponent).join(\"/\")\n return encodeURIComponent(val)\n })\n}\n\n/** Find a route record by name (recursive, O(n)). Prefer buildNameIndex for repeated lookups. */\nexport function findRouteByName(name: string, routes: RouteRecord[]): RouteRecord | null {\n for (const route of routes) {\n if (route.name === name) return route\n if (route.children) {\n const found = findRouteByName(name, route.children)\n if (found) return found\n }\n }\n return null\n}\n\n/**\n * Pre-build a name → RouteRecord index from a route tree for O(1) named navigation.\n * Called once at router creation time; avoids O(n) depth-first search per push({ name }).\n */\nexport function buildNameIndex(routes: RouteRecord[]): Map<string, RouteRecord> {\n const index = new Map<string, RouteRecord>()\n function walk(list: RouteRecord[]): void {\n for (const route of list) {\n if (route.name) index.set(route.name, route)\n if (route.children) walk(route.children)\n }\n }\n walk(routes)\n return index\n}\n","import type { ResolvedRoute, RouterOptions } from \"./types\"\n\n/**\n * Scroll restoration manager.\n *\n * Saves scroll position before each navigation and restores it when\n * navigating back to a previously visited path.\n */\nexport class ScrollManager {\n private readonly _positions = new Map<string, number>()\n private readonly _behavior: RouterOptions[\"scrollBehavior\"]\n\n constructor(behavior: RouterOptions[\"scrollBehavior\"] = \"top\") {\n this._behavior = behavior\n }\n\n /** Call before navigating away — saves current scroll position for `fromPath` */\n save(fromPath: string): void {\n // save/restore are only called from browser navigation paths (guarded by caller)\n this._positions.set(fromPath, window.scrollY)\n }\n\n /** Call after navigation is committed — applies scroll behavior */\n restore(to: ResolvedRoute, from: ResolvedRoute): void {\n const behavior = (to.meta.scrollBehavior as typeof this._behavior) ?? this._behavior ?? \"top\"\n\n if (typeof behavior === \"function\") {\n const saved = this._positions.get(to.path) ?? null\n const result = behavior(to, from, saved)\n this._applyResult(result, to.path)\n return\n }\n\n this._applyResult(behavior, to.path)\n }\n\n private _applyResult(result: \"top\" | \"restore\" | \"none\" | number, toPath: string): void {\n if (result === \"none\") return\n if (result === \"top\" || result === undefined) {\n window.scrollTo({ top: 0, behavior: \"instant\" as ScrollBehavior })\n return\n }\n if (result === \"restore\") {\n const saved = this._positions.get(toPath) ?? 0\n window.scrollTo({ top: saved, behavior: \"instant\" as ScrollBehavior })\n return\n }\n // At this point result must be a number (all string cases handled above)\n window.scrollTo({ top: result as number, behavior: \"instant\" as ScrollBehavior })\n }\n\n getSavedPosition(path: string): number | null {\n return this._positions.get(path) ?? null\n }\n}\n","import type { ComponentFn } from \"@pyreon/core\"\n\nexport type { ComponentFn }\n\n// ─── Path param extraction ────────────────────────────────────────────────────\n\n/**\n * Extracts typed params from a path string at compile time.\n * Supports optional params via `:param?` — their type is `string | undefined`.\n *\n * @example\n * ExtractParams<'/user/:id/posts/:postId'>\n * // → { id: string; postId: string }\n *\n * ExtractParams<'/user/:id?'>\n * // → { id?: string | undefined }\n */\nexport type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}`\n ? { [K in Param]: string } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}*`\n ? { [K in Param]: string }\n : T extends `${string}:${infer Param}?/${infer Rest}`\n ? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}?`\n ? { [K in Param]?: string | undefined }\n : T extends `${string}:${infer Param}/${infer Rest}`\n ? { [K in Param]: string } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}`\n ? { [K in Param]: string }\n : Record<never, never>\n\n// ─── Route meta ───────────────────────────────────────────────────────────────\n\n/**\n * Route metadata interface. Extend it via module augmentation to add custom fields:\n *\n * @example\n * // globals.d.ts\n * declare module \"@pyreon/router\" {\n * interface RouteMeta {\n * requiresRole?: \"admin\" | \"user\"\n * pageTitle?: string\n * }\n * }\n */\nexport interface RouteMeta {\n /** Sets document.title on navigation */\n title?: string\n /** Page description (for meta tags) */\n description?: string\n /** If true, guards can redirect to login */\n requiresAuth?: boolean\n /** Scroll behavior for this route */\n scrollBehavior?: \"top\" | \"restore\" | \"none\"\n}\n\n// ─── Resolved route ───────────────────────────────────────────────────────────\n\nexport interface ResolvedRoute<\n P extends Record<string, string | undefined> = Record<string, string>,\n Q extends Record<string, string> = Record<string, string>,\n> {\n path: string\n params: P\n query: Q\n hash: string\n /** All matched records from root to leaf (one per nesting level) */\n matched: RouteRecord[]\n meta: RouteMeta\n}\n\n// ─── Lazy component ───────────────────────────────────────────────────────────\n\nexport const LAZY_SYMBOL = Symbol(\"pyreon.lazy\")\n\nexport interface LazyComponent {\n readonly [LAZY_SYMBOL]: true\n readonly loader: () => Promise<ComponentFn | { default: ComponentFn }>\n /** Optional component shown while the lazy chunk is loading */\n readonly loadingComponent?: ComponentFn\n /** Optional component shown after all retries have failed */\n readonly errorComponent?: ComponentFn\n}\n\nexport function lazy(\n loader: () => Promise<ComponentFn | { default: ComponentFn }>,\n options?: { loading?: ComponentFn; error?: ComponentFn },\n): LazyComponent {\n return {\n [LAZY_SYMBOL]: true,\n loader,\n ...(options?.loading ? { loadingComponent: options.loading } : {}),\n ...(options?.error ? { errorComponent: options.error } : {}),\n }\n}\n\nexport function isLazy(c: RouteComponent): c is LazyComponent {\n return typeof c === \"object\" && c !== null && (c as LazyComponent)[LAZY_SYMBOL] === true\n}\n\nexport type RouteComponent = ComponentFn | LazyComponent\n\n// ─── Navigation guard ─────────────────────────────────────────────────────────\n\nexport type NavigationGuardResult = boolean | string | undefined\nexport type NavigationGuard = (\n to: ResolvedRoute,\n from: ResolvedRoute,\n) => NavigationGuardResult | Promise<NavigationGuardResult>\n\nexport type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void\n\n// ─── Navigation blockers ──────────────────────────────────────────────────────\n\n/**\n * Called before each navigation. Return `true` to block, `false` to allow.\n * Async blockers are supported (e.g. to show a confirmation dialog).\n */\nexport type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>\n\nexport interface Blocker {\n /** Unregister this blocker so future navigations proceed freely. */\n remove(): void\n}\n\n// ─── Route loaders ────────────────────────────────────────────────────────────\n\nexport interface LoaderContext {\n params: Record<string, string>\n query: Record<string, string>\n /** Aborted when a newer navigation supersedes this one */\n signal: AbortSignal\n}\n\nexport type RouteLoaderFn = (ctx: LoaderContext) => Promise<unknown>\n\n// ─── Route record ─────────────────────────────────────────────────────────────\n\nexport interface RouteRecord<TPath extends string = string> {\n /** Path pattern — supports `:param` segments and `(.*)` wildcard */\n path: TPath\n component: RouteComponent\n /** Optional route name for named navigation */\n name?: string\n /** Metadata attached to this route */\n meta?: RouteMeta\n /**\n * Redirect target. Evaluated before guards.\n * String: redirect to that path.\n * Function: called with the resolved route, return path string.\n */\n redirect?: string | ((to: ResolvedRoute) => string)\n /** Guard(s) run only for this route, before global beforeEach guards */\n beforeEnter?: NavigationGuard | NavigationGuard[]\n /** Guard(s) run before leaving this route. Return false to cancel. */\n beforeLeave?: NavigationGuard | NavigationGuard[]\n /**\n * Alternative path(s) for this route. Alias paths render the same component\n * and share guards, loaders, and metadata with the primary path.\n *\n * @example\n * { path: \"/user/:id\", alias: [\"/profile/:id\"], component: UserPage }\n */\n alias?: string | string[]\n /** Child routes rendered inside this route's component via <RouterView /> */\n children?: RouteRecord[]\n /**\n * Data loader — runs before navigation commits, in parallel with sibling loaders.\n * The result is accessible via `useLoaderData()` inside the route component.\n * Receives an AbortSignal that fires if a newer navigation supersedes this one.\n */\n loader?: RouteLoaderFn\n /**\n * When true, the router shows cached loader data immediately (stale) and\n * revalidates in the background. The component re-renders once fresh data arrives.\n * Only applies when navigating to a route that already has cached loader data.\n */\n staleWhileRevalidate?: boolean\n /** Component rendered when this route's loader throws an error */\n errorComponent?: ComponentFn\n}\n\n// ─── Router options ───────────────────────────────────────────────────────────\n\nexport type ScrollBehaviorFn = (\n to: ResolvedRoute,\n from: ResolvedRoute,\n savedPosition: number | null,\n) => \"top\" | \"restore\" | \"none\" | number\n\nexport interface RouterOptions {\n routes: RouteRecord[]\n /** \"hash\" (default) uses location.hash; \"history\" uses pushState */\n mode?: \"hash\" | \"history\"\n /**\n * Base path for the application. Used when deploying to a sub-path\n * (e.g. `\"/app\"` for `https://example.com/app/`).\n * Only applies in history mode. Must start with `/`.\n * Default: `\"\"` (no base path).\n */\n base?: string\n /**\n * Global scroll behavior. Per-route meta.scrollBehavior takes precedence.\n * Default: \"top\"\n */\n scrollBehavior?: ScrollBehaviorFn | \"top\" | \"restore\" | \"none\"\n /**\n * Initial URL for SSR. On the server, window.location is unavailable;\n * pass the request URL here so the router resolves the correct route.\n *\n * @example\n * // In your SSR handler:\n * const router = createRouter({ routes, url: req.url })\n */\n url?: string\n /**\n * Called when a route loader throws. If not provided, errors are logged\n * and the navigation continues with `undefined` data for the failed loader.\n * Return `false` to cancel the navigation.\n */\n onError?: (err: unknown, route: ResolvedRoute) => undefined | false\n /**\n * Maximum number of resolved lazy components to cache.\n * When exceeded, the oldest entry is evicted.\n * Default: 100.\n */\n maxCacheSize?: number\n /**\n * Trailing slash handling:\n * - `\"strip\"` — removes trailing slashes before matching (default)\n * - `\"add\"` — ensures paths always end with `/`\n * - `\"ignore\"` — no normalization\n */\n trailingSlash?: \"strip\" | \"add\" | \"ignore\"\n}\n\n// ─── Router interface ─────────────────────────────────────────────────────────\n\nexport interface Router {\n /** Navigate to a path */\n push(path: string): Promise<void>\n /** Navigate to a path by name */\n push(location: {\n name: string\n params?: Record<string, string>\n query?: Record<string, string>\n }): Promise<void>\n /** Replace current history entry */\n replace(path: string): Promise<void>\n /** Replace current history entry using a named route */\n replace(location: {\n name: string\n params?: Record<string, string>\n query?: Record<string, string>\n }): Promise<void>\n /** Go back one step in history */\n back(): void\n /** Go forward one step in history */\n forward(): void\n /** Navigate forward or backward by `delta` steps in the history stack */\n go(delta: number): void\n /** Register a global before-navigation guard. Returns an unregister function. */\n beforeEach(guard: NavigationGuard): () => void\n /** Register a global after-navigation hook. Returns an unregister function. */\n afterEach(hook: AfterEachHook): () => void\n /** Current resolved route (reactive signal) */\n readonly currentRoute: () => ResolvedRoute\n /** True while a navigation (guards + loaders) is in flight */\n readonly loading: () => boolean\n /**\n * Promise that resolves once the initial navigation is complete.\n * Useful for SSR and for delaying rendering until the first route is resolved.\n */\n isReady(): Promise<void>\n /** Remove all event listeners, clear caches, and abort in-flight navigations. */\n destroy(): void\n}\n\n// ─── Internal router instance ─────────────────────────────────────────────────\n\nimport type { Computed, Signal } from \"@pyreon/reactivity\"\n\nexport interface RouterInstance extends Router {\n routes: RouteRecord[]\n mode: \"hash\" | \"history\"\n /** Normalized base path (e.g. \"/app\"), empty string if none */\n _base: string\n _currentPath: Signal<string>\n _currentRoute: Computed<ResolvedRoute>\n _componentCache: Map<RouteRecord, ComponentFn>\n _loadingSignal: Signal<number>\n _resolve(rawPath: string): ResolvedRoute\n _scrollPositions: Map<string, number>\n _scrollBehavior: RouterOptions[\"scrollBehavior\"]\n _onError: RouterOptions[\"onError\"]\n _maxCacheSize: number\n /**\n * Current RouterView nesting depth. Incremented by each RouterView as it\n * mounts (in tree order = depth-first), so each view knows which level of\n * `matched[]` to render. Reset to 0 by RouterProvider.\n */\n _viewDepth: number\n /** Route records whose lazy chunk permanently failed (all retries exhausted) */\n _erroredChunks: Set<RouteRecord>\n /** Loader data keyed by route record — populated before each navigation commits */\n _loaderData: Map<RouteRecord, unknown>\n /** AbortController for the in-flight loader batch — aborted when a newer navigation starts */\n _abortController: AbortController | null\n /** Registered navigation blockers */\n _blockers: Set<BlockerFn>\n /** Resolves the isReady() promise after initial navigation completes */\n _readyResolve: (() => void) | null\n /** The isReady() promise instance */\n _readyPromise: Promise<void>\n}\n","import { createContext, onUnmount, useContext } from \"@pyreon/core\"\nimport { computed, signal } from \"@pyreon/reactivity\"\nimport { buildNameIndex, buildPath, resolveRoute, stringifyQuery } from \"./match\"\nimport { ScrollManager } from \"./scroll\"\nimport {\n type AfterEachHook,\n type Blocker,\n type BlockerFn,\n type ComponentFn,\n isLazy,\n type LoaderContext,\n type NavigationGuard,\n type NavigationGuardResult,\n type ResolvedRoute,\n type RouteRecord,\n type Router,\n type RouterInstance,\n type RouterOptions,\n} from \"./types\"\n\n// Evaluated once at module load — collapses to `true` in browser / happy-dom,\n// `false` on the server. Using a constant avoids per-call `typeof` branches\n// that are uncoverable in test environments.\nconst _isBrowser = typeof window !== \"undefined\"\n\n// ─── Router context ───────────────────────────────────────────────────────────\n// Context-based access: isolated per request in SSR (ALS-backed via\n// @pyreon/runtime-server), isolated per component tree in CSR.\n// Falls back to the module-level singleton for code running outside a component\n// tree (e.g. programmatic navigation from event handlers).\n\nexport const RouterContext = createContext<RouterInstance | null>(null)\n\n// Module-level fallback — safe for CSR (single-threaded), not for concurrent SSR.\n// RouterProvider also sets this so legacy useRouter() calls outside the tree work.\nlet _activeRouter: RouterInstance | null = null\n\nexport function getActiveRouter(): RouterInstance | null {\n return useContext(RouterContext) ?? _activeRouter\n}\n\nexport function setActiveRouter(router: RouterInstance | null): void {\n if (router) router._viewDepth = 0\n _activeRouter = router\n}\n\n// ─── Hooks ────────────────────────────────────────────────────────────────────\n\nexport function useRouter(): Router {\n const router = useContext(RouterContext) ?? _activeRouter\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n return router\n}\n\nexport function useRoute<TPath extends string = string>(): () => ResolvedRoute<\n import(\"./types\").ExtractParams<TPath> & Record<string, string>,\n Record<string, string>\n> {\n const router = useContext(RouterContext) ?? _activeRouter\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n return router.currentRoute as never\n}\n\n/**\n * In-component guard: called before the component's route is left.\n * Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed.\n * Automatically removed on component unmount.\n *\n * @example\n * onBeforeRouteLeave((to, from) => {\n * if (hasUnsavedChanges()) return false\n * })\n */\nexport function onBeforeRouteLeave(guard: NavigationGuard): () => void {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n // Register as a global guard that only fires when leaving the current route\n const currentMatched = router.currentRoute().matched\n const wrappedGuard: NavigationGuard = (to, from) => {\n // Only fire if we're actually leaving one of the matched routes\n const isLeaving = from.matched.some((r) => currentMatched.includes(r))\n if (!isLeaving) return undefined\n return guard(to, from)\n }\n const remove = router.beforeEach(wrappedGuard)\n onUnmount(() => remove())\n return remove\n}\n\n/**\n * In-component guard: called when the route changes but the component is reused\n * (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes.\n * Automatically removed on component unmount.\n *\n * @example\n * onBeforeRouteUpdate((to, from) => {\n * if (!isValidId(to.params.id)) return false\n * })\n */\nexport function onBeforeRouteUpdate(guard: NavigationGuard): () => void {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n const currentMatched = router.currentRoute().matched\n const wrappedGuard: NavigationGuard = (to, from) => {\n // Only fire when the same component is reused (matched routes overlap)\n const isReused = to.matched.some((r) => currentMatched.includes(r))\n if (!isReused) return undefined\n return guard(to, from)\n }\n const remove = router.beforeEach(wrappedGuard)\n onUnmount(() => remove())\n return remove\n}\n\n/**\n * Register a navigation blocker. The `fn` callback is called before each\n * navigation — return `true` (or resolve to `true`) to block it.\n *\n * Automatically removed on component unmount if called during component setup.\n * Also installs a `beforeunload` handler so the browser shows a confirmation\n * dialog when the user tries to close the tab while a blocker is active.\n *\n * @example\n * const blocker = useBlocker((to, from) => {\n * return hasUnsavedChanges() && !confirm(\"Discard changes?\")\n * })\n * // later: blocker.remove()\n */\nexport function useBlocker(fn: BlockerFn): Blocker {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n router._blockers.add(fn)\n\n // Warn before tab/window close while this blocker is registered\n const beforeUnloadHandler = _isBrowser\n ? (e: BeforeUnloadEvent) => {\n e.preventDefault()\n }\n : null\n if (beforeUnloadHandler) {\n window.addEventListener(\"beforeunload\", beforeUnloadHandler)\n }\n\n const remove = () => {\n router._blockers.delete(fn)\n if (beforeUnloadHandler) {\n window.removeEventListener(\"beforeunload\", beforeUnloadHandler)\n }\n }\n\n // Auto-remove when the component that called useBlocker unmounts\n onUnmount(() => remove())\n\n return { remove }\n}\n\n/**\n * Reactive read/write access to the current route's query parameters.\n *\n * Returns `[get, set]` where `get` is a reactive signal producing the merged\n * query object and `set` navigates to the current path with updated params.\n *\n * @example\n * const [params, setParams] = useSearchParams({ page: \"1\", sort: \"name\" })\n * params().page // \"1\" if not in URL\n * setParams({ page: \"2\" }) // navigates to ?page=2&sort=name\n */\nexport function useSearchParams<T extends Record<string, string>>(\n defaults?: T,\n): [get: () => T, set: (updates: Partial<T>) => Promise<void>] {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n const get = (): T => {\n const query = router.currentRoute().query\n if (!defaults) return query as T\n return { ...defaults, ...query } as T\n }\n const set = (updates: Partial<T>): Promise<void> => {\n const merged = { ...get(), ...updates }\n const path = router.currentRoute().path + stringifyQuery(merged as Record<string, string>)\n return router.replace(path)\n }\n return [get, set]\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\nexport function createRouter(options: RouterOptions | RouteRecord[]): Router {\n const opts: RouterOptions = Array.isArray(options) ? { routes: options } : options\n const {\n routes,\n mode = \"hash\",\n scrollBehavior,\n onError,\n maxCacheSize = 100,\n trailingSlash = \"strip\",\n } = opts\n\n // Base path only applies to history mode — hash-based routing already namespaces via #\n const base = mode === \"history\" ? normalizeBase(opts.base ?? \"\") : \"\"\n\n // Pre-built O(1) name → record index. Computed once at startup.\n const nameIndex = buildNameIndex(routes)\n\n const guards: NavigationGuard[] = []\n const afterHooks: AfterEachHook[] = []\n const scrollManager = new ScrollManager(scrollBehavior)\n\n // Navigation generation counter — cancels in-flight navigations when a newer\n // one starts. Prevents out-of-order completion from stale async guards.\n let _navGen = 0\n\n // ── Initial location ──────────────────────────────────────────────────────\n\n const getInitialLocation = (): string => {\n // SSR: use explicitly provided url (strip base if present)\n if (opts.url) return stripBase(opts.url, base)\n if (!_isBrowser) return \"/\"\n if (mode === \"history\") {\n return stripBase(window.location.pathname, base) + window.location.search\n }\n const hash = window.location.hash\n return hash.startsWith(\"#\") ? hash.slice(1) || \"/\" : \"/\"\n }\n\n const getCurrentLocation = (): string => {\n if (!_isBrowser) return currentPath()\n if (mode === \"history\") {\n return stripBase(window.location.pathname, base) + window.location.search\n }\n const hash = window.location.hash\n return hash.startsWith(\"#\") ? hash.slice(1) || \"/\" : \"/\"\n }\n\n // ── Signals ───────────────────────────────────────────────────────────────\n\n const currentPath = signal(normalizeTrailingSlash(getInitialLocation(), trailingSlash))\n const currentRoute = computed<ResolvedRoute>(() => resolveRoute(currentPath(), routes))\n\n // Browser event listeners — stored so destroy() can remove them\n let _popstateHandler: (() => void) | null = null\n let _hashchangeHandler: (() => void) | null = null\n\n if (_isBrowser) {\n if (mode === \"history\") {\n _popstateHandler = () => currentPath.set(getCurrentLocation())\n window.addEventListener(\"popstate\", _popstateHandler)\n } else {\n _hashchangeHandler = () => currentPath.set(getCurrentLocation())\n window.addEventListener(\"hashchange\", _hashchangeHandler)\n }\n }\n\n const componentCache = new Map<RouteRecord, ComponentFn>()\n const loadingSignal = signal(0)\n\n // ── Navigation ────────────────────────────────────────────────────────────\n\n type GuardOutcome =\n | { action: \"continue\" }\n | { action: \"cancel\" }\n | { action: \"redirect\"; target: string }\n\n async function evaluateGuard(\n guard: NavigationGuard,\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n const result = await runGuard(guard, to, from)\n if (gen !== _navGen) return { action: \"cancel\" }\n if (result === false) return { action: \"cancel\" }\n if (typeof result === \"string\") return { action: \"redirect\", target: result }\n return { action: \"continue\" }\n }\n\n async function runRouteGuards(\n records: RouteRecord[],\n guardKey: \"beforeLeave\" | \"beforeEnter\",\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n for (const record of records) {\n const raw = record[guardKey]\n if (!raw) continue\n const routeGuards = Array.isArray(raw) ? raw : [raw]\n for (const guard of routeGuards) {\n const outcome = await evaluateGuard(guard, to, from, gen)\n if (outcome.action !== \"continue\") return outcome\n }\n }\n return { action: \"continue\" }\n }\n\n async function runGlobalGuards(\n globalGuards: NavigationGuard[],\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n for (const guard of globalGuards) {\n const outcome = await evaluateGuard(guard, to, from, gen)\n if (outcome.action !== \"continue\") return outcome\n }\n return { action: \"continue\" }\n }\n\n function processLoaderResult(\n result: PromiseSettledResult<unknown>,\n record: RouteRecord,\n ac: AbortController,\n to: ResolvedRoute,\n ): boolean {\n if (result.status === \"fulfilled\") {\n router._loaderData.set(record, result.value)\n return true\n }\n if (ac.signal.aborted) return true\n if (router._onError) {\n const cancel = router._onError(result.reason, to)\n if (cancel === false) return false\n }\n router._loaderData.set(record, undefined)\n return true\n }\n\n function syncBrowserUrl(path: string, replace: boolean): void {\n if (!_isBrowser) return\n const url = mode === \"history\" ? `${base}${path}` : `#${path}`\n if (replace) {\n window.history.replaceState(null, \"\", url)\n } else {\n window.history.pushState(null, \"\", url)\n }\n }\n\n function resolveRedirect(to: ResolvedRoute): string | null {\n const leaf = to.matched[to.matched.length - 1]\n if (!leaf?.redirect) return null\n return sanitizePath(typeof leaf.redirect === \"function\" ? leaf.redirect(to) : leaf.redirect)\n }\n\n async function runAllGuards(\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n const leaveOutcome = await runRouteGuards(from.matched, \"beforeLeave\", to, from, gen)\n if (leaveOutcome.action !== \"continue\") return leaveOutcome\n\n const enterOutcome = await runRouteGuards(to.matched, \"beforeEnter\", to, from, gen)\n if (enterOutcome.action !== \"continue\") return enterOutcome\n\n return runGlobalGuards(guards, to, from, gen)\n }\n\n async function runBlockingLoaders(\n records: RouteRecord[],\n to: ResolvedRoute,\n gen: number,\n ac: AbortController,\n ): Promise<boolean> {\n const loaderCtx: LoaderContext = { params: to.params, query: to.query, signal: ac.signal }\n const results = await Promise.allSettled(\n records.map((r) => (r.loader ? r.loader(loaderCtx) : Promise.resolve(undefined))),\n )\n if (gen !== _navGen) return false\n for (let i = 0; i < records.length; i++) {\n const result = results[i]\n const record = records[i]\n if (!result || !record) continue\n if (!processLoaderResult(result, record, ac, to)) return false\n }\n return true\n }\n\n /** Fire-and-forget background revalidation for stale-while-revalidate routes. */\n function revalidateSwrLoaders(records: RouteRecord[], to: ResolvedRoute, ac: AbortController) {\n const loaderCtx: LoaderContext = { params: to.params, query: to.query, signal: ac.signal }\n for (const r of records) {\n if (!r.loader) continue\n r.loader(loaderCtx)\n .then((data) => {\n if (!ac.signal.aborted) {\n router._loaderData.set(r, data)\n // Bump loadingSignal to trigger reactive re-render with fresh data\n loadingSignal.update((n) => n + 1)\n loadingSignal.update((n) => n - 1)\n }\n })\n .catch(() => {\n /* Background revalidation failure — stale data remains valid */\n })\n }\n }\n\n async function runLoaders(to: ResolvedRoute, gen: number, ac: AbortController): Promise<boolean> {\n const loadableRecords = to.matched.filter((r) => r.loader)\n if (loadableRecords.length === 0) return true\n\n const blocking: RouteRecord[] = []\n const swr: RouteRecord[] = []\n for (const r of loadableRecords) {\n if (r.staleWhileRevalidate && router._loaderData.has(r)) {\n swr.push(r)\n } else {\n blocking.push(r)\n }\n }\n\n if (blocking.length > 0) {\n const ok = await runBlockingLoaders(blocking, to, gen, ac)\n if (!ok) return false\n }\n if (swr.length > 0) revalidateSwrLoaders(swr, to, ac)\n return true\n }\n\n function commitNavigation(\n path: string,\n replace: boolean,\n to: ResolvedRoute,\n from: ResolvedRoute,\n ): void {\n scrollManager.save(from.path)\n currentPath.set(path)\n syncBrowserUrl(path, replace)\n\n if (_isBrowser && to.meta.title) {\n document.title = to.meta.title\n }\n\n for (const record of router._loaderData.keys()) {\n if (!to.matched.includes(record)) {\n router._loaderData.delete(record)\n }\n }\n\n for (const hook of afterHooks) {\n try {\n hook(to, from)\n } catch (_err) {\n /* hook errors silently ignored */\n }\n }\n\n if (_isBrowser) {\n queueMicrotask(() => scrollManager.restore(to, from))\n }\n }\n\n async function checkBlockers(\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<\"continue\" | \"cancel\"> {\n for (const blocker of router._blockers) {\n const blocked = await blocker(to, from)\n if (gen !== _navGen || blocked) return \"cancel\"\n }\n return \"continue\"\n }\n\n async function navigate(rawPath: string, replace: boolean, redirectDepth = 0): Promise<void> {\n if (redirectDepth > 10) return\n\n const path = normalizeTrailingSlash(rawPath, trailingSlash)\n const gen = ++_navGen\n loadingSignal.update((n) => n + 1)\n\n const to = resolveRoute(path, routes)\n const from = currentRoute()\n\n const redirectTarget = resolveRedirect(to)\n if (redirectTarget !== null) {\n loadingSignal.update((n) => n - 1)\n return navigate(redirectTarget, replace, redirectDepth + 1)\n }\n\n const blockerResult = await checkBlockers(to, from, gen)\n if (blockerResult !== \"continue\") {\n loadingSignal.update((n) => n - 1)\n return\n }\n\n const guardOutcome = await runAllGuards(to, from, gen)\n if (guardOutcome.action !== \"continue\") {\n loadingSignal.update((n) => n - 1)\n if (guardOutcome.action === \"redirect\") {\n return navigate(sanitizePath(guardOutcome.target), replace, redirectDepth + 1)\n }\n return\n }\n\n router._abortController?.abort()\n const ac = new AbortController()\n router._abortController = ac\n\n const loadersOk = await runLoaders(to, gen, ac)\n if (!loadersOk) {\n loadingSignal.update((n) => n - 1)\n return\n }\n\n commitNavigation(path, replace, to, from)\n loadingSignal.update((n) => n - 1)\n }\n\n // ── isReady promise ─────────────────────────────────────────────────────\n // Resolves after the first navigation (including guards + loaders) completes.\n\n let _readyResolve: (() => void) | null = null\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n // ── Public router object ──────────────────────────────────────────────────\n\n const router: RouterInstance = {\n routes,\n mode,\n _base: base,\n currentRoute,\n _currentPath: currentPath,\n _currentRoute: currentRoute,\n _componentCache: componentCache,\n _loadingSignal: loadingSignal,\n _scrollPositions: new Map(),\n _scrollBehavior: scrollBehavior,\n _viewDepth: 0,\n _erroredChunks: new Set(),\n _loaderData: new Map(),\n _abortController: null,\n _blockers: new Set(),\n _readyResolve,\n _readyPromise,\n _onError: onError,\n _maxCacheSize: maxCacheSize,\n\n async push(\n location:\n | string\n | { name: string; params?: Record<string, string>; query?: Record<string, string> },\n ) {\n if (typeof location === \"string\") {\n const resolved = resolveRelativePath(location, currentPath())\n return navigate(sanitizePath(resolved), false)\n }\n const path = resolveNamedPath(\n location.name,\n location.params ?? {},\n location.query ?? {},\n nameIndex,\n )\n return navigate(path, false)\n },\n\n async replace(\n location:\n | string\n | { name: string; params?: Record<string, string>; query?: Record<string, string> },\n ) {\n if (typeof location === \"string\") {\n const resolved = resolveRelativePath(location, currentPath())\n return navigate(sanitizePath(resolved), true)\n }\n const path = resolveNamedPath(\n location.name,\n location.params ?? {},\n location.query ?? {},\n nameIndex,\n )\n return navigate(path, true)\n },\n\n back() {\n if (_isBrowser) window.history.back()\n },\n\n forward() {\n if (_isBrowser) window.history.forward()\n },\n\n go(delta: number) {\n if (_isBrowser) window.history.go(delta)\n },\n\n beforeEach(guard: NavigationGuard) {\n guards.push(guard)\n return () => {\n const idx = guards.indexOf(guard)\n if (idx >= 0) guards.splice(idx, 1)\n }\n },\n\n afterEach(hook: AfterEachHook) {\n afterHooks.push(hook)\n return () => {\n const idx = afterHooks.indexOf(hook)\n if (idx >= 0) afterHooks.splice(idx, 1)\n }\n },\n\n loading: () => loadingSignal() > 0,\n\n isReady() {\n return router._readyPromise\n },\n\n destroy() {\n if (_popstateHandler) {\n window.removeEventListener(\"popstate\", _popstateHandler)\n _popstateHandler = null\n }\n if (_hashchangeHandler) {\n window.removeEventListener(\"hashchange\", _hashchangeHandler)\n _hashchangeHandler = null\n }\n guards.length = 0\n afterHooks.length = 0\n router._blockers.clear()\n componentCache.clear()\n router._loaderData.clear()\n router._abortController?.abort()\n router._abortController = null\n },\n\n _resolve: (rawPath: string) => resolveRoute(rawPath, routes),\n }\n\n // Initial route is resolved synchronously — mark ready on next microtask\n // so consumers can await isReady() before the first render.\n queueMicrotask(() => {\n if (router._readyResolve) {\n router._readyResolve()\n router._readyResolve = null\n }\n })\n\n return router\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nasync function runGuard(\n guard: NavigationGuard,\n to: ResolvedRoute,\n from: ResolvedRoute,\n): Promise<NavigationGuardResult> {\n try {\n return await guard(to, from)\n } catch (_err) {\n return false\n }\n}\n\nfunction resolveNamedPath(\n name: string,\n params: Record<string, string>,\n query: Record<string, string>,\n index: Map<string, RouteRecord>,\n): string {\n const record = index.get(name)\n if (!record) {\n return \"/\"\n }\n let path = buildPath(record.path, params)\n const qs = Object.entries(query)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\")\n if (qs) path += `?${qs}`\n return path\n}\n\n/** Normalize a base path: ensure leading `/`, strip trailing `/`. */\nfunction normalizeBase(raw: string): string {\n if (!raw) return \"\"\n let b = raw\n if (!b.startsWith(\"/\")) b = `/${b}`\n if (b.endsWith(\"/\")) b = b.slice(0, -1)\n return b\n}\n\n/** Strip the base prefix from a full URL path. Returns the app-relative path. */\nfunction stripBase(path: string, base: string): string {\n if (!base) return path\n if (path === base || path === `${base}/`) return \"/\"\n if (path.startsWith(`${base}/`)) return path.slice(base.length)\n return path\n}\n\n/** Normalize trailing slash on a path according to the configured strategy. */\nfunction normalizeTrailingSlash(path: string, strategy: \"strip\" | \"add\" | \"ignore\"): string {\n if (strategy === \"ignore\" || path === \"/\") return path\n // Split off query string + hash so we only touch the path portion\n const qIdx = path.indexOf(\"?\")\n const hIdx = path.indexOf(\"#\")\n const endIdx = qIdx >= 0 ? qIdx : hIdx >= 0 ? hIdx : path.length\n const pathPart = path.slice(0, endIdx)\n const suffix = path.slice(endIdx)\n if (strategy === \"strip\") {\n return pathPart.length > 1 && pathPart.endsWith(\"/\") ? pathPart.slice(0, -1) + suffix : path\n }\n // strategy === \"add\"\n return !pathPart.endsWith(\"/\") ? `${pathPart}/${suffix}` : path\n}\n\n/**\n * Resolve a relative path (starting with `.` or `..`) against the current path.\n * Non-relative paths are returned as-is.\n */\nfunction resolveRelativePath(to: string, from: string): string {\n if (!to.startsWith(\"./\") && !to.startsWith(\"../\") && to !== \".\" && to !== \"..\") return to\n\n // Split current path into segments, drop the last segment (file-like resolution)\n const fromSegments = from.split(\"/\").filter(Boolean)\n fromSegments.pop()\n\n const toSegments = to.split(\"/\").filter(Boolean)\n for (const seg of toSegments) {\n if (seg === \"..\") {\n fromSegments.pop()\n } else if (seg !== \".\") {\n fromSegments.push(seg)\n }\n }\n return `/${fromSegments.join(\"/\")}`\n}\n\n/** Block unsafe navigation targets: javascript/data/vbscript URIs and absolute URLs. */\nfunction sanitizePath(path: string): string {\n const trimmed = path.trim()\n if (/^(?:javascript|data|vbscript):/i.test(trimmed)) {\n return \"/\"\n }\n // Block absolute URLs and protocol-relative URLs — router only handles same-origin paths\n if (/^\\/\\/|^https?:/i.test(trimmed)) {\n return \"/\"\n }\n return path\n}\n\nexport { isLazy }\n","import type { ComponentFn, Props, VNode, VNodeChild } from \"@pyreon/core\"\nimport { createRef, h, onUnmount, popContext, pushContext, useContext } from \"@pyreon/core\"\nimport { LoaderDataContext, prefetchLoaderData } from \"./loader\"\nimport { isLazy, RouterContext, setActiveRouter } from \"./router\"\nimport type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from \"./types\"\n\n// Track prefetched paths per router to avoid duplicate fetches\nconst _prefetched = new WeakMap<RouterInstance, Set<string>>()\n\n// ─── RouterProvider ───────────────────────────────────────────────────────────\n\nexport interface RouterProviderProps extends Props {\n router: Router\n children?: VNode | VNodeChild | null\n}\n\nexport const RouterProvider: ComponentFn<RouterProviderProps> = (props) => {\n const router = props.router as RouterInstance\n // Push router into the context stack — isolated per request in SSR via ALS,\n // isolated per component tree in CSR.\n const frame = new Map([[RouterContext.id, router]])\n pushContext(frame)\n onUnmount(() => {\n popContext()\n // Clean up event listeners, caches, abort in-flight navigations.\n // Safe to call multiple times (destroy is idempotent).\n router.destroy()\n setActiveRouter(null)\n })\n // Also set the module fallback so programmatic useRouter() outside a component\n // tree (e.g. navigation guards in event handlers) still works in CSR.\n setActiveRouter(router)\n return (props.children ?? null) as VNode | null\n}\n\n// ─── RouterView ───────────────────────────────────────────────────────────────\n\nexport interface RouterViewProps extends Props {\n /** Explicitly pass a router (optional — uses the active router by default) */\n router?: Router\n}\n\n/**\n * Renders the matched route component at this nesting level.\n *\n * Nested layouts work by placing a second `<RouterView />` inside the layout\n * component — it automatically renders the next level of the matched route.\n *\n * How depth tracking works:\n * Pyreon components run once in depth-first tree order. Each `RouterView`\n * captures `router._viewDepth` at setup time and immediately increments it,\n * so sibling and child views get the correct index. `onUnmount` decrements\n * the counter so dynamic route swaps work correctly.\n *\n * @example\n * // Route config:\n * { path: \"/admin\", component: AdminLayout, children: [\n * { path: \"users\", component: AdminUsers },\n * ]}\n *\n * // AdminLayout renders a nested RouterView:\n * function AdminLayout() {\n * return <div><Sidebar /><RouterView /></div>\n * }\n */\nexport const RouterView: ComponentFn<RouterViewProps> = (props) => {\n const router = ((props.router as RouterInstance | undefined) ??\n useContext(RouterContext)) as RouterInstance | null\n if (!router) return null\n\n // Claim this view's depth at setup time (depth-first component init order)\n const depth = router._viewDepth\n router._viewDepth++\n\n onUnmount(() => {\n router._viewDepth--\n })\n\n const child = (): VNodeChild => {\n router._loadingSignal() // reactive — re-renders after lazy load completes\n\n const route = router.currentRoute()\n\n if (route.matched.length === 0) return null\n\n // Render the matched record at this view's depth level\n const record = route.matched[depth]\n if (!record) return null // no component at this nesting level\n\n const cached = router._componentCache.get(record)\n if (cached) {\n return renderWithLoader(router, record, cached, route)\n }\n\n const raw = record.component\n\n if (!isLazy(raw)) {\n cacheSet(router, record, raw)\n return renderWithLoader(router, record, raw, route)\n }\n\n return renderLazyRoute(router, record, raw)\n }\n\n return h(\"div\", { \"data-pyreon-router-view\": true }, child as unknown as VNodeChild)\n}\n\n// ─── RouterLink ───────────────────────────────────────────────────────────────\n\nexport interface RouterLinkProps extends Props {\n to: string\n /** If true, uses router.replace() instead of router.push() */\n replace?: boolean\n /** CSS class applied when this link is active (default: \"router-link-active\") */\n activeClass?: string\n /** CSS class for exact-match active state (default: \"router-link-exact-active\") */\n exactActiveClass?: string\n /** If true, only applies activeClass on exact match */\n exact?: boolean\n /**\n * Prefetch strategy for loader data:\n * - \"hover\" (default) — prefetch when the user hovers over the link\n * - \"viewport\" — prefetch when the link scrolls into the viewport\n * - \"none\" — no prefetching\n */\n prefetch?: \"hover\" | \"viewport\" | \"none\"\n children?: VNodeChild | null\n}\n\nexport const RouterLink: ComponentFn<RouterLinkProps> = (props) => {\n const router = useContext(RouterContext)\n const prefetchMode = props.prefetch ?? \"hover\"\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault()\n if (!router) return\n if (props.replace) {\n router.replace(props.to)\n } else {\n router.push(props.to)\n }\n }\n\n const handleMouseEnter = () => {\n if (prefetchMode !== \"hover\" || !router) return\n prefetchRoute(router as RouterInstance, props.to)\n }\n\n const inst = router as RouterInstance | null\n const href = inst?.mode === \"history\" ? `${inst._base}${props.to}` : `#${props.to}`\n\n const activeClass = (): string => {\n if (!router) return \"\"\n const current = router.currentRoute().path\n const target = props.to\n const isExact = current === target\n const isActive = isExact || (!props.exact && isSegmentPrefix(current, target))\n\n const classes: string[] = []\n if (isActive) classes.push(props.activeClass ?? \"router-link-active\")\n if (isExact) classes.push(props.exactActiveClass ?? \"router-link-exact-active\")\n return classes.join(\" \").trim()\n }\n\n // Viewport prefetching — observe link visibility with IntersectionObserver\n const ref = createRef<Element>()\n if (prefetchMode === \"viewport\" && router && typeof IntersectionObserver !== \"undefined\") {\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n prefetchRoute(router as RouterInstance, props.to)\n observer.disconnect()\n break\n }\n }\n })\n // Observe after mount — the ref will be populated once the element is in the DOM\n queueMicrotask(() => {\n observer.observe(ref.current as Element)\n })\n onUnmount(() => observer.disconnect())\n }\n\n return h(\n \"a\",\n { ref, href, class: activeClass, onClick: handleClick, onMouseEnter: handleMouseEnter },\n props.children ?? props.to,\n )\n}\n\n/** Prefetch loader data for a route (only once per router + path). */\nfunction prefetchRoute(router: RouterInstance, path: string): void {\n let set = _prefetched.get(router)\n if (!set) {\n set = new Set()\n _prefetched.set(router, set)\n }\n if (set.has(path)) return\n set.add(path)\n prefetchLoaderData(router, path).catch(() => {\n // Silently ignore — prefetch is best-effort\n set?.delete(path)\n })\n}\n\nfunction renderLazyRoute(\n router: RouterInstance,\n record: RouteRecord,\n raw: LazyComponent,\n): VNodeChild {\n if (router._erroredChunks.has(record)) {\n return raw.errorComponent ? h(raw.errorComponent, {}) : null\n }\n\n const tryLoad = (attempt: number): Promise<void> =>\n raw\n .loader()\n .then((mod) => {\n const resolved = typeof mod === \"function\" ? mod : mod.default\n cacheSet(router, record, resolved)\n router._loadingSignal.update((n) => n + 1)\n })\n .catch((err: unknown) => {\n if (attempt < 3) {\n return new Promise<void>((res) => setTimeout(res, 500 * 2 ** attempt)).then(() =>\n tryLoad(attempt + 1),\n )\n }\n if (typeof window !== \"undefined\" && isStaleChunk(err)) {\n window.location.reload()\n return\n }\n\n router._erroredChunks.add(record)\n router._loadingSignal.update((n) => n + 1)\n })\n\n tryLoad(0)\n return raw.loadingComponent ? h(raw.loadingComponent, {}) : null\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Wraps the route component with a LoaderDataProvider so `useLoaderData()` works\n * inside the component. If the record has no loader, renders the component directly.\n */\nfunction renderWithLoader(\n router: RouterInstance,\n record: RouteRecord,\n Comp: ComponentFn,\n route: Pick<ResolvedRoute, \"params\" | \"query\" | \"meta\">,\n): VNodeChild {\n const routeProps = { params: route.params, query: route.query, meta: route.meta }\n if (!record.loader) {\n return h(Comp, routeProps)\n }\n const data = router._loaderData.get(record)\n // If loader data is undefined and route has an errorComponent, render it\n if (data === undefined && record.errorComponent) {\n return h(record.errorComponent, routeProps)\n }\n return h(LoaderDataProvider, { data, children: h(Comp, routeProps) })\n}\n\n/**\n * Thin provider component that pushes LoaderDataContext before children mount.\n * Uses Pyreon's context stack so useLoaderData() reads it during child setup.\n */\nfunction LoaderDataProvider(props: { data: unknown; children: VNode | null }): VNode | null {\n const frame = new Map([[LoaderDataContext.id, props.data]])\n pushContext(frame)\n onUnmount(() => popContext())\n return props.children\n}\n\n/** Evict oldest cache entries when the component cache exceeds maxCacheSize. */\nfunction cacheSet(router: RouterInstance, record: RouteRecord, comp: ComponentFn): void {\n router._componentCache.set(record, comp)\n if (router._componentCache.size > router._maxCacheSize) {\n // Map iterates in insertion order — first key is oldest\n const oldest = router._componentCache.keys().next().value as RouteRecord\n router._componentCache.delete(oldest)\n }\n}\n\n/**\n * Segment-aware prefix check for active link matching.\n * `/admin` is a prefix of `/admin/users` but NOT of `/admin-panel`.\n */\nfunction isSegmentPrefix(current: string, target: string): boolean {\n if (target === \"/\") return false\n const cs = current.split(\"/\").filter(Boolean)\n const ts = target.split(\"/\").filter(Boolean)\n if (ts.length > cs.length) return false\n return ts.every((seg, i) => seg === cs[i])\n}\n\n/**\n * Detect a stale chunk error — happens post-deploy when the browser requests\n * a hashed filename that no longer exists on the server. Trigger a full reload\n * so the user gets the new bundle instead of a broken loading state.\n */\nfunction isStaleChunk(err: unknown): boolean {\n if (err instanceof TypeError && String(err.message).includes(\"Failed to fetch\")) return true\n if (err instanceof SyntaxError) return true\n return false\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/loader.ts","../src/match.ts","../src/scroll.ts","../src/types.ts","../src/router.ts","../src/components.tsx"],"sourcesContent":["import type { Context } from \"@pyreon/core\"\nimport { createContext, useContext } from \"@pyreon/core\"\nimport type { RouterInstance } from \"./types\"\n\n/**\n * Context frame that holds the loader data for the currently rendered route record.\n * Pushed by RouterView's withLoaderData wrapper before invoking the route component.\n */\nexport const LoaderDataContext: Context<unknown> = createContext<unknown>(undefined)\n\n/**\n * Returns the data resolved by the current route's `loader` function.\n * Must be called inside a route component rendered by <RouterView />.\n *\n * @example\n * const routes = [{ path: \"/users\", component: Users, loader: fetchUsers }]\n *\n * function Users() {\n * const users = useLoaderData<User[]>()\n * return h(\"ul\", null, users.map(u => h(\"li\", null, u.name)))\n * }\n */\nexport function useLoaderData<T = unknown>(): T {\n return useContext(LoaderDataContext) as T\n}\n\n/**\n * SSR helper: pre-run all loaders for the given path before rendering.\n * Call this before `renderToString` so route components can read data via `useLoaderData()`.\n *\n * @example\n * const router = createRouter({ routes, url: req.url })\n * await prefetchLoaderData(router, req.url)\n * const html = await renderToString(h(App, { router }))\n */\nexport async function prefetchLoaderData(router: RouterInstance, path: string): Promise<void> {\n const route = router._resolve(path)\n const ac = new AbortController()\n router._abortController = ac\n await Promise.all(\n route.matched\n .filter((r) => r.loader)\n .map(async (r) => {\n const data = await r.loader?.({\n params: route.params,\n query: route.query,\n signal: ac.signal,\n })\n router._loaderData.set(r, data)\n }),\n )\n}\n\n/**\n * Serialize loader data to a JSON-safe plain object for embedding in SSR HTML.\n * Keys are route path patterns (stable across server and client).\n *\n * @example — SSR handler:\n * await prefetchLoaderData(router, req.url)\n * const { html, head } = await renderWithHead(h(App, null))\n * const page = `...${head}\n * <script>window.__PYREON_LOADER_DATA__=${JSON.stringify(serializeLoaderData(router))}</script>\n * ...${html}...`\n */\nexport function serializeLoaderData(router: RouterInstance): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [record, data] of router._loaderData) {\n result[record.path] = data\n }\n return result\n}\n\n/**\n * Hydrate loader data from a serialized object (e.g. `window.__PYREON_LOADER_DATA__`).\n * Populates the router's internal `_loaderData` map so the initial render uses\n * server-fetched data without re-running loaders on the client.\n *\n * Call this before `mount()`, after `createRouter()`.\n *\n * @example — client entry:\n * import { hydrateLoaderData } from \"@pyreon/router\"\n * const router = createRouter({ routes })\n * hydrateLoaderData(router, window.__PYREON_LOADER_DATA__ ?? {})\n * mount(h(App, null), document.getElementById(\"app\")!)\n */\nexport function hydrateLoaderData(\n router: RouterInstance,\n serialized: Record<string, unknown>,\n): void {\n if (!serialized || typeof serialized !== \"object\") return\n const route = router._resolve(router.currentRoute().path)\n for (const record of route.matched) {\n if (Object.hasOwn(serialized, record.path)) {\n router._loaderData.set(record, serialized[record.path])\n }\n }\n}\n","import type { ResolvedRoute, RouteMeta, RouteRecord } from \"./types\"\n\n// ─── Query string ─────────────────────────────────────────────────────────────\n\n/**\n * Parse a query string into key-value pairs. Duplicate keys are overwritten\n * (last value wins). Use `parseQueryMulti` to preserve duplicates as arrays.\n */\nexport function parseQuery(qs: string): Record<string, string> {\n if (!qs) return {}\n const result: Record<string, string> = {}\n for (const part of qs.split(\"&\")) {\n const eqIdx = part.indexOf(\"=\")\n if (eqIdx < 0) {\n const key = decodeURIComponent(part)\n if (key) result[key] = \"\"\n } else {\n const key = decodeURIComponent(part.slice(0, eqIdx))\n const val = decodeURIComponent(part.slice(eqIdx + 1))\n if (key) result[key] = val\n }\n }\n return result\n}\n\n/**\n * Parse a query string preserving duplicate keys as arrays.\n *\n * @example\n * parseQueryMulti(\"color=red&color=blue&size=lg\")\n * // → { color: [\"red\", \"blue\"], size: \"lg\" }\n */\nexport function parseQueryMulti(qs: string): Record<string, string | string[]> {\n if (!qs) return {}\n const result: Record<string, string | string[]> = {}\n for (const part of qs.split(\"&\")) {\n const eqIdx = part.indexOf(\"=\")\n let key: string\n let val: string\n if (eqIdx < 0) {\n key = decodeURIComponent(part)\n val = \"\"\n } else {\n key = decodeURIComponent(part.slice(0, eqIdx))\n val = decodeURIComponent(part.slice(eqIdx + 1))\n }\n if (!key) continue\n const existing = result[key]\n if (existing === undefined) {\n result[key] = val\n } else if (Array.isArray(existing)) {\n existing.push(val)\n } else {\n result[key] = [existing, val]\n }\n }\n return result\n}\n\nexport function stringifyQuery(query: Record<string, string>): string {\n const parts: string[] = []\n for (const [k, v] of Object.entries(query)) {\n parts.push(v ? `${encodeURIComponent(k)}=${encodeURIComponent(v)}` : encodeURIComponent(k))\n }\n return parts.length ? `?${parts.join(\"&\")}` : \"\"\n}\n\n// ─── Compiled route structures ───────────────────────────────────────────────\n\n/**\n * Pre-compiled segment info — computed once per route, reused on every match.\n * Avoids repeated split()/filter()/startsWith(\":\") on hot paths.\n */\ninterface CompiledSegment {\n /** Original segment string */\n raw: string\n /** true if this segment is a `:param` */\n isParam: boolean\n /** true if this segment is a `:param*` splat */\n isSplat: boolean\n /** true if this segment is a `:param?` optional */\n isOptional: boolean\n /** Param name (without leading `:` and trailing `*`/`?`) — empty for static segments */\n paramName: string\n}\n\ninterface CompiledRoute {\n route: RouteRecord\n /** true for wildcard patterns: `(.*)` or `*` */\n isWildcard: boolean\n /** Pre-split and classified segments */\n segments: CompiledSegment[]\n /** Number of segments */\n segmentCount: number\n /** true if route has no dynamic segments (pure static) */\n isStatic: boolean\n /** For static routes: the normalized path (e.g. \"/about\") for Map lookup */\n staticPath: string | null\n /** Compiled children (lazily populated) */\n children: CompiledRoute[] | null\n /** First static segment (for dispatch index), null if first segment is dynamic or route is wildcard */\n firstSegment: string | null\n}\n\n/**\n * A flattened route entry — pre-joins parent+child segments at compile time\n * so nested routes can be matched in a single pass without recursion.\n */\ninterface FlattenedRoute {\n /** All segments from root to leaf, concatenated */\n segments: CompiledSegment[]\n segmentCount: number\n /** The full matched chain from root to leaf (e.g. [adminLayout, usersPage]) */\n matchedChain: RouteRecord[]\n /** true if all segments are static */\n isStatic: boolean\n /** For static flattened routes: the full joined path */\n staticPath: string | null\n /** Pre-merged meta from all routes in the chain */\n meta: RouteMeta\n /** First static segment for dispatch index */\n firstSegment: string | null\n /** true if any segment is a splat */\n hasSplat: boolean\n /** true if this is a wildcard catch-all route (`*` or `(.*)`) */\n isWildcard: boolean\n /** true if any segment is optional (`:param?`) */\n hasOptional: boolean\n /** Minimum number of segments that must be present (excluding trailing optionals) */\n minSegments: number\n}\n\n/** WeakMap cache: compile each RouteRecord[] once */\nconst _compiledCache = new WeakMap<RouteRecord[], CompiledRoute[]>()\n\nfunction compileSegment(raw: string): CompiledSegment {\n if (raw.endsWith(\"*\") && raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: true, isOptional: false, paramName: raw.slice(1, -1) }\n }\n if (raw.endsWith(\"?\") && raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: false, isOptional: true, paramName: raw.slice(1, -1) }\n }\n if (raw.startsWith(\":\")) {\n return { raw, isParam: true, isSplat: false, isOptional: false, paramName: raw.slice(1) }\n }\n return { raw, isParam: false, isSplat: false, isOptional: false, paramName: \"\" }\n}\n\nfunction compileRoute(route: RouteRecord): CompiledRoute {\n const pattern = route.path\n const isWildcard = pattern === \"(.*)\" || pattern === \"*\"\n\n if (isWildcard) {\n return {\n route,\n isWildcard: true,\n segments: [],\n segmentCount: 0,\n isStatic: false,\n staticPath: null,\n children: null,\n firstSegment: null,\n }\n }\n\n const segments = pattern.split(\"/\").filter(Boolean).map(compileSegment)\n const isStatic = segments.every((s) => !s.isParam)\n const staticPath = isStatic ? `/${segments.map((s) => s.raw).join(\"/\")}` : null\n const first = segments.length > 0 ? segments[0] : undefined\n const firstSegment = first && !first.isParam ? first.raw : null\n\n return {\n route,\n isWildcard: false,\n segments,\n segmentCount: segments.length,\n isStatic,\n staticPath,\n children: null,\n firstSegment,\n }\n}\n\n/** Expand alias paths into additional compiled entries sharing the original RouteRecord */\nfunction expandAliases(r: RouteRecord, c: CompiledRoute): CompiledRoute[] {\n if (!r.alias) return []\n const aliases = Array.isArray(r.alias) ? r.alias : [r.alias]\n return aliases.map((aliasPath) => {\n const { alias: _, ...withoutAlias } = r\n const ac = compileRoute({ ...withoutAlias, path: aliasPath })\n ac.children = c.children\n ac.route = r\n return ac\n })\n}\n\nfunction compileRoutes(routes: RouteRecord[]): CompiledRoute[] {\n const cached = _compiledCache.get(routes)\n if (cached) return cached\n\n const compiled: CompiledRoute[] = []\n for (const r of routes) {\n const c = compileRoute(r)\n if (r.children && r.children.length > 0) {\n c.children = compileRoutes(r.children)\n }\n compiled.push(c)\n compiled.push(...expandAliases(r, c))\n }\n _compiledCache.set(routes, compiled)\n return compiled\n}\n\n// ─── Route flattening ────────────────────────────────────────────────────────\n\n/** Extract first static segment from a segment list, or null if dynamic/empty */\nfunction getFirstSegment(segments: CompiledSegment[]): string | null {\n const first = segments[0]\n if (first && !first.isParam) return first.raw\n return null\n}\n\n/** Build a FlattenedRoute from segments + metadata */\nfunction makeFlatEntry(\n segments: CompiledSegment[],\n chain: RouteRecord[],\n meta: RouteMeta,\n isWildcard: boolean,\n): FlattenedRoute {\n const isStatic = !isWildcard && segments.every((s) => !s.isParam)\n const hasOptional = segments.some((s) => s.isOptional)\n // minSegments: count of segments up to and not including trailing optionals\n let minSegs = segments.length\n if (hasOptional) {\n while (minSegs > 0 && segments[minSegs - 1]?.isOptional) minSegs--\n }\n return {\n segments,\n segmentCount: segments.length,\n matchedChain: chain,\n isStatic,\n staticPath: isStatic ? `/${segments.map((s) => s.raw).join(\"/\")}` : null,\n meta,\n firstSegment: getFirstSegment(segments),\n hasSplat: segments.some((s) => s.isSplat),\n isWildcard,\n hasOptional,\n minSegments: minSegs,\n }\n}\n\n/**\n * Flatten nested routes into leaf entries with pre-joined segments.\n * This eliminates recursion during matching for the common case.\n */\nfunction flattenRoutes(compiled: CompiledRoute[]): FlattenedRoute[] {\n const result: FlattenedRoute[] = []\n flattenWalk(result, compiled, [], [], {})\n return result\n}\n\nfunction flattenWalk(\n result: FlattenedRoute[],\n routes: CompiledRoute[],\n parentSegments: CompiledSegment[],\n parentChain: RouteRecord[],\n parentMeta: RouteMeta,\n): void {\n for (const c of routes) {\n const chain = [...parentChain, c.route]\n const meta = c.route.meta ? { ...parentMeta, ...c.route.meta } : { ...parentMeta }\n flattenOne(result, c, parentSegments, chain, meta)\n }\n}\n\nfunction flattenOne(\n result: FlattenedRoute[],\n c: CompiledRoute,\n parentSegments: CompiledSegment[],\n chain: RouteRecord[],\n meta: RouteMeta,\n): void {\n if (c.isWildcard) {\n result.push(makeFlatEntry(parentSegments, chain, meta, true))\n if (c.children && c.children.length > 0) {\n flattenWalk(result, c.children, parentSegments, chain, meta)\n }\n return\n }\n\n const joined = [...parentSegments, ...c.segments]\n if (c.children && c.children.length > 0) {\n flattenWalk(result, c.children, joined, chain, meta)\n }\n result.push(makeFlatEntry(joined, chain, meta, false))\n}\n\n// ─── Combined index ─────────────────────────────────────────────────────────\n\ninterface RouteIndex {\n /** O(1) lookup for fully static paths (including nested) */\n staticMap: Map<string, FlattenedRoute>\n /** First-segment dispatch: maps first path segment → candidate routes */\n segmentMap: Map<string, FlattenedRoute[]>\n /** Routes whose first segment is dynamic (fallback) */\n dynamicFirst: FlattenedRoute[]\n /** Wildcard/catch-all routes */\n wildcards: FlattenedRoute[]\n}\n\nconst _indexCache = new WeakMap<RouteRecord[], RouteIndex>()\n\n/** Classify a single flattened route into the appropriate index bucket */\nfunction indexFlatRoute(\n f: FlattenedRoute,\n staticMap: Map<string, FlattenedRoute>,\n segmentMap: Map<string, FlattenedRoute[]>,\n dynamicFirst: FlattenedRoute[],\n wildcards: FlattenedRoute[],\n): void {\n // Static map: first static entry wins (preserves definition order)\n if (f.isStatic && f.staticPath && !staticMap.has(f.staticPath)) {\n staticMap.set(f.staticPath, f)\n }\n\n if (f.isWildcard) {\n wildcards.push(f)\n return\n }\n\n // Root route \"/\" has 0 segments — already in static map\n if (f.segmentCount === 0) return\n\n // First-segment dispatch\n if (f.firstSegment) {\n let bucket = segmentMap.get(f.firstSegment)\n if (!bucket) {\n bucket = []\n segmentMap.set(f.firstSegment, bucket)\n }\n bucket.push(f)\n } else {\n dynamicFirst.push(f)\n }\n}\n\nfunction buildRouteIndex(routes: RouteRecord[], compiled: CompiledRoute[]): RouteIndex {\n const cached = _indexCache.get(routes)\n if (cached) return cached\n\n const flattened = flattenRoutes(compiled)\n\n const staticMap = new Map<string, FlattenedRoute>()\n const segmentMap = new Map<string, FlattenedRoute[]>()\n const dynamicFirst: FlattenedRoute[] = []\n const wildcards: FlattenedRoute[] = []\n\n for (const f of flattened) {\n indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards)\n }\n\n const index: RouteIndex = { staticMap, segmentMap, dynamicFirst, wildcards }\n _indexCache.set(routes, index)\n return index\n}\n\n// ─── Fast path splitting ─────────────────────────────────────────────────────\n\n/** Split path into segments without allocating a filtered array */\nfunction splitPath(path: string): string[] {\n // Fast path for common cases\n if (path === \"/\") return []\n // Remove leading slash, split, no filter needed if path is clean\n const start = path.charCodeAt(0) === 47 /* / */ ? 1 : 0\n const end = path.length\n if (start >= end) return []\n\n const parts: string[] = []\n let segStart = start\n for (let i = start; i <= end; i++) {\n if (i === end || path.charCodeAt(i) === 47 /* / */) {\n if (i > segStart) {\n parts.push(path.substring(segStart, i))\n }\n segStart = i + 1\n }\n }\n return parts\n}\n\n/** Decode only if the segment contains a `%` character */\nfunction decodeSafe(s: string): string {\n return s.indexOf(\"%\") >= 0 ? decodeURIComponent(s) : s\n}\n\n// ─── Path matching (compiled) ────────────────────────────────────────────────\n\n/**\n * Match a single route pattern against a path segment.\n * Returns extracted params or null if no match.\n *\n * Supports:\n * - Exact segments: \"/about\"\n * - Param segments: \"/user/:id\"\n * - Wildcard: \"(.*)\" matches everything\n */\n/** Match a single pattern segment against a path segment, extracting params. Returns false on mismatch. */\nfunction matchPatternSegment(\n pp: string,\n pt: string | undefined,\n params: Record<string, string>,\n pathParts: string[],\n i: number,\n): \"splat\" | \"continue\" | \"fail\" {\n if (pp.endsWith(\"*\") && pp.startsWith(\":\")) {\n params[pp.slice(1, -1)] = pathParts.slice(i).map(decodeURIComponent).join(\"/\")\n return \"splat\"\n }\n if (pp.endsWith(\"?\") && pp.startsWith(\":\")) {\n if (pt !== undefined) params[pp.slice(1, -1)] = decodeURIComponent(pt)\n return \"continue\"\n }\n if (pt === undefined) return \"fail\"\n if (pp.startsWith(\":\")) {\n params[pp.slice(1)] = decodeURIComponent(pt)\n return \"continue\"\n }\n return pp === pt ? \"continue\" : \"fail\"\n}\n\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n if (pattern === \"(.*)\" || pattern === \"*\") return {}\n\n const patternParts = pattern.split(\"/\").filter(Boolean)\n const pathParts = path.split(\"/\").filter(Boolean)\n\n const params: Record<string, string> = {}\n for (let i = 0; i < patternParts.length; i++) {\n const result = matchPatternSegment(\n patternParts[i] as string,\n pathParts[i],\n params,\n pathParts,\n i,\n )\n if (result === \"splat\") return params\n if (result === \"fail\") return null\n }\n\n if (pathParts.length > patternParts.length) return null\n return params\n}\n\n// ─── Compiled matching helpers ────────────────────────────────────────────────\n\n/** Collect remaining path segments as a decoded splat value */\nfunction captureSplat(pathParts: string[], from: number, pathLen: number): string {\n const remaining: string[] = []\n for (let j = from; j < pathLen; j++) {\n const p = pathParts[j]\n if (p !== undefined) remaining.push(decodeSafe(p))\n }\n return remaining.join(\"/\")\n}\n\n// ─── Flattened route matching ─────────────────────────────────────────────────\n\n/** Check whether a flattened route's segment count is compatible with the path length */\nfunction isSegmentCountCompatible(f: FlattenedRoute, pathLen: number): boolean {\n if (f.segmentCount === pathLen) return true\n if (f.hasSplat && pathLen >= f.segmentCount) return true\n if (f.hasOptional && pathLen >= f.minSegments && pathLen <= f.segmentCount) return true\n return false\n}\n\n/** Try to match a flattened route against path parts */\nfunction matchFlattened(\n f: FlattenedRoute,\n pathParts: string[],\n pathLen: number,\n): Record<string, string> | null {\n if (!isSegmentCountCompatible(f, pathLen)) return null\n\n const params: Record<string, string> = {}\n const segments = f.segments\n const count = f.segmentCount\n for (let i = 0; i < count; i++) {\n const seg = segments[i]\n const pt = pathParts[i]\n if (!seg) return null\n if (seg.isSplat) {\n params[seg.paramName] = captureSplat(pathParts, i, pathLen)\n return params\n }\n if (pt === undefined) {\n if (!seg.isOptional) return null\n continue\n }\n if (seg.isParam) {\n params[seg.paramName] = decodeSafe(pt)\n } else if (seg.raw !== pt) {\n return null\n }\n }\n return params\n}\n\n/** Search a list of flattened candidates for a match */\nfunction searchCandidates(\n candidates: FlattenedRoute[],\n pathParts: string[],\n pathLen: number,\n): MatchResult | null {\n for (let i = 0; i < candidates.length; i++) {\n const f = candidates[i]\n if (!f) continue\n const params = matchFlattened(f, pathParts, pathLen)\n if (params) {\n return { params, matched: f.matchedChain }\n }\n }\n return null\n}\n\n// ─── Route resolution ─────────────────────────────────────────────────────────\n\ninterface MatchResult {\n params: Record<string, string>\n matched: RouteRecord[]\n}\n\n/**\n * Resolve a raw path (including query string and hash) against the route tree.\n * Uses flattened index for O(1) static lookup and first-segment dispatch.\n */\nexport function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute {\n const qIdx = rawPath.indexOf(\"?\")\n const pathAndHash = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath\n const queryPart = qIdx >= 0 ? rawPath.slice(qIdx + 1) : \"\"\n\n const hIdx = pathAndHash.indexOf(\"#\")\n const cleanPath = hIdx >= 0 ? pathAndHash.slice(0, hIdx) : pathAndHash\n const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : \"\"\n\n const query = parseQuery(queryPart)\n\n // Build index (cached after first call)\n const compiled = compileRoutes(routes)\n const index = buildRouteIndex(routes, compiled)\n\n // Fast path 1: O(1) static Map lookup (covers nested static too)\n const staticMatch = index.staticMap.get(cleanPath)\n if (staticMatch) {\n return {\n path: cleanPath,\n params: {},\n query,\n hash,\n matched: staticMatch.matchedChain,\n meta: staticMatch.meta,\n }\n }\n\n // Split path for segment-based matching\n const pathParts = splitPath(cleanPath)\n const pathLen = pathParts.length\n\n // Fast path 2: first-segment dispatch (O(1) bucket lookup + small scan)\n if (pathLen > 0) {\n const first = pathParts[0] as string\n const bucket = index.segmentMap.get(first)\n if (bucket) {\n const match = searchCandidates(bucket, pathParts, pathLen)\n if (match) {\n return {\n path: cleanPath,\n params: match.params,\n query,\n hash,\n matched: match.matched,\n meta: mergeMeta(match.matched),\n }\n }\n }\n }\n\n // Fallback: dynamic-first-segment routes\n const dynMatch = searchCandidates(index.dynamicFirst, pathParts, pathLen)\n if (dynMatch) {\n return {\n path: cleanPath,\n params: dynMatch.params,\n query,\n hash,\n matched: dynMatch.matched,\n meta: mergeMeta(dynMatch.matched),\n }\n }\n\n // Fallback: wildcard/catch-all routes\n const w = index.wildcards[0]\n if (w) {\n return {\n path: cleanPath,\n params: {},\n query,\n hash,\n matched: w.matchedChain,\n meta: w.meta,\n }\n }\n\n return { path: cleanPath, params: {}, query, hash, matched: [], meta: {} }\n}\n\n/** Merge meta from matched routes (leaf takes precedence) */\nfunction mergeMeta(matched: RouteRecord[]): RouteMeta {\n const meta: RouteMeta = {}\n for (const record of matched) {\n if (record.meta) Object.assign(meta, record.meta)\n }\n return meta\n}\n\n/** Build a path string from a named route's pattern and params */\nexport function buildPath(pattern: string, params: Record<string, string>): string {\n const built = pattern.replace(/\\/:([^/]+)\\?/g, (_match, key) => {\n const val = params[key]\n // Optional param — omit the entire segment if no value provided\n if (!val) return \"\"\n return `/${encodeURIComponent(val)}`\n })\n return built.replace(/:([^/]+)\\*?/g, (match, key) => {\n const val = params[key] ?? \"\"\n // Splat params contain slashes — don't encode them\n if (match.endsWith(\"*\")) return val.split(\"/\").map(encodeURIComponent).join(\"/\")\n return encodeURIComponent(val)\n })\n}\n\n/** Find a route record by name (recursive, O(n)). Prefer buildNameIndex for repeated lookups. */\nexport function findRouteByName(name: string, routes: RouteRecord[]): RouteRecord | null {\n for (const route of routes) {\n if (route.name === name) return route\n if (route.children) {\n const found = findRouteByName(name, route.children)\n if (found) return found\n }\n }\n return null\n}\n\n/**\n * Pre-build a name → RouteRecord index from a route tree for O(1) named navigation.\n * Called once at router creation time; avoids O(n) depth-first search per push({ name }).\n */\nexport function buildNameIndex(routes: RouteRecord[]): Map<string, RouteRecord> {\n const index = new Map<string, RouteRecord>()\n function walk(list: RouteRecord[]): void {\n for (const route of list) {\n if (route.name) index.set(route.name, route)\n if (route.children) walk(route.children)\n }\n }\n walk(routes)\n return index\n}\n","import type { ResolvedRoute, RouterOptions } from \"./types\"\n\n/**\n * Scroll restoration manager.\n *\n * Saves scroll position before each navigation and restores it when\n * navigating back to a previously visited path.\n */\nexport class ScrollManager {\n private readonly _positions = new Map<string, number>()\n private readonly _behavior: RouterOptions[\"scrollBehavior\"]\n\n constructor(behavior: RouterOptions[\"scrollBehavior\"] = \"top\") {\n this._behavior = behavior\n }\n\n /** Call before navigating away — saves current scroll position for `fromPath` */\n save(fromPath: string): void {\n // save/restore are only called from browser navigation paths (guarded by caller)\n this._positions.set(fromPath, window.scrollY)\n }\n\n /** Call after navigation is committed — applies scroll behavior */\n restore(to: ResolvedRoute, from: ResolvedRoute): void {\n const behavior = (to.meta.scrollBehavior as typeof this._behavior) ?? this._behavior ?? \"top\"\n\n if (typeof behavior === \"function\") {\n const saved = this._positions.get(to.path) ?? null\n const result = behavior(to, from, saved)\n this._applyResult(result, to.path)\n return\n }\n\n this._applyResult(behavior, to.path)\n }\n\n private _applyResult(result: \"top\" | \"restore\" | \"none\" | number, toPath: string): void {\n if (result === \"none\") return\n if (result === \"top\" || result === undefined) {\n window.scrollTo({ top: 0, behavior: \"instant\" as ScrollBehavior })\n return\n }\n if (result === \"restore\") {\n const saved = this._positions.get(toPath) ?? 0\n window.scrollTo({ top: saved, behavior: \"instant\" as ScrollBehavior })\n return\n }\n // At this point result must be a number (all string cases handled above)\n window.scrollTo({ top: result as number, behavior: \"instant\" as ScrollBehavior })\n }\n\n getSavedPosition(path: string): number | null {\n return this._positions.get(path) ?? null\n }\n}\n","import type { ComponentFn } from \"@pyreon/core\"\n\nexport type { ComponentFn }\n\n// ─── Path param extraction ────────────────────────────────────────────────────\n\n/**\n * Extracts typed params from a path string at compile time.\n * Supports optional params via `:param?` — their type is `string | undefined`.\n *\n * @example\n * ExtractParams<'/user/:id/posts/:postId'>\n * // → { id: string; postId: string }\n *\n * ExtractParams<'/user/:id?'>\n * // → { id?: string | undefined }\n */\nexport type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}`\n ? { [K in Param]: string } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}*`\n ? { [K in Param]: string }\n : T extends `${string}:${infer Param}?/${infer Rest}`\n ? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}?`\n ? { [K in Param]?: string | undefined }\n : T extends `${string}:${infer Param}/${infer Rest}`\n ? { [K in Param]: string } & ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}`\n ? { [K in Param]: string }\n : Record<never, never>\n\n// ─── Route meta ───────────────────────────────────────────────────────────────\n\n/**\n * Route metadata interface. Extend it via module augmentation to add custom fields:\n *\n * @example\n * // globals.d.ts\n * declare module \"@pyreon/router\" {\n * interface RouteMeta {\n * requiresRole?: \"admin\" | \"user\"\n * pageTitle?: string\n * }\n * }\n */\nexport interface RouteMeta {\n /** Sets document.title on navigation */\n title?: string\n /** Page description (for meta tags) */\n description?: string\n /** If true, guards can redirect to login */\n requiresAuth?: boolean\n /** Scroll behavior for this route */\n scrollBehavior?: \"top\" | \"restore\" | \"none\"\n}\n\n// ─── Resolved route ───────────────────────────────────────────────────────────\n\nexport interface ResolvedRoute<\n P extends Record<string, string | undefined> = Record<string, string>,\n Q extends Record<string, string> = Record<string, string>,\n> {\n path: string\n params: P\n query: Q\n hash: string\n /** All matched records from root to leaf (one per nesting level) */\n matched: RouteRecord[]\n meta: RouteMeta\n}\n\n// ─── Lazy component ───────────────────────────────────────────────────────────\n\nexport const LAZY_SYMBOL = Symbol(\"pyreon.lazy\")\n\nexport interface LazyComponent {\n readonly [LAZY_SYMBOL]: true\n readonly loader: () => Promise<ComponentFn | { default: ComponentFn }>\n /** Optional component shown while the lazy chunk is loading */\n readonly loadingComponent?: ComponentFn\n /** Optional component shown after all retries have failed */\n readonly errorComponent?: ComponentFn\n}\n\nexport function lazy(\n loader: () => Promise<ComponentFn | { default: ComponentFn }>,\n options?: { loading?: ComponentFn; error?: ComponentFn },\n): LazyComponent {\n return {\n [LAZY_SYMBOL]: true,\n loader,\n ...(options?.loading ? { loadingComponent: options.loading } : {}),\n ...(options?.error ? { errorComponent: options.error } : {}),\n }\n}\n\nexport function isLazy(c: RouteComponent): c is LazyComponent {\n return typeof c === \"object\" && c !== null && (c as LazyComponent)[LAZY_SYMBOL] === true\n}\n\nexport type RouteComponent = ComponentFn | LazyComponent\n\n// ─── Navigation guard ─────────────────────────────────────────────────────────\n\nexport type NavigationGuardResult = boolean | string | undefined\nexport type NavigationGuard = (\n to: ResolvedRoute,\n from: ResolvedRoute,\n) => NavigationGuardResult | Promise<NavigationGuardResult>\n\nexport type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void\n\n// ─── Navigation blockers ──────────────────────────────────────────────────────\n\n/**\n * Called before each navigation. Return `true` to block, `false` to allow.\n * Async blockers are supported (e.g. to show a confirmation dialog).\n */\nexport type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>\n\nexport interface Blocker {\n /** Unregister this blocker so future navigations proceed freely. */\n remove(): void\n}\n\n// ─── Route loaders ────────────────────────────────────────────────────────────\n\nexport interface LoaderContext {\n params: Record<string, string>\n query: Record<string, string>\n /** Aborted when a newer navigation supersedes this one */\n signal: AbortSignal\n}\n\nexport type RouteLoaderFn = (ctx: LoaderContext) => Promise<unknown>\n\n// ─── Route record ─────────────────────────────────────────────────────────────\n\nexport interface RouteRecord<TPath extends string = string> {\n /** Path pattern — supports `:param` segments and `(.*)` wildcard */\n path: TPath\n component: RouteComponent\n /** Optional route name for named navigation */\n name?: string\n /** Metadata attached to this route */\n meta?: RouteMeta\n /**\n * Redirect target. Evaluated before guards.\n * String: redirect to that path.\n * Function: called with the resolved route, return path string.\n */\n redirect?: string | ((to: ResolvedRoute) => string)\n /** Guard(s) run only for this route, before global beforeEach guards */\n beforeEnter?: NavigationGuard | NavigationGuard[]\n /** Guard(s) run before leaving this route. Return false to cancel. */\n beforeLeave?: NavigationGuard | NavigationGuard[]\n /**\n * Alternative path(s) for this route. Alias paths render the same component\n * and share guards, loaders, and metadata with the primary path.\n *\n * @example\n * { path: \"/user/:id\", alias: [\"/profile/:id\"], component: UserPage }\n */\n alias?: string | string[]\n /** Child routes rendered inside this route's component via <RouterView /> */\n children?: RouteRecord[]\n /**\n * Data loader — runs before navigation commits, in parallel with sibling loaders.\n * The result is accessible via `useLoaderData()` inside the route component.\n * Receives an AbortSignal that fires if a newer navigation supersedes this one.\n */\n loader?: RouteLoaderFn\n /**\n * When true, the router shows cached loader data immediately (stale) and\n * revalidates in the background. The component re-renders once fresh data arrives.\n * Only applies when navigating to a route that already has cached loader data.\n */\n staleWhileRevalidate?: boolean\n /** Component rendered when this route's loader throws an error */\n errorComponent?: ComponentFn\n}\n\n// ─── Router options ───────────────────────────────────────────────────────────\n\nexport type ScrollBehaviorFn = (\n to: ResolvedRoute,\n from: ResolvedRoute,\n savedPosition: number | null,\n) => \"top\" | \"restore\" | \"none\" | number\n\nexport interface RouterOptions {\n routes: RouteRecord[]\n /** \"hash\" (default) uses location.hash; \"history\" uses pushState */\n mode?: \"hash\" | \"history\"\n /**\n * Base path for the application. Used when deploying to a sub-path\n * (e.g. `\"/app\"` for `https://example.com/app/`).\n * Only applies in history mode. Must start with `/`.\n * Default: `\"\"` (no base path).\n */\n base?: string\n /**\n * Global scroll behavior. Per-route meta.scrollBehavior takes precedence.\n * Default: \"top\"\n */\n scrollBehavior?: ScrollBehaviorFn | \"top\" | \"restore\" | \"none\"\n /**\n * Initial URL for SSR. On the server, window.location is unavailable;\n * pass the request URL here so the router resolves the correct route.\n *\n * @example\n * // In your SSR handler:\n * const router = createRouter({ routes, url: req.url })\n */\n url?: string\n /**\n * Called when a route loader throws. If not provided, errors are logged\n * and the navigation continues with `undefined` data for the failed loader.\n * Return `false` to cancel the navigation.\n */\n onError?: (err: unknown, route: ResolvedRoute) => undefined | false\n /**\n * Maximum number of resolved lazy components to cache.\n * When exceeded, the oldest entry is evicted.\n * Default: 100.\n */\n maxCacheSize?: number\n /**\n * Trailing slash handling:\n * - `\"strip\"` — removes trailing slashes before matching (default)\n * - `\"add\"` — ensures paths always end with `/`\n * - `\"ignore\"` — no normalization\n */\n trailingSlash?: \"strip\" | \"add\" | \"ignore\"\n}\n\n// ─── Router interface ─────────────────────────────────────────────────────────\n\nexport interface Router {\n /** Navigate to a path */\n push(path: string): Promise<void>\n /** Navigate to a path by name */\n push(location: {\n name: string\n params?: Record<string, string>\n query?: Record<string, string>\n }): Promise<void>\n /** Replace current history entry */\n replace(path: string): Promise<void>\n /** Replace current history entry using a named route */\n replace(location: {\n name: string\n params?: Record<string, string>\n query?: Record<string, string>\n }): Promise<void>\n /** Go back one step in history */\n back(): void\n /** Go forward one step in history */\n forward(): void\n /** Navigate forward or backward by `delta` steps in the history stack */\n go(delta: number): void\n /** Register a global before-navigation guard. Returns an unregister function. */\n beforeEach(guard: NavigationGuard): () => void\n /** Register a global after-navigation hook. Returns an unregister function. */\n afterEach(hook: AfterEachHook): () => void\n /** Current resolved route (reactive signal) */\n readonly currentRoute: () => ResolvedRoute\n /** True while a navigation (guards + loaders) is in flight */\n readonly loading: () => boolean\n /**\n * Promise that resolves once the initial navigation is complete.\n * Useful for SSR and for delaying rendering until the first route is resolved.\n */\n isReady(): Promise<void>\n /** Remove all event listeners, clear caches, and abort in-flight navigations. */\n destroy(): void\n}\n\n// ─── Internal router instance ─────────────────────────────────────────────────\n\nimport type { Computed, Signal } from \"@pyreon/reactivity\"\n\nexport interface RouterInstance extends Router {\n routes: RouteRecord[]\n mode: \"hash\" | \"history\"\n /** Normalized base path (e.g. \"/app\"), empty string if none */\n _base: string\n _currentPath: Signal<string>\n _currentRoute: Computed<ResolvedRoute>\n _componentCache: Map<RouteRecord, ComponentFn>\n _loadingSignal: Signal<number>\n _resolve(rawPath: string): ResolvedRoute\n _scrollPositions: Map<string, number>\n _scrollBehavior: RouterOptions[\"scrollBehavior\"]\n _onError: RouterOptions[\"onError\"]\n _maxCacheSize: number\n /**\n * Current RouterView nesting depth. Incremented by each RouterView as it\n * mounts (in tree order = depth-first), so each view knows which level of\n * `matched[]` to render. Reset to 0 by RouterProvider.\n */\n _viewDepth: number\n /** Route records whose lazy chunk permanently failed (all retries exhausted) */\n _erroredChunks: Set<RouteRecord>\n /** Loader data keyed by route record — populated before each navigation commits */\n _loaderData: Map<RouteRecord, unknown>\n /** AbortController for the in-flight loader batch — aborted when a newer navigation starts */\n _abortController: AbortController | null\n /** Registered navigation blockers */\n _blockers: Set<BlockerFn>\n /** Resolves the isReady() promise after initial navigation completes */\n _readyResolve: (() => void) | null\n /** The isReady() promise instance */\n _readyPromise: Promise<void>\n}\n","import { createContext, onUnmount, useContext } from \"@pyreon/core\"\nimport { computed, signal } from \"@pyreon/reactivity\"\nimport { buildNameIndex, buildPath, resolveRoute, stringifyQuery } from \"./match\"\nimport { ScrollManager } from \"./scroll\"\nimport {\n type AfterEachHook,\n type Blocker,\n type BlockerFn,\n type ComponentFn,\n isLazy,\n type LoaderContext,\n type NavigationGuard,\n type NavigationGuardResult,\n type ResolvedRoute,\n type RouteRecord,\n type Router,\n type RouterInstance,\n type RouterOptions,\n} from \"./types\"\n\n// Evaluated once at module load — collapses to `true` in browser / happy-dom,\n// `false` on the server. Using a constant avoids per-call `typeof` branches\n// that are uncoverable in test environments.\nconst _isBrowser = typeof window !== \"undefined\"\n\n// ─── Router context ───────────────────────────────────────────────────────────\n// Context-based access: isolated per request in SSR (ALS-backed via\n// @pyreon/runtime-server), isolated per component tree in CSR.\n// Falls back to the module-level singleton for code running outside a component\n// tree (e.g. programmatic navigation from event handlers).\n\nexport const RouterContext = createContext<RouterInstance | null>(null)\n\n// Module-level fallback — safe for CSR (single-threaded), not for concurrent SSR.\n// RouterProvider also sets this so legacy useRouter() calls outside the tree work.\nlet _activeRouter: RouterInstance | null = null\n\nexport function getActiveRouter(): RouterInstance | null {\n return useContext(RouterContext) ?? _activeRouter\n}\n\nexport function setActiveRouter(router: RouterInstance | null): void {\n if (router) router._viewDepth = 0\n _activeRouter = router\n}\n\n// ─── Hooks ────────────────────────────────────────────────────────────────────\n\nexport function useRouter(): Router {\n const router = useContext(RouterContext) ?? _activeRouter\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n return router\n}\n\nexport function useRoute<TPath extends string = string>(): () => ResolvedRoute<\n import(\"./types\").ExtractParams<TPath> & Record<string, string>,\n Record<string, string>\n> {\n const router = useContext(RouterContext) ?? _activeRouter\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n return router.currentRoute as never\n}\n\n/**\n * In-component guard: called before the component's route is left.\n * Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed.\n * Automatically removed on component unmount.\n *\n * @example\n * onBeforeRouteLeave((to, from) => {\n * if (hasUnsavedChanges()) return false\n * })\n */\nexport function onBeforeRouteLeave(guard: NavigationGuard): () => void {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n // Register as a global guard that only fires when leaving the current route\n const currentMatched = router.currentRoute().matched\n const wrappedGuard: NavigationGuard = (to, from) => {\n // Only fire if we're actually leaving one of the matched routes\n const isLeaving = from.matched.some((r) => currentMatched.includes(r))\n if (!isLeaving) return undefined\n return guard(to, from)\n }\n const remove = router.beforeEach(wrappedGuard)\n onUnmount(() => remove())\n return remove\n}\n\n/**\n * In-component guard: called when the route changes but the component is reused\n * (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes.\n * Automatically removed on component unmount.\n *\n * @example\n * onBeforeRouteUpdate((to, from) => {\n * if (!isValidId(to.params.id)) return false\n * })\n */\nexport function onBeforeRouteUpdate(guard: NavigationGuard): () => void {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n const currentMatched = router.currentRoute().matched\n const wrappedGuard: NavigationGuard = (to, from) => {\n // Only fire when the same component is reused (matched routes overlap)\n const isReused = to.matched.some((r) => currentMatched.includes(r))\n if (!isReused) return undefined\n return guard(to, from)\n }\n const remove = router.beforeEach(wrappedGuard)\n onUnmount(() => remove())\n return remove\n}\n\n/**\n * Register a navigation blocker. The `fn` callback is called before each\n * navigation — return `true` (or resolve to `true`) to block it.\n *\n * Automatically removed on component unmount if called during component setup.\n * Also installs a `beforeunload` handler so the browser shows a confirmation\n * dialog when the user tries to close the tab while a blocker is active.\n *\n * @example\n * const blocker = useBlocker((to, from) => {\n * return hasUnsavedChanges() && !confirm(\"Discard changes?\")\n * })\n * // later: blocker.remove()\n */\nexport function useBlocker(fn: BlockerFn): Blocker {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n router._blockers.add(fn)\n\n // Warn before tab/window close while this blocker is registered\n const beforeUnloadHandler = _isBrowser\n ? (e: BeforeUnloadEvent) => {\n e.preventDefault()\n }\n : null\n if (beforeUnloadHandler) {\n window.addEventListener(\"beforeunload\", beforeUnloadHandler)\n }\n\n const remove = () => {\n router._blockers.delete(fn)\n if (beforeUnloadHandler) {\n window.removeEventListener(\"beforeunload\", beforeUnloadHandler)\n }\n }\n\n // Auto-remove when the component that called useBlocker unmounts\n onUnmount(() => remove())\n\n return { remove }\n}\n\n/**\n * Reactive read/write access to the current route's query parameters.\n *\n * Returns `[get, set]` where `get` is a reactive signal producing the merged\n * query object and `set` navigates to the current path with updated params.\n *\n * @example\n * const [params, setParams] = useSearchParams({ page: \"1\", sort: \"name\" })\n * params().page // \"1\" if not in URL\n * setParams({ page: \"2\" }) // navigates to ?page=2&sort=name\n */\nexport function useSearchParams<T extends Record<string, string>>(\n defaults?: T,\n): [get: () => T, set: (updates: Partial<T>) => Promise<void>] {\n const router = (useContext(RouterContext) ?? _activeRouter) as RouterInstance | null\n if (!router)\n throw new Error(\n \"[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.\",\n )\n const get = (): T => {\n const query = router.currentRoute().query\n if (!defaults) return query as T\n return { ...defaults, ...query } as T\n }\n const set = (updates: Partial<T>): Promise<void> => {\n const merged = { ...get(), ...updates }\n const path = router.currentRoute().path + stringifyQuery(merged as Record<string, string>)\n return router.replace(path)\n }\n return [get, set]\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\nexport function createRouter(options: RouterOptions | RouteRecord[]): Router {\n const opts: RouterOptions = Array.isArray(options) ? { routes: options } : options\n const {\n routes,\n mode = \"hash\",\n scrollBehavior,\n onError,\n maxCacheSize = 100,\n trailingSlash = \"strip\",\n } = opts\n\n // Base path only applies to history mode — hash-based routing already namespaces via #\n const base = mode === \"history\" ? normalizeBase(opts.base ?? \"\") : \"\"\n\n // Pre-built O(1) name → record index. Computed once at startup.\n const nameIndex = buildNameIndex(routes)\n\n const guards: NavigationGuard[] = []\n const afterHooks: AfterEachHook[] = []\n const scrollManager = new ScrollManager(scrollBehavior)\n\n // Navigation generation counter — cancels in-flight navigations when a newer\n // one starts. Prevents out-of-order completion from stale async guards.\n let _navGen = 0\n\n // ── Initial location ──────────────────────────────────────────────────────\n\n const getInitialLocation = (): string => {\n // SSR: use explicitly provided url (strip base if present)\n if (opts.url) return stripBase(opts.url, base)\n if (!_isBrowser) return \"/\"\n if (mode === \"history\") {\n return stripBase(window.location.pathname, base) + window.location.search\n }\n const hash = window.location.hash\n return hash.startsWith(\"#\") ? hash.slice(1) || \"/\" : \"/\"\n }\n\n const getCurrentLocation = (): string => {\n if (!_isBrowser) return currentPath()\n if (mode === \"history\") {\n return stripBase(window.location.pathname, base) + window.location.search\n }\n const hash = window.location.hash\n return hash.startsWith(\"#\") ? hash.slice(1) || \"/\" : \"/\"\n }\n\n // ── Signals ───────────────────────────────────────────────────────────────\n\n const currentPath = signal(normalizeTrailingSlash(getInitialLocation(), trailingSlash))\n const currentRoute = computed<ResolvedRoute>(() => resolveRoute(currentPath(), routes))\n\n // Browser event listeners — stored so destroy() can remove them\n let _popstateHandler: (() => void) | null = null\n let _hashchangeHandler: (() => void) | null = null\n\n if (_isBrowser) {\n if (mode === \"history\") {\n _popstateHandler = () => currentPath.set(getCurrentLocation())\n window.addEventListener(\"popstate\", _popstateHandler)\n } else {\n _hashchangeHandler = () => currentPath.set(getCurrentLocation())\n window.addEventListener(\"hashchange\", _hashchangeHandler)\n }\n }\n\n const componentCache = new Map<RouteRecord, ComponentFn>()\n const loadingSignal = signal(0)\n\n // ── Navigation ────────────────────────────────────────────────────────────\n\n type GuardOutcome =\n | { action: \"continue\" }\n | { action: \"cancel\" }\n | { action: \"redirect\"; target: string }\n\n async function evaluateGuard(\n guard: NavigationGuard,\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n const result = await runGuard(guard, to, from)\n if (gen !== _navGen) return { action: \"cancel\" }\n if (result === false) return { action: \"cancel\" }\n if (typeof result === \"string\") return { action: \"redirect\", target: result }\n return { action: \"continue\" }\n }\n\n async function runRouteGuards(\n records: RouteRecord[],\n guardKey: \"beforeLeave\" | \"beforeEnter\",\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n for (const record of records) {\n const raw = record[guardKey]\n if (!raw) continue\n const routeGuards = Array.isArray(raw) ? raw : [raw]\n for (const guard of routeGuards) {\n const outcome = await evaluateGuard(guard, to, from, gen)\n if (outcome.action !== \"continue\") return outcome\n }\n }\n return { action: \"continue\" }\n }\n\n async function runGlobalGuards(\n globalGuards: NavigationGuard[],\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n for (const guard of globalGuards) {\n const outcome = await evaluateGuard(guard, to, from, gen)\n if (outcome.action !== \"continue\") return outcome\n }\n return { action: \"continue\" }\n }\n\n function processLoaderResult(\n result: PromiseSettledResult<unknown>,\n record: RouteRecord,\n ac: AbortController,\n to: ResolvedRoute,\n ): boolean {\n if (result.status === \"fulfilled\") {\n router._loaderData.set(record, result.value)\n return true\n }\n if (ac.signal.aborted) return true\n if (router._onError) {\n const cancel = router._onError(result.reason, to)\n if (cancel === false) return false\n }\n router._loaderData.set(record, undefined)\n return true\n }\n\n function syncBrowserUrl(path: string, replace: boolean): void {\n if (!_isBrowser) return\n const url = mode === \"history\" ? `${base}${path}` : `#${path}`\n if (replace) {\n window.history.replaceState(null, \"\", url)\n } else {\n window.history.pushState(null, \"\", url)\n }\n }\n\n function resolveRedirect(to: ResolvedRoute): string | null {\n const leaf = to.matched[to.matched.length - 1]\n if (!leaf?.redirect) return null\n return sanitizePath(typeof leaf.redirect === \"function\" ? leaf.redirect(to) : leaf.redirect)\n }\n\n async function runAllGuards(\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<GuardOutcome> {\n const leaveOutcome = await runRouteGuards(from.matched, \"beforeLeave\", to, from, gen)\n if (leaveOutcome.action !== \"continue\") return leaveOutcome\n\n const enterOutcome = await runRouteGuards(to.matched, \"beforeEnter\", to, from, gen)\n if (enterOutcome.action !== \"continue\") return enterOutcome\n\n return runGlobalGuards(guards, to, from, gen)\n }\n\n async function runBlockingLoaders(\n records: RouteRecord[],\n to: ResolvedRoute,\n gen: number,\n ac: AbortController,\n ): Promise<boolean> {\n const loaderCtx: LoaderContext = { params: to.params, query: to.query, signal: ac.signal }\n const results = await Promise.allSettled(\n records.map((r) => (r.loader ? r.loader(loaderCtx) : Promise.resolve(undefined))),\n )\n if (gen !== _navGen) return false\n for (let i = 0; i < records.length; i++) {\n const result = results[i]\n const record = records[i]\n if (!result || !record) continue\n if (!processLoaderResult(result, record, ac, to)) return false\n }\n return true\n }\n\n /** Fire-and-forget background revalidation for stale-while-revalidate routes. */\n function revalidateSwrLoaders(records: RouteRecord[], to: ResolvedRoute, ac: AbortController) {\n const loaderCtx: LoaderContext = { params: to.params, query: to.query, signal: ac.signal }\n for (const r of records) {\n if (!r.loader) continue\n r.loader(loaderCtx)\n .then((data) => {\n if (!ac.signal.aborted) {\n router._loaderData.set(r, data)\n // Bump loadingSignal to trigger reactive re-render with fresh data\n loadingSignal.update((n) => n + 1)\n loadingSignal.update((n) => n - 1)\n }\n })\n .catch(() => {\n /* Background revalidation failure — stale data remains valid */\n })\n }\n }\n\n async function runLoaders(to: ResolvedRoute, gen: number, ac: AbortController): Promise<boolean> {\n const loadableRecords = to.matched.filter((r) => r.loader)\n if (loadableRecords.length === 0) return true\n\n const blocking: RouteRecord[] = []\n const swr: RouteRecord[] = []\n for (const r of loadableRecords) {\n if (r.staleWhileRevalidate && router._loaderData.has(r)) {\n swr.push(r)\n } else {\n blocking.push(r)\n }\n }\n\n if (blocking.length > 0) {\n const ok = await runBlockingLoaders(blocking, to, gen, ac)\n if (!ok) return false\n }\n if (swr.length > 0) revalidateSwrLoaders(swr, to, ac)\n return true\n }\n\n function commitNavigation(\n path: string,\n replace: boolean,\n to: ResolvedRoute,\n from: ResolvedRoute,\n ): void {\n scrollManager.save(from.path)\n currentPath.set(path)\n syncBrowserUrl(path, replace)\n\n if (_isBrowser && to.meta.title) {\n document.title = to.meta.title\n }\n\n for (const record of router._loaderData.keys()) {\n if (!to.matched.includes(record)) {\n router._loaderData.delete(record)\n }\n }\n\n for (const hook of afterHooks) {\n try {\n hook(to, from)\n } catch (_err) {\n /* hook errors silently ignored */\n }\n }\n\n if (_isBrowser) {\n queueMicrotask(() => scrollManager.restore(to, from))\n }\n }\n\n async function checkBlockers(\n to: ResolvedRoute,\n from: ResolvedRoute,\n gen: number,\n ): Promise<\"continue\" | \"cancel\"> {\n for (const blocker of router._blockers) {\n const blocked = await blocker(to, from)\n if (gen !== _navGen || blocked) return \"cancel\"\n }\n return \"continue\"\n }\n\n async function navigate(rawPath: string, replace: boolean, redirectDepth = 0): Promise<void> {\n if (redirectDepth > 10) return\n\n const path = normalizeTrailingSlash(rawPath, trailingSlash)\n const gen = ++_navGen\n loadingSignal.update((n) => n + 1)\n\n const to = resolveRoute(path, routes)\n const from = currentRoute()\n\n const redirectTarget = resolveRedirect(to)\n if (redirectTarget !== null) {\n loadingSignal.update((n) => n - 1)\n return navigate(redirectTarget, replace, redirectDepth + 1)\n }\n\n const blockerResult = await checkBlockers(to, from, gen)\n if (blockerResult !== \"continue\") {\n loadingSignal.update((n) => n - 1)\n return\n }\n\n const guardOutcome = await runAllGuards(to, from, gen)\n if (guardOutcome.action !== \"continue\") {\n loadingSignal.update((n) => n - 1)\n if (guardOutcome.action === \"redirect\") {\n return navigate(sanitizePath(guardOutcome.target), replace, redirectDepth + 1)\n }\n return\n }\n\n router._abortController?.abort()\n const ac = new AbortController()\n router._abortController = ac\n\n const loadersOk = await runLoaders(to, gen, ac)\n if (!loadersOk) {\n loadingSignal.update((n) => n - 1)\n return\n }\n\n commitNavigation(path, replace, to, from)\n loadingSignal.update((n) => n - 1)\n }\n\n // ── isReady promise ─────────────────────────────────────────────────────\n // Resolves after the first navigation (including guards + loaders) completes.\n\n let _readyResolve: (() => void) | null = null\n const _readyPromise = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n // ── Public router object ──────────────────────────────────────────────────\n\n const router: RouterInstance = {\n routes,\n mode,\n _base: base,\n currentRoute,\n _currentPath: currentPath,\n _currentRoute: currentRoute,\n _componentCache: componentCache,\n _loadingSignal: loadingSignal,\n _scrollPositions: new Map(),\n _scrollBehavior: scrollBehavior,\n _viewDepth: 0,\n _erroredChunks: new Set(),\n _loaderData: new Map(),\n _abortController: null,\n _blockers: new Set(),\n _readyResolve,\n _readyPromise,\n _onError: onError,\n _maxCacheSize: maxCacheSize,\n\n async push(\n location:\n | string\n | { name: string; params?: Record<string, string>; query?: Record<string, string> },\n ) {\n if (typeof location === \"string\") {\n const resolved = resolveRelativePath(location, currentPath())\n return navigate(sanitizePath(resolved), false)\n }\n const path = resolveNamedPath(\n location.name,\n location.params ?? {},\n location.query ?? {},\n nameIndex,\n )\n return navigate(path, false)\n },\n\n async replace(\n location:\n | string\n | { name: string; params?: Record<string, string>; query?: Record<string, string> },\n ) {\n if (typeof location === \"string\") {\n const resolved = resolveRelativePath(location, currentPath())\n return navigate(sanitizePath(resolved), true)\n }\n const path = resolveNamedPath(\n location.name,\n location.params ?? {},\n location.query ?? {},\n nameIndex,\n )\n return navigate(path, true)\n },\n\n back() {\n if (_isBrowser) window.history.back()\n },\n\n forward() {\n if (_isBrowser) window.history.forward()\n },\n\n go(delta: number) {\n if (_isBrowser) window.history.go(delta)\n },\n\n beforeEach(guard: NavigationGuard) {\n guards.push(guard)\n return () => {\n const idx = guards.indexOf(guard)\n if (idx >= 0) guards.splice(idx, 1)\n }\n },\n\n afterEach(hook: AfterEachHook) {\n afterHooks.push(hook)\n return () => {\n const idx = afterHooks.indexOf(hook)\n if (idx >= 0) afterHooks.splice(idx, 1)\n }\n },\n\n loading: () => loadingSignal() > 0,\n\n isReady() {\n return router._readyPromise\n },\n\n destroy() {\n if (_popstateHandler) {\n window.removeEventListener(\"popstate\", _popstateHandler)\n _popstateHandler = null\n }\n if (_hashchangeHandler) {\n window.removeEventListener(\"hashchange\", _hashchangeHandler)\n _hashchangeHandler = null\n }\n guards.length = 0\n afterHooks.length = 0\n router._blockers.clear()\n componentCache.clear()\n router._loaderData.clear()\n router._abortController?.abort()\n router._abortController = null\n },\n\n _resolve: (rawPath: string) => resolveRoute(rawPath, routes),\n }\n\n // Initial route is resolved synchronously — mark ready on next microtask\n // so consumers can await isReady() before the first render.\n queueMicrotask(() => {\n if (router._readyResolve) {\n router._readyResolve()\n router._readyResolve = null\n }\n })\n\n return router\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nasync function runGuard(\n guard: NavigationGuard,\n to: ResolvedRoute,\n from: ResolvedRoute,\n): Promise<NavigationGuardResult> {\n try {\n return await guard(to, from)\n } catch (_err) {\n return false\n }\n}\n\nfunction resolveNamedPath(\n name: string,\n params: Record<string, string>,\n query: Record<string, string>,\n index: Map<string, RouteRecord>,\n): string {\n const record = index.get(name)\n if (!record) {\n return \"/\"\n }\n let path = buildPath(record.path, params)\n const qs = Object.entries(query)\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join(\"&\")\n if (qs) path += `?${qs}`\n return path\n}\n\n/** Normalize a base path: ensure leading `/`, strip trailing `/`. */\nfunction normalizeBase(raw: string): string {\n if (!raw) return \"\"\n let b = raw\n if (!b.startsWith(\"/\")) b = `/${b}`\n if (b.endsWith(\"/\")) b = b.slice(0, -1)\n return b\n}\n\n/** Strip the base prefix from a full URL path. Returns the app-relative path. */\nfunction stripBase(path: string, base: string): string {\n if (!base) return path\n if (path === base || path === `${base}/`) return \"/\"\n if (path.startsWith(`${base}/`)) return path.slice(base.length)\n return path\n}\n\n/** Normalize trailing slash on a path according to the configured strategy. */\nfunction normalizeTrailingSlash(path: string, strategy: \"strip\" | \"add\" | \"ignore\"): string {\n if (strategy === \"ignore\" || path === \"/\") return path\n // Split off query string + hash so we only touch the path portion\n const qIdx = path.indexOf(\"?\")\n const hIdx = path.indexOf(\"#\")\n const endIdx = qIdx >= 0 ? qIdx : hIdx >= 0 ? hIdx : path.length\n const pathPart = path.slice(0, endIdx)\n const suffix = path.slice(endIdx)\n if (strategy === \"strip\") {\n return pathPart.length > 1 && pathPart.endsWith(\"/\") ? pathPart.slice(0, -1) + suffix : path\n }\n // strategy === \"add\"\n return !pathPart.endsWith(\"/\") ? `${pathPart}/${suffix}` : path\n}\n\n/**\n * Resolve a relative path (starting with `.` or `..`) against the current path.\n * Non-relative paths are returned as-is.\n */\nfunction resolveRelativePath(to: string, from: string): string {\n if (!to.startsWith(\"./\") && !to.startsWith(\"../\") && to !== \".\" && to !== \"..\") return to\n\n // Split current path into segments, drop the last segment (file-like resolution)\n const fromSegments = from.split(\"/\").filter(Boolean)\n fromSegments.pop()\n\n const toSegments = to.split(\"/\").filter(Boolean)\n for (const seg of toSegments) {\n if (seg === \"..\") {\n fromSegments.pop()\n } else if (seg !== \".\") {\n fromSegments.push(seg)\n }\n }\n return `/${fromSegments.join(\"/\")}`\n}\n\n/** Block unsafe navigation targets: javascript/data/vbscript URIs and absolute URLs. */\nfunction sanitizePath(path: string): string {\n const trimmed = path.trim()\n if (/^(?:javascript|data|vbscript):/i.test(trimmed)) {\n return \"/\"\n }\n // Block absolute URLs and protocol-relative URLs — router only handles same-origin paths\n if (/^\\/\\/|^https?:/i.test(trimmed)) {\n return \"/\"\n }\n return path\n}\n\nexport { isLazy }\n","import type { ComponentFn, Props, VNodeChild } from \"@pyreon/core\"\nimport { createRef, h, onUnmount, provide, useContext } from \"@pyreon/core\"\nimport { LoaderDataContext, prefetchLoaderData } from \"./loader\"\nimport { isLazy, RouterContext, setActiveRouter } from \"./router\"\nimport type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from \"./types\"\n\n// Track prefetched paths per router to avoid duplicate fetches\nconst _prefetched = new WeakMap<RouterInstance, Set<string>>()\n\n// ─── RouterProvider ───────────────────────────────────────────────────────────\n\nexport interface RouterProviderProps extends Props {\n router: Router\n children?: VNodeChild\n}\n\nexport const RouterProvider: ComponentFn<RouterProviderProps> = (props) => {\n const router = props.router as RouterInstance\n // Push router into the context stack — isolated per request in SSR via ALS,\n // isolated per component tree in CSR.\n provide(RouterContext, router)\n onUnmount(() => {\n // Clean up event listeners, caches, abort in-flight navigations.\n // Safe to call multiple times (destroy is idempotent).\n router.destroy()\n setActiveRouter(null)\n })\n // Also set the module fallback so programmatic useRouter() outside a component\n // tree (e.g. navigation guards in event handlers) still works in CSR.\n setActiveRouter(router)\n return props.children ?? null\n}\n\n// ─── RouterView ───────────────────────────────────────────────────────────────\n\nexport interface RouterViewProps extends Props {\n /** Explicitly pass a router (optional — uses the active router by default) */\n router?: Router\n}\n\n/**\n * Renders the matched route component at this nesting level.\n *\n * Nested layouts work by placing a second `<RouterView />` inside the layout\n * component — it automatically renders the next level of the matched route.\n *\n * How depth tracking works:\n * Pyreon components run once in depth-first tree order. Each `RouterView`\n * captures `router._viewDepth` at setup time and immediately increments it,\n * so sibling and child views get the correct index. `onUnmount` decrements\n * the counter so dynamic route swaps work correctly.\n *\n * @example\n * // Route config:\n * { path: \"/admin\", component: AdminLayout, children: [\n * { path: \"users\", component: AdminUsers },\n * ]}\n *\n * // AdminLayout renders a nested RouterView:\n * function AdminLayout() {\n * return <div><Sidebar /><RouterView /></div>\n * }\n */\nexport const RouterView: ComponentFn<RouterViewProps> = (props) => {\n const router = ((props.router as RouterInstance | undefined) ??\n useContext(RouterContext)) as RouterInstance | null\n if (!router) return null\n\n // Claim this view's depth at setup time (depth-first component init order)\n const depth = router._viewDepth\n router._viewDepth++\n\n onUnmount(() => {\n router._viewDepth--\n })\n\n const child = (): VNodeChild => {\n router._loadingSignal() // reactive — re-renders after lazy load completes\n\n const route = router.currentRoute()\n\n if (route.matched.length === 0) return null\n\n // Render the matched record at this view's depth level\n const record = route.matched[depth]\n if (!record) return null // no component at this nesting level\n\n const cached = router._componentCache.get(record)\n if (cached) {\n return renderWithLoader(router, record, cached, route)\n }\n\n const raw = record.component\n\n if (!isLazy(raw)) {\n cacheSet(router, record, raw)\n return renderWithLoader(router, record, raw, route)\n }\n\n return renderLazyRoute(router, record, raw)\n }\n\n return h(\"div\", { \"data-pyreon-router-view\": true }, child as unknown as VNodeChild)\n}\n\n// ─── RouterLink ───────────────────────────────────────────────────────────────\n\nexport interface RouterLinkProps extends Props {\n to: string\n /** If true, uses router.replace() instead of router.push() */\n replace?: boolean\n /** CSS class applied when this link is active (default: \"router-link-active\") */\n activeClass?: string\n /** CSS class for exact-match active state (default: \"router-link-exact-active\") */\n exactActiveClass?: string\n /** If true, only applies activeClass on exact match */\n exact?: boolean\n /**\n * Prefetch strategy for loader data:\n * - \"hover\" (default) — prefetch when the user hovers over the link\n * - \"viewport\" — prefetch when the link scrolls into the viewport\n * - \"none\" — no prefetching\n */\n prefetch?: \"hover\" | \"viewport\" | \"none\"\n children?: VNodeChild | null\n}\n\nexport const RouterLink: ComponentFn<RouterLinkProps> = (props) => {\n const router = useContext(RouterContext)\n const prefetchMode = props.prefetch ?? \"hover\"\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault()\n if (!router) return\n if (props.replace) {\n router.replace(props.to)\n } else {\n router.push(props.to)\n }\n }\n\n const handleMouseEnter = () => {\n if (prefetchMode !== \"hover\" || !router) return\n prefetchRoute(router as RouterInstance, props.to)\n }\n\n const inst = router as RouterInstance | null\n const href = inst?.mode === \"history\" ? `${inst._base}${props.to}` : `#${props.to}`\n\n const activeClass = (): string => {\n if (!router) return \"\"\n const current = router.currentRoute().path\n const target = props.to\n const isExact = current === target\n const isActive = isExact || (!props.exact && isSegmentPrefix(current, target))\n\n const classes: string[] = []\n if (isActive) classes.push(props.activeClass ?? \"router-link-active\")\n if (isExact) classes.push(props.exactActiveClass ?? \"router-link-exact-active\")\n return classes.join(\" \").trim()\n }\n\n // Viewport prefetching — observe link visibility with IntersectionObserver\n const ref = createRef<Element>()\n if (prefetchMode === \"viewport\" && router && typeof IntersectionObserver !== \"undefined\") {\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n prefetchRoute(router as RouterInstance, props.to)\n observer.disconnect()\n break\n }\n }\n })\n // Observe after mount — the ref will be populated once the element is in the DOM\n queueMicrotask(() => {\n observer.observe(ref.current as Element)\n })\n onUnmount(() => observer.disconnect())\n }\n\n return h(\n \"a\",\n { ref, href, class: activeClass, onClick: handleClick, onMouseEnter: handleMouseEnter },\n props.children ?? props.to,\n )\n}\n\n/** Prefetch loader data for a route (only once per router + path). */\nfunction prefetchRoute(router: RouterInstance, path: string): void {\n let set = _prefetched.get(router)\n if (!set) {\n set = new Set()\n _prefetched.set(router, set)\n }\n if (set.has(path)) return\n set.add(path)\n prefetchLoaderData(router, path).catch(() => {\n // Silently ignore — prefetch is best-effort\n set?.delete(path)\n })\n}\n\nfunction renderLazyRoute(\n router: RouterInstance,\n record: RouteRecord,\n raw: LazyComponent,\n): VNodeChild {\n if (router._erroredChunks.has(record)) {\n return raw.errorComponent ? h(raw.errorComponent, {}) : null\n }\n\n const tryLoad = (attempt: number): Promise<void> =>\n raw\n .loader()\n .then((mod) => {\n const resolved = typeof mod === \"function\" ? mod : mod.default\n cacheSet(router, record, resolved)\n router._loadingSignal.update((n) => n + 1)\n })\n .catch((err: unknown) => {\n if (attempt < 3) {\n return new Promise<void>((res) => setTimeout(res, 500 * 2 ** attempt)).then(() =>\n tryLoad(attempt + 1),\n )\n }\n if (typeof window !== \"undefined\" && isStaleChunk(err)) {\n window.location.reload()\n return\n }\n\n router._erroredChunks.add(record)\n router._loadingSignal.update((n) => n + 1)\n })\n\n tryLoad(0)\n return raw.loadingComponent ? h(raw.loadingComponent, {}) : null\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Wraps the route component with a LoaderDataProvider so `useLoaderData()` works\n * inside the component. If the record has no loader, renders the component directly.\n */\nfunction renderWithLoader(\n router: RouterInstance,\n record: RouteRecord,\n Comp: ComponentFn,\n route: Pick<ResolvedRoute, \"params\" | \"query\" | \"meta\">,\n): VNodeChild {\n const routeProps = { params: route.params, query: route.query, meta: route.meta }\n if (!record.loader) {\n return h(Comp, routeProps)\n }\n const data = router._loaderData.get(record)\n // If loader data is undefined and route has an errorComponent, render it\n if (data === undefined && record.errorComponent) {\n return h(record.errorComponent, routeProps)\n }\n return h(LoaderDataProvider, { data, children: h(Comp, routeProps) })\n}\n\n/**\n * Thin provider component that pushes LoaderDataContext before children mount.\n * Uses Pyreon's context stack so useLoaderData() reads it during child setup.\n */\nfunction LoaderDataProvider(props: { data: unknown; children: VNodeChild }): VNodeChild {\n provide(LoaderDataContext, props.data)\n return props.children\n}\n\n/** Evict oldest cache entries when the component cache exceeds maxCacheSize. */\nfunction cacheSet(router: RouterInstance, record: RouteRecord, comp: ComponentFn): void {\n router._componentCache.set(record, comp)\n if (router._componentCache.size > router._maxCacheSize) {\n // Map iterates in insertion order — first key is oldest\n const oldest = router._componentCache.keys().next().value as RouteRecord\n router._componentCache.delete(oldest)\n }\n}\n\n/**\n * Segment-aware prefix check for active link matching.\n * `/admin` is a prefix of `/admin/users` but NOT of `/admin-panel`.\n */\nfunction isSegmentPrefix(current: string, target: string): boolean {\n if (target === \"/\") return false\n const cs = current.split(\"/\").filter(Boolean)\n const ts = target.split(\"/\").filter(Boolean)\n if (ts.length > cs.length) return false\n return ts.every((seg, i) => seg === cs[i])\n}\n\n/**\n * Detect a stale chunk error — happens post-deploy when the browser requests\n * a hashed filename that no longer exists on the server. Trigger a full reload\n * so the user gets the new bundle instead of a broken loading state.\n */\nfunction isStaleChunk(err: unknown): boolean {\n if (err instanceof TypeError && String(err.message).includes(\"Failed to fetch\")) return true\n if (err instanceof SyntaxError) return true\n return false\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import * as _pyreon_core0 from "@pyreon/core";
2
- import { ComponentFn, ComponentFn as ComponentFn$1, Props, VNode, VNodeChild } from "@pyreon/core";
2
+ import { ComponentFn, ComponentFn as ComponentFn$1, Props, VNodeChild } from "@pyreon/core";
3
3
  import { Computed, Signal } from "@pyreon/reactivity";
4
4
 
5
5
  //#region src/types.d.ts
@@ -248,7 +248,7 @@ interface RouterInstance extends Router {
248
248
  //#region src/components.d.ts
249
249
  interface RouterProviderProps extends Props {
250
250
  router: Router;
251
- children?: VNode | VNodeChild | null;
251
+ children?: VNodeChild;
252
252
  }
253
253
  declare const RouterProvider: ComponentFn<RouterProviderProps>;
254
254
  interface RouterViewProps extends Props {
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/components.tsx","../../../src/loader.ts","../../../src/match.ts","../../../src/router.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/components.tsx","../../../src/loader.ts","../../../src/match.ts","../../../src/router.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/router",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Official router for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@pyreon/core": "^0.5.7",
43
- "@pyreon/reactivity": "^0.5.7",
44
- "@pyreon/runtime-dom": "^0.5.7"
42
+ "@pyreon/core": "^0.6.0",
43
+ "@pyreon/reactivity": "^0.6.0",
44
+ "@pyreon/runtime-dom": "^0.6.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@happy-dom/global-registrator": "^20.8.3",
@@ -1,5 +1,5 @@
1
- import type { ComponentFn, Props, VNode, VNodeChild } from "@pyreon/core"
2
- import { createRef, h, onUnmount, popContext, pushContext, useContext } from "@pyreon/core"
1
+ import type { ComponentFn, Props, VNodeChild } from "@pyreon/core"
2
+ import { createRef, h, onUnmount, provide, useContext } from "@pyreon/core"
3
3
  import { LoaderDataContext, prefetchLoaderData } from "./loader"
4
4
  import { isLazy, RouterContext, setActiveRouter } from "./router"
5
5
  import type { LazyComponent, ResolvedRoute, RouteRecord, Router, RouterInstance } from "./types"
@@ -11,17 +11,15 @@ const _prefetched = new WeakMap<RouterInstance, Set<string>>()
11
11
 
12
12
  export interface RouterProviderProps extends Props {
13
13
  router: Router
14
- children?: VNode | VNodeChild | null
14
+ children?: VNodeChild
15
15
  }
16
16
 
17
17
  export const RouterProvider: ComponentFn<RouterProviderProps> = (props) => {
18
18
  const router = props.router as RouterInstance
19
19
  // Push router into the context stack — isolated per request in SSR via ALS,
20
20
  // isolated per component tree in CSR.
21
- const frame = new Map([[RouterContext.id, router]])
22
- pushContext(frame)
21
+ provide(RouterContext, router)
23
22
  onUnmount(() => {
24
- popContext()
25
23
  // Clean up event listeners, caches, abort in-flight navigations.
26
24
  // Safe to call multiple times (destroy is idempotent).
27
25
  router.destroy()
@@ -30,7 +28,7 @@ export const RouterProvider: ComponentFn<RouterProviderProps> = (props) => {
30
28
  // Also set the module fallback so programmatic useRouter() outside a component
31
29
  // tree (e.g. navigation guards in event handlers) still works in CSR.
32
30
  setActiveRouter(router)
33
- return (props.children ?? null) as VNode | null
31
+ return props.children ?? null
34
32
  }
35
33
 
36
34
  // ─── RouterView ───────────────────────────────────────────────────────────────
@@ -267,10 +265,8 @@ function renderWithLoader(
267
265
  * Thin provider component that pushes LoaderDataContext before children mount.
268
266
  * Uses Pyreon's context stack so useLoaderData() reads it during child setup.
269
267
  */
270
- function LoaderDataProvider(props: { data: unknown; children: VNode | null }): VNode | null {
271
- const frame = new Map([[LoaderDataContext.id, props.data]])
272
- pushContext(frame)
273
- onUnmount(() => popContext())
268
+ function LoaderDataProvider(props: { data: unknown; children: VNodeChild }): VNodeChild {
269
+ provide(LoaderDataContext, props.data)
274
270
  return props.children
275
271
  }
276
272