import { ComponentFn, ComponentFn as ComponentFn$1, Props, VNodeChild } from "@pyreon/core"; import { Computed, Signal } from "@pyreon/reactivity"; import { SizedMap } from "@pyreon/sized-map"; //#region src/typed-routes.d.ts /** * Type-safe route paths + external-link classification for ``. * * ── Type safety ──────────────────────────────────────────────────────────── * `RegisteredRoutes` is an EMPTY, augmentable interface. By default it has no * keys, so `RoutePath` widens to `string` — the router package is fully usable * with no codegen and no typed routes (backward-compatible). When a build step * (e.g. `@pyreon/zero`'s `typedRoutes: true`) emits a `.d.ts` that AUGMENTS * `RegisteredRoutes` with one key per real route, `RoutePath` narrows to that * union and `` gains: * - autocomplete of every route path * - a REAL type error on a typo (`to="/rezume"` → TS2820 "did you mean …") * - dynamic `string` values still accepted with NO cast * - external URLs (`https://…`, `mailto:`, `#hash`, …) still accepted * all at once — via the generic `RouterLink` + {@link CheckHref}. This * is a strict superset of a plain `to: string`; nothing that compiled before * stops compiling. * * ── External links ───────────────────────────────────────────────────────── * `` inspects `to` at runtime ({@link classifyHref}) and, for * external URLs, renders a plain `` * (full browser navigation, not client-side routing) — the security-safe * default. Same-origin absolute URLs are treated as internal by default * (client-nav), configurable per-router ({@link LinkConfig}) and overridable * per-link (`external` / `target` / `rel` props). */ /** * Augmentable registry of known route paths. Empty by default (→ `RoutePath` * is `string`). A codegen step augments it: * * @example * declare module '@pyreon/router' { * interface RegisteredRoutes { * '/': Record * '/resume': Record * } * } */ interface RegisteredRoutes {} /** * The union of registered route paths, or `string` when none are registered * (no typed routes → the historical untyped behaviour, unchanged). */ type RoutePath = keyof RegisteredRoutes extends never ? string : keyof RegisteredRoutes & string; /** * Turn a route PATTERN's `:param` segments into `${string}` so a CONCRETE path * matches the pattern: * - `/posts/:id` → `` `/posts/${string}` `` (so `/posts/42` matches) * - `/a/:x/b/:y` → `` `/a/${string}/b/${string}` `` * - `/about` → `/about` (no params, unchanged) * * Without this, a registry key like `/posts/:id` would only accept the literal * string `"/posts/:id"` and reject `"/posts/42"` — the concrete path apps * actually navigate to. Distributes over a union of patterns. */ type InterpolateRoute

= P extends `${infer Pre}:${string}/${infer Rest}` ? `${Pre}${string}/${InterpolateRoute}` : P extends `${infer Pre}:${string}` ? `${Pre}${string}` : P; /** * String shapes that are UNAMBIGUOUSLY external (or non-route) and must NOT be * typo-checked against {@link RoutePath}: any URL with a scheme, a * protocol-relative URL, a `mailto:`/`tel:`/`sms:` handler, or a bare `#hash` * anchor. A literal matching one of these is accepted as-is by {@link CheckHref}. */ type ExternalHref = `${string}://${string}` | `mailto:${string}` | `tel:${string}` | `sms:${string}` | `//${string}` | `#${string}`; /** * The `to` validator for the generic `` (and, with an explicit * `Routes` argument, any other route-aware link — e.g. `@pyreon/zero`'s * `` binds it to zero's own registry): * - a dynamic `string` variable → accepted as-is (no cast needed) * - a literal that IS a registered route (incl. a concrete path matching a * `:param` pattern via {@link InterpolateRoute}) → accepted * - a literal that is an external URL / mailto / hash → accepted * - a literal that LOOKS like an internal path but isn't registered → * collapses to `Routes`, producing a "not assignable, did you mean …" * error at the call site. * * `Routes` defaults to this package's {@link RoutePath}. When no routes are * registered `Routes` is `string`, so every branch returns `T` and this is * transparently permissive. */ type CheckHref = string extends T ? T : T extends InterpolateRoute ? T : T extends ExternalHref ? T : Routes; /** Per-router configuration for `` external-link handling. */ interface LinkConfig { /** * How to treat a SAME-ORIGIN absolute URL (`https://this-site.com/about`): * - `'internal'` (default) — strip to its path and client-navigate. * - `'external'` — treat as external (full navigation, new-tab eligible). */ sameOriginAbsolute?: 'internal' | 'external'; /** External links open in a new tab (`target="_blank"`). Default `true`. */ externalNewTab?: boolean; /** * `rel` applied to external new-tab links. Default `'noopener noreferrer'` * (prevents `window.opener` hijacking + strips the referrer). */ externalRel?: string; } /** * How a `to` value should navigate: * - `internal` — client-side router navigation. * - `external` — full browser navigation, new-tab eligible (http(s) to * another origin, or protocol-relative). * - `hash` — same-page anchor (`#section`) → browser scroll. * - `protocol` — `mailto:` / `tel:` / `sms:` / other scheme → plain ``. */ type LinkKind = 'internal' | 'external' | 'hash' | 'protocol'; /** * Classify a `to` value into a {@link LinkKind}. Pure + SSR-safe (falls back to * treating same-origin-undecidable absolutes as external when there's no * `location`). The `sameOriginAbsolute` policy only affects absolute http(s) * URLs whose origin matches the current page. */ declare function classifyHref(to: string, config?: LinkConfig): LinkKind; /** * For an internal-classified value that is nonetheless an absolute same-origin * URL, strip it to a router path (`/about?x#y`). Non-absolute values pass * through unchanged. */ declare function toRouterPath(to: string): string; //#endregion //#region src/types.d.ts /** * Extracts typed params from a path string at compile time. * Supports optional params via `:param?` — their type is `string | undefined`. * * @example * ExtractParams<'/user/:id/posts/:postId'> * // → { id: string; postId: string } * * ExtractParams<'/user/:id?'> * // → { id?: string | undefined } */ type ExtractParams = T extends `${string}:${infer Param}*/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}*` ? { [K in Param]: string } : T extends `${string}:${infer Param}?/${infer Rest}` ? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}?` ? { [K in Param]?: string | undefined } : T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : Record; /** * Route metadata interface. Extend it via module augmentation to add custom fields: * * @example * // globals.d.ts * declare module "@pyreon/router" { * interface RouteMeta { * requiresRole?: "admin" | "user" * pageTitle?: string * } * } */ interface RouteMeta { /** Sets document.title on navigation */ title?: string; /** Page description (for meta tags) */ description?: string; /** If true, guards can redirect to login */ requiresAuth?: boolean; /** Scroll behavior for this route */ scrollBehavior?: 'top' | 'restore' | 'none'; /** Set to false to disable View Transitions API for this route. Default: true */ viewTransition?: boolean; } interface ResolvedRoute

