@pyreon/router 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -2
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +467 -56
- package/lib/types/index.d.ts +218 -13
- package/package.json +6 -5
- package/src/components.tsx +299 -32
- package/src/env.d.ts +6 -0
- package/src/index.ts +5 -0
- package/src/loader.ts +18 -2
- package/src/manifest.ts +63 -0
- package/src/match.ts +48 -8
- package/src/not-found.ts +75 -0
- package/src/redirect.ts +63 -0
- package/src/router.ts +263 -45
- package/src/tests/loader.test.ts +149 -0
- package/src/tests/manifest-snapshot.test.ts +5 -1
- package/src/tests/match.test.ts +31 -0
- package/src/tests/native-markers.test.ts +18 -0
- package/src/tests/redirect.test.ts +96 -0
- package/src/tests/router.browser.test.tsx +68 -1
- package/src/tests/router.test.ts +686 -1
- package/src/tests/routerlink-reactive-to.browser.test.tsx +158 -0
- package/src/types.ts +95 -1
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ErrorBoundary, createContext, createRef, h, onUnmount, provide, useContext } from "@pyreon/core";
|
|
1
|
+
import { ErrorBoundary, createContext, createRef, cx, h, nativeCompat, onUnmount, provide, useContext } from "@pyreon/core";
|
|
2
2
|
import { computed, signal } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
4
|
//#region src/loader.ts
|
|
5
|
+
const __DEV__$1 = process.env.NODE_ENV !== "production";
|
|
6
|
+
const _countSink$1 = globalThis;
|
|
5
7
|
/**
|
|
6
8
|
* Context frame that holds the loader data for the currently rendered route record.
|
|
7
9
|
* Pushed by RouterView's withLoaderData wrapper before invoking the route component.
|
|
@@ -26,19 +28,27 @@ function useLoaderData() {
|
|
|
26
28
|
* SSR helper: pre-run all loaders for the given path before rendering.
|
|
27
29
|
* Call this before `renderToString` so route components can read data via `useLoaderData()`.
|
|
28
30
|
*
|
|
31
|
+
* The optional `request` is forwarded to each loader's `LoaderContext.request`,
|
|
32
|
+
* letting server-side loaders read cookies / auth headers and `throw redirect()`
|
|
33
|
+
* before the layout renders. A loader that throws `redirect()` propagates the
|
|
34
|
+
* thrown error here — the SSR handler's `catch` converts it into a 302/307
|
|
35
|
+
* `Location:` Response.
|
|
36
|
+
*
|
|
29
37
|
* @example
|
|
30
38
|
* const router = createRouter({ routes, url: req.url })
|
|
31
|
-
* await prefetchLoaderData(router, req.url)
|
|
39
|
+
* await prefetchLoaderData(router, req.url, request)
|
|
32
40
|
* const html = await renderToString(h(App, { router }))
|
|
33
41
|
*/
|
|
34
|
-
async function prefetchLoaderData(router, path) {
|
|
42
|
+
async function prefetchLoaderData(router, path, request) {
|
|
43
|
+
if (__DEV__$1) _countSink$1.__pyreon_count__?.("router.prefetch");
|
|
35
44
|
const route = router._resolve(path);
|
|
36
45
|
const ac = new AbortController();
|
|
37
46
|
await Promise.all(route.matched.filter((r) => r.loader).map(async (r) => {
|
|
38
47
|
const data = await r.loader?.({
|
|
39
48
|
params: route.params,
|
|
40
49
|
query: route.query,
|
|
41
|
-
signal: ac.signal
|
|
50
|
+
signal: ac.signal,
|
|
51
|
+
...request ? { request } : {}
|
|
42
52
|
});
|
|
43
53
|
router._loaderData.set(r, data);
|
|
44
54
|
}));
|
|
@@ -84,17 +94,21 @@ function hydrateLoaderData(router, serialized) {
|
|
|
84
94
|
* Parse a query string into key-value pairs. Duplicate keys are overwritten
|
|
85
95
|
* (last value wins). Use `parseQueryMulti` to preserve duplicates as arrays.
|
|
86
96
|
*/
|
|
97
|
+
/** Decode a query component: `+` → space (per application/x-www-form-urlencoded), then URI-decode. */
|
|
98
|
+
function decodeQueryComponent(raw) {
|
|
99
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
100
|
+
}
|
|
87
101
|
function parseQuery(qs) {
|
|
88
102
|
if (!qs) return {};
|
|
89
103
|
const result = {};
|
|
90
104
|
for (const part of qs.split("&")) {
|
|
91
105
|
const eqIdx = part.indexOf("=");
|
|
92
106
|
if (eqIdx < 0) {
|
|
93
|
-
const key =
|
|
107
|
+
const key = decodeQueryComponent(part);
|
|
94
108
|
if (key) result[key] = "";
|
|
95
109
|
} else {
|
|
96
|
-
const key =
|
|
97
|
-
const val =
|
|
110
|
+
const key = decodeQueryComponent(part.slice(0, eqIdx));
|
|
111
|
+
const val = decodeQueryComponent(part.slice(eqIdx + 1));
|
|
98
112
|
if (key) result[key] = val;
|
|
99
113
|
}
|
|
100
114
|
}
|
|
@@ -115,11 +129,11 @@ function parseQueryMulti(qs) {
|
|
|
115
129
|
let key;
|
|
116
130
|
let val;
|
|
117
131
|
if (eqIdx < 0) {
|
|
118
|
-
key =
|
|
132
|
+
key = decodeQueryComponent(part);
|
|
119
133
|
val = "";
|
|
120
134
|
} else {
|
|
121
|
-
key =
|
|
122
|
-
val =
|
|
135
|
+
key = decodeQueryComponent(part.slice(0, eqIdx));
|
|
136
|
+
val = decodeQueryComponent(part.slice(eqIdx + 1));
|
|
123
137
|
}
|
|
124
138
|
if (!key) continue;
|
|
125
139
|
const existing = result[key];
|
|
@@ -268,7 +282,8 @@ function flattenOne(result, c, parentSegments, chain, meta) {
|
|
|
268
282
|
if (c.children && c.children.length > 0) flattenWalk(result, c.children, parentSegments, chain, meta);
|
|
269
283
|
return;
|
|
270
284
|
}
|
|
271
|
-
const
|
|
285
|
+
const childPath = c.route.path;
|
|
286
|
+
const joined = typeof childPath === "string" && childPath.startsWith("/") ? c.segments : [...parentSegments, ...c.segments];
|
|
272
287
|
if (c.children && c.children.length > 0) flattenWalk(result, c.children, joined, chain, meta);
|
|
273
288
|
result.push(makeFlatEntry(joined, chain, meta, false));
|
|
274
289
|
}
|
|
@@ -398,7 +413,8 @@ function resolveRoute(rawPath, routes) {
|
|
|
398
413
|
query,
|
|
399
414
|
hash,
|
|
400
415
|
matched: staticMatch.matchedChain,
|
|
401
|
-
meta: staticMatch.meta
|
|
416
|
+
meta: staticMatch.meta,
|
|
417
|
+
search: runValidateSearch(staticMatch.matchedChain, query)
|
|
402
418
|
};
|
|
403
419
|
const pathParts = splitPath(cleanPath);
|
|
404
420
|
const pathLen = pathParts.length;
|
|
@@ -413,7 +429,8 @@ function resolveRoute(rawPath, routes) {
|
|
|
413
429
|
query,
|
|
414
430
|
hash,
|
|
415
431
|
matched: match.matched,
|
|
416
|
-
meta: mergeMeta(match.matched)
|
|
432
|
+
meta: mergeMeta(match.matched),
|
|
433
|
+
search: runValidateSearch(match.matched, query)
|
|
417
434
|
};
|
|
418
435
|
}
|
|
419
436
|
}
|
|
@@ -424,7 +441,8 @@ function resolveRoute(rawPath, routes) {
|
|
|
424
441
|
query,
|
|
425
442
|
hash,
|
|
426
443
|
matched: dynMatch.matched,
|
|
427
|
-
meta: mergeMeta(dynMatch.matched)
|
|
444
|
+
meta: mergeMeta(dynMatch.matched),
|
|
445
|
+
search: runValidateSearch(dynMatch.matched, query)
|
|
428
446
|
};
|
|
429
447
|
const w = index.wildcards[0];
|
|
430
448
|
if (w) return {
|
|
@@ -433,7 +451,8 @@ function resolveRoute(rawPath, routes) {
|
|
|
433
451
|
query,
|
|
434
452
|
hash,
|
|
435
453
|
matched: w.matchedChain,
|
|
436
|
-
meta: w.meta
|
|
454
|
+
meta: w.meta,
|
|
455
|
+
search: runValidateSearch(w.matchedChain, query)
|
|
437
456
|
};
|
|
438
457
|
return {
|
|
439
458
|
path: cleanPath,
|
|
@@ -441,9 +460,22 @@ function resolveRoute(rawPath, routes) {
|
|
|
441
460
|
query,
|
|
442
461
|
hash,
|
|
443
462
|
matched: [],
|
|
444
|
-
meta: {}
|
|
463
|
+
meta: {},
|
|
464
|
+
search: {}
|
|
445
465
|
};
|
|
446
466
|
}
|
|
467
|
+
/** Run validateSearch from the deepest matched route that has one. */
|
|
468
|
+
function runValidateSearch(matched, query) {
|
|
469
|
+
for (let i = matched.length - 1; i >= 0; i--) {
|
|
470
|
+
const validate = matched[i]?.validateSearch;
|
|
471
|
+
if (validate) try {
|
|
472
|
+
return validate(query);
|
|
473
|
+
} catch {
|
|
474
|
+
return { ...query };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
447
479
|
/** Merge meta from matched routes (leaf takes precedence) */
|
|
448
480
|
function mergeMeta(matched) {
|
|
449
481
|
const meta = {};
|
|
@@ -489,6 +521,58 @@ function buildNameIndex(routes) {
|
|
|
489
521
|
return index;
|
|
490
522
|
}
|
|
491
523
|
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/redirect.ts
|
|
526
|
+
const REDIRECT = Symbol.for("pyreon.redirect");
|
|
527
|
+
/**
|
|
528
|
+
* Throw inside a route loader to redirect the navigation server-side
|
|
529
|
+
* (during SSR returns a 302/307 `Location:` response) and client-side
|
|
530
|
+
* (during CSR triggers `router.replace()` before the layout renders).
|
|
531
|
+
*
|
|
532
|
+
* The auth-gate use case: replaces the fragile `onMount + router.push()`
|
|
533
|
+
* workaround. `onMount` doesn't fire reliably under nested-layout dev SSR +
|
|
534
|
+
* hydration — so the layout renders briefly before the push happens, leaking
|
|
535
|
+
* authenticated UI to unauthenticated users. `redirect()` runs in the loader
|
|
536
|
+
* BEFORE the layout's component is invoked, so the unauthenticated UI never
|
|
537
|
+
* mounts in the first place.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```ts
|
|
541
|
+
* // src/routes/app/_layout.tsx
|
|
542
|
+
* export const loader = async ({ request }) => {
|
|
543
|
+
* const session = await getSession(request)
|
|
544
|
+
* if (!session) redirect('/login')
|
|
545
|
+
* return { user: session.user }
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*
|
|
549
|
+
* @param url - Target URL (typically a path like `/login` or absolute URL for cross-origin).
|
|
550
|
+
* @param status - HTTP redirect status. Default `307` (Temporary Redirect, method-preserving).
|
|
551
|
+
* Use `301`/`308` for permanent moves, `302`/`303` to force GET on the target.
|
|
552
|
+
*/
|
|
553
|
+
function redirect(url, status = 307) {
|
|
554
|
+
const err = /* @__PURE__ */ new Error(`Redirect to ${url}`);
|
|
555
|
+
err[REDIRECT] = {
|
|
556
|
+
url,
|
|
557
|
+
status
|
|
558
|
+
};
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
561
|
+
/** Check if an error is a RedirectError thrown by `redirect()`. */
|
|
562
|
+
function isRedirectError(err) {
|
|
563
|
+
return typeof err === "object" && err !== null && typeof err[REDIRECT] === "object";
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Extract the redirect URL and status from a thrown RedirectError. Returns
|
|
567
|
+
* `null` if `err` isn't a RedirectError. Used by the router's loader-runner
|
|
568
|
+
* (CSR) and the SSR handler to convert the thrown error into the right kind
|
|
569
|
+
* of response (a `router.replace()` call or a `302`/`307` Response).
|
|
570
|
+
*/
|
|
571
|
+
function getRedirectInfo(err) {
|
|
572
|
+
if (!isRedirectError(err)) return null;
|
|
573
|
+
return err[REDIRECT] ?? null;
|
|
574
|
+
}
|
|
575
|
+
|
|
492
576
|
//#endregion
|
|
493
577
|
//#region src/scroll.ts
|
|
494
578
|
/**
|
|
@@ -587,7 +671,8 @@ function isLazy(c) {
|
|
|
587
671
|
//#endregion
|
|
588
672
|
//#region src/router.ts
|
|
589
673
|
const _isBrowser = typeof window !== "undefined";
|
|
590
|
-
const __DEV__ =
|
|
674
|
+
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
675
|
+
const _countSink = globalThis;
|
|
591
676
|
const RouterContext = createContext(null);
|
|
592
677
|
let _activeRouter = null;
|
|
593
678
|
function setActiveRouter(router) {
|
|
@@ -806,8 +891,10 @@ function useTypedSearchParams(schema) {
|
|
|
806
891
|
const result = {};
|
|
807
892
|
for (const [key, type] of Object.entries(schema)) {
|
|
808
893
|
const raw = query[key];
|
|
809
|
-
if (type === "number")
|
|
810
|
-
|
|
894
|
+
if (type === "number") {
|
|
895
|
+
const n = raw !== void 0 ? Number(raw) : 0;
|
|
896
|
+
result[key] = Number.isNaN(n) ? 0 : n;
|
|
897
|
+
} else if (type === "boolean") result[key] = raw === "true" || raw === "1";
|
|
811
898
|
else result[key] = raw ?? "";
|
|
812
899
|
}
|
|
813
900
|
return result;
|
|
@@ -824,6 +911,44 @@ function useTypedSearchParams(schema) {
|
|
|
824
911
|
};
|
|
825
912
|
return [get, set];
|
|
826
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* Read the validated search params from the current route's `validateSearch`.
|
|
916
|
+
* Returns a reactive accessor that re-evaluates when the route changes.
|
|
917
|
+
*
|
|
918
|
+
* The generic `T` should match the return type of your `validateSearch` function.
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```tsx
|
|
922
|
+
* // Route config:
|
|
923
|
+
* { path: '/search', validateSearch: (raw) => ({
|
|
924
|
+
* page: Number(raw.page) || 1,
|
|
925
|
+
* q: raw.q ?? '',
|
|
926
|
+
* }), component: SearchPage }
|
|
927
|
+
*
|
|
928
|
+
* // In SearchPage:
|
|
929
|
+
* const search = useValidatedSearch<{ page: number; q: string }>()
|
|
930
|
+
* // search().page — typed as number
|
|
931
|
+
* // search().q — typed as string
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
function useValidatedSearch() {
|
|
935
|
+
const router = _getRouter();
|
|
936
|
+
let prev = null;
|
|
937
|
+
return () => {
|
|
938
|
+
const next = router.currentRoute().search;
|
|
939
|
+
if (prev && shallowEqual(prev, next)) return prev;
|
|
940
|
+
prev = next;
|
|
941
|
+
return next;
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/** Shallow equality check for plain objects — keys + strict value comparison. */
|
|
945
|
+
function shallowEqual(a, b) {
|
|
946
|
+
const keysA = Object.keys(a);
|
|
947
|
+
const keysB = Object.keys(b);
|
|
948
|
+
if (keysA.length !== keysB.length) return false;
|
|
949
|
+
for (const key of keysA) if (a[key] !== b[key]) return false;
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
827
952
|
function _getRouter() {
|
|
828
953
|
const router = useContext(RouterContext) ?? _activeRouter;
|
|
829
954
|
if (!router) throw new Error("[Pyreon] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
@@ -927,14 +1052,19 @@ function createRouter(options) {
|
|
|
927
1052
|
function processLoaderResult(result, record, ac, to) {
|
|
928
1053
|
if (result.status === "fulfilled") {
|
|
929
1054
|
router._loaderData.set(record, result.value);
|
|
930
|
-
return
|
|
1055
|
+
return { action: "continue" };
|
|
931
1056
|
}
|
|
932
|
-
if (ac.signal.aborted) return
|
|
1057
|
+
if (ac.signal.aborted) return { action: "continue" };
|
|
1058
|
+
const info = getRedirectInfo(result.reason);
|
|
1059
|
+
if (info) return {
|
|
1060
|
+
action: "redirect",
|
|
1061
|
+
target: info.url
|
|
1062
|
+
};
|
|
933
1063
|
if (router._onError) {
|
|
934
|
-
if (router._onError(result.reason, to) === false) return
|
|
1064
|
+
if (router._onError(result.reason, to) === false) return { action: "cancel" };
|
|
935
1065
|
}
|
|
936
1066
|
router._loaderData.set(record, void 0);
|
|
937
|
-
return
|
|
1067
|
+
return { action: "continue" };
|
|
938
1068
|
}
|
|
939
1069
|
function syncBrowserUrl(path, replace) {
|
|
940
1070
|
if (!_isBrowser) return;
|
|
@@ -954,21 +1084,89 @@ function createRouter(options) {
|
|
|
954
1084
|
if (enterOutcome.action !== "continue") return enterOutcome;
|
|
955
1085
|
return runGlobalGuards(guards, to, from, gen);
|
|
956
1086
|
}
|
|
1087
|
+
/** Default cache key: path + serialized params */
|
|
1088
|
+
function defaultLoaderKey(record, ctx) {
|
|
1089
|
+
return `${record.path}:${JSON.stringify(ctx.params)}`;
|
|
1090
|
+
}
|
|
1091
|
+
/** Get cache key for a route record + context. */
|
|
1092
|
+
function getCacheKey(record, ctx) {
|
|
1093
|
+
return record.loaderKey ? record.loaderKey(ctx) : defaultLoaderKey(record, ctx);
|
|
1094
|
+
}
|
|
1095
|
+
/** Check if a cached entry is still fresh (not expired by gcTime). */
|
|
1096
|
+
function isCacheFresh(entry, record) {
|
|
1097
|
+
const gcTime = record.gcTime ?? 3e5;
|
|
1098
|
+
if (gcTime === 0) return false;
|
|
1099
|
+
return Date.now() - entry.timestamp < gcTime;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Bounded set into `_loaderCache`: evicts the oldest entry (insertion-order
|
|
1103
|
+
* FIFO) when the cap is exceeded. The `gcTime` TTL handles staleness, but
|
|
1104
|
+
* without a size cap a long-running SPA navigating across many distinct
|
|
1105
|
+
* loader keys (e.g. `/posts/:id` with hundreds of unique IDs) would
|
|
1106
|
+
* accumulate cache entries indefinitely until manual `invalidateLoader()`
|
|
1107
|
+
* — `_maxCacheSize` was wired through from `RouterOptions.maxCacheSize`
|
|
1108
|
+
* (default 100) but the loader cache write paths never read it. Mirrors
|
|
1109
|
+
* the same pattern used for `_componentCache` in `components.tsx`.
|
|
1110
|
+
*/
|
|
1111
|
+
function loaderCacheSet(key, data) {
|
|
1112
|
+
router._loaderCache.set(key, {
|
|
1113
|
+
data,
|
|
1114
|
+
timestamp: Date.now()
|
|
1115
|
+
});
|
|
1116
|
+
if (router._loaderCache.size > router._maxCacheSize) {
|
|
1117
|
+
const oldest = router._loaderCache.keys().next().value;
|
|
1118
|
+
if (oldest !== void 0) router._loaderCache.delete(oldest);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Execute a loader with cache + dedup:
|
|
1123
|
+
* 1. Cache hit + fresh → return cached data (skip loader entirely)
|
|
1124
|
+
* 2. In-flight for same key → dedup (return existing promise)
|
|
1125
|
+
* 3. Otherwise → run loader, cache result, clean up in-flight
|
|
1126
|
+
*/
|
|
1127
|
+
function executeLoader(record, loaderCtx) {
|
|
1128
|
+
if (!record.loader) return Promise.resolve(void 0);
|
|
1129
|
+
const key = getCacheKey(record, loaderCtx);
|
|
1130
|
+
if (!record.staleWhileRevalidate) {
|
|
1131
|
+
const cached = router._loaderCache.get(key);
|
|
1132
|
+
if (cached && isCacheFresh(cached, record)) {
|
|
1133
|
+
if (__DEV__) _countSink.__pyreon_count__?.("router.loaderCache.hit");
|
|
1134
|
+
return Promise.resolve(cached.data);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const inflight = router._loaderInflight.get(key);
|
|
1138
|
+
if (inflight && !inflight.signal.aborted) return inflight.promise;
|
|
1139
|
+
if (__DEV__) _countSink.__pyreon_count__?.("router.loaderRun");
|
|
1140
|
+
const promise = Promise.resolve().then(() => record.loader(loaderCtx)).then((data) => {
|
|
1141
|
+
loaderCacheSet(key, data);
|
|
1142
|
+
if (router._loaderInflight.get(key)?.promise === promise) router._loaderInflight.delete(key);
|
|
1143
|
+
return data;
|
|
1144
|
+
}).catch((err) => {
|
|
1145
|
+
if (router._loaderInflight.get(key)?.promise === promise) router._loaderInflight.delete(key);
|
|
1146
|
+
throw err;
|
|
1147
|
+
});
|
|
1148
|
+
router._loaderInflight.set(key, {
|
|
1149
|
+
promise,
|
|
1150
|
+
signal: loaderCtx.signal
|
|
1151
|
+
});
|
|
1152
|
+
return promise;
|
|
1153
|
+
}
|
|
957
1154
|
async function runBlockingLoaders(records, to, gen, ac) {
|
|
958
1155
|
const loaderCtx = {
|
|
959
1156
|
params: to.params,
|
|
960
1157
|
query: to.query,
|
|
961
1158
|
signal: ac.signal
|
|
962
1159
|
};
|
|
963
|
-
const results = await Promise.allSettled(records.map((r) => r
|
|
964
|
-
if (gen !== _navGen) return
|
|
1160
|
+
const results = await Promise.allSettled(records.map((r) => executeLoader(r, loaderCtx)));
|
|
1161
|
+
if (gen !== _navGen) return { action: "cancel" };
|
|
965
1162
|
for (let i = 0; i < records.length; i++) {
|
|
966
1163
|
const result = results[i];
|
|
967
1164
|
const record = records[i];
|
|
968
1165
|
if (!result || !record) continue;
|
|
969
|
-
|
|
1166
|
+
const outcome = processLoaderResult(result, record, ac, to);
|
|
1167
|
+
if (outcome.action !== "continue") return outcome;
|
|
970
1168
|
}
|
|
971
|
-
return
|
|
1169
|
+
return { action: "continue" };
|
|
972
1170
|
}
|
|
973
1171
|
/** Fire-and-forget background revalidation for stale-while-revalidate routes. */
|
|
974
1172
|
function revalidateSwrLoaders(records, to, ac) {
|
|
@@ -982,6 +1180,7 @@ function createRouter(options) {
|
|
|
982
1180
|
r.loader(loaderCtx).then((data) => {
|
|
983
1181
|
if (!ac.signal.aborted) {
|
|
984
1182
|
router._loaderData.set(r, data);
|
|
1183
|
+
loaderCacheSet(getCacheKey(r, loaderCtx), data);
|
|
985
1184
|
loadingSignal.update((n) => n + 1);
|
|
986
1185
|
loadingSignal.update((n) => n - 1);
|
|
987
1186
|
}
|
|
@@ -990,16 +1189,17 @@ function createRouter(options) {
|
|
|
990
1189
|
}
|
|
991
1190
|
async function runLoaders(to, gen, ac) {
|
|
992
1191
|
const loadableRecords = to.matched.filter((r) => r.loader);
|
|
993
|
-
if (loadableRecords.length === 0) return
|
|
1192
|
+
if (loadableRecords.length === 0) return { action: "continue" };
|
|
994
1193
|
const blocking = [];
|
|
995
1194
|
const swr = [];
|
|
996
1195
|
for (const r of loadableRecords) if (r.staleWhileRevalidate && router._loaderData.has(r)) swr.push(r);
|
|
997
1196
|
else blocking.push(r);
|
|
998
1197
|
if (blocking.length > 0) {
|
|
999
|
-
|
|
1198
|
+
const outcome = await runBlockingLoaders(blocking, to, gen, ac);
|
|
1199
|
+
if (outcome.action !== "continue") return outcome;
|
|
1000
1200
|
}
|
|
1001
1201
|
if (swr.length > 0) revalidateSwrLoaders(swr, to, ac);
|
|
1002
|
-
return
|
|
1202
|
+
return { action: "continue" };
|
|
1003
1203
|
}
|
|
1004
1204
|
async function commitNavigation(path, replace, to, from) {
|
|
1005
1205
|
scrollManager.save(from.path);
|
|
@@ -1059,6 +1259,8 @@ function createRouter(options) {
|
|
|
1059
1259
|
return { action: "continue" };
|
|
1060
1260
|
}
|
|
1061
1261
|
async function navigate(rawPath, replace, redirectDepth = 0) {
|
|
1262
|
+
if (__DEV__) _countSink.__pyreon_count__?.("router.navigate");
|
|
1263
|
+
router._navigationStartTime = Date.now();
|
|
1062
1264
|
if (redirectDepth > 10) {
|
|
1063
1265
|
if (__DEV__) console.warn(`[Pyreon] Navigation to "${rawPath}" aborted: redirect depth exceeded 10 levels. This likely indicates a redirect loop in your route configuration.`);
|
|
1064
1266
|
return;
|
|
@@ -1092,8 +1294,10 @@ function createRouter(options) {
|
|
|
1092
1294
|
router._abortController?.abort();
|
|
1093
1295
|
const ac = new AbortController();
|
|
1094
1296
|
router._abortController = ac;
|
|
1095
|
-
|
|
1297
|
+
const loaderOutcome = await runLoaders(to, gen, ac);
|
|
1298
|
+
if (loaderOutcome.action !== "continue") {
|
|
1096
1299
|
loadingSignal.update((n) => n - 1);
|
|
1300
|
+
if (loaderOutcome.action === "redirect") return navigate(sanitizePath(loaderOutcome.target), replace, redirectDepth + 1);
|
|
1097
1301
|
return;
|
|
1098
1302
|
}
|
|
1099
1303
|
await commitNavigation(path, replace, to, from);
|
|
@@ -1123,6 +1327,9 @@ function createRouter(options) {
|
|
|
1123
1327
|
_readyPromise,
|
|
1124
1328
|
_onError: onError,
|
|
1125
1329
|
_maxCacheSize: maxCacheSize,
|
|
1330
|
+
_navigationStartTime: Date.now(),
|
|
1331
|
+
_loaderCache: /* @__PURE__ */ new Map(),
|
|
1332
|
+
_loaderInflight: /* @__PURE__ */ new Map(),
|
|
1126
1333
|
async push(location) {
|
|
1127
1334
|
if (typeof location === "string") return navigate(sanitizePath(resolveRelativePath(location, currentPath())), false);
|
|
1128
1335
|
return navigate(resolveNamedPath(location.name, location.params ?? {}, location.query ?? {}, nameIndex), false);
|
|
@@ -1158,7 +1365,7 @@ function createRouter(options) {
|
|
|
1158
1365
|
isReady() {
|
|
1159
1366
|
return router._readyPromise;
|
|
1160
1367
|
},
|
|
1161
|
-
async preload(path) {
|
|
1368
|
+
async preload(path, request) {
|
|
1162
1369
|
const resolved = resolveRoute(path, routes);
|
|
1163
1370
|
await Promise.all(resolved.matched.map(async (record) => {
|
|
1164
1371
|
if (componentCache.has(record)) return;
|
|
@@ -1173,14 +1380,31 @@ function createRouter(options) {
|
|
|
1173
1380
|
}));
|
|
1174
1381
|
const ac = new AbortController();
|
|
1175
1382
|
await Promise.all(resolved.matched.filter((r) => r.loader).map(async (r) => {
|
|
1176
|
-
const data = await r.loader
|
|
1383
|
+
const data = await Promise.resolve().then(() => r.loader({
|
|
1177
1384
|
params: resolved.params,
|
|
1178
1385
|
query: resolved.query,
|
|
1179
|
-
signal: ac.signal
|
|
1180
|
-
|
|
1386
|
+
signal: ac.signal,
|
|
1387
|
+
...request ? { request } : {}
|
|
1388
|
+
}));
|
|
1181
1389
|
router._loaderData.set(r, data);
|
|
1182
1390
|
}));
|
|
1183
1391
|
},
|
|
1392
|
+
invalidateLoader(keyOrPredicate) {
|
|
1393
|
+
if (!keyOrPredicate) {
|
|
1394
|
+
router._loaderCache.clear();
|
|
1395
|
+
router._loaderInflight.clear();
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (typeof keyOrPredicate === "string") {
|
|
1399
|
+
router._loaderCache.delete(keyOrPredicate);
|
|
1400
|
+
router._loaderInflight.delete(keyOrPredicate);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
for (const key of [...router._loaderCache.keys()]) if (keyOrPredicate(key)) {
|
|
1404
|
+
router._loaderCache.delete(key);
|
|
1405
|
+
router._loaderInflight.delete(key);
|
|
1406
|
+
}
|
|
1407
|
+
},
|
|
1184
1408
|
destroy() {
|
|
1185
1409
|
if (_popstateHandler) window.removeEventListener("popstate", _popstateHandler);
|
|
1186
1410
|
if (_hashchangeHandler) window.removeEventListener("hashchange", _hashchangeHandler);
|
|
@@ -1190,6 +1414,8 @@ function createRouter(options) {
|
|
|
1190
1414
|
router._blockers.clear();
|
|
1191
1415
|
componentCache.clear();
|
|
1192
1416
|
router._loaderData.clear();
|
|
1417
|
+
router._loaderCache.clear();
|
|
1418
|
+
router._loaderInflight.clear();
|
|
1193
1419
|
router._abortController?.abort();
|
|
1194
1420
|
router._abortController = null;
|
|
1195
1421
|
if (_activeRouter === router) _activeRouter = null;
|
|
@@ -1214,7 +1440,10 @@ async function runGuard(guard, to, from) {
|
|
|
1214
1440
|
}
|
|
1215
1441
|
function resolveNamedPath(name, params, query, index) {
|
|
1216
1442
|
const record = index.get(name);
|
|
1217
|
-
if (!record)
|
|
1443
|
+
if (!record) {
|
|
1444
|
+
if (__DEV__) console.warn(`[Pyreon Router] Unknown route name "${name}". Available names: ${[...index.keys()].join(", ") || "(none)"}. Falling back to "/".`);
|
|
1445
|
+
return "/";
|
|
1446
|
+
}
|
|
1218
1447
|
let path = buildPath(record.path, params);
|
|
1219
1448
|
const qs = Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
1220
1449
|
if (qs) path += `?${qs}`;
|
|
@@ -1311,38 +1540,81 @@ const RouterView = (props) => {
|
|
|
1311
1540
|
onUnmount(() => {
|
|
1312
1541
|
router._viewDepth--;
|
|
1313
1542
|
});
|
|
1314
|
-
const
|
|
1315
|
-
router._loadingSignal();
|
|
1543
|
+
const depthEntry = computed(() => {
|
|
1316
1544
|
const route = router.currentRoute();
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1545
|
+
const rec = route.matched[depth] ?? null;
|
|
1546
|
+
if (!rec) return {
|
|
1547
|
+
rec: null,
|
|
1548
|
+
comp: null,
|
|
1549
|
+
errored: false,
|
|
1550
|
+
route
|
|
1551
|
+
};
|
|
1552
|
+
router._loadingSignal();
|
|
1553
|
+
if (router._erroredChunks.has(rec)) return {
|
|
1554
|
+
rec,
|
|
1555
|
+
comp: null,
|
|
1556
|
+
errored: true,
|
|
1557
|
+
route
|
|
1558
|
+
};
|
|
1559
|
+
const cached = router._componentCache.get(rec);
|
|
1560
|
+
if (cached) return {
|
|
1561
|
+
rec,
|
|
1562
|
+
comp: cached,
|
|
1563
|
+
errored: false,
|
|
1564
|
+
route
|
|
1565
|
+
};
|
|
1566
|
+
const raw = rec.component;
|
|
1323
1567
|
if (!isLazy(raw)) {
|
|
1324
|
-
cacheSet(router,
|
|
1325
|
-
return
|
|
1568
|
+
cacheSet(router, rec, raw);
|
|
1569
|
+
return {
|
|
1570
|
+
rec,
|
|
1571
|
+
comp: raw,
|
|
1572
|
+
errored: false,
|
|
1573
|
+
route
|
|
1574
|
+
};
|
|
1326
1575
|
}
|
|
1327
|
-
return
|
|
1576
|
+
return {
|
|
1577
|
+
rec,
|
|
1578
|
+
comp: null,
|
|
1579
|
+
errored: false,
|
|
1580
|
+
route
|
|
1581
|
+
};
|
|
1582
|
+
}, { equals: (a, b) => a.rec === b.rec && a.comp === b.comp && a.errored === b.errored && a.route === b.route });
|
|
1583
|
+
const child = () => {
|
|
1584
|
+
const { rec, comp, route } = depthEntry();
|
|
1585
|
+
if (!rec) return null;
|
|
1586
|
+
if (comp) return renderWithLoader(router, rec, comp, route);
|
|
1587
|
+
return renderLazyRoute(router, rec, rec.component);
|
|
1328
1588
|
};
|
|
1329
1589
|
return h("div", { "data-pyreon-router-view": true }, child);
|
|
1330
1590
|
};
|
|
1331
1591
|
const RouterLink = (props) => {
|
|
1332
1592
|
const router = useContext(RouterContext);
|
|
1333
|
-
const prefetchMode = props.prefetch ?? "
|
|
1593
|
+
const prefetchMode = props.prefetch ?? "intent";
|
|
1334
1594
|
const handleClick = (e) => {
|
|
1335
1595
|
e.preventDefault();
|
|
1336
1596
|
if (!router) return;
|
|
1337
1597
|
if (props.replace) router.replace(props.to);
|
|
1338
1598
|
else router.push(props.to);
|
|
1339
1599
|
};
|
|
1340
|
-
const
|
|
1341
|
-
if (
|
|
1600
|
+
const triggerPrefetch = () => {
|
|
1601
|
+
if (!router) return;
|
|
1342
1602
|
prefetchRoute(router, props.to);
|
|
1343
1603
|
};
|
|
1604
|
+
const handleMouseEnter = () => {
|
|
1605
|
+
if (prefetchMode === "hover" || prefetchMode === "intent") triggerPrefetch();
|
|
1606
|
+
};
|
|
1607
|
+
const handleFocus = () => {
|
|
1608
|
+
if (prefetchMode === "intent") triggerPrefetch();
|
|
1609
|
+
};
|
|
1344
1610
|
const inst = router;
|
|
1345
|
-
const href = inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`;
|
|
1611
|
+
const href = () => inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`;
|
|
1612
|
+
const isExactMatch = () => {
|
|
1613
|
+
if (!router) return false;
|
|
1614
|
+
const target = props.to;
|
|
1615
|
+
if (typeof target !== "string") return false;
|
|
1616
|
+
return router.currentRoute().path === target;
|
|
1617
|
+
};
|
|
1346
1618
|
const activeClass = () => {
|
|
1347
1619
|
if (!router) return "";
|
|
1348
1620
|
const current = router.currentRoute().path;
|
|
@@ -1355,6 +1627,7 @@ const RouterLink = (props) => {
|
|
|
1355
1627
|
if (isExact) classes.push(props.exactActiveClass ?? "router-link-exact-active");
|
|
1356
1628
|
return classes.join(" ").trim();
|
|
1357
1629
|
};
|
|
1630
|
+
const ariaCurrent = () => isExactMatch() ? "page" : void 0;
|
|
1358
1631
|
const ref = createRef();
|
|
1359
1632
|
if (prefetchMode === "viewport" && router && typeof IntersectionObserver !== "undefined") {
|
|
1360
1633
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -1369,17 +1642,23 @@ const RouterLink = (props) => {
|
|
|
1369
1642
|
});
|
|
1370
1643
|
onUnmount(() => observer.disconnect());
|
|
1371
1644
|
}
|
|
1372
|
-
const { to: _to, replace: _replace, activeClass: _ac, exactActiveClass: _eac, exact: _exact, prefetch: _prefetch, children, ...rest } = props;
|
|
1645
|
+
const { to: _to, replace: _replace, activeClass: _ac, exactActiveClass: _eac, exact: _exact, prefetch: _prefetch, class: userClass, children, ...rest } = props;
|
|
1646
|
+
const mergedClass = () => {
|
|
1647
|
+
return cx([typeof userClass === "function" ? userClass() : userClass, activeClass()]);
|
|
1648
|
+
};
|
|
1373
1649
|
return h("a", {
|
|
1374
1650
|
...rest,
|
|
1375
1651
|
ref,
|
|
1376
1652
|
href,
|
|
1377
|
-
class:
|
|
1653
|
+
class: mergedClass,
|
|
1654
|
+
"aria-current": ariaCurrent,
|
|
1378
1655
|
onClick: handleClick,
|
|
1379
|
-
onMouseEnter: handleMouseEnter
|
|
1656
|
+
onMouseEnter: handleMouseEnter,
|
|
1657
|
+
onFocus: handleFocus
|
|
1380
1658
|
}, children ?? props.to);
|
|
1381
1659
|
};
|
|
1382
1660
|
/** Prefetch loader data for a route (only once per router + path). */
|
|
1661
|
+
const MAX_PREFETCH_CACHE = 50;
|
|
1383
1662
|
function prefetchRoute(router, path) {
|
|
1384
1663
|
let set = _prefetched.get(router);
|
|
1385
1664
|
if (!set) {
|
|
@@ -1387,6 +1666,10 @@ function prefetchRoute(router, path) {
|
|
|
1387
1666
|
_prefetched.set(router, set);
|
|
1388
1667
|
}
|
|
1389
1668
|
if (set.has(path)) return;
|
|
1669
|
+
if (set.size >= MAX_PREFETCH_CACHE) {
|
|
1670
|
+
const first = set.values().next().value;
|
|
1671
|
+
set.delete(first);
|
|
1672
|
+
}
|
|
1390
1673
|
set.add(path);
|
|
1391
1674
|
prefetchLoaderData(router, path).catch(() => {
|
|
1392
1675
|
set?.delete(path);
|
|
@@ -1431,13 +1714,88 @@ function renderWithLoader(router, record, Comp, route) {
|
|
|
1431
1714
|
}
|
|
1432
1715
|
function renderLoaderContent(router, record, Comp, routeProps) {
|
|
1433
1716
|
const data = router._loaderData.get(record);
|
|
1434
|
-
if (data
|
|
1717
|
+
if (data !== void 0) return h(LoaderDataProvider, {
|
|
1718
|
+
data,
|
|
1719
|
+
children: h(Comp, routeProps)
|
|
1720
|
+
});
|
|
1721
|
+
if (record.pendingComponent) return h(PendingLoader, {
|
|
1722
|
+
router,
|
|
1723
|
+
record,
|
|
1724
|
+
Comp,
|
|
1725
|
+
routeProps
|
|
1726
|
+
});
|
|
1727
|
+
if (record.errorComponent) return h(record.errorComponent, routeProps);
|
|
1435
1728
|
return h(LoaderDataProvider, {
|
|
1436
1729
|
data,
|
|
1437
1730
|
children: h(Comp, routeProps)
|
|
1438
1731
|
});
|
|
1439
1732
|
}
|
|
1440
1733
|
/**
|
|
1734
|
+
* Signal-based pending component with timing control.
|
|
1735
|
+
*
|
|
1736
|
+
* State machine: hidden → pending → ready
|
|
1737
|
+
* - hidden: initial state, nothing shown (lasts pendingMs)
|
|
1738
|
+
* - pending: pendingComponent shown (lasts at least pendingMinMs)
|
|
1739
|
+
* - ready: real component shown (loader data arrived + minTime elapsed)
|
|
1740
|
+
*/
|
|
1741
|
+
function PendingLoader(props) {
|
|
1742
|
+
const { router, record, Comp, routeProps } = props;
|
|
1743
|
+
const pendingMs = record.pendingMs ?? 0;
|
|
1744
|
+
const pendingMinMs = record.pendingMinMs ?? 200;
|
|
1745
|
+
const phase = signal(pendingMs === 0 ? "pending" : "hidden");
|
|
1746
|
+
let pendingTimer = null;
|
|
1747
|
+
let minTimer = null;
|
|
1748
|
+
let minTimeElapsed = pendingMs === 0 ? false : true;
|
|
1749
|
+
let dataReady = false;
|
|
1750
|
+
if (pendingMs === 0) {
|
|
1751
|
+
minTimeElapsed = false;
|
|
1752
|
+
minTimer = setTimeout(() => {
|
|
1753
|
+
minTimeElapsed = true;
|
|
1754
|
+
minTimer = null;
|
|
1755
|
+
if (dataReady) phase.set("ready");
|
|
1756
|
+
}, pendingMinMs);
|
|
1757
|
+
} else pendingTimer = setTimeout(() => {
|
|
1758
|
+
pendingTimer = null;
|
|
1759
|
+
if (dataReady) phase.set("ready");
|
|
1760
|
+
else {
|
|
1761
|
+
phase.set("pending");
|
|
1762
|
+
minTimeElapsed = false;
|
|
1763
|
+
minTimer = setTimeout(() => {
|
|
1764
|
+
minTimeElapsed = true;
|
|
1765
|
+
minTimer = null;
|
|
1766
|
+
if (dataReady) phase.set("ready");
|
|
1767
|
+
}, pendingMinMs);
|
|
1768
|
+
}
|
|
1769
|
+
}, pendingMs);
|
|
1770
|
+
const checkData = () => {
|
|
1771
|
+
if (router._loaderData.get(record) !== void 0) {
|
|
1772
|
+
dataReady = true;
|
|
1773
|
+
if (phase.peek() === "hidden") {
|
|
1774
|
+
if (pendingTimer) {
|
|
1775
|
+
clearTimeout(pendingTimer);
|
|
1776
|
+
pendingTimer = null;
|
|
1777
|
+
}
|
|
1778
|
+
phase.set("ready");
|
|
1779
|
+
} else if (minTimeElapsed) phase.set("ready");
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
onUnmount(() => {
|
|
1783
|
+
if (pendingTimer) clearTimeout(pendingTimer);
|
|
1784
|
+
if (minTimer) clearTimeout(minTimer);
|
|
1785
|
+
});
|
|
1786
|
+
return (() => {
|
|
1787
|
+
router._loadingSignal();
|
|
1788
|
+
checkData();
|
|
1789
|
+
const p = phase();
|
|
1790
|
+
if (p === "hidden") return null;
|
|
1791
|
+
if (p === "pending") return h(record.pendingComponent, routeProps);
|
|
1792
|
+
return h(LoaderDataProvider, {
|
|
1793
|
+
data: router._loaderData.get(record),
|
|
1794
|
+
children: h(Comp, routeProps)
|
|
1795
|
+
});
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1441
1799
|
* Thin provider component that pushes LoaderDataContext before children mount.
|
|
1442
1800
|
* Uses Pyreon's context stack so useLoaderData() reads it during child setup.
|
|
1443
1801
|
*/
|
|
@@ -1474,7 +1832,60 @@ function isStaleChunk(err) {
|
|
|
1474
1832
|
if (err instanceof SyntaxError) return true;
|
|
1475
1833
|
return false;
|
|
1476
1834
|
}
|
|
1835
|
+
nativeCompat(RouterProvider);
|
|
1836
|
+
nativeCompat(RouterView);
|
|
1837
|
+
nativeCompat(RouterLink);
|
|
1838
|
+
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region src/not-found.ts
|
|
1841
|
+
const NOT_FOUND = Symbol.for("pyreon.notFound");
|
|
1842
|
+
/**
|
|
1843
|
+
* Throw inside a route loader or component to trigger the nearest
|
|
1844
|
+
* NotFoundBoundary. Inspired by Next.js's `notFound()`.
|
|
1845
|
+
*
|
|
1846
|
+
* @example
|
|
1847
|
+
* ```ts
|
|
1848
|
+
* // In a loader:
|
|
1849
|
+
* loader: async ({ params }) => {
|
|
1850
|
+
* const user = await fetchUser(params.id)
|
|
1851
|
+
* if (!user) notFound()
|
|
1852
|
+
* return user
|
|
1853
|
+
* }
|
|
1854
|
+
* ```
|
|
1855
|
+
*/
|
|
1856
|
+
function notFound(message) {
|
|
1857
|
+
const err = new Error(message ?? "Not Found");
|
|
1858
|
+
err[NOT_FOUND] = true;
|
|
1859
|
+
throw err;
|
|
1860
|
+
}
|
|
1861
|
+
/** Check if an error is a NotFoundError thrown by `notFound()`. */
|
|
1862
|
+
function isNotFoundError(err) {
|
|
1863
|
+
return typeof err === "object" && err !== null && err[NOT_FOUND] === true;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Catches `notFound()` errors from child route components or loaders
|
|
1867
|
+
* and renders the fallback. Wraps Pyreon's ErrorBoundary with notFound
|
|
1868
|
+
* detection — non-notFound errors propagate to parent error boundaries.
|
|
1869
|
+
*
|
|
1870
|
+
* @example
|
|
1871
|
+
* ```tsx
|
|
1872
|
+
* <NotFoundBoundary fallback={<NotFoundPage />}>
|
|
1873
|
+
* <RouterView />
|
|
1874
|
+
* </NotFoundBoundary>
|
|
1875
|
+
* ```
|
|
1876
|
+
*/
|
|
1877
|
+
const NotFoundBoundary = (props) => {
|
|
1878
|
+
return h(ErrorBoundary, { fallback: (err, reset) => {
|
|
1879
|
+
if (!isNotFoundError(err)) throw err;
|
|
1880
|
+
const fb = props.fallback;
|
|
1881
|
+
if (typeof fb === "function" && fb.length <= 1) return h(fb, {
|
|
1882
|
+
error: err,
|
|
1883
|
+
reset
|
|
1884
|
+
});
|
|
1885
|
+
return fb;
|
|
1886
|
+
} }, props.children);
|
|
1887
|
+
};
|
|
1477
1888
|
|
|
1478
1889
|
//#endregion
|
|
1479
|
-
export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams };
|
|
1890
|
+
export { NotFoundBoundary, RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, getRedirectInfo, hydrateLoaderData, isNotFoundError, isRedirectError, lazy, notFound, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, redirect, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams, useValidatedSearch };
|
|
1480
1891
|
//# sourceMappingURL=index.js.map
|