@pyreon/router 0.24.5 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -6
- package/src/components.tsx +0 -650
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -106
- package/src/loader.ts +0 -200
- package/src/manifest.ts +0 -399
- package/src/match.ts +0 -921
- package/src/not-found.ts +0 -75
- package/src/redirect.ts +0 -63
- package/src/router.ts +0 -1424
- package/src/scroll.ts +0 -93
- package/src/tests/integration.test.tsx +0 -298
- package/src/tests/loader.test.ts +0 -1024
- package/src/tests/manifest-snapshot.test.ts +0 -101
- package/src/tests/match.test.ts +0 -782
- package/src/tests/native-markers.test.ts +0 -18
- package/src/tests/redirect.test.ts +0 -96
- package/src/tests/router.browser.test.tsx +0 -509
- package/src/tests/router.test.ts +0 -5498
- package/src/tests/routerlink-reactive-to.browser.test.tsx +0 -158
- package/src/tests/scroll.test.ts +0 -31
- package/src/tests/setup.ts +0 -3
- package/src/types.ts +0 -517
package/src/types.ts
DELETED
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
import type { ComponentFn } from '@pyreon/core'
|
|
2
|
-
|
|
3
|
-
export type { ComponentFn }
|
|
4
|
-
|
|
5
|
-
// ─── Path param extraction ────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Extracts typed params from a path string at compile time.
|
|
9
|
-
* Supports optional params via `:param?` — their type is `string | undefined`.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ExtractParams<'/user/:id/posts/:postId'>
|
|
13
|
-
* // → { id: string; postId: string }
|
|
14
|
-
*
|
|
15
|
-
* ExtractParams<'/user/:id?'>
|
|
16
|
-
* // → { id?: string | undefined }
|
|
17
|
-
*/
|
|
18
|
-
export type ExtractParams<T extends string> = T extends `${string}:${infer Param}*/${infer Rest}`
|
|
19
|
-
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
|
|
20
|
-
: T extends `${string}:${infer Param}*`
|
|
21
|
-
? { [K in Param]: string }
|
|
22
|
-
: T extends `${string}:${infer Param}?/${infer Rest}`
|
|
23
|
-
? { [K in Param]?: string | undefined } & ExtractParams<`/${Rest}`>
|
|
24
|
-
: T extends `${string}:${infer Param}?`
|
|
25
|
-
? { [K in Param]?: string | undefined }
|
|
26
|
-
: T extends `${string}:${infer Param}/${infer Rest}`
|
|
27
|
-
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
|
|
28
|
-
: T extends `${string}:${infer Param}`
|
|
29
|
-
? { [K in Param]: string }
|
|
30
|
-
: Record<never, never>
|
|
31
|
-
|
|
32
|
-
// ─── Route meta ───────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Route metadata interface. Extend it via module augmentation to add custom fields:
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* // globals.d.ts
|
|
39
|
-
* declare module "@pyreon/router" {
|
|
40
|
-
* interface RouteMeta {
|
|
41
|
-
* requiresRole?: "admin" | "user"
|
|
42
|
-
* pageTitle?: string
|
|
43
|
-
* }
|
|
44
|
-
* }
|
|
45
|
-
*/
|
|
46
|
-
export interface RouteMeta {
|
|
47
|
-
/** Sets document.title on navigation */
|
|
48
|
-
title?: string
|
|
49
|
-
/** Page description (for meta tags) */
|
|
50
|
-
description?: string
|
|
51
|
-
/** If true, guards can redirect to login */
|
|
52
|
-
requiresAuth?: boolean
|
|
53
|
-
/** Scroll behavior for this route */
|
|
54
|
-
scrollBehavior?: 'top' | 'restore' | 'none'
|
|
55
|
-
/** Set to false to disable View Transitions API for this route. Default: true */
|
|
56
|
-
viewTransition?: boolean
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ─── Resolved route ───────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
export interface ResolvedRoute<
|
|
62
|
-
P extends Record<string, string | undefined> = Record<string, string>,
|
|
63
|
-
Q extends Record<string, string> = Record<string, string>,
|
|
64
|
-
> {
|
|
65
|
-
path: string
|
|
66
|
-
params: P
|
|
67
|
-
query: Q
|
|
68
|
-
hash: string
|
|
69
|
-
/** All matched records from root to leaf (one per nesting level) */
|
|
70
|
-
matched: RouteRecord[]
|
|
71
|
-
meta: RouteMeta
|
|
72
|
-
/**
|
|
73
|
-
* Validated search params — populated when the matched route has `validateSearch`.
|
|
74
|
-
* Contains the typed result of `validateSearch(query)`. Use `useValidatedSearch()`
|
|
75
|
-
* to access this in components with full type inference.
|
|
76
|
-
* Empty object `{}` when no `validateSearch` is configured.
|
|
77
|
-
*/
|
|
78
|
-
search?: Record<string, unknown> | undefined
|
|
79
|
-
/** Middleware data attached during navigation (populated by middleware chain) */
|
|
80
|
-
_middlewareData?: Record<string, unknown> | undefined
|
|
81
|
-
/**
|
|
82
|
-
* `true` when the URL didn't match any route AND a parent record's
|
|
83
|
-
* `notFoundComponent` was used as a synthetic fallback leaf. The
|
|
84
|
-
* `matched` chain ends with a synthetic `RouteRecord` rendering the
|
|
85
|
-
* not-found component INSIDE all its ancestor layouts — so 404 pages
|
|
86
|
-
* carry the same chrome (headers, footers, navigation) as regular
|
|
87
|
-
* pages. SSR handlers read this to set HTTP status 404.
|
|
88
|
-
*/
|
|
89
|
-
isNotFound?: boolean
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── Lazy component ───────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
export const LAZY_SYMBOL = Symbol('pyreon.lazy')
|
|
95
|
-
|
|
96
|
-
export interface LazyComponent {
|
|
97
|
-
readonly [LAZY_SYMBOL]: true
|
|
98
|
-
readonly loader: () => Promise<ComponentFn | { default: ComponentFn }>
|
|
99
|
-
/** Optional component shown while the lazy chunk is loading */
|
|
100
|
-
readonly loadingComponent?: ComponentFn
|
|
101
|
-
/** Optional component shown after all retries have failed */
|
|
102
|
-
readonly errorComponent?: ComponentFn
|
|
103
|
-
/**
|
|
104
|
-
* Dev-only module id, emitted by `@pyreon/zero`'s fs-router codegen as
|
|
105
|
-
* `lazy(() => import("/abs/X"), { hmrId: "/abs/X" })`. The HMR coordinator
|
|
106
|
-
* keys the active route's matched records by this id so a hot-updated
|
|
107
|
-
* module can be swapped IN PLACE (no page reload) using the fresh module
|
|
108
|
-
* Vite hands the `import.meta.hot.accept` callback — sidestepping the
|
|
109
|
-
* stale-`?t=` problem where re-running the dynamic-import thunk inside a
|
|
110
|
-
* non-invalidated virtual routes module would return the OLD module.
|
|
111
|
-
* Inert in production (no coordinator is registered when not in dev).
|
|
112
|
-
*/
|
|
113
|
-
readonly _hmrId?: string
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function lazy(
|
|
117
|
-
loader: () => Promise<ComponentFn | { default: ComponentFn }>,
|
|
118
|
-
options?: { loading?: ComponentFn; error?: ComponentFn; hmrId?: string },
|
|
119
|
-
): LazyComponent {
|
|
120
|
-
return {
|
|
121
|
-
[LAZY_SYMBOL]: true,
|
|
122
|
-
loader,
|
|
123
|
-
...(options?.loading ? { loadingComponent: options.loading } : {}),
|
|
124
|
-
...(options?.error ? { errorComponent: options.error } : {}),
|
|
125
|
-
...(options?.hmrId ? { _hmrId: options.hmrId } : {}),
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function isLazy(c: RouteComponent): c is LazyComponent {
|
|
130
|
-
return typeof c === 'object' && c !== null && (c as LazyComponent)[LAZY_SYMBOL] === true
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export type RouteComponent = ComponentFn | LazyComponent
|
|
134
|
-
|
|
135
|
-
// ─── Navigation guard ─────────────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
export type NavigationGuardResult = boolean | string | undefined
|
|
138
|
-
export type NavigationGuard = (
|
|
139
|
-
to: ResolvedRoute,
|
|
140
|
-
from: ResolvedRoute,
|
|
141
|
-
) => NavigationGuardResult | Promise<NavigationGuardResult>
|
|
142
|
-
|
|
143
|
-
export type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void
|
|
144
|
-
|
|
145
|
-
// ─── Route middleware ────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Context object passed through the middleware chain.
|
|
149
|
-
* Middleware can read/write arbitrary data on `ctx.data`.
|
|
150
|
-
*/
|
|
151
|
-
export interface RouteMiddlewareContext {
|
|
152
|
-
/** The route being navigated to. */
|
|
153
|
-
to: ResolvedRoute
|
|
154
|
-
/** The route being navigated from. */
|
|
155
|
-
from: ResolvedRoute
|
|
156
|
-
/** Shared data — middleware can accumulate state here for downstream middleware/components. */
|
|
157
|
-
data: Record<string, unknown>
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Route middleware function. Called before guards.
|
|
162
|
-
* - Return nothing/undefined to continue
|
|
163
|
-
* - Return `false` to cancel navigation
|
|
164
|
-
* - Return a string to redirect
|
|
165
|
-
*/
|
|
166
|
-
export type RouteMiddleware = (
|
|
167
|
-
ctx: RouteMiddlewareContext,
|
|
168
|
-
) => void | false | string | Promise<void | false | string>
|
|
169
|
-
|
|
170
|
-
// ─── Navigation blockers ──────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Called before each navigation. Return `true` to block, `false` to allow.
|
|
174
|
-
* Async blockers are supported (e.g. to show a confirmation dialog).
|
|
175
|
-
*/
|
|
176
|
-
export type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>
|
|
177
|
-
|
|
178
|
-
export interface Blocker {
|
|
179
|
-
/** Unregister this blocker so future navigations proceed freely. */
|
|
180
|
-
remove(): void
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ─── Route loaders ────────────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
export interface LoaderContext {
|
|
186
|
-
params: Record<string, string>
|
|
187
|
-
query: Record<string, string>
|
|
188
|
-
/** Aborted when a newer navigation supersedes this one */
|
|
189
|
-
signal: AbortSignal
|
|
190
|
-
/**
|
|
191
|
-
* The incoming HTTP `Request` — populated only when the loader runs during
|
|
192
|
-
* SSR (via `prefetchLoaderData`); `undefined` on every CSR navigation.
|
|
193
|
-
* Lets server-side loaders read cookies / auth headers and decide whether
|
|
194
|
-
* to `throw redirect('/login')` BEFORE the layout renders.
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* loader: ({ request }) => {
|
|
198
|
-
* const cookie = request?.headers.get('cookie') ?? ''
|
|
199
|
-
* const sid = cookie.match(/sid=([^;]+)/)?.[1]
|
|
200
|
-
* if (!sid) redirect('/login')
|
|
201
|
-
* return { sid }
|
|
202
|
-
* }
|
|
203
|
-
*/
|
|
204
|
-
request?: Request
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export type RouteLoaderFn = (ctx: LoaderContext) => Promise<unknown>
|
|
208
|
-
|
|
209
|
-
// ─── Route record ─────────────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
export interface RouteRecord<TPath extends string = string> {
|
|
212
|
-
/** Path pattern — supports `:param` segments and `(.*)` wildcard */
|
|
213
|
-
path: TPath
|
|
214
|
-
component: RouteComponent
|
|
215
|
-
/** Optional route name for named navigation */
|
|
216
|
-
name?: string
|
|
217
|
-
/** Metadata attached to this route */
|
|
218
|
-
meta?: RouteMeta
|
|
219
|
-
/**
|
|
220
|
-
* Redirect target. Evaluated before guards.
|
|
221
|
-
* String: redirect to that path.
|
|
222
|
-
* Function: called with the resolved route, return path string.
|
|
223
|
-
*/
|
|
224
|
-
redirect?: string | ((to: ResolvedRoute) => string)
|
|
225
|
-
/** Guard(s) run only for this route, before global beforeEach guards */
|
|
226
|
-
beforeEnter?: NavigationGuard | NavigationGuard[]
|
|
227
|
-
/** Guard(s) run before leaving this route. Return false to cancel. */
|
|
228
|
-
beforeLeave?: NavigationGuard | NavigationGuard[]
|
|
229
|
-
/**
|
|
230
|
-
* Alternative path(s) for this route. Alias paths render the same component
|
|
231
|
-
* and share guards, loaders, and metadata with the primary path.
|
|
232
|
-
*
|
|
233
|
-
* @example
|
|
234
|
-
* { path: "/user/:id", alias: ["/profile/:id"], component: UserPage }
|
|
235
|
-
*/
|
|
236
|
-
alias?: string | string[]
|
|
237
|
-
/** Child routes rendered inside this route's component via <RouterView /> */
|
|
238
|
-
children?: RouteRecord[]
|
|
239
|
-
/**
|
|
240
|
-
* Data loader — runs before navigation commits, in parallel with sibling loaders.
|
|
241
|
-
* The result is accessible via `useLoaderData()` inside the route component.
|
|
242
|
-
* Receives an AbortSignal that fires if a newer navigation supersedes this one.
|
|
243
|
-
*/
|
|
244
|
-
loader?: RouteLoaderFn
|
|
245
|
-
/**
|
|
246
|
-
* When true, the router shows cached loader data immediately (stale) and
|
|
247
|
-
* revalidates in the background. The component re-renders once fresh data arrives.
|
|
248
|
-
* Only applies when navigating to a route that already has cached loader data.
|
|
249
|
-
*/
|
|
250
|
-
staleWhileRevalidate?: boolean
|
|
251
|
-
/**
|
|
252
|
-
* Cache key function for loader data. Returns a string key derived from
|
|
253
|
-
* route params/query. When the key matches cached data, the loader is
|
|
254
|
-
* skipped (cache hit). Default: `path + JSON.stringify(params)`.
|
|
255
|
-
*
|
|
256
|
-
* @example
|
|
257
|
-
* ```ts
|
|
258
|
-
* loaderKey: ({ params }) => `user-${params.id}`
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
loaderKey?: (ctx: Pick<LoaderContext, 'params' | 'query'>) => string
|
|
262
|
-
/**
|
|
263
|
-
* Time in ms to keep cached loader data before garbage collection.
|
|
264
|
-
* Default: 300000 (5 minutes). Set to 0 to disable caching.
|
|
265
|
-
* Stale data is still served immediately if `staleWhileRevalidate` is true.
|
|
266
|
-
*/
|
|
267
|
-
gcTime?: number
|
|
268
|
-
/** Component rendered when this route's loader throws an error */
|
|
269
|
-
errorComponent?: ComponentFn
|
|
270
|
-
/**
|
|
271
|
-
* Component rendered when a URL doesn't match any descendant route under
|
|
272
|
-
* this record's path. Acts as a "404 within layout" — the matched chain
|
|
273
|
-
* is `[...ancestors, this, syntheticLeaf]` so the not-found component
|
|
274
|
-
* renders INSIDE this layout's chrome. fs-router attaches this when it
|
|
275
|
-
* detects a `_404.tsx` / `_not-found.tsx` file under this layout.
|
|
276
|
-
*/
|
|
277
|
-
notFoundComponent?: ComponentFn
|
|
278
|
-
/**
|
|
279
|
-
* Component rendered while this route's loader is running.
|
|
280
|
-
* Only shown after `pendingMs` (default: 0) to avoid flash on fast loads.
|
|
281
|
-
* Once shown, displayed for at least `pendingMinMs` (default: 200) to avoid flicker.
|
|
282
|
-
*/
|
|
283
|
-
pendingComponent?: ComponentFn
|
|
284
|
-
/** Delay in ms before showing pendingComponent (default: 0). Prevents flash on fast loaders. */
|
|
285
|
-
pendingMs?: number
|
|
286
|
-
/** Minimum display time in ms for pendingComponent once shown (default: 200). Prevents flicker. */
|
|
287
|
-
pendingMinMs?: number
|
|
288
|
-
/**
|
|
289
|
-
* Validate and transform raw query string parameters into typed values.
|
|
290
|
-
* Receives the raw `Record<string, string>` from the URL and returns
|
|
291
|
-
* a typed object. The validated result is available via `useValidatedSearch()`.
|
|
292
|
-
*
|
|
293
|
-
* Accepts any function — use Zod `.parse`, Valibot, or a plain function:
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* ```ts
|
|
297
|
-
* // Plain function:
|
|
298
|
-
* validateSearch: (raw) => ({
|
|
299
|
-
* page: Number(raw.page) || 1,
|
|
300
|
-
* q: raw.q ?? '',
|
|
301
|
-
* })
|
|
302
|
-
*
|
|
303
|
-
* // With Zod:
|
|
304
|
-
* validateSearch: z.object({
|
|
305
|
-
* page: z.coerce.number().default(1),
|
|
306
|
-
* q: z.string().default(''),
|
|
307
|
-
* }).parse
|
|
308
|
-
* ```
|
|
309
|
-
*/
|
|
310
|
-
validateSearch?: (raw: Record<string, string>) => Record<string, unknown>
|
|
311
|
-
/** Per-route middleware — runs before guards, can accumulate context data. */
|
|
312
|
-
middleware?: RouteMiddleware | RouteMiddleware[]
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ─── Router options ───────────────────────────────────────────────────────────
|
|
316
|
-
|
|
317
|
-
export type ScrollBehaviorFn = (
|
|
318
|
-
to: ResolvedRoute,
|
|
319
|
-
from: ResolvedRoute,
|
|
320
|
-
savedPosition: number | null,
|
|
321
|
-
) => 'top' | 'restore' | 'none' | number
|
|
322
|
-
|
|
323
|
-
export interface RouterOptions {
|
|
324
|
-
routes: RouteRecord[]
|
|
325
|
-
/** "hash" (default) uses location.hash; "history" uses pushState */
|
|
326
|
-
mode?: 'hash' | 'history'
|
|
327
|
-
/**
|
|
328
|
-
* Base path for the application. Used when deploying to a sub-path
|
|
329
|
-
* (e.g. `"/app"` for `https://example.com/app/`).
|
|
330
|
-
* Only applies in history mode. Must start with `/`.
|
|
331
|
-
* Default: `""` (no base path).
|
|
332
|
-
*/
|
|
333
|
-
base?: string
|
|
334
|
-
/**
|
|
335
|
-
* Global scroll behavior. Per-route meta.scrollBehavior takes precedence.
|
|
336
|
-
* Default: "top"
|
|
337
|
-
*/
|
|
338
|
-
scrollBehavior?: ScrollBehaviorFn | 'top' | 'restore' | 'none'
|
|
339
|
-
/**
|
|
340
|
-
* Initial URL for SSR. On the server, window.location is unavailable;
|
|
341
|
-
* pass the request URL here so the router resolves the correct route.
|
|
342
|
-
*
|
|
343
|
-
* @example
|
|
344
|
-
* // In your SSR handler:
|
|
345
|
-
* const router = createRouter({ routes, url: req.url })
|
|
346
|
-
*/
|
|
347
|
-
url?: string
|
|
348
|
-
/**
|
|
349
|
-
* Called when a route loader throws. If not provided, errors are logged
|
|
350
|
-
* and the navigation continues with `undefined` data for the failed loader.
|
|
351
|
-
* Return `false` to cancel the navigation.
|
|
352
|
-
*/
|
|
353
|
-
onError?: (err: unknown, route: ResolvedRoute) => undefined | false
|
|
354
|
-
/**
|
|
355
|
-
* Maximum number of resolved lazy components to cache.
|
|
356
|
-
* When exceeded, the oldest entry is evicted.
|
|
357
|
-
* Default: 100.
|
|
358
|
-
*/
|
|
359
|
-
maxCacheSize?: number
|
|
360
|
-
/**
|
|
361
|
-
* Trailing slash handling:
|
|
362
|
-
* - `"strip"` — removes trailing slashes before matching (default)
|
|
363
|
-
* - `"add"` — ensures paths always end with `/`
|
|
364
|
-
* - `"ignore"` — no normalization
|
|
365
|
-
*/
|
|
366
|
-
trailingSlash?: 'strip' | 'add' | 'ignore'
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ─── Router interface ─────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Router interface. Parameterized by route name union for type-safe named navigation.
|
|
373
|
-
*
|
|
374
|
-
* @example
|
|
375
|
-
* ```ts
|
|
376
|
-
* type MyRoutes = 'home' | 'user' | 'settings'
|
|
377
|
-
* const router: Router<MyRoutes> = createRouter({ routes })
|
|
378
|
-
* router.push({ name: 'user', params: { id: '42' } }) // ✓
|
|
379
|
-
* router.push({ name: 'typo' }) // TS error
|
|
380
|
-
* ```
|
|
381
|
-
*/
|
|
382
|
-
export interface Router<TNames extends string = string> {
|
|
383
|
-
/** Navigate to a path */
|
|
384
|
-
push(path: string): Promise<void>
|
|
385
|
-
/** Navigate to a named route */
|
|
386
|
-
push(location: {
|
|
387
|
-
name: TNames
|
|
388
|
-
params?: Record<string, string>
|
|
389
|
-
query?: Record<string, string>
|
|
390
|
-
}): Promise<void>
|
|
391
|
-
/** Replace current history entry */
|
|
392
|
-
replace(path: string): Promise<void>
|
|
393
|
-
/** Replace current history entry using a named route */
|
|
394
|
-
replace(location: {
|
|
395
|
-
name: TNames
|
|
396
|
-
params?: Record<string, string>
|
|
397
|
-
query?: Record<string, string>
|
|
398
|
-
}): Promise<void>
|
|
399
|
-
/** Go back one step in history */
|
|
400
|
-
back(): void
|
|
401
|
-
/** Go forward one step in history */
|
|
402
|
-
forward(): void
|
|
403
|
-
/** Navigate forward or backward by `delta` steps in the history stack */
|
|
404
|
-
go(delta: number): void
|
|
405
|
-
/** Register a global before-navigation guard. Returns an unregister function. */
|
|
406
|
-
beforeEach(guard: NavigationGuard): () => void
|
|
407
|
-
/** Register a global after-navigation hook. Returns an unregister function. */
|
|
408
|
-
afterEach(hook: AfterEachHook): () => void
|
|
409
|
-
/** Current resolved route (reactive signal) */
|
|
410
|
-
readonly currentRoute: () => ResolvedRoute
|
|
411
|
-
/** True while a navigation (guards + loaders) is in flight */
|
|
412
|
-
readonly loading: () => boolean
|
|
413
|
-
/**
|
|
414
|
-
* Promise that resolves once the initial navigation is complete.
|
|
415
|
-
* Useful for SSR and for delaying rendering until the first route is resolved.
|
|
416
|
-
*/
|
|
417
|
-
isReady(): Promise<void>
|
|
418
|
-
/**
|
|
419
|
-
* Resolve `path` and prepare everything needed to render it: load any lazy
|
|
420
|
-
* route components into the router's cache and run the matched routes'
|
|
421
|
-
* loaders. After this resolves, a `RouterView` rendered against this router
|
|
422
|
-
* for `path` will produce final HTML synchronously — no loading fallbacks,
|
|
423
|
-
* no `useLoaderData()` returning `undefined`.
|
|
424
|
-
*
|
|
425
|
-
* Used by SSR/SSG to hydrate the route tree before `renderToString`.
|
|
426
|
-
* The router's `currentRoute` is NOT changed by `preload` — pass the path
|
|
427
|
-
* separately when creating the router (`createRouter({ url, ... })`) or
|
|
428
|
-
* call this for the same `url` you initialised the router with.
|
|
429
|
-
*/
|
|
430
|
-
preload(
|
|
431
|
-
path: string,
|
|
432
|
-
request?: Request,
|
|
433
|
-
options?: { skipLoaders?: boolean },
|
|
434
|
-
): Promise<void>
|
|
435
|
-
/**
|
|
436
|
-
* Invalidate cached loader data. Forces loaders to re-run on next navigation.
|
|
437
|
-
* - No args: invalidate ALL cached loader data
|
|
438
|
-
* - String: invalidate by cache key (as returned by `loaderKey`)
|
|
439
|
-
* - Function: invalidate entries where the predicate returns true
|
|
440
|
-
*/
|
|
441
|
-
invalidateLoader(keyOrPredicate?: string | ((key: string) => boolean)): void
|
|
442
|
-
/** Remove all event listeners, clear caches, and abort in-flight navigations. */
|
|
443
|
-
destroy(): void
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// ─── Internal router instance ─────────────────────────────────────────────────
|
|
447
|
-
|
|
448
|
-
import type { Computed, Signal } from '@pyreon/reactivity'
|
|
449
|
-
|
|
450
|
-
export interface RouterInstance extends Router {
|
|
451
|
-
routes: RouteRecord[]
|
|
452
|
-
mode: 'hash' | 'history'
|
|
453
|
-
/** Normalized base path (e.g. "/app"), empty string if none */
|
|
454
|
-
_base: string
|
|
455
|
-
_currentPath: Signal<string>
|
|
456
|
-
_currentRoute: Computed<ResolvedRoute>
|
|
457
|
-
_componentCache: Map<RouteRecord, ComponentFn>
|
|
458
|
-
_loadingSignal: Signal<number>
|
|
459
|
-
_resolve(rawPath: string): ResolvedRoute
|
|
460
|
-
_scrollPositions: Map<string, number>
|
|
461
|
-
_scrollBehavior: RouterOptions['scrollBehavior']
|
|
462
|
-
_onError: RouterOptions['onError']
|
|
463
|
-
_maxCacheSize: number
|
|
464
|
-
/**
|
|
465
|
-
* Current RouterView nesting depth. Incremented by each RouterView as it
|
|
466
|
-
* mounts (in tree order = depth-first), so each view knows which level of
|
|
467
|
-
* `matched[]` to render. Reset to 0 by RouterProvider.
|
|
468
|
-
*/
|
|
469
|
-
_viewDepth: number
|
|
470
|
-
/** Route records whose lazy chunk permanently failed (all retries exhausted) */
|
|
471
|
-
_erroredChunks: Set<RouteRecord>
|
|
472
|
-
/** Loader data keyed by route record — populated before each navigation commits */
|
|
473
|
-
_loaderData: Map<RouteRecord, unknown>
|
|
474
|
-
/** AbortController for the in-flight loader batch — aborted when a newer navigation starts */
|
|
475
|
-
_abortController: AbortController | null
|
|
476
|
-
/** Registered navigation blockers */
|
|
477
|
-
_blockers: Set<BlockerFn>
|
|
478
|
-
/** Resolves the isReady() promise after initial navigation completes */
|
|
479
|
-
_readyResolve: (() => void) | null
|
|
480
|
-
/** The isReady() promise instance */
|
|
481
|
-
_readyPromise: Promise<void>
|
|
482
|
-
/** Timestamp when the current navigation started — used for pendingMs timing */
|
|
483
|
-
_navigationStartTime: number
|
|
484
|
-
/** Key-based loader cache: cacheKey → { data, timestamp } */
|
|
485
|
-
_loaderCache: Map<string, { data: unknown; timestamp: number }>
|
|
486
|
-
/**
|
|
487
|
-
* In-flight loader dedup: cacheKey → { promise, signal }.
|
|
488
|
-
* Tracking the signal lets dedup skip an in-flight entry whose signal is
|
|
489
|
-
* already aborted — otherwise nav-2 would inherit nav-1's aborted promise
|
|
490
|
-
* (`router.push` aborts the previous nav's controller before starting the
|
|
491
|
-
* next, so back-to-back nav to the same path could resolve nav-2 against
|
|
492
|
-
* nav-1's aborted fetch).
|
|
493
|
-
*/
|
|
494
|
-
_loaderInflight: Map<string, { promise: Promise<unknown>; signal: AbortSignal }>
|
|
495
|
-
/**
|
|
496
|
-
* Dev-only HMR coordinator. Given a hot-updated module's id and the FRESH
|
|
497
|
-
* module namespace Vite handed `import.meta.hot.accept`, swaps the new
|
|
498
|
-
* component into every matched record whose lazy `_hmrId` equals `id`,
|
|
499
|
-
* then bumps `_loadingSignal` so `RouterView` re-renders ONLY that subtree
|
|
500
|
-
* in place — no page reload, so `__pyreon_hmr_registry__` (module-scope
|
|
501
|
-
* signal values) survives and `__hmr_signal` restores them.
|
|
502
|
-
*
|
|
503
|
-
* Using the namespace Vite passed (not a re-run of the lazy thunk)
|
|
504
|
-
* sidesteps the stale-`?t=` trap: the dynamic-import thunk lives in the
|
|
505
|
-
* virtual routes module, which is NOT invalidated when a leaf route
|
|
506
|
-
* self-accepts, so re-importing it would return the OLD module.
|
|
507
|
-
*
|
|
508
|
-
* Returns `true` when at least one matched component was swapped. `false`
|
|
509
|
-
* tells `@pyreon/vite-plugin`'s accept handler the edit was outside the
|
|
510
|
-
* active route tree (a nested non-route component, an unrelated route, a
|
|
511
|
-
* signal-only module) so it falls back to `import.meta.hot.invalidate()`
|
|
512
|
-
* → an automatic full reload (no manual refresh either way).
|
|
513
|
-
*
|
|
514
|
-
* Present only when the router is created in a dev browser context.
|
|
515
|
-
*/
|
|
516
|
-
_hmrSwap?: (id: string, mod: unknown) => boolean
|
|
517
|
-
}
|