= Record, Q extends Record = Record> { path: string; params: P; query: Q; hash: string; /** All matched records from root to leaf (one per nesting level) */ matched: RouteRecord[]; /** * Pre-merged route metadata for the resolved chain. * * **Frozen — do not mutate.** This object is shared across every * navigation that resolves through the same FlattenedRoute (in * particular, dynamic routes like `/posts/[id]` see the SAME meta * object identity for /posts/42 and /posts/99 — that's the cache that * keeps `resolveRoute` O(1)). Mutation would silently pollute the * cache for every future navigation; `Object.freeze` makes such * mutation throw in strict mode (every module file is strict by * default). To carry per-navigation state, attach it to your own * store / context — never write through `route.meta`. */ meta: Readonly; /** * Validated search params — populated when the matched route has `validateSearch`. * Contains the typed result of `validateSearch(query)`. Use `useValidatedSearch()` * to access this in components with full type inference. * Empty object `{}` when no `validateSearch` is configured. */ search?: Record | undefined; /** Middleware data attached during navigation (populated by middleware chain) */ _middlewareData?: Record | undefined; /** * `true` when the URL didn't match any route AND a parent record's * `notFoundComponent` was used as a synthetic fallback leaf. The * `matched` chain ends with a synthetic `RouteRecord` rendering the * not-found component INSIDE all its ancestor layouts — so 404 pages * carry the same chrome (headers, footers, navigation) as regular * pages. SSR handlers read this to set HTTP status 404. */ isNotFound?: boolean; } declare const LAZY_SYMBOL: unique symbol; interface LazyComponent { readonly [LAZY_SYMBOL]: true; readonly loader: () => Promise; /** Optional component shown while the lazy chunk is loading */ readonly loadingComponent?: ComponentFn$1; /** Optional component shown after all retries have failed */ readonly errorComponent?: ComponentFn$1; /** * Dev-only module id, emitted by `@pyreon/zero`'s fs-router codegen as * `lazy(() => import("/abs/X"), { hmrId: "/abs/X" })`. The HMR coordinator * keys the active route's matched records by this id so a hot-updated * module can be swapped IN PLACE (no page reload) using the fresh module * Vite hands the `import.meta.hot.accept` callback — sidestepping the * stale-`?t=` problem where re-running the dynamic-import thunk inside a * non-invalidated virtual routes module would return the OLD module. * Inert in production (no coordinator is registered when not in dev). */ readonly _hmrId?: string; } declare function lazy(loader: () => Promise, options?: { loading?: ComponentFn$1; error?: ComponentFn$1; hmrId?: string; }): LazyComponent; type RouteComponent = ComponentFn$1 | LazyComponent; type NavigationGuardResult = boolean | string | undefined; type NavigationGuard = (to: ResolvedRoute, from: ResolvedRoute) => NavigationGuardResult | Promise; /** * What a navigation ultimately did — the resolved value of * `router.push()` / `router.replace()`: * - `'committed'` — the target route (or a redirect it chained into) is * now the current route; * - `'cancelled'` — a blocker / guard / middleware / the redirect-depth * cap refused it; the current route is unchanged; * - `'superseded'` — a newer navigation started while this one was in * flight and won; this one never committed. * * Mirrors Vue Router's navigation-failure detection in a value-first shape: * `if (await router.push('/checkout') !== 'committed') { … }`. Callers that * ignore the value keep working (`await router.push(...)` — the promise * still settles exactly when the navigation settles). */ type NavigationResult = 'committed' | 'cancelled' | 'superseded'; type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void; /** * Context object passed through the middleware chain. * Middleware can read/write arbitrary data on `ctx.data`. */ interface RouteMiddlewareContext { /** The route being navigated to. */ to: ResolvedRoute; /** The route being navigated from. */ from: ResolvedRoute; /** Shared data — middleware can accumulate state here for downstream middleware/components. */ data: Record; } /** * Route middleware function. Called before guards. * - Return nothing/undefined to continue * - Return `false` to cancel navigation * - Return a string to redirect */ type RouteMiddleware = (ctx: RouteMiddlewareContext) => void | false | string | Promise; /** * Called before each navigation. Return `true` to block, `false` to allow. * Async blockers are supported (e.g. to show a confirmation dialog). */ type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise; interface Blocker { /** Unregister this blocker so future navigations proceed freely. */ remove(): void; } interface LoaderContext { params: Record; query: Record; /** Aborted when a newer navigation supersedes this one */ signal: AbortSignal; /** * The incoming HTTP `Request` — populated only when the loader runs during * SSR (via `prefetchLoaderData`); `undefined` on every CSR navigation. * Lets server-side loaders read cookies / auth headers and decide whether * to `throw redirect('/login')` BEFORE the layout renders. * * @example * loader: ({ request }) => { * const cookie = request?.headers.get('cookie') ?? '' * const sid = cookie.match(/sid=([^;]+)/)?.[1] * if (!sid) redirect('/login') * return { sid } * } */ request?: Request; } type RouteLoaderFn = (ctx: LoaderContext) => Promise; /** * Derive a loader's RESOLVED data type from the loader function itself — * "derive, don't annotate twice". Works for async loaders (unwraps the * Promise via `Awaited`) and sync-returning ones alike. * * Type-only, zero runtime bytes. Pair it with `useLoaderData`: * * @example * ```ts * // routes/posts.tsx * export const loader = async () => ({ posts: await fetchPosts() }) * * function PostsPage() { * const data = useLoaderData>() * // data: { posts: Post[] } — follows the loader, no second annotation * } * ``` */ type LoaderData = L extends ((...args: never[]) => infer R) ? Awaited : never; interface RouteRecord { /** Path pattern — supports `:param` segments and `(.*)` wildcard */ path: TPath; component: RouteComponent; /** Optional route name for named navigation */ name?: string; /** Metadata attached to this route */ meta?: RouteMeta; /** * Redirect target. Evaluated before guards. * String: redirect to that path. * Function: called with the resolved route, return path string. */ redirect?: string | ((to: ResolvedRoute) => string); /** Guard(s) run only for this route, before global beforeEach guards */ beforeEnter?: NavigationGuard | NavigationGuard[]; /** Guard(s) run before leaving this route. Return false to cancel. */ beforeLeave?: NavigationGuard | NavigationGuard[]; /** * Alternative path(s) for this route. Alias paths render the same component * and share guards, loaders, and metadata with the primary path. * * @example * { path: "/user/:id", alias: ["/profile/:id"], component: UserPage } */ alias?: string | string[]; /** Child routes rendered inside this route's component via */ children?: RouteRecord[]; /** * Data loader — runs before navigation commits, in parallel with sibling loaders. * The result is accessible via `useLoaderData()` inside the route component. * Receives an AbortSignal that fires if a newer navigation supersedes this one. */ loader?: RouteLoaderFn; /** * Phase 5 — SERVER loader. Present as a real function ONLY in the * server module graph (zero's fs-router emits the `.server.ts` sibling * import for SSR builds exclusively); the client graph carries just * `hasServerLoader`. On the server it runs exactly like `loader` (full * `LoaderContext`, incl. `request` during SSR). A record must not have * BOTH — zero's scanner fails the build with the fix spelled out. */ serverLoader?: RouteLoaderFn; /** * Phase 5 — serializable marker that this record has a server loader. * On client-side navigations the router fetches the whole matched * chain's server-loader data in ONE request from the data endpoint * (default `/_pyreon/data`) instead of running anything locally. */ hasServerLoader?: boolean; /** * When true, the router shows cached loader data immediately (stale) and * revalidates in the background. The component re-renders once fresh data arrives. * Only applies when navigating to a route that already has cached loader data. */ staleWhileRevalidate?: boolean; /** * Cache key function for loader data. Returns a string key derived from * route params/query. When the key matches cached data, the loader is * skipped (cache hit). Default: `path + JSON.stringify(params)`. * * @example * ```ts * loaderKey: ({ params }) => `user-${params.id}` * ``` */ loaderKey?: (ctx: Pick) => string; /** * Time in ms to keep cached loader data before garbage collection. * Default: 300000 (5 minutes). Set to 0 to disable caching. * Stale data is still served immediately if `staleWhileRevalidate` is true. */ gcTime?: number; /** Component rendered when this route's loader throws an error */ errorComponent?: ComponentFn$1; /** * Component rendered when a URL doesn't match any descendant route under * this record's path. Acts as a "404 within layout" — the matched chain * is `[...ancestors, this, syntheticLeaf]` so the not-found component * renders INSIDE this layout's chrome. fs-router attaches this when it * detects a `_404.tsx` / `_not-found.tsx` file under this layout. */ notFoundComponent?: ComponentFn$1; /** * Component rendered while this route's loader is running. * Only shown after `pendingMs` (default: 0) to avoid flash on fast loads. * Once shown, displayed for at least `pendingMinMs` (default: 200) to avoid flicker. */ pendingComponent?: ComponentFn$1; /** Delay in ms before showing pendingComponent (default: 0). Prevents flash on fast loaders. */ pendingMs?: number; /** Minimum display time in ms for pendingComponent once shown (default: 200). Prevents flicker. */ pendingMinMs?: number; /** * Validate and transform raw query string parameters into typed values. * Receives the raw `Record` from the URL and returns * a typed object. The validated result is available via `useValidatedSearch()`. * * Accepts any function — use Zod `.parse`, Valibot, or a plain function: * * @example * ```ts * // Plain function: * validateSearch: (raw) => ({ * page: Number(raw.page) || 1, * q: raw.q ?? '', * }) * * // With Zod: * validateSearch: z.object({ * page: z.coerce.number().default(1), * q: z.string().default(''), * }).parse * ``` */ validateSearch?: (raw: Record) => Record; /** Per-route middleware — runs before guards, can accumulate context data. */ middleware?: RouteMiddleware | RouteMiddleware[]; } type ScrollBehaviorFn = (to: ResolvedRoute, from: ResolvedRoute, savedPosition: number | null) => 'top' | 'restore' | 'none' | number; interface RouterOptions { routes: RouteRecord[]; /** "hash" (default) uses location.hash; "history" uses pushState */ mode?: 'hash' | 'history'; /** * Phase 5 — endpoint the client-side router fetches server-loader data * from on navigations (one request per navigation for the whole matched * chain). Default `"/_pyreon/data"` — zero's `createServer` auto-mounts * it; prefix with your `base` for sub-path deploys. */ dataEndpoint?: string; /** * Base path for the application. Used when deploying to a sub-path * (e.g. `"/app"` for `https://example.com/app/`). * Only applies in history mode. Must start with `/`. * Default: `""` (no base path). */ base?: string; /** * Global scroll behavior. Per-route meta.scrollBehavior takes precedence. * Default: "top" */ scrollBehavior?: ScrollBehaviorFn | 'top' | 'restore' | 'none'; /** * Initial URL for SSR. On the server, window.location is unavailable; * pass the request URL here so the router resolves the correct route. * * @example * // In your SSR handler: * const router = createRouter({ routes, url: req.url }) */ url?: string; /** * Called when a route loader throws. If not provided, errors are logged * and the navigation continues with `undefined` data for the failed loader. * Return `false` to cancel the navigation. */ onError?: (err: unknown, route: ResolvedRoute) => undefined | false; /** * Maximum number of resolved lazy components to cache. * When exceeded, the oldest entry is evicted. * Default: 100. */ maxCacheSize?: number; /** * Trailing slash handling: * - `"strip"` — removes trailing slashes before matching (default) * - `"add"` — ensures paths always end with `/` * - `"ignore"` — no normalization */ trailingSlash?: 'strip' | 'add' | 'ignore'; /** * `` external-link behaviour (same-origin-absolute policy, * new-tab, and `rel`). See {@link LinkConfig}. */ links?: LinkConfig; } /** * Router interface. Parameterized by route name union for type-safe named navigation. * * @example * ```ts * type MyRoutes = 'home' | 'user' | 'settings' * const router: Router = createRouter({ routes }) * router.push({ name: 'user', params: { id: '42' } }) // ✓ * router.push({ name: 'typo' }) // TS error * ``` */ interface Router { /** Navigate to a path. Resolves with what the navigation did — see {@link NavigationResult}. */ push(path: string): Promise; /** Navigate to a named route */ push(location: { name: TNames; params?: Record; query?: Record; }): Promise; /** Replace current history entry. Resolves with what the navigation did — see {@link NavigationResult}. */ replace(path: string): Promise; /** Replace current history entry using a named route */ replace(location: { name: TNames; params?: Record; query?: Record; }): Promise; /** Go back one step in history */ back(): void; /** Go forward one step in history */ forward(): void; /** Navigate forward or backward by `delta` steps in the history stack */ go(delta: number): void; /** Register a global before-navigation guard. Returns an unregister function. */ beforeEach(guard: NavigationGuard): () => void; /** Register a global after-navigation hook. Returns an unregister function. */ afterEach(hook: AfterEachHook): () => void; /** Current resolved route (reactive signal) */ readonly currentRoute: () => ResolvedRoute; /** True while a navigation (guards + loaders) is in flight */ readonly loading: () => boolean; /** * Promise that resolves once the initial navigation is complete. * Useful for SSR and for delaying rendering until the first route is resolved. */ isReady(): Promise; /** * Resolve `path` and prepare everything needed to render it: load any lazy * route components into the router's cache and run the matched routes' * loaders. After this resolves, a `RouterView` rendered against this router * for `path` will produce final HTML synchronously — no loading fallbacks, * no `useLoaderData()` returning `undefined`. * * Used by SSR/SSG to hydrate the route tree before `renderToString`. * The router's `currentRoute` is NOT changed by `preload` — pass the path * separately when creating the router (`createRouter({ url, ... })`) or * call this for the same `url` you initialised the router with. */ preload(path: string, request?: Request, options?: { skipLoaders?: boolean; }): Promise; /** * Phase 5 — run ONLY the matched chain's `serverLoader` records for the * single-fetch data endpoint, keyed by matched-chain index. Returns the * index-keyed data, or a redirect descriptor when a server loader threw * `redirect()`. Server-only (the `serverLoader` fn exists only in the SSR * module graph). */ runServerLoaders(path: string, request?: Request): Promise<{ kind: 'data'; data: Record; } | { kind: 'redirect'; to: string; status: number; }>; /** * Invalidate cached loader data. Forces loaders to re-run on next navigation. * - No args: invalidate ALL cached loader data * - String: invalidate by cache key (as returned by `loaderKey`) * - Function: invalidate entries where the predicate returns true * * To refresh the CURRENT route's data in place (mutation-then-refresh * flows), use {@link revalidate} instead — `invalidateLoader` only takes * effect on the next navigation. */ invalidateLoader(keyOrPredicate?: string | ((key: string) => boolean)): void; /** * Re-run the CURRENT route's loaders in place and re-render the affected * route components with fresh data — no navigation required. The * mutation-then-refresh primitive (`useRevalidator` in React Router, * `router.invalidate()` in TanStack): * * - drops the current chain's cached loader entries, then re-runs its * loaders through the normal pipeline (server-loader records re-fetch * from the data endpoint; `staleWhileRevalidate` records refresh in the * background per their contract); * - a loader that throws `redirect()` navigates (replace semantics); * - a real navigation starting mid-revalidate supersedes it (the fresh * navigation's loaders win — no torn state). * * @example * await api.deletePost(id) * await router.revalidate() // list loader re-runs, view updates in place */ revalidate(): Promise; /** Remove all event listeners, clear caches, and abort in-flight navigations. */ destroy(): void; } interface RouterInstance extends Router { routes: RouteRecord[]; mode: 'hash' | 'history'; /** Normalized base path (e.g. "/app"), empty string if none */ _base: string; _currentPath: Signal; _currentRoute: Computed; _componentCache: SizedMap; _loadingSignal: Signal; /** * PR-S8: dev-only counter — bumped by `_hmrSwap` after a successful * component cache swap. `RouterView`'s `depthEntry` computed * subscribes to it alongside `_loadingSignal` so a swap forces a * re-emit without leaking into the navigation-loading counter * (`useTransition()`). `undefined` in production builds where * `_hmrSwap` itself is undefined. */ _hmrTick?: Signal; _resolve(rawPath: string): ResolvedRoute; _scrollPositions: Map; _scrollBehavior: RouterOptions['scrollBehavior']; _onError: RouterOptions['onError']; _linkConfig: LinkConfig | undefined; _maxCacheSize: number; /** * Current RouterView nesting depth. Incremented by each RouterView as it * mounts (in tree order = depth-first), so each view knows which level of * `matched[]` to render. Reset to 0 by RouterProvider. */ _viewDepth: number; /** Route records whose lazy chunk permanently failed (all retries exhausted) */ _erroredChunks: Set; /** Loader data keyed by route record — populated before each navigation commits */ _loaderData: Map; /** AbortController for the in-flight loader batch — aborted when a newer navigation starts */ _abortController: AbortController | null; /** Registered navigation blockers */ _blockers: Set; /** Resolves the isReady() promise after initial navigation completes */ _readyResolve: (() => void) | null; /** The isReady() promise instance */ _readyPromise: Promise; /** Timestamp when the current navigation started — used for pendingMs timing */ _navigationStartTime: number; /** * Middleware data of the last COMMITTED navigation. `runMiddleware` * accumulates onto the in-flight `to` object, but `currentRoute()` is a * computed that re-resolves a FRESH ResolvedRoute from the path — the * in-flight object (and anything attached to it) never becomes the * current route. `commitNavigation` copies the accumulated data here * (before flipping `currentPath`), and `useMiddlewareData()` reads it * alongside a reactive `currentRoute()` subscription. Reset to `{}` on * navigations whose chain has no middleware (data is per-navigation). */ _committedMiddlewareData: Record; /** Key-based loader cache: cacheKey → { data, timestamp } */ _loaderCache: SizedMap; /** * Run a record's loader through the router's cache + in-flight dedup * (cache hit → cached data; same-key in-flight with a live signal → * the existing promise; else run + cache). The SAME path navigations * take — `prefetchLoaderData` routes through this so a hover/viewport * prefetch and the click that follows it share ONE loader run instead * of double-fetching (pre-fix prefetch called `record.loader()` raw, * bypassing `_loaderCache` and `_loaderInflight` entirely). */ _executeLoader(record: RouteRecord, ctx: LoaderContext): Promise; /** * In-flight loader dedup: cacheKey → { promise, signal }. * Tracking the signal lets dedup skip an in-flight entry whose signal is * already aborted — otherwise nav-2 would inherit nav-1's aborted promise * (`router.push` aborts the previous nav's controller before starting the * next, so back-to-back nav to the same path could resolve nav-2 against * nav-1's aborted fetch). */ _loaderInflight: Map; signal: AbortSignal; }>; /** * Dev-only HMR coordinator. Given a hot-updated module's id and the FRESH * module namespace Vite handed `import.meta.hot.accept`, swaps the new * component into every matched record whose lazy `_hmrId` equals `id`, * then bumps `_hmrTick` (PR-S8) so `RouterView` re-renders ONLY that * subtree in place — no page reload, so `__pyreon_hmr_registry__` * (module-scope signal values) survives and `__hmr_signal` restores them. * * Using the namespace Vite passed (not a re-run of the lazy thunk) * sidesteps the stale-`?t=` trap: the dynamic-import thunk lives in the * virtual routes module, which is NOT invalidated when a leaf route * self-accepts, so re-importing it would return the OLD module. * * Returns `true` when at least one matched component was swapped. `false` * tells `@pyreon/vite-plugin`'s accept handler the edit was outside the * active route tree (a nested non-route component, an unrelated route, a * signal-only module) so it falls back to `import.meta.hot.invalidate()` * → an automatic full reload (no manual refresh either way). * * Present only when the router is created in a dev browser context. */ _hmrSwap?: (id: string, mod: unknown) => boolean; } //#endregion //#region src/components.d.ts interface RouterProviderProps extends Props { router: Router; children?: VNodeChild; } interface RouterViewProps extends Props { /** Explicitly pass a router (optional — uses the active router by default) */ router?: Router; /** * Announce route changes to screen readers via a visually-hidden * `aria-live` region (the new page's `document.title`, falling back to the * pathname). Default `true`. Only the ROOT `` announces — nested * (layout) views ignore this. Set `false` to opt out (e.g. if you run your * own route announcer). */ announceRouteChanges?: boolean; } interface RouterLinkProps extends Props { /** * The destination. Typo-checked against the app's registered routes when * typed routes are enabled (else any `string`); external URLs (`https://…`, * `mailto:`, `#hash`, …) and dynamic `string` variables are always accepted. * See `CheckHref` / `RegisteredRoutes`. */ to: CheckHref; /** If true, uses router.replace() instead of router.push() */ replace?: boolean; /** CSS class applied when this link is active (default: "router-link-active") */ activeClass?: string; /** CSS class for exact-match active state (default: "router-link-exact-active") */ exactActiveClass?: string; /** If true, only applies activeClass on exact match */ exact?: boolean; /** * Prefetch strategy for loader data: * - "intent" (default) — prefetch on hover AND focus (covers mouse + keyboard) * - "hover" — prefetch on hover only * - "viewport" — prefetch when the link scrolls into the viewport * - "none" — no prefetching */ prefetch?: 'intent' | 'hover' | 'viewport' | 'none'; /** * Override the auto internal/external classification of `to`: * - `true` — force external (full browser navigation, new-tab eligible). * - `false` — force internal (client-side routing), e.g. for a same-origin * absolute URL you want handled by the router. * - omitted — auto-detect (URLs with a scheme / protocol-relative / * cross-origin → external; registered/relative paths → internal). */ external?: boolean; /** Override the anchor `target` (auto: `_blank` for external new-tab links). */ target?: string; /** Override the anchor `rel` (auto: `noopener noreferrer` for `_blank`). */ rel?: string; children?: VNodeChild | null; } /** * `` — client-side navigation for internal routes, with automatic * external-link handling. Generic over the `to` literal so it validates against * the app's registered routes (typo → TS error + "did you mean …"), while still * accepting dynamic `string`s and external URLs. The runtime is * {@link RouterLinkImpl}; this const only refines the call signature. */ declare const RouterLink: { (props: RouterLinkProps): VNodeChild; }; declare const _RouterProvider: ComponentFn; declare const _RouterView: ComponentFn; //#endregion //#region src/not-found.d.ts /** * Throw inside a route loader or component to trigger the nearest * NotFoundBoundary. Inspired by Next.js's `notFound()`. * * @example * ```ts * // In a loader: * loader: async ({ params }) => { * const user = await fetchUser(params.id) * if (!user) notFound() * return user * } * ``` */ declare function notFound(message?: string): never; /** Check if an error is a NotFoundError thrown by `notFound()`. */ declare function isNotFoundError(err: unknown): boolean; interface NotFoundBoundaryProps extends Props { /** Component or VNode to render when notFound() is thrown */ fallback: ComponentFn | VNodeChild; children?: VNodeChild; } /** * Catches `notFound()` errors from child route components or loaders * and renders the fallback. Wraps Pyreon's ErrorBoundary with notFound * detection — non-notFound errors propagate to parent error boundaries. * * @example * ```tsx * }> * * * ``` */ declare const NotFoundBoundary: ComponentFn; //#endregion //#region src/redirect.d.ts /** Standard redirect status codes. 307/308 preserve the request method, 302/303 don't. */ type RedirectStatus = 301 | 302 | 303 | 307 | 308; interface RedirectInfo { url: string; status: RedirectStatus; } /** * Throw inside a route loader to redirect the navigation server-side * (during SSR returns a 302/307 `Location:` response) and client-side * (during CSR triggers `router.replace()` before the layout renders). * * The auth-gate use case: replaces the fragile `onMount + router.push()` * workaround. `onMount` doesn't fire reliably under nested-layout dev SSR + * hydration — so the layout renders briefly before the push happens, leaking * authenticated UI to unauthenticated users. `redirect()` runs in the loader * BEFORE the layout's component is invoked, so the unauthenticated UI never * mounts in the first place. * * @example * ```ts * // src/routes/app/_layout.tsx * export const loader = async ({ request }) => { * const session = await getSession(request) * if (!session) redirect('/login') * return { user: session.user } * } * ``` * * @param url - Target URL (typically a path like `/login` or absolute URL for cross-origin). * @param status - HTTP redirect status. Default `307` (Temporary Redirect, method-preserving). * Use `301`/`308` for permanent moves, `302`/`303` to force GET on the target. */ declare function redirect(url: string, status?: RedirectStatus): never; /** Check if an error is a RedirectError thrown by `redirect()`. */ declare function isRedirectError(err: unknown): boolean; /** * Extract the redirect URL and status from a thrown RedirectError. Returns * `null` if `err` isn't a RedirectError. Used by the router's loader-runner * (CSR) and the SSR handler to convert the thrown error into the right kind * of response (a `router.replace()` call or a `302`/`307` Response). */ declare function getRedirectInfo(err: unknown): RedirectInfo | null; //#endregion //#region src/loader.d.ts /** * Returns the data resolved by the current route's `loader` function. * Must be called inside a route component rendered by . * * @example * const routes = [{ path: "/users", component: Users, loader: fetchUsers }] * * function Users() { * const users = useLoaderData() * return h("ul", null, users.map(u => h("li", null, u.name))) * } */ declare function useLoaderData(): T; /** * SSR helper: pre-run all loaders for the given path before rendering. * Call this before `renderToString` so route components can read data via `useLoaderData()`. * * NOTE: this runs LOADERS only — it does NOT resolve lazy route *components*. * The SSR handler additionally calls `router.preload(path)` to resolve lazy * components into the cache before the synchronous render (an unresolved * `lazy()` would otherwise fall back to its empty loading state and ship a * blank page). This function stays loaders-only because it is also the * RouterLink-prefetch path, which should warm loader DATA on hover without * eagerly downloading every route's component chunk. * * The optional `request` is forwarded to each loader's `LoaderContext.request`, * letting server-side loaders read cookies / auth headers and `throw redirect()` * before the layout renders. A loader that throws `redirect()` propagates the * thrown error here — the SSR handler's `catch` converts it into a 302/307 * `Location:` Response. * * @example * const router = createRouter({ routes, url: req.url }) * await prefetchLoaderData(router, req.url, request) * const html = await renderToString(h(App, { router })) */ declare function prefetchLoaderData(router: RouterInstance, path: string, request?: Request): Promise; /** * Serialize loader data to a JSON-safe plain object for embedding in SSR HTML. * Keys are route path patterns (stable across server and client). * * @example — SSR handler: * await prefetchLoaderData(router, req.url) * const { html, head } = await renderWithHead(h(App, null)) * const page = `...${head} * * ...${html}...` */ declare function serializeLoaderData(router: RouterInstance): Record; /** * Serialize loader data to JSON for embedding in an SSR `` */ declare function stringifyLoaderData(loaderData: Record): string; /** * Hydrate loader data from a serialized object (e.g. `window.__PYREON_LOADER_DATA__`). * Populates the router's internal `_loaderData` map so the initial render uses * server-fetched data without re-running loaders on the client. * * Call this before `mount()`, after `createRouter()`. * * @example — client entry: * import { hydrateLoaderData } from "@pyreon/router" * const router = createRouter({ routes }) * hydrateLoaderData(router, window.__PYREON_LOADER_DATA__ ?? {}) * mount(h(App, null), document.getElementById("app")!) */ declare function hydrateLoaderData(router: RouterInstance, serialized: Record): void; //#endregion //#region src/match.d.ts declare function parseQuery(qs: string): Record; /** * Parse a query string preserving duplicate keys as arrays. * * @example * parseQueryMulti("color=red&color=blue&size=lg") * // → { color: ["red", "blue"], size: "lg" } */ declare function parseQueryMulti(qs: string): Record; declare function stringifyQuery(query: Record): string; /** * Resolve a raw path (including query string and hash) against the route tree. * Uses flattened index for O(1) static lookup and first-segment dispatch. */ declare function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute; /** Build a path string from a named route's pattern and params */ declare function buildPath(pattern: string, params: Record): string; /** Find a route record by name (recursive, O(n)). Prefer buildNameIndex for repeated lookups. */ declare function findRouteByName(name: string, routes: RouteRecord[]): RouteRecord | null; //#endregion //#region src/router.d.ts declare const RouterContext: import("@pyreon/core").Context; declare function getActiveRouter(): RouterInstance | null; declare function setActiveRouter(router: RouterInstance | null): void; declare function useRouter(): Router; declare function useRoute(): () => ResolvedRoute & Record, Record>; /** * Programmatic navigation hook. Returns a callable that pushes the * given path onto the active router's stack — mirrors the canonical * `useNavigate()` shape exposed by `@pyreon/native-router-swift` and * `@pyreon/native-router-kotlin`, so the SAME `.tsx` source can call * `useNavigate()` on all three targets. * * @example * const navigate = useNavigate() * navigate('/dashboard') */ declare function useNavigate(): (path: string) => void; /** * Read path parameters for the current route. Returns a snapshot map * of `{ paramName: value }` extracted from the matched route pattern. * Mirrors the canonical `useParams()` shape on native runtimes for * cross-target source parity. * * The generic `T` lets callers type the params shape they expect (e.g. * `useParams<{ id: string }>()`); at runtime it's still a string map. * * @example * const params = useParams<{ id: string }>() * console.log(params.id) */ declare function useParams = Record>(): T; /** * In-component guard: called before the component's route is left. * Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed. * Automatically removed on component unmount. * * @example * onBeforeRouteLeave((to, from) => { * if (hasUnsavedChanges()) return false * }) */ declare function onBeforeRouteLeave(guard: NavigationGuard): () => void; /** * In-component guard: called when the route changes but the component is reused * (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes. * Automatically removed on component unmount. * * @example * onBeforeRouteUpdate((to, from) => { * if (!isValidId(to.params.id)) return false * }) */ declare function onBeforeRouteUpdate(guard: NavigationGuard): () => void; declare function useBlocker(fn: BlockerFn): Blocker; /** * Reactive read/write access to the current route's query parameters. * * Returns `[get, set]` where `get` is a reactive signal producing the merged * query object and `set` navigates to the current path with updated params. * * @example * const [params, setParams] = useSearchParams({ page: "1", sort: "name" }) * params().page // "1" if not in URL * setParams({ page: "2" }) // navigates to ?page=2&sort=name */ /** * Check if a path is active (matches the current route). * Returns a reactive boolean signal. * * - Exact mode: `/admin` matches only `/admin` * - Partial mode (default): `/admin` matches `/admin`, `/admin/users`, `/admin/settings` * Uses segment-aware prefix matching — `/admin` does NOT match `/admin-panel` * * @example * ```tsx * const isAdmin = useIsActive("/admin") // partial — matches /admin/* * const isExact = useIsActive("/admin", true) // exact — only /admin * *

