@pyreon/router 0.5.6 → 0.5.7
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/lib/types/index.d.ts +413 -1068
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/types/index.d.ts
CHANGED
|
@@ -1,1090 +1,435 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as _pyreon_core0 from "@pyreon/core";
|
|
2
|
+
import { ComponentFn, ComponentFn as ComponentFn$1, Props, VNode, VNodeChild } from "@pyreon/core";
|
|
3
|
+
import { Computed, Signal } from "@pyreon/reactivity";
|
|
3
4
|
|
|
4
|
-
//#region src/
|
|
5
|
+
//#region src/types.d.ts
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* function Users() {
|
|
18
|
-
* const users = useLoaderData<User[]>()
|
|
19
|
-
* return h("ul", null, users.map(u => h("li", null, u.name)))
|
|
20
|
-
* }
|
|
21
|
-
*/
|
|
22
|
-
function useLoaderData() {
|
|
23
|
-
return useContext(LoaderDataContext);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* SSR helper: pre-run all loaders for the given path before rendering.
|
|
27
|
-
* Call this before `renderToString` so route components can read data via `useLoaderData()`.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* const router = createRouter({ routes, url: req.url })
|
|
31
|
-
* await prefetchLoaderData(router, req.url)
|
|
32
|
-
* const html = await renderToString(h(App, { router }))
|
|
33
|
-
*/
|
|
34
|
-
async function prefetchLoaderData(router, path) {
|
|
35
|
-
const route = router._resolve(path);
|
|
36
|
-
const ac = new AbortController();
|
|
37
|
-
router._abortController = ac;
|
|
38
|
-
await Promise.all(route.matched.filter(r => r.loader).map(async r => {
|
|
39
|
-
const data = await r.loader?.({
|
|
40
|
-
params: route.params,
|
|
41
|
-
query: route.query,
|
|
42
|
-
signal: ac.signal
|
|
43
|
-
});
|
|
44
|
-
router._loaderData.set(r, data);
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
7
|
+
* Extracts typed params from a path string at compile time.
|
|
8
|
+
* Supports optional params via `:param?` — their type is `string | undefined`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ExtractParams<'/user/:id/posts/:postId'>
|
|
12
|
+
* // → { id: string; postId: string }
|
|
13
|
+
*
|
|
14
|
+
* ExtractParams<'/user/:id?'>
|
|
15
|
+
* // → { id?: string | undefined }
|
|
16
|
+
*/
|
|
17
|
+
type ExtractParams<T extends string> = 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<never, never>;
|
|
47
18
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
19
|
+
* Route metadata interface. Extend it via module augmentation to add custom fields:
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // globals.d.ts
|
|
23
|
+
* declare module "@pyreon/router" {
|
|
24
|
+
* interface RouteMeta {
|
|
25
|
+
* requiresRole?: "admin" | "user"
|
|
26
|
+
* pageTitle?: string
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
interface RouteMeta {
|
|
31
|
+
/** Sets document.title on navigation */
|
|
32
|
+
title?: string;
|
|
33
|
+
/** Page description (for meta tags) */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** If true, guards can redirect to login */
|
|
36
|
+
requiresAuth?: boolean;
|
|
37
|
+
/** Scroll behavior for this route */
|
|
38
|
+
scrollBehavior?: "top" | "restore" | "none";
|
|
39
|
+
}
|
|
40
|
+
interface ResolvedRoute<P extends Record<string, string | undefined> = Record<string, string>, Q extends Record<string, string> = Record<string, string>> {
|
|
41
|
+
path: string;
|
|
42
|
+
params: P;
|
|
43
|
+
query: Q;
|
|
44
|
+
hash: string;
|
|
45
|
+
/** All matched records from root to leaf (one per nesting level) */
|
|
46
|
+
matched: RouteRecord[];
|
|
47
|
+
meta: RouteMeta;
|
|
48
|
+
}
|
|
49
|
+
declare const LAZY_SYMBOL: unique symbol;
|
|
50
|
+
interface LazyComponent {
|
|
51
|
+
readonly [LAZY_SYMBOL]: true;
|
|
52
|
+
readonly loader: () => Promise<ComponentFn$1 | {
|
|
53
|
+
default: ComponentFn$1;
|
|
54
|
+
}>;
|
|
55
|
+
/** Optional component shown while the lazy chunk is loading */
|
|
56
|
+
readonly loadingComponent?: ComponentFn$1;
|
|
57
|
+
/** Optional component shown after all retries have failed */
|
|
58
|
+
readonly errorComponent?: ComponentFn$1;
|
|
59
|
+
}
|
|
60
|
+
declare function lazy(loader: () => Promise<ComponentFn$1 | {
|
|
61
|
+
default: ComponentFn$1;
|
|
62
|
+
}>, options?: {
|
|
63
|
+
loading?: ComponentFn$1;
|
|
64
|
+
error?: ComponentFn$1;
|
|
65
|
+
}): LazyComponent;
|
|
66
|
+
type RouteComponent = ComponentFn$1 | LazyComponent;
|
|
67
|
+
type NavigationGuardResult = boolean | string | undefined;
|
|
68
|
+
type NavigationGuard = (to: ResolvedRoute, from: ResolvedRoute) => NavigationGuardResult | Promise<NavigationGuardResult>;
|
|
69
|
+
type AfterEachHook = (to: ResolvedRoute, from: ResolvedRoute) => void;
|
|
63
70
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
* Called before each navigation. Return `true` to block, `false` to allow.
|
|
72
|
+
* Async blockers are supported (e.g. to show a confirmation dialog).
|
|
73
|
+
*/
|
|
74
|
+
type BlockerFn = (to: ResolvedRoute, from: ResolvedRoute) => boolean | Promise<boolean>;
|
|
75
|
+
interface Blocker {
|
|
76
|
+
/** Unregister this blocker so future navigations proceed freely. */
|
|
77
|
+
remove(): void;
|
|
78
|
+
}
|
|
79
|
+
interface LoaderContext {
|
|
80
|
+
params: Record<string, string>;
|
|
81
|
+
query: Record<string, string>;
|
|
82
|
+
/** Aborted when a newer navigation supersedes this one */
|
|
83
|
+
signal: AbortSignal;
|
|
84
|
+
}
|
|
85
|
+
type RouteLoaderFn = (ctx: LoaderContext) => Promise<unknown>;
|
|
86
|
+
interface RouteRecord<TPath extends string = string> {
|
|
87
|
+
/** Path pattern — supports `:param` segments and `(.*)` wildcard */
|
|
88
|
+
path: TPath;
|
|
89
|
+
component: RouteComponent;
|
|
90
|
+
/** Optional route name for named navigation */
|
|
91
|
+
name?: string;
|
|
92
|
+
/** Metadata attached to this route */
|
|
93
|
+
meta?: RouteMeta;
|
|
94
|
+
/**
|
|
95
|
+
* Redirect target. Evaluated before guards.
|
|
96
|
+
* String: redirect to that path.
|
|
97
|
+
* Function: called with the resolved route, return path string.
|
|
98
|
+
*/
|
|
99
|
+
redirect?: string | ((to: ResolvedRoute) => string);
|
|
100
|
+
/** Guard(s) run only for this route, before global beforeEach guards */
|
|
101
|
+
beforeEnter?: NavigationGuard | NavigationGuard[];
|
|
102
|
+
/** Guard(s) run before leaving this route. Return false to cancel. */
|
|
103
|
+
beforeLeave?: NavigationGuard | NavigationGuard[];
|
|
104
|
+
/**
|
|
105
|
+
* Alternative path(s) for this route. Alias paths render the same component
|
|
106
|
+
* and share guards, loaders, and metadata with the primary path.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* { path: "/user/:id", alias: ["/profile/:id"], component: UserPage }
|
|
110
|
+
*/
|
|
111
|
+
alias?: string | string[];
|
|
112
|
+
/** Child routes rendered inside this route's component via <RouterView /> */
|
|
113
|
+
children?: RouteRecord[];
|
|
114
|
+
/**
|
|
115
|
+
* Data loader — runs before navigation commits, in parallel with sibling loaders.
|
|
116
|
+
* The result is accessible via `useLoaderData()` inside the route component.
|
|
117
|
+
* Receives an AbortSignal that fires if a newer navigation supersedes this one.
|
|
118
|
+
*/
|
|
119
|
+
loader?: RouteLoaderFn;
|
|
120
|
+
/**
|
|
121
|
+
* When true, the router shows cached loader data immediately (stale) and
|
|
122
|
+
* revalidates in the background. The component re-renders once fresh data arrives.
|
|
123
|
+
* Only applies when navigating to a route that already has cached loader data.
|
|
124
|
+
*/
|
|
125
|
+
staleWhileRevalidate?: boolean;
|
|
126
|
+
/** Component rendered when this route's loader throws an error */
|
|
127
|
+
errorComponent?: ComponentFn$1;
|
|
128
|
+
}
|
|
129
|
+
type ScrollBehaviorFn = (to: ResolvedRoute, from: ResolvedRoute, savedPosition: number | null) => "top" | "restore" | "none" | number;
|
|
130
|
+
interface RouterOptions {
|
|
131
|
+
routes: RouteRecord[];
|
|
132
|
+
/** "hash" (default) uses location.hash; "history" uses pushState */
|
|
133
|
+
mode?: "hash" | "history";
|
|
134
|
+
/**
|
|
135
|
+
* Base path for the application. Used when deploying to a sub-path
|
|
136
|
+
* (e.g. `"/app"` for `https://example.com/app/`).
|
|
137
|
+
* Only applies in history mode. Must start with `/`.
|
|
138
|
+
* Default: `""` (no base path).
|
|
139
|
+
*/
|
|
140
|
+
base?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Global scroll behavior. Per-route meta.scrollBehavior takes precedence.
|
|
143
|
+
* Default: "top"
|
|
144
|
+
*/
|
|
145
|
+
scrollBehavior?: ScrollBehaviorFn | "top" | "restore" | "none";
|
|
146
|
+
/**
|
|
147
|
+
* Initial URL for SSR. On the server, window.location is unavailable;
|
|
148
|
+
* pass the request URL here so the router resolves the correct route.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* // In your SSR handler:
|
|
152
|
+
* const router = createRouter({ routes, url: req.url })
|
|
153
|
+
*/
|
|
154
|
+
url?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Called when a route loader throws. If not provided, errors are logged
|
|
157
|
+
* and the navigation continues with `undefined` data for the failed loader.
|
|
158
|
+
* Return `false` to cancel the navigation.
|
|
159
|
+
*/
|
|
160
|
+
onError?: (err: unknown, route: ResolvedRoute) => undefined | false;
|
|
161
|
+
/**
|
|
162
|
+
* Maximum number of resolved lazy components to cache.
|
|
163
|
+
* When exceeded, the oldest entry is evicted.
|
|
164
|
+
* Default: 100.
|
|
165
|
+
*/
|
|
166
|
+
maxCacheSize?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Trailing slash handling:
|
|
169
|
+
* - `"strip"` — removes trailing slashes before matching (default)
|
|
170
|
+
* - `"add"` — ensures paths always end with `/`
|
|
171
|
+
* - `"ignore"` — no normalization
|
|
172
|
+
*/
|
|
173
|
+
trailingSlash?: "strip" | "add" | "ignore";
|
|
174
|
+
}
|
|
175
|
+
interface Router {
|
|
176
|
+
/** Navigate to a path */
|
|
177
|
+
push(path: string): Promise<void>;
|
|
178
|
+
/** Navigate to a path by name */
|
|
179
|
+
push(location: {
|
|
180
|
+
name: string;
|
|
181
|
+
params?: Record<string, string>;
|
|
182
|
+
query?: Record<string, string>;
|
|
183
|
+
}): Promise<void>;
|
|
184
|
+
/** Replace current history entry */
|
|
185
|
+
replace(path: string): Promise<void>;
|
|
186
|
+
/** Replace current history entry using a named route */
|
|
187
|
+
replace(location: {
|
|
188
|
+
name: string;
|
|
189
|
+
params?: Record<string, string>;
|
|
190
|
+
query?: Record<string, string>;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
/** Go back one step in history */
|
|
193
|
+
back(): void;
|
|
194
|
+
/** Go forward one step in history */
|
|
195
|
+
forward(): void;
|
|
196
|
+
/** Navigate forward or backward by `delta` steps in the history stack */
|
|
197
|
+
go(delta: number): void;
|
|
198
|
+
/** Register a global before-navigation guard. Returns an unregister function. */
|
|
199
|
+
beforeEach(guard: NavigationGuard): () => void;
|
|
200
|
+
/** Register a global after-navigation hook. Returns an unregister function. */
|
|
201
|
+
afterEach(hook: AfterEachHook): () => void;
|
|
202
|
+
/** Current resolved route (reactive signal) */
|
|
203
|
+
readonly currentRoute: () => ResolvedRoute;
|
|
204
|
+
/** True while a navigation (guards + loaders) is in flight */
|
|
205
|
+
readonly loading: () => boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Promise that resolves once the initial navigation is complete.
|
|
208
|
+
* Useful for SSR and for delaying rendering until the first route is resolved.
|
|
209
|
+
*/
|
|
210
|
+
isReady(): Promise<void>;
|
|
211
|
+
/** Remove all event listeners, clear caches, and abort in-flight navigations. */
|
|
212
|
+
destroy(): void;
|
|
213
|
+
}
|
|
214
|
+
interface RouterInstance extends Router {
|
|
215
|
+
routes: RouteRecord[];
|
|
216
|
+
mode: "hash" | "history";
|
|
217
|
+
/** Normalized base path (e.g. "/app"), empty string if none */
|
|
218
|
+
_base: string;
|
|
219
|
+
_currentPath: Signal<string>;
|
|
220
|
+
_currentRoute: Computed<ResolvedRoute>;
|
|
221
|
+
_componentCache: Map<RouteRecord, ComponentFn$1>;
|
|
222
|
+
_loadingSignal: Signal<number>;
|
|
223
|
+
_resolve(rawPath: string): ResolvedRoute;
|
|
224
|
+
_scrollPositions: Map<string, number>;
|
|
225
|
+
_scrollBehavior: RouterOptions["scrollBehavior"];
|
|
226
|
+
_onError: RouterOptions["onError"];
|
|
227
|
+
_maxCacheSize: number;
|
|
228
|
+
/**
|
|
229
|
+
* Current RouterView nesting depth. Incremented by each RouterView as it
|
|
230
|
+
* mounts (in tree order = depth-first), so each view knows which level of
|
|
231
|
+
* `matched[]` to render. Reset to 0 by RouterProvider.
|
|
232
|
+
*/
|
|
233
|
+
_viewDepth: number;
|
|
234
|
+
/** Route records whose lazy chunk permanently failed (all retries exhausted) */
|
|
235
|
+
_erroredChunks: Set<RouteRecord>;
|
|
236
|
+
/** Loader data keyed by route record — populated before each navigation commits */
|
|
237
|
+
_loaderData: Map<RouteRecord, unknown>;
|
|
238
|
+
/** AbortController for the in-flight loader batch — aborted when a newer navigation starts */
|
|
239
|
+
_abortController: AbortController | null;
|
|
240
|
+
/** Registered navigation blockers */
|
|
241
|
+
_blockers: Set<BlockerFn>;
|
|
242
|
+
/** Resolves the isReady() promise after initial navigation completes */
|
|
243
|
+
_readyResolve: (() => void) | null;
|
|
244
|
+
/** The isReady() promise instance */
|
|
245
|
+
_readyPromise: Promise<void>;
|
|
80
246
|
}
|
|
81
|
-
|
|
82
247
|
//#endregion
|
|
83
|
-
//#region src/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
function parseQuery(qs) {
|
|
89
|
-
if (!qs) return {};
|
|
90
|
-
const result = {};
|
|
91
|
-
for (const part of qs.split("&")) {
|
|
92
|
-
const eqIdx = part.indexOf("=");
|
|
93
|
-
if (eqIdx < 0) {
|
|
94
|
-
const key = decodeURIComponent(part);
|
|
95
|
-
if (key) result[key] = "";
|
|
96
|
-
} else {
|
|
97
|
-
const key = decodeURIComponent(part.slice(0, eqIdx));
|
|
98
|
-
const val = decodeURIComponent(part.slice(eqIdx + 1));
|
|
99
|
-
if (key) result[key] = val;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Parse a query string preserving duplicate keys as arrays.
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* parseQueryMulti("color=red&color=blue&size=lg")
|
|
109
|
-
* // → { color: ["red", "blue"], size: "lg" }
|
|
110
|
-
*/
|
|
111
|
-
function parseQueryMulti(qs) {
|
|
112
|
-
if (!qs) return {};
|
|
113
|
-
const result = {};
|
|
114
|
-
for (const part of qs.split("&")) {
|
|
115
|
-
const eqIdx = part.indexOf("=");
|
|
116
|
-
let key;
|
|
117
|
-
let val;
|
|
118
|
-
if (eqIdx < 0) {
|
|
119
|
-
key = decodeURIComponent(part);
|
|
120
|
-
val = "";
|
|
121
|
-
} else {
|
|
122
|
-
key = decodeURIComponent(part.slice(0, eqIdx));
|
|
123
|
-
val = decodeURIComponent(part.slice(eqIdx + 1));
|
|
124
|
-
}
|
|
125
|
-
if (!key) continue;
|
|
126
|
-
const existing = result[key];
|
|
127
|
-
if (existing === void 0) result[key] = val;else if (Array.isArray(existing)) existing.push(val);else result[key] = [existing, val];
|
|
128
|
-
}
|
|
129
|
-
return result;
|
|
248
|
+
//#region src/components.d.ts
|
|
249
|
+
interface RouterProviderProps extends Props {
|
|
250
|
+
router: Router;
|
|
251
|
+
children?: VNode | VNodeChild | null;
|
|
130
252
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
/** WeakMap cache: compile each RouteRecord[] once */
|
|
137
|
-
|
|
138
|
-
function compileSegment(raw) {
|
|
139
|
-
if (raw.endsWith("*") && raw.startsWith(":")) return {
|
|
140
|
-
raw,
|
|
141
|
-
isParam: true,
|
|
142
|
-
isSplat: true,
|
|
143
|
-
isOptional: false,
|
|
144
|
-
paramName: raw.slice(1, -1)
|
|
145
|
-
};
|
|
146
|
-
if (raw.endsWith("?") && raw.startsWith(":")) return {
|
|
147
|
-
raw,
|
|
148
|
-
isParam: true,
|
|
149
|
-
isSplat: false,
|
|
150
|
-
isOptional: true,
|
|
151
|
-
paramName: raw.slice(1, -1)
|
|
152
|
-
};
|
|
153
|
-
if (raw.startsWith(":")) return {
|
|
154
|
-
raw,
|
|
155
|
-
isParam: true,
|
|
156
|
-
isSplat: false,
|
|
157
|
-
isOptional: false,
|
|
158
|
-
paramName: raw.slice(1)
|
|
159
|
-
};
|
|
160
|
-
return {
|
|
161
|
-
raw,
|
|
162
|
-
isParam: false,
|
|
163
|
-
isSplat: false,
|
|
164
|
-
isOptional: false,
|
|
165
|
-
paramName: ""
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
function compileRoute(route) {
|
|
169
|
-
const pattern = route.path;
|
|
170
|
-
if (pattern === "(.*)" || pattern === "*") return {
|
|
171
|
-
route,
|
|
172
|
-
isWildcard: true,
|
|
173
|
-
segments: [],
|
|
174
|
-
segmentCount: 0,
|
|
175
|
-
isStatic: false,
|
|
176
|
-
staticPath: null,
|
|
177
|
-
children: null,
|
|
178
|
-
firstSegment: null
|
|
179
|
-
};
|
|
180
|
-
const segments = pattern.split("/").filter(Boolean).map(compileSegment);
|
|
181
|
-
const isStatic = segments.every(s => !s.isParam);
|
|
182
|
-
const staticPath = isStatic ? `/${segments.map(s => s.raw).join("/")}` : null;
|
|
183
|
-
const first = segments.length > 0 ? segments[0] : void 0;
|
|
184
|
-
const firstSegment = first && !first.isParam ? first.raw : null;
|
|
185
|
-
return {
|
|
186
|
-
route,
|
|
187
|
-
isWildcard: false,
|
|
188
|
-
segments,
|
|
189
|
-
segmentCount: segments.length,
|
|
190
|
-
isStatic,
|
|
191
|
-
staticPath,
|
|
192
|
-
children: null,
|
|
193
|
-
firstSegment
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
/** Expand alias paths into additional compiled entries sharing the original RouteRecord */
|
|
197
|
-
function expandAliases(r, c) {
|
|
198
|
-
if (!r.alias) return [];
|
|
199
|
-
return (Array.isArray(r.alias) ? r.alias : [r.alias]).map(aliasPath => {
|
|
200
|
-
const {
|
|
201
|
-
alias: _,
|
|
202
|
-
...withoutAlias
|
|
203
|
-
} = r;
|
|
204
|
-
const ac = compileRoute({
|
|
205
|
-
...withoutAlias,
|
|
206
|
-
path: aliasPath
|
|
207
|
-
});
|
|
208
|
-
ac.children = c.children;
|
|
209
|
-
ac.route = r;
|
|
210
|
-
return ac;
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
function compileRoutes(routes) {
|
|
214
|
-
const cached = _compiledCache.get(routes);
|
|
215
|
-
if (cached) return cached;
|
|
216
|
-
const compiled = [];
|
|
217
|
-
for (const r of routes) {
|
|
218
|
-
const c = compileRoute(r);
|
|
219
|
-
if (r.children && r.children.length > 0) c.children = compileRoutes(r.children);
|
|
220
|
-
compiled.push(c);
|
|
221
|
-
compiled.push(...expandAliases(r, c));
|
|
222
|
-
}
|
|
223
|
-
_compiledCache.set(routes, compiled);
|
|
224
|
-
return compiled;
|
|
225
|
-
}
|
|
226
|
-
/** Extract first static segment from a segment list, or null if dynamic/empty */
|
|
227
|
-
function getFirstSegment(segments) {
|
|
228
|
-
const first = segments[0];
|
|
229
|
-
if (first && !first.isParam) return first.raw;
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
/** Build a FlattenedRoute from segments + metadata */
|
|
233
|
-
function makeFlatEntry(segments, chain, meta, isWildcard) {
|
|
234
|
-
const isStatic = !isWildcard && segments.every(s => !s.isParam);
|
|
235
|
-
const hasOptional = segments.some(s => s.isOptional);
|
|
236
|
-
let minSegs = segments.length;
|
|
237
|
-
if (hasOptional) while (minSegs > 0 && segments[minSegs - 1]?.isOptional) minSegs--;
|
|
238
|
-
return {
|
|
239
|
-
segments,
|
|
240
|
-
segmentCount: segments.length,
|
|
241
|
-
matchedChain: chain,
|
|
242
|
-
isStatic,
|
|
243
|
-
staticPath: isStatic ? `/${segments.map(s => s.raw).join("/")}` : null,
|
|
244
|
-
meta,
|
|
245
|
-
firstSegment: getFirstSegment(segments),
|
|
246
|
-
hasSplat: segments.some(s => s.isSplat),
|
|
247
|
-
isWildcard,
|
|
248
|
-
hasOptional,
|
|
249
|
-
minSegments: minSegs
|
|
250
|
-
};
|
|
253
|
+
declare const RouterProvider: ComponentFn<RouterProviderProps>;
|
|
254
|
+
interface RouterViewProps extends Props {
|
|
255
|
+
/** Explicitly pass a router (optional — uses the active router by default) */
|
|
256
|
+
router?: Router;
|
|
251
257
|
}
|
|
252
258
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
function buildRouteIndex(routes, compiled) {
|
|
297
|
-
const cached = _indexCache.get(routes);
|
|
298
|
-
if (cached) return cached;
|
|
299
|
-
const flattened = flattenRoutes(compiled);
|
|
300
|
-
const staticMap = /* @__PURE__ */new Map();
|
|
301
|
-
const segmentMap = /* @__PURE__ */new Map();
|
|
302
|
-
const dynamicFirst = [];
|
|
303
|
-
const wildcards = [];
|
|
304
|
-
for (const f of flattened) indexFlatRoute(f, staticMap, segmentMap, dynamicFirst, wildcards);
|
|
305
|
-
const index = {
|
|
306
|
-
staticMap,
|
|
307
|
-
segmentMap,
|
|
308
|
-
dynamicFirst,
|
|
309
|
-
wildcards
|
|
310
|
-
};
|
|
311
|
-
_indexCache.set(routes, index);
|
|
312
|
-
return index;
|
|
313
|
-
}
|
|
314
|
-
/** Split path into segments without allocating a filtered array */
|
|
315
|
-
function splitPath(path) {
|
|
316
|
-
if (path === "/") return [];
|
|
317
|
-
const start = path.charCodeAt(0) === 47 ? 1 : 0;
|
|
318
|
-
const end = path.length;
|
|
319
|
-
if (start >= end) return [];
|
|
320
|
-
const parts = [];
|
|
321
|
-
let segStart = start;
|
|
322
|
-
for (let i = start; i <= end; i++) if (i === end || path.charCodeAt(i) === 47) {
|
|
323
|
-
if (i > segStart) parts.push(path.substring(segStart, i));
|
|
324
|
-
segStart = i + 1;
|
|
325
|
-
}
|
|
326
|
-
return parts;
|
|
327
|
-
}
|
|
328
|
-
/** Decode only if the segment contains a `%` character */
|
|
329
|
-
function decodeSafe(s) {
|
|
330
|
-
return s.indexOf("%") >= 0 ? decodeURIComponent(s) : s;
|
|
331
|
-
}
|
|
332
|
-
/** Collect remaining path segments as a decoded splat value */
|
|
333
|
-
function captureSplat(pathParts, from, pathLen) {
|
|
334
|
-
const remaining = [];
|
|
335
|
-
for (let j = from; j < pathLen; j++) {
|
|
336
|
-
const p = pathParts[j];
|
|
337
|
-
if (p !== void 0) remaining.push(decodeSafe(p));
|
|
338
|
-
}
|
|
339
|
-
return remaining.join("/");
|
|
340
|
-
}
|
|
341
|
-
/** Check whether a flattened route's segment count is compatible with the path length */
|
|
342
|
-
function isSegmentCountCompatible(f, pathLen) {
|
|
343
|
-
if (f.segmentCount === pathLen) return true;
|
|
344
|
-
if (f.hasSplat && pathLen >= f.segmentCount) return true;
|
|
345
|
-
if (f.hasOptional && pathLen >= f.minSegments && pathLen <= f.segmentCount) return true;
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
/** Try to match a flattened route against path parts */
|
|
349
|
-
function matchFlattened(f, pathParts, pathLen) {
|
|
350
|
-
if (!isSegmentCountCompatible(f, pathLen)) return null;
|
|
351
|
-
const params = {};
|
|
352
|
-
const segments = f.segments;
|
|
353
|
-
const count = f.segmentCount;
|
|
354
|
-
for (let i = 0; i < count; i++) {
|
|
355
|
-
const seg = segments[i];
|
|
356
|
-
const pt = pathParts[i];
|
|
357
|
-
if (!seg) return null;
|
|
358
|
-
if (seg.isSplat) {
|
|
359
|
-
params[seg.paramName] = captureSplat(pathParts, i, pathLen);
|
|
360
|
-
return params;
|
|
361
|
-
}
|
|
362
|
-
if (pt === void 0) {
|
|
363
|
-
if (!seg.isOptional) return null;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (seg.isParam) params[seg.paramName] = decodeSafe(pt);else if (seg.raw !== pt) return null;
|
|
367
|
-
}
|
|
368
|
-
return params;
|
|
369
|
-
}
|
|
370
|
-
/** Search a list of flattened candidates for a match */
|
|
371
|
-
function searchCandidates(candidates, pathParts, pathLen) {
|
|
372
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
373
|
-
const f = candidates[i];
|
|
374
|
-
if (!f) continue;
|
|
375
|
-
const params = matchFlattened(f, pathParts, pathLen);
|
|
376
|
-
if (params) return {
|
|
377
|
-
params,
|
|
378
|
-
matched: f.matchedChain
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Resolve a raw path (including query string and hash) against the route tree.
|
|
385
|
-
* Uses flattened index for O(1) static lookup and first-segment dispatch.
|
|
386
|
-
*/
|
|
387
|
-
function resolveRoute(rawPath, routes) {
|
|
388
|
-
const qIdx = rawPath.indexOf("?");
|
|
389
|
-
const pathAndHash = qIdx >= 0 ? rawPath.slice(0, qIdx) : rawPath;
|
|
390
|
-
const queryPart = qIdx >= 0 ? rawPath.slice(qIdx + 1) : "";
|
|
391
|
-
const hIdx = pathAndHash.indexOf("#");
|
|
392
|
-
const cleanPath = hIdx >= 0 ? pathAndHash.slice(0, hIdx) : pathAndHash;
|
|
393
|
-
const hash = hIdx >= 0 ? pathAndHash.slice(hIdx + 1) : "";
|
|
394
|
-
const query = parseQuery(queryPart);
|
|
395
|
-
const index = buildRouteIndex(routes, compileRoutes(routes));
|
|
396
|
-
const staticMatch = index.staticMap.get(cleanPath);
|
|
397
|
-
if (staticMatch) return {
|
|
398
|
-
path: cleanPath,
|
|
399
|
-
params: {},
|
|
400
|
-
query,
|
|
401
|
-
hash,
|
|
402
|
-
matched: staticMatch.matchedChain,
|
|
403
|
-
meta: staticMatch.meta
|
|
404
|
-
};
|
|
405
|
-
const pathParts = splitPath(cleanPath);
|
|
406
|
-
const pathLen = pathParts.length;
|
|
407
|
-
if (pathLen > 0) {
|
|
408
|
-
const first = pathParts[0];
|
|
409
|
-
const bucket = index.segmentMap.get(first);
|
|
410
|
-
if (bucket) {
|
|
411
|
-
const match = searchCandidates(bucket, pathParts, pathLen);
|
|
412
|
-
if (match) return {
|
|
413
|
-
path: cleanPath,
|
|
414
|
-
params: match.params,
|
|
415
|
-
query,
|
|
416
|
-
hash,
|
|
417
|
-
matched: match.matched,
|
|
418
|
-
meta: mergeMeta(match.matched)
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
const dynMatch = searchCandidates(index.dynamicFirst, pathParts, pathLen);
|
|
423
|
-
if (dynMatch) return {
|
|
424
|
-
path: cleanPath,
|
|
425
|
-
params: dynMatch.params,
|
|
426
|
-
query,
|
|
427
|
-
hash,
|
|
428
|
-
matched: dynMatch.matched,
|
|
429
|
-
meta: mergeMeta(dynMatch.matched)
|
|
430
|
-
};
|
|
431
|
-
const w = index.wildcards[0];
|
|
432
|
-
if (w) return {
|
|
433
|
-
path: cleanPath,
|
|
434
|
-
params: {},
|
|
435
|
-
query,
|
|
436
|
-
hash,
|
|
437
|
-
matched: w.matchedChain,
|
|
438
|
-
meta: w.meta
|
|
439
|
-
};
|
|
440
|
-
return {
|
|
441
|
-
path: cleanPath,
|
|
442
|
-
params: {},
|
|
443
|
-
query,
|
|
444
|
-
hash,
|
|
445
|
-
matched: [],
|
|
446
|
-
meta: {}
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
/** Merge meta from matched routes (leaf takes precedence) */
|
|
450
|
-
function mergeMeta(matched) {
|
|
451
|
-
const meta = {};
|
|
452
|
-
for (const record of matched) if (record.meta) Object.assign(meta, record.meta);
|
|
453
|
-
return meta;
|
|
454
|
-
}
|
|
455
|
-
/** Build a path string from a named route's pattern and params */
|
|
456
|
-
function buildPath(pattern, params) {
|
|
457
|
-
return pattern.replace(/\/:([^/]+)\?/g, (_match, key) => {
|
|
458
|
-
const val = params[key];
|
|
459
|
-
if (!val) return "";
|
|
460
|
-
return `/${encodeURIComponent(val)}`;
|
|
461
|
-
}).replace(/:([^/]+)\*?/g, (match, key) => {
|
|
462
|
-
const val = params[key] ?? "";
|
|
463
|
-
if (match.endsWith("*")) return val.split("/").map(encodeURIComponent).join("/");
|
|
464
|
-
return encodeURIComponent(val);
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
/** Find a route record by name (recursive, O(n)). Prefer buildNameIndex for repeated lookups. */
|
|
468
|
-
function findRouteByName(name, routes) {
|
|
469
|
-
for (const route of routes) {
|
|
470
|
-
if (route.name === name) return route;
|
|
471
|
-
if (route.children) {
|
|
472
|
-
const found = findRouteByName(name, route.children);
|
|
473
|
-
if (found) return found;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Pre-build a name → RouteRecord index from a route tree for O(1) named navigation.
|
|
480
|
-
* Called once at router creation time; avoids O(n) depth-first search per push({ name }).
|
|
481
|
-
*/
|
|
482
|
-
function buildNameIndex(routes) {
|
|
483
|
-
const index = /* @__PURE__ */new Map();
|
|
484
|
-
function walk(list) {
|
|
485
|
-
for (const route of list) {
|
|
486
|
-
if (route.name) index.set(route.name, route);
|
|
487
|
-
if (route.children) walk(route.children);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
walk(routes);
|
|
491
|
-
return index;
|
|
492
|
-
}
|
|
493
|
-
|
|
259
|
+
* Renders the matched route component at this nesting level.
|
|
260
|
+
*
|
|
261
|
+
* Nested layouts work by placing a second `<RouterView />` inside the layout
|
|
262
|
+
* component — it automatically renders the next level of the matched route.
|
|
263
|
+
*
|
|
264
|
+
* How depth tracking works:
|
|
265
|
+
* Pyreon components run once in depth-first tree order. Each `RouterView`
|
|
266
|
+
* captures `router._viewDepth` at setup time and immediately increments it,
|
|
267
|
+
* so sibling and child views get the correct index. `onUnmount` decrements
|
|
268
|
+
* the counter so dynamic route swaps work correctly.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* // Route config:
|
|
272
|
+
* { path: "/admin", component: AdminLayout, children: [
|
|
273
|
+
* { path: "users", component: AdminUsers },
|
|
274
|
+
* ]}
|
|
275
|
+
*
|
|
276
|
+
* // AdminLayout renders a nested RouterView:
|
|
277
|
+
* function AdminLayout() {
|
|
278
|
+
* return <div><Sidebar /><RouterView /></div>
|
|
279
|
+
* }
|
|
280
|
+
*/
|
|
281
|
+
declare const RouterView: ComponentFn<RouterViewProps>;
|
|
282
|
+
interface RouterLinkProps extends Props {
|
|
283
|
+
to: string;
|
|
284
|
+
/** If true, uses router.replace() instead of router.push() */
|
|
285
|
+
replace?: boolean;
|
|
286
|
+
/** CSS class applied when this link is active (default: "router-link-active") */
|
|
287
|
+
activeClass?: string;
|
|
288
|
+
/** CSS class for exact-match active state (default: "router-link-exact-active") */
|
|
289
|
+
exactActiveClass?: string;
|
|
290
|
+
/** If true, only applies activeClass on exact match */
|
|
291
|
+
exact?: boolean;
|
|
292
|
+
/**
|
|
293
|
+
* Prefetch strategy for loader data:
|
|
294
|
+
* - "hover" (default) — prefetch when the user hovers over the link
|
|
295
|
+
* - "viewport" — prefetch when the link scrolls into the viewport
|
|
296
|
+
* - "none" — no prefetching
|
|
297
|
+
*/
|
|
298
|
+
prefetch?: "hover" | "viewport" | "none";
|
|
299
|
+
children?: VNodeChild | null;
|
|
300
|
+
}
|
|
301
|
+
declare const RouterLink: ComponentFn<RouterLinkProps>;
|
|
494
302
|
//#endregion
|
|
495
|
-
//#region src/
|
|
303
|
+
//#region src/loader.d.ts
|
|
496
304
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
} : {}),
|
|
510
|
-
...(options?.error ? {
|
|
511
|
-
errorComponent: options.error
|
|
512
|
-
} : {})
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
function isLazy(c) {
|
|
516
|
-
return typeof c === "object" && c !== null && c[LAZY_SYMBOL] === true;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
//#endregion
|
|
520
|
-
//#region src/router.ts
|
|
521
|
-
|
|
522
|
-
function setActiveRouter(router) {
|
|
523
|
-
if (router) router._viewDepth = 0;
|
|
524
|
-
_activeRouter = router;
|
|
525
|
-
}
|
|
526
|
-
function useRouter() {
|
|
527
|
-
const router = useContext(RouterContext) ?? _activeRouter;
|
|
528
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
529
|
-
return router;
|
|
530
|
-
}
|
|
531
|
-
function useRoute() {
|
|
532
|
-
const router = useContext(RouterContext) ?? _activeRouter;
|
|
533
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
534
|
-
return router.currentRoute;
|
|
535
|
-
}
|
|
305
|
+
* Returns the data resolved by the current route's `loader` function.
|
|
306
|
+
* Must be called inside a route component rendered by <RouterView />.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* const routes = [{ path: "/users", component: Users, loader: fetchUsers }]
|
|
310
|
+
*
|
|
311
|
+
* function Users() {
|
|
312
|
+
* const users = useLoaderData<User[]>()
|
|
313
|
+
* return h("ul", null, users.map(u => h("li", null, u.name)))
|
|
314
|
+
* }
|
|
315
|
+
*/
|
|
316
|
+
declare function useLoaderData<T = unknown>(): T;
|
|
536
317
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
function onBeforeRouteLeave(guard) {
|
|
547
|
-
const router = useContext(RouterContext) ?? _activeRouter;
|
|
548
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
549
|
-
const currentMatched = router.currentRoute().matched;
|
|
550
|
-
const wrappedGuard = (to, from) => {
|
|
551
|
-
if (!from.matched.some(r => currentMatched.includes(r))) return void 0;
|
|
552
|
-
return guard(to, from);
|
|
553
|
-
};
|
|
554
|
-
const remove = router.beforeEach(wrappedGuard);
|
|
555
|
-
onUnmount(() => remove());
|
|
556
|
-
return remove;
|
|
557
|
-
}
|
|
318
|
+
* SSR helper: pre-run all loaders for the given path before rendering.
|
|
319
|
+
* Call this before `renderToString` so route components can read data via `useLoaderData()`.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const router = createRouter({ routes, url: req.url })
|
|
323
|
+
* await prefetchLoaderData(router, req.url)
|
|
324
|
+
* const html = await renderToString(h(App, { router }))
|
|
325
|
+
*/
|
|
326
|
+
declare function prefetchLoaderData(router: RouterInstance, path: string): Promise<void>;
|
|
558
327
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
571
|
-
const currentMatched = router.currentRoute().matched;
|
|
572
|
-
const wrappedGuard = (to, from) => {
|
|
573
|
-
if (!to.matched.some(r => currentMatched.includes(r))) return void 0;
|
|
574
|
-
return guard(to, from);
|
|
575
|
-
};
|
|
576
|
-
const remove = router.beforeEach(wrappedGuard);
|
|
577
|
-
onUnmount(() => remove());
|
|
578
|
-
return remove;
|
|
579
|
-
}
|
|
328
|
+
* Serialize loader data to a JSON-safe plain object for embedding in SSR HTML.
|
|
329
|
+
* Keys are route path patterns (stable across server and client).
|
|
330
|
+
*
|
|
331
|
+
* @example — SSR handler:
|
|
332
|
+
* await prefetchLoaderData(router, req.url)
|
|
333
|
+
* const { html, head } = await renderWithHead(h(App, null))
|
|
334
|
+
* const page = `...${head}
|
|
335
|
+
* <script>window.__PYREON_LOADER_DATA__=${JSON.stringify(serializeLoaderData(router))}</script>
|
|
336
|
+
* ...${html}...`
|
|
337
|
+
*/
|
|
338
|
+
declare function serializeLoaderData(router: RouterInstance): Record<string, unknown>;
|
|
580
339
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* @
|
|
589
|
-
* const
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
597
|
-
router._blockers.add(fn);
|
|
598
|
-
const beforeUnloadHandler = _isBrowser ? e => {
|
|
599
|
-
e.preventDefault();
|
|
600
|
-
} : null;
|
|
601
|
-
if (beforeUnloadHandler) window.addEventListener("beforeunload", beforeUnloadHandler);
|
|
602
|
-
const remove = () => {
|
|
603
|
-
router._blockers.delete(fn);
|
|
604
|
-
if (beforeUnloadHandler) window.removeEventListener("beforeunload", beforeUnloadHandler);
|
|
605
|
-
};
|
|
606
|
-
onUnmount(() => remove());
|
|
607
|
-
return {
|
|
608
|
-
remove
|
|
609
|
-
};
|
|
610
|
-
}
|
|
340
|
+
* Hydrate loader data from a serialized object (e.g. `window.__PYREON_LOADER_DATA__`).
|
|
341
|
+
* Populates the router's internal `_loaderData` map so the initial render uses
|
|
342
|
+
* server-fetched data without re-running loaders on the client.
|
|
343
|
+
*
|
|
344
|
+
* Call this before `mount()`, after `createRouter()`.
|
|
345
|
+
*
|
|
346
|
+
* @example — client entry:
|
|
347
|
+
* import { hydrateLoaderData } from "@pyreon/router"
|
|
348
|
+
* const router = createRouter({ routes })
|
|
349
|
+
* hydrateLoaderData(router, window.__PYREON_LOADER_DATA__ ?? {})
|
|
350
|
+
* mount(h(App, null), document.getElementById("app")!)
|
|
351
|
+
*/
|
|
352
|
+
declare function hydrateLoaderData(router: RouterInstance, serialized: Record<string, unknown>): void;
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/match.d.ts
|
|
611
355
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
*
|
|
617
|
-
* @example
|
|
618
|
-
* const [params, setParams] = useSearchParams({ page: "1", sort: "name" })
|
|
619
|
-
* params().page // "1" if not in URL
|
|
620
|
-
* setParams({ page: "2" }) // navigates to ?page=2&sort=name
|
|
621
|
-
*/
|
|
622
|
-
function useSearchParams(defaults) {
|
|
623
|
-
const router = useContext(RouterContext) ?? _activeRouter;
|
|
624
|
-
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
625
|
-
const get = () => {
|
|
626
|
-
const query = router.currentRoute().query;
|
|
627
|
-
if (!defaults) return query;
|
|
628
|
-
return {
|
|
629
|
-
...defaults,
|
|
630
|
-
...query
|
|
631
|
-
};
|
|
632
|
-
};
|
|
633
|
-
const set = updates => {
|
|
634
|
-
const merged = {
|
|
635
|
-
...get(),
|
|
636
|
-
...updates
|
|
637
|
-
};
|
|
638
|
-
const path = router.currentRoute().path + stringifyQuery(merged);
|
|
639
|
-
return router.replace(path);
|
|
640
|
-
};
|
|
641
|
-
return [get, set];
|
|
642
|
-
}
|
|
643
|
-
function createRouter(options) {
|
|
644
|
-
const opts = Array.isArray(options) ? {
|
|
645
|
-
routes: options
|
|
646
|
-
} : options;
|
|
647
|
-
const {
|
|
648
|
-
routes,
|
|
649
|
-
mode = "hash",
|
|
650
|
-
scrollBehavior,
|
|
651
|
-
onError,
|
|
652
|
-
maxCacheSize = 100,
|
|
653
|
-
trailingSlash = "strip"
|
|
654
|
-
} = opts;
|
|
655
|
-
const base = mode === "history" ? normalizeBase(opts.base ?? "") : "";
|
|
656
|
-
const nameIndex = buildNameIndex(routes);
|
|
657
|
-
const guards = [];
|
|
658
|
-
const afterHooks = [];
|
|
659
|
-
const scrollManager = new ScrollManager(scrollBehavior);
|
|
660
|
-
let _navGen = 0;
|
|
661
|
-
const getInitialLocation = () => {
|
|
662
|
-
if (opts.url) return stripBase(opts.url, base);
|
|
663
|
-
if (!_isBrowser) return "/";
|
|
664
|
-
if (mode === "history") return stripBase(window.location.pathname, base) + window.location.search;
|
|
665
|
-
const hash = window.location.hash;
|
|
666
|
-
return hash.startsWith("#") ? hash.slice(1) || "/" : "/";
|
|
667
|
-
};
|
|
668
|
-
const getCurrentLocation = () => {
|
|
669
|
-
if (!_isBrowser) return currentPath();
|
|
670
|
-
if (mode === "history") return stripBase(window.location.pathname, base) + window.location.search;
|
|
671
|
-
const hash = window.location.hash;
|
|
672
|
-
return hash.startsWith("#") ? hash.slice(1) || "/" : "/";
|
|
673
|
-
};
|
|
674
|
-
const currentPath = signal(normalizeTrailingSlash(getInitialLocation(), trailingSlash));
|
|
675
|
-
const currentRoute = computed(() => resolveRoute(currentPath(), routes));
|
|
676
|
-
let _popstateHandler = null;
|
|
677
|
-
let _hashchangeHandler = null;
|
|
678
|
-
if (_isBrowser) if (mode === "history") {
|
|
679
|
-
_popstateHandler = () => currentPath.set(getCurrentLocation());
|
|
680
|
-
window.addEventListener("popstate", _popstateHandler);
|
|
681
|
-
} else {
|
|
682
|
-
_hashchangeHandler = () => currentPath.set(getCurrentLocation());
|
|
683
|
-
window.addEventListener("hashchange", _hashchangeHandler);
|
|
684
|
-
}
|
|
685
|
-
const componentCache = /* @__PURE__ */new Map();
|
|
686
|
-
const loadingSignal = signal(0);
|
|
687
|
-
async function evaluateGuard(guard, to, from, gen) {
|
|
688
|
-
const result = await runGuard(guard, to, from);
|
|
689
|
-
if (gen !== _navGen) return {
|
|
690
|
-
action: "cancel"
|
|
691
|
-
};
|
|
692
|
-
if (result === false) return {
|
|
693
|
-
action: "cancel"
|
|
694
|
-
};
|
|
695
|
-
if (typeof result === "string") return {
|
|
696
|
-
action: "redirect",
|
|
697
|
-
target: result
|
|
698
|
-
};
|
|
699
|
-
return {
|
|
700
|
-
action: "continue"
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
async function runRouteGuards(records, guardKey, to, from, gen) {
|
|
704
|
-
for (const record of records) {
|
|
705
|
-
const raw = record[guardKey];
|
|
706
|
-
if (!raw) continue;
|
|
707
|
-
const routeGuards = Array.isArray(raw) ? raw : [raw];
|
|
708
|
-
for (const guard of routeGuards) {
|
|
709
|
-
const outcome = await evaluateGuard(guard, to, from, gen);
|
|
710
|
-
if (outcome.action !== "continue") return outcome;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
return {
|
|
714
|
-
action: "continue"
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
async function runGlobalGuards(globalGuards, to, from, gen) {
|
|
718
|
-
for (const guard of globalGuards) {
|
|
719
|
-
const outcome = await evaluateGuard(guard, to, from, gen);
|
|
720
|
-
if (outcome.action !== "continue") return outcome;
|
|
721
|
-
}
|
|
722
|
-
return {
|
|
723
|
-
action: "continue"
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
function processLoaderResult(result, record, ac, to) {
|
|
727
|
-
if (result.status === "fulfilled") {
|
|
728
|
-
router._loaderData.set(record, result.value);
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
if (ac.signal.aborted) return true;
|
|
732
|
-
if (router._onError) {
|
|
733
|
-
if (router._onError(result.reason, to) === false) return false;
|
|
734
|
-
}
|
|
735
|
-
router._loaderData.set(record, void 0);
|
|
736
|
-
return true;
|
|
737
|
-
}
|
|
738
|
-
function syncBrowserUrl(path, replace) {
|
|
739
|
-
if (!_isBrowser) return;
|
|
740
|
-
const url = mode === "history" ? `${base}${path}` : `#${path}`;
|
|
741
|
-
if (replace) window.history.replaceState(null, "", url);else window.history.pushState(null, "", url);
|
|
742
|
-
}
|
|
743
|
-
function resolveRedirect(to) {
|
|
744
|
-
const leaf = to.matched[to.matched.length - 1];
|
|
745
|
-
if (!leaf?.redirect) return null;
|
|
746
|
-
return sanitizePath(typeof leaf.redirect === "function" ? leaf.redirect(to) : leaf.redirect);
|
|
747
|
-
}
|
|
748
|
-
async function runAllGuards(to, from, gen) {
|
|
749
|
-
const leaveOutcome = await runRouteGuards(from.matched, "beforeLeave", to, from, gen);
|
|
750
|
-
if (leaveOutcome.action !== "continue") return leaveOutcome;
|
|
751
|
-
const enterOutcome = await runRouteGuards(to.matched, "beforeEnter", to, from, gen);
|
|
752
|
-
if (enterOutcome.action !== "continue") return enterOutcome;
|
|
753
|
-
return runGlobalGuards(guards, to, from, gen);
|
|
754
|
-
}
|
|
755
|
-
async function runBlockingLoaders(records, to, gen, ac) {
|
|
756
|
-
const loaderCtx = {
|
|
757
|
-
params: to.params,
|
|
758
|
-
query: to.query,
|
|
759
|
-
signal: ac.signal
|
|
760
|
-
};
|
|
761
|
-
const results = await Promise.allSettled(records.map(r => r.loader ? r.loader(loaderCtx) : Promise.resolve(void 0)));
|
|
762
|
-
if (gen !== _navGen) return false;
|
|
763
|
-
for (let i = 0; i < records.length; i++) {
|
|
764
|
-
const result = results[i];
|
|
765
|
-
const record = records[i];
|
|
766
|
-
if (!result || !record) continue;
|
|
767
|
-
if (!processLoaderResult(result, record, ac, to)) return false;
|
|
768
|
-
}
|
|
769
|
-
return true;
|
|
770
|
-
}
|
|
771
|
-
/** Fire-and-forget background revalidation for stale-while-revalidate routes. */
|
|
772
|
-
function revalidateSwrLoaders(records, to, ac) {
|
|
773
|
-
const loaderCtx = {
|
|
774
|
-
params: to.params,
|
|
775
|
-
query: to.query,
|
|
776
|
-
signal: ac.signal
|
|
777
|
-
};
|
|
778
|
-
for (const r of records) {
|
|
779
|
-
if (!r.loader) continue;
|
|
780
|
-
r.loader(loaderCtx).then(data => {
|
|
781
|
-
if (!ac.signal.aborted) {
|
|
782
|
-
router._loaderData.set(r, data);
|
|
783
|
-
loadingSignal.update(n => n + 1);
|
|
784
|
-
loadingSignal.update(n => n - 1);
|
|
785
|
-
}
|
|
786
|
-
}).catch(() => {});
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
async function runLoaders(to, gen, ac) {
|
|
790
|
-
const loadableRecords = to.matched.filter(r => r.loader);
|
|
791
|
-
if (loadableRecords.length === 0) return true;
|
|
792
|
-
const blocking = [];
|
|
793
|
-
const swr = [];
|
|
794
|
-
for (const r of loadableRecords) if (r.staleWhileRevalidate && router._loaderData.has(r)) swr.push(r);else blocking.push(r);
|
|
795
|
-
if (blocking.length > 0) {
|
|
796
|
-
if (!(await runBlockingLoaders(blocking, to, gen, ac))) return false;
|
|
797
|
-
}
|
|
798
|
-
if (swr.length > 0) revalidateSwrLoaders(swr, to, ac);
|
|
799
|
-
return true;
|
|
800
|
-
}
|
|
801
|
-
function commitNavigation(path, replace, to, from) {
|
|
802
|
-
scrollManager.save(from.path);
|
|
803
|
-
currentPath.set(path);
|
|
804
|
-
syncBrowserUrl(path, replace);
|
|
805
|
-
if (_isBrowser && to.meta.title) document.title = to.meta.title;
|
|
806
|
-
for (const record of router._loaderData.keys()) if (!to.matched.includes(record)) router._loaderData.delete(record);
|
|
807
|
-
for (const hook of afterHooks) try {
|
|
808
|
-
hook(to, from);
|
|
809
|
-
} catch (_err) {}
|
|
810
|
-
if (_isBrowser) queueMicrotask(() => scrollManager.restore(to, from));
|
|
811
|
-
}
|
|
812
|
-
async function checkBlockers(to, from, gen) {
|
|
813
|
-
for (const blocker of router._blockers) {
|
|
814
|
-
const blocked = await blocker(to, from);
|
|
815
|
-
if (gen !== _navGen || blocked) return "cancel";
|
|
816
|
-
}
|
|
817
|
-
return "continue";
|
|
818
|
-
}
|
|
819
|
-
async function navigate(rawPath, replace, redirectDepth = 0) {
|
|
820
|
-
if (redirectDepth > 10) return;
|
|
821
|
-
const path = normalizeTrailingSlash(rawPath, trailingSlash);
|
|
822
|
-
const gen = ++_navGen;
|
|
823
|
-
loadingSignal.update(n => n + 1);
|
|
824
|
-
const to = resolveRoute(path, routes);
|
|
825
|
-
const from = currentRoute();
|
|
826
|
-
const redirectTarget = resolveRedirect(to);
|
|
827
|
-
if (redirectTarget !== null) {
|
|
828
|
-
loadingSignal.update(n => n - 1);
|
|
829
|
-
return navigate(redirectTarget, replace, redirectDepth + 1);
|
|
830
|
-
}
|
|
831
|
-
if ((await checkBlockers(to, from, gen)) !== "continue") {
|
|
832
|
-
loadingSignal.update(n => n - 1);
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const guardOutcome = await runAllGuards(to, from, gen);
|
|
836
|
-
if (guardOutcome.action !== "continue") {
|
|
837
|
-
loadingSignal.update(n => n - 1);
|
|
838
|
-
if (guardOutcome.action === "redirect") return navigate(sanitizePath(guardOutcome.target), replace, redirectDepth + 1);
|
|
839
|
-
return;
|
|
840
|
-
}
|
|
841
|
-
router._abortController?.abort();
|
|
842
|
-
const ac = new AbortController();
|
|
843
|
-
router._abortController = ac;
|
|
844
|
-
if (!(await runLoaders(to, gen, ac))) {
|
|
845
|
-
loadingSignal.update(n => n - 1);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
commitNavigation(path, replace, to, from);
|
|
849
|
-
loadingSignal.update(n => n - 1);
|
|
850
|
-
}
|
|
851
|
-
let _readyResolve = null;
|
|
852
|
-
const _readyPromise = new Promise(resolve => {
|
|
853
|
-
_readyResolve = resolve;
|
|
854
|
-
});
|
|
855
|
-
const router = {
|
|
856
|
-
routes,
|
|
857
|
-
mode,
|
|
858
|
-
_base: base,
|
|
859
|
-
currentRoute,
|
|
860
|
-
_currentPath: currentPath,
|
|
861
|
-
_currentRoute: currentRoute,
|
|
862
|
-
_componentCache: componentCache,
|
|
863
|
-
_loadingSignal: loadingSignal,
|
|
864
|
-
_scrollPositions: /* @__PURE__ */new Map(),
|
|
865
|
-
_scrollBehavior: scrollBehavior,
|
|
866
|
-
_viewDepth: 0,
|
|
867
|
-
_erroredChunks: /* @__PURE__ */new Set(),
|
|
868
|
-
_loaderData: /* @__PURE__ */new Map(),
|
|
869
|
-
_abortController: null,
|
|
870
|
-
_blockers: /* @__PURE__ */new Set(),
|
|
871
|
-
_readyResolve,
|
|
872
|
-
_readyPromise,
|
|
873
|
-
_onError: onError,
|
|
874
|
-
_maxCacheSize: maxCacheSize,
|
|
875
|
-
async push(location) {
|
|
876
|
-
if (typeof location === "string") return navigate(sanitizePath(resolveRelativePath(location, currentPath())), false);
|
|
877
|
-
return navigate(resolveNamedPath(location.name, location.params ?? {}, location.query ?? {}, nameIndex), false);
|
|
878
|
-
},
|
|
879
|
-
async replace(location) {
|
|
880
|
-
if (typeof location === "string") return navigate(sanitizePath(resolveRelativePath(location, currentPath())), true);
|
|
881
|
-
return navigate(resolveNamedPath(location.name, location.params ?? {}, location.query ?? {}, nameIndex), true);
|
|
882
|
-
},
|
|
883
|
-
back() {
|
|
884
|
-
if (_isBrowser) window.history.back();
|
|
885
|
-
},
|
|
886
|
-
forward() {
|
|
887
|
-
if (_isBrowser) window.history.forward();
|
|
888
|
-
},
|
|
889
|
-
go(delta) {
|
|
890
|
-
if (_isBrowser) window.history.go(delta);
|
|
891
|
-
},
|
|
892
|
-
beforeEach(guard) {
|
|
893
|
-
guards.push(guard);
|
|
894
|
-
return () => {
|
|
895
|
-
const idx = guards.indexOf(guard);
|
|
896
|
-
if (idx >= 0) guards.splice(idx, 1);
|
|
897
|
-
};
|
|
898
|
-
},
|
|
899
|
-
afterEach(hook) {
|
|
900
|
-
afterHooks.push(hook);
|
|
901
|
-
return () => {
|
|
902
|
-
const idx = afterHooks.indexOf(hook);
|
|
903
|
-
if (idx >= 0) afterHooks.splice(idx, 1);
|
|
904
|
-
};
|
|
905
|
-
},
|
|
906
|
-
loading: () => loadingSignal() > 0,
|
|
907
|
-
isReady() {
|
|
908
|
-
return router._readyPromise;
|
|
909
|
-
},
|
|
910
|
-
destroy() {
|
|
911
|
-
if (_popstateHandler) {
|
|
912
|
-
window.removeEventListener("popstate", _popstateHandler);
|
|
913
|
-
_popstateHandler = null;
|
|
914
|
-
}
|
|
915
|
-
if (_hashchangeHandler) {
|
|
916
|
-
window.removeEventListener("hashchange", _hashchangeHandler);
|
|
917
|
-
_hashchangeHandler = null;
|
|
918
|
-
}
|
|
919
|
-
guards.length = 0;
|
|
920
|
-
afterHooks.length = 0;
|
|
921
|
-
router._blockers.clear();
|
|
922
|
-
componentCache.clear();
|
|
923
|
-
router._loaderData.clear();
|
|
924
|
-
router._abortController?.abort();
|
|
925
|
-
router._abortController = null;
|
|
926
|
-
},
|
|
927
|
-
_resolve: rawPath => resolveRoute(rawPath, routes)
|
|
928
|
-
};
|
|
929
|
-
queueMicrotask(() => {
|
|
930
|
-
if (router._readyResolve) {
|
|
931
|
-
router._readyResolve();
|
|
932
|
-
router._readyResolve = null;
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
return router;
|
|
936
|
-
}
|
|
937
|
-
async function runGuard(guard, to, from) {
|
|
938
|
-
try {
|
|
939
|
-
return await guard(to, from);
|
|
940
|
-
} catch (_err) {
|
|
941
|
-
return false;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
function resolveNamedPath(name, params, query, index) {
|
|
945
|
-
const record = index.get(name);
|
|
946
|
-
if (!record) return "/";
|
|
947
|
-
let path = buildPath(record.path, params);
|
|
948
|
-
const qs = Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
949
|
-
if (qs) path += `?${qs}`;
|
|
950
|
-
return path;
|
|
951
|
-
}
|
|
952
|
-
/** Normalize a base path: ensure leading `/`, strip trailing `/`. */
|
|
953
|
-
function normalizeBase(raw) {
|
|
954
|
-
if (!raw) return "";
|
|
955
|
-
let b = raw;
|
|
956
|
-
if (!b.startsWith("/")) b = `/${b}`;
|
|
957
|
-
if (b.endsWith("/")) b = b.slice(0, -1);
|
|
958
|
-
return b;
|
|
959
|
-
}
|
|
960
|
-
/** Strip the base prefix from a full URL path. Returns the app-relative path. */
|
|
961
|
-
function stripBase(path, base) {
|
|
962
|
-
if (!base) return path;
|
|
963
|
-
if (path === base || path === `${base}/`) return "/";
|
|
964
|
-
if (path.startsWith(`${base}/`)) return path.slice(base.length);
|
|
965
|
-
return path;
|
|
966
|
-
}
|
|
967
|
-
/** Normalize trailing slash on a path according to the configured strategy. */
|
|
968
|
-
function normalizeTrailingSlash(path, strategy) {
|
|
969
|
-
if (strategy === "ignore" || path === "/") return path;
|
|
970
|
-
const qIdx = path.indexOf("?");
|
|
971
|
-
const hIdx = path.indexOf("#");
|
|
972
|
-
const endIdx = qIdx >= 0 ? qIdx : hIdx >= 0 ? hIdx : path.length;
|
|
973
|
-
const pathPart = path.slice(0, endIdx);
|
|
974
|
-
const suffix = path.slice(endIdx);
|
|
975
|
-
if (strategy === "strip") return pathPart.length > 1 && pathPart.endsWith("/") ? pathPart.slice(0, -1) + suffix : path;
|
|
976
|
-
return !pathPart.endsWith("/") ? `${pathPart}/${suffix}` : path;
|
|
977
|
-
}
|
|
356
|
+
* Parse a query string into key-value pairs. Duplicate keys are overwritten
|
|
357
|
+
* (last value wins). Use `parseQueryMulti` to preserve duplicates as arrays.
|
|
358
|
+
*/
|
|
359
|
+
declare function parseQuery(qs: string): Record<string, string>;
|
|
978
360
|
/**
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
|
|
361
|
+
* Parse a query string preserving duplicate keys as arrays.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* parseQueryMulti("color=red&color=blue&size=lg")
|
|
365
|
+
* // → { color: ["red", "blue"], size: "lg" }
|
|
366
|
+
*/
|
|
367
|
+
declare function parseQueryMulti(qs: string): Record<string, string | string[]>;
|
|
368
|
+
declare function stringifyQuery(query: Record<string, string>): string;
|
|
369
|
+
/**
|
|
370
|
+
* Resolve a raw path (including query string and hash) against the route tree.
|
|
371
|
+
* Uses flattened index for O(1) static lookup and first-segment dispatch.
|
|
372
|
+
*/
|
|
373
|
+
declare function resolveRoute(rawPath: string, routes: RouteRecord[]): ResolvedRoute;
|
|
374
|
+
/** Build a path string from a named route's pattern and params */
|
|
375
|
+
declare function buildPath(pattern: string, params: Record<string, string>): string;
|
|
376
|
+
/** Find a route record by name (recursive, O(n)). Prefer buildNameIndex for repeated lookups. */
|
|
377
|
+
declare function findRouteByName(name: string, routes: RouteRecord[]): RouteRecord | null;
|
|
998
378
|
//#endregion
|
|
999
|
-
//#region src/
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
function
|
|
1003
|
-
let set = _prefetched.get(router);
|
|
1004
|
-
if (!set) {
|
|
1005
|
-
set = /* @__PURE__ */new Set();
|
|
1006
|
-
_prefetched.set(router, set);
|
|
1007
|
-
}
|
|
1008
|
-
if (set.has(path)) return;
|
|
1009
|
-
set.add(path);
|
|
1010
|
-
prefetchLoaderData(router, path).catch(() => {
|
|
1011
|
-
set?.delete(path);
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
function renderLazyRoute(router, record, raw) {
|
|
1015
|
-
if (router._erroredChunks.has(record)) return raw.errorComponent ? h(raw.errorComponent, {}) : null;
|
|
1016
|
-
const tryLoad = attempt => raw.loader().then(mod => {
|
|
1017
|
-
cacheSet(router, record, typeof mod === "function" ? mod : mod.default);
|
|
1018
|
-
router._loadingSignal.update(n => n + 1);
|
|
1019
|
-
}).catch(err => {
|
|
1020
|
-
if (attempt < 3) return new Promise(res => setTimeout(res, 500 * 2 ** attempt)).then(() => tryLoad(attempt + 1));
|
|
1021
|
-
if (typeof window !== "undefined" && isStaleChunk(err)) {
|
|
1022
|
-
window.location.reload();
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
router._erroredChunks.add(record);
|
|
1026
|
-
router._loadingSignal.update(n => n + 1);
|
|
1027
|
-
});
|
|
1028
|
-
tryLoad(0);
|
|
1029
|
-
return raw.loadingComponent ? h(raw.loadingComponent, {}) : null;
|
|
1030
|
-
}
|
|
379
|
+
//#region src/router.d.ts
|
|
380
|
+
declare const RouterContext: _pyreon_core0.Context<RouterInstance | null>;
|
|
381
|
+
declare function useRouter(): Router;
|
|
382
|
+
declare function useRoute<TPath extends string = string>(): () => ResolvedRoute<ExtractParams<TPath> & Record<string, string>, Record<string, string>>;
|
|
1031
383
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const data = router._loaderData.get(record);
|
|
1043
|
-
if (data === void 0 && record.errorComponent) return h(record.errorComponent, routeProps);
|
|
1044
|
-
return h(LoaderDataProvider, {
|
|
1045
|
-
data,
|
|
1046
|
-
children: h(Comp, routeProps)
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
384
|
+
* In-component guard: called before the component's route is left.
|
|
385
|
+
* Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed.
|
|
386
|
+
* Automatically removed on component unmount.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* onBeforeRouteLeave((to, from) => {
|
|
390
|
+
* if (hasUnsavedChanges()) return false
|
|
391
|
+
* })
|
|
392
|
+
*/
|
|
393
|
+
declare function onBeforeRouteLeave(guard: NavigationGuard): () => void;
|
|
1049
394
|
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function
|
|
1060
|
-
router._componentCache.set(record, comp);
|
|
1061
|
-
if (router._componentCache.size > router._maxCacheSize) {
|
|
1062
|
-
const oldest = router._componentCache.keys().next().value;
|
|
1063
|
-
router._componentCache.delete(oldest);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
395
|
+
* In-component guard: called when the route changes but the component is reused
|
|
396
|
+
* (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes.
|
|
397
|
+
* Automatically removed on component unmount.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* onBeforeRouteUpdate((to, from) => {
|
|
401
|
+
* if (!isValidId(to.params.id)) return false
|
|
402
|
+
* })
|
|
403
|
+
*/
|
|
404
|
+
declare function onBeforeRouteUpdate(guard: NavigationGuard): () => void;
|
|
1066
405
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
406
|
+
* Register a navigation blocker. The `fn` callback is called before each
|
|
407
|
+
* navigation — return `true` (or resolve to `true`) to block it.
|
|
408
|
+
*
|
|
409
|
+
* Automatically removed on component unmount if called during component setup.
|
|
410
|
+
* Also installs a `beforeunload` handler so the browser shows a confirmation
|
|
411
|
+
* dialog when the user tries to close the tab while a blocker is active.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* const blocker = useBlocker((to, from) => {
|
|
415
|
+
* return hasUnsavedChanges() && !confirm("Discard changes?")
|
|
416
|
+
* })
|
|
417
|
+
* // later: blocker.remove()
|
|
418
|
+
*/
|
|
419
|
+
declare function useBlocker(fn: BlockerFn): Blocker;
|
|
1077
420
|
/**
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1087
|
-
|
|
421
|
+
* Reactive read/write access to the current route's query parameters.
|
|
422
|
+
*
|
|
423
|
+
* Returns `[get, set]` where `get` is a reactive signal producing the merged
|
|
424
|
+
* query object and `set` navigates to the current path with updated params.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* const [params, setParams] = useSearchParams({ page: "1", sort: "name" })
|
|
428
|
+
* params().page // "1" if not in URL
|
|
429
|
+
* setParams({ page: "2" }) // navigates to ?page=2&sort=name
|
|
430
|
+
*/
|
|
431
|
+
declare function useSearchParams<T extends Record<string, string>>(defaults?: T): [get: () => T, set: (updates: Partial<T>) => Promise<void>];
|
|
432
|
+
declare function createRouter(options: RouterOptions | RouteRecord[]): Router;
|
|
1088
433
|
//#endregion
|
|
1089
|
-
export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useLoaderData, useRoute, useRouter, useSearchParams };
|
|
1090
|
-
//# sourceMappingURL=
|
|
434
|
+
export { type AfterEachHook, type Blocker, type BlockerFn, type ExtractParams, type LazyComponent, type LoaderContext, type NavigationGuard, type NavigationGuardResult, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, RouterProvider, type RouterProviderProps, RouterView, type RouterViewProps, type ScrollBehaviorFn, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useLoaderData, useRoute, useRouter, useSearchParams };
|
|
435
|
+
//# sourceMappingURL=index2.d.ts.map
|