* Active * ``` */ declare function useIsActive(path: string, exact?: boolean): () => boolean; /** Schema entry for typed search params. */ type SearchParamSchema = { [key: string]: 'string' | 'number' | 'boolean'; }; /** Infer the typed result from a search param schema. */ type InferSearchParams = { [K in keyof T]: T[K] extends 'number' ? number : T[K] extends 'boolean' ? boolean : string }; /** * Read and write URL search params reactively. * * @example Basic (untyped) * ```ts * const [params, setParams] = useSearchParams({ page: "1" }) * params().page // "1" * setParams({ page: "2" }) // updates URL * ``` * * @example Typed with schema * ```ts * const [params, setParams] = useSearchParams({ * page: 'number', * sort: 'string', * desc: 'boolean', * }) * params().page // number (auto-coerced) * params().desc // boolean * ``` */ declare function useSearchParams>(defaults?: T): [get: () => T, set: (updates: Partial) => Promise]; /** * Typed search params with auto-coercion. * * Schema values define the type: `'string'`, `'number'`, or `'boolean'`. * Query string values are automatically coerced to the declared type. * * @example * ```ts * const [params, setParams] = useTypedSearchParams({ * page: 'number', * sort: 'string', * desc: 'boolean', * }) * params().page // number (coerced from "3" → 3) * params().desc // boolean (coerced from "true" → true) * setParams({ page: 2 }) // updates URL with ?page=2 * ``` */ declare function useTypedSearchParams(schema: T): [get: () => InferSearchParams, set: (updates: Partial>) => Promise]; /** * Read the validated search params from the current route's `validateSearch`. * Returns a reactive accessor that re-evaluates when the route changes. * * The generic `T` should match the return type of your `validateSearch` function. * * @example * ```tsx * // Route config: * { path: '/search', validateSearch: (raw) => ({ * page: Number(raw.page) || 1, * q: raw.q ?? '', * }), component: SearchPage } * * // In SearchPage: * const search = useValidatedSearch<{ page: number; q: string }>() * // search().page — typed as number * // search().q — typed as string * ``` */ declare function useValidatedSearch = Record>(): () => T; /** * Returns true while a navigation is in progress (guards + loaders running). * Use this to show loading indicators during route transitions. * * @example * ```tsx * const isNavigating = useTransition() * * * * ``` */ declare function useTransition(): () => boolean; /** * Read data accumulated by route middleware. * * @example * ```ts * // In middleware: * const authMiddleware: RouteMiddleware = async (ctx) => { * ctx.data.user = await getUser(ctx.to) * if (!ctx.data.user) return '/login' * } * * // In component: * const data = useMiddlewareData() * const user = () => data().user as User * ``` */ declare function useMiddlewareData(): () => Record; declare function createRouter(options: RouterOptions | RouteRecord[]): Router; //#endregion export { type AfterEachHook, type Blocker, type BlockerFn, type CheckHref, type ExternalHref, type ExtractParams, type InterpolateRoute, type LazyComponent, type LinkConfig, type LinkKind, type LoaderContext, type LoaderData, type NavigationGuard, type NavigationGuardResult, type NavigationResult, NotFoundBoundary, type NotFoundBoundaryProps, type RedirectStatus, type RegisteredRoutes, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteMiddleware, type RouteMiddlewareContext, type RoutePath, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, _RouterProvider as RouterProvider, type RouterProviderProps, _RouterView as RouterView, type RouterViewProps, type ScrollBehaviorFn, buildPath, classifyHref, createRouter, findRouteByName, getActiveRouter, getRedirectInfo, hydrateLoaderData, isNotFoundError, isRedirectError, lazy, notFound, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, redirect, resolveRoute, serializeLoaderData, setActiveRouter, stringifyLoaderData, stringifyQuery, toRouterPath, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useNavigate, useParams, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams, useValidatedSearch };