@pyreon/router 0.14.0 → 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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +174 -51
- package/lib/types/index.d.ts +83 -8
- package/package.json +5 -4
- package/src/components.tsx +162 -27
- package/src/env.d.ts +6 -0
- package/src/index.ts +2 -0
- package/src/loader.ts +14 -4
- package/src/manifest.ts +63 -0
- package/src/match.ts +12 -1
- package/src/redirect.ts +63 -0
- package/src/router.ts +94 -34
- package/src/tests/loader.test.ts +149 -0
- package/src/tests/manifest-snapshot.test.ts +5 -1
- 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 +149 -0
- package/src/tests/routerlink-reactive-to.browser.test.tsx +158 -0
- package/src/types.ts +25 -3
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"25bb3a92-1","name":"loader.ts"},{"uid":"25bb3a92-3","name":"match.ts"},{"uid":"25bb3a92-5","name":"redirect.ts"},{"uid":"25bb3a92-7","name":"scroll.ts"},{"uid":"25bb3a92-9","name":"types.ts"},{"uid":"25bb3a92-11","name":"router.ts"},{"uid":"25bb3a92-13","name":"components.tsx"},{"uid":"25bb3a92-15","name":"not-found.ts"},{"uid":"25bb3a92-17","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"25bb3a92-1":{"renderedLength":3376,"gzipLength":1507,"brotliLength":0,"metaUid":"25bb3a92-0"},"25bb3a92-3":{"renderedLength":13072,"gzipLength":3951,"brotliLength":0,"metaUid":"25bb3a92-2"},"25bb3a92-5":{"renderedLength":1966,"gzipLength":1043,"brotliLength":0,"metaUid":"25bb3a92-4"},"25bb3a92-7":{"renderedLength":2194,"gzipLength":899,"brotliLength":0,"metaUid":"25bb3a92-6"},"25bb3a92-9":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"25bb3a92-8"},"25bb3a92-11":{"renderedLength":29129,"gzipLength":8078,"brotliLength":0,"metaUid":"25bb3a92-10"},"25bb3a92-13":{"renderedLength":10571,"gzipLength":3518,"brotliLength":0,"metaUid":"25bb3a92-12"},"25bb3a92-15":{"renderedLength":1315,"gzipLength":682,"brotliLength":0,"metaUid":"25bb3a92-14"},"25bb3a92-17":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"25bb3a92-16"}},"nodeMetas":{"25bb3a92-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"25bb3a92-1"},"imported":[{"uid":"25bb3a92-18"}],"importedBy":[{"uid":"25bb3a92-16"},{"uid":"25bb3a92-12"}]},"25bb3a92-2":{"id":"/src/match.ts","moduleParts":{"index.js":"25bb3a92-3"},"imported":[],"importedBy":[{"uid":"25bb3a92-16"},{"uid":"25bb3a92-10"}]},"25bb3a92-4":{"id":"/src/redirect.ts","moduleParts":{"index.js":"25bb3a92-5"},"imported":[],"importedBy":[{"uid":"25bb3a92-16"},{"uid":"25bb3a92-10"}]},"25bb3a92-6":{"id":"/src/scroll.ts","moduleParts":{"index.js":"25bb3a92-7"},"imported":[],"importedBy":[{"uid":"25bb3a92-10"}]},"25bb3a92-8":{"id":"/src/types.ts","moduleParts":{"index.js":"25bb3a92-9"},"imported":[],"importedBy":[{"uid":"25bb3a92-16"},{"uid":"25bb3a92-10"}]},"25bb3a92-10":{"id":"/src/router.ts","moduleParts":{"index.js":"25bb3a92-11"},"imported":[{"uid":"25bb3a92-18"},{"uid":"25bb3a92-19"},{"uid":"25bb3a92-2"},{"uid":"25bb3a92-4"},{"uid":"25bb3a92-6"},{"uid":"25bb3a92-8"}],"importedBy":[{"uid":"25bb3a92-16"},{"uid":"25bb3a92-12"}]},"25bb3a92-12":{"id":"/src/components.tsx","moduleParts":{"index.js":"25bb3a92-13"},"imported":[{"uid":"25bb3a92-18"},{"uid":"25bb3a92-19"},{"uid":"25bb3a92-0"},{"uid":"25bb3a92-10"}],"importedBy":[{"uid":"25bb3a92-16"}]},"25bb3a92-14":{"id":"/src/not-found.ts","moduleParts":{"index.js":"25bb3a92-15"},"imported":[{"uid":"25bb3a92-18"}],"importedBy":[{"uid":"25bb3a92-16"}]},"25bb3a92-16":{"id":"/src/index.ts","moduleParts":{"index.js":"25bb3a92-17"},"imported":[{"uid":"25bb3a92-12"},{"uid":"25bb3a92-14"},{"uid":"25bb3a92-4"},{"uid":"25bb3a92-0"},{"uid":"25bb3a92-2"},{"uid":"25bb3a92-10"},{"uid":"25bb3a92-8"}],"importedBy":[],"isEntry":true},"25bb3a92-18":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"25bb3a92-12"},{"uid":"25bb3a92-14"},{"uid":"25bb3a92-0"},{"uid":"25bb3a92-10"}]},"25bb3a92-19":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"25bb3a92-12"},{"uid":"25bb3a92-10"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 =
|
|
5
|
+
const __DEV__$1 = process.env.NODE_ENV !== "production";
|
|
6
6
|
const _countSink$1 = globalThis;
|
|
7
7
|
/**
|
|
8
8
|
* Context frame that holds the loader data for the currently rendered route record.
|
|
@@ -28,12 +28,18 @@ function useLoaderData() {
|
|
|
28
28
|
* SSR helper: pre-run all loaders for the given path before rendering.
|
|
29
29
|
* Call this before `renderToString` so route components can read data via `useLoaderData()`.
|
|
30
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
|
+
*
|
|
31
37
|
* @example
|
|
32
38
|
* const router = createRouter({ routes, url: req.url })
|
|
33
|
-
* await prefetchLoaderData(router, req.url)
|
|
39
|
+
* await prefetchLoaderData(router, req.url, request)
|
|
34
40
|
* const html = await renderToString(h(App, { router }))
|
|
35
41
|
*/
|
|
36
|
-
async function prefetchLoaderData(router, path) {
|
|
42
|
+
async function prefetchLoaderData(router, path, request) {
|
|
37
43
|
if (__DEV__$1) _countSink$1.__pyreon_count__?.("router.prefetch");
|
|
38
44
|
const route = router._resolve(path);
|
|
39
45
|
const ac = new AbortController();
|
|
@@ -41,7 +47,8 @@ async function prefetchLoaderData(router, path) {
|
|
|
41
47
|
const data = await r.loader?.({
|
|
42
48
|
params: route.params,
|
|
43
49
|
query: route.query,
|
|
44
|
-
signal: ac.signal
|
|
50
|
+
signal: ac.signal,
|
|
51
|
+
...request ? { request } : {}
|
|
45
52
|
});
|
|
46
53
|
router._loaderData.set(r, data);
|
|
47
54
|
}));
|
|
@@ -275,7 +282,8 @@ function flattenOne(result, c, parentSegments, chain, meta) {
|
|
|
275
282
|
if (c.children && c.children.length > 0) flattenWalk(result, c.children, parentSegments, chain, meta);
|
|
276
283
|
return;
|
|
277
284
|
}
|
|
278
|
-
const
|
|
285
|
+
const childPath = c.route.path;
|
|
286
|
+
const joined = typeof childPath === "string" && childPath.startsWith("/") ? c.segments : [...parentSegments, ...c.segments];
|
|
279
287
|
if (c.children && c.children.length > 0) flattenWalk(result, c.children, joined, chain, meta);
|
|
280
288
|
result.push(makeFlatEntry(joined, chain, meta, false));
|
|
281
289
|
}
|
|
@@ -513,6 +521,58 @@ function buildNameIndex(routes) {
|
|
|
513
521
|
return index;
|
|
514
522
|
}
|
|
515
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
|
+
|
|
516
576
|
//#endregion
|
|
517
577
|
//#region src/scroll.ts
|
|
518
578
|
/**
|
|
@@ -611,7 +671,7 @@ function isLazy(c) {
|
|
|
611
671
|
//#endregion
|
|
612
672
|
//#region src/router.ts
|
|
613
673
|
const _isBrowser = typeof window !== "undefined";
|
|
614
|
-
const __DEV__ =
|
|
674
|
+
const __DEV__ = process.env.NODE_ENV !== "production";
|
|
615
675
|
const _countSink = globalThis;
|
|
616
676
|
const RouterContext = createContext(null);
|
|
617
677
|
let _activeRouter = null;
|
|
@@ -992,14 +1052,19 @@ function createRouter(options) {
|
|
|
992
1052
|
function processLoaderResult(result, record, ac, to) {
|
|
993
1053
|
if (result.status === "fulfilled") {
|
|
994
1054
|
router._loaderData.set(record, result.value);
|
|
995
|
-
return
|
|
1055
|
+
return { action: "continue" };
|
|
996
1056
|
}
|
|
997
|
-
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
|
+
};
|
|
998
1063
|
if (router._onError) {
|
|
999
|
-
if (router._onError(result.reason, to) === false) return
|
|
1064
|
+
if (router._onError(result.reason, to) === false) return { action: "cancel" };
|
|
1000
1065
|
}
|
|
1001
1066
|
router._loaderData.set(record, void 0);
|
|
1002
|
-
return
|
|
1067
|
+
return { action: "continue" };
|
|
1003
1068
|
}
|
|
1004
1069
|
function syncBrowserUrl(path, replace) {
|
|
1005
1070
|
if (!_isBrowser) return;
|
|
@@ -1034,6 +1099,26 @@ function createRouter(options) {
|
|
|
1034
1099
|
return Date.now() - entry.timestamp < gcTime;
|
|
1035
1100
|
}
|
|
1036
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
|
+
/**
|
|
1037
1122
|
* Execute a loader with cache + dedup:
|
|
1038
1123
|
* 1. Cache hit + fresh → return cached data (skip loader entirely)
|
|
1039
1124
|
* 2. In-flight for same key → dedup (return existing promise)
|
|
@@ -1050,20 +1135,20 @@ function createRouter(options) {
|
|
|
1050
1135
|
}
|
|
1051
1136
|
}
|
|
1052
1137
|
const inflight = router._loaderInflight.get(key);
|
|
1053
|
-
if (inflight) return inflight;
|
|
1138
|
+
if (inflight && !inflight.signal.aborted) return inflight.promise;
|
|
1054
1139
|
if (__DEV__) _countSink.__pyreon_count__?.("router.loaderRun");
|
|
1055
|
-
const promise = record.loader(loaderCtx).then((data) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
timestamp: Date.now()
|
|
1059
|
-
});
|
|
1060
|
-
router._loaderInflight.delete(key);
|
|
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);
|
|
1061
1143
|
return data;
|
|
1062
1144
|
}).catch((err) => {
|
|
1063
|
-
router._loaderInflight.delete(key);
|
|
1145
|
+
if (router._loaderInflight.get(key)?.promise === promise) router._loaderInflight.delete(key);
|
|
1064
1146
|
throw err;
|
|
1065
1147
|
});
|
|
1066
|
-
router._loaderInflight.set(key,
|
|
1148
|
+
router._loaderInflight.set(key, {
|
|
1149
|
+
promise,
|
|
1150
|
+
signal: loaderCtx.signal
|
|
1151
|
+
});
|
|
1067
1152
|
return promise;
|
|
1068
1153
|
}
|
|
1069
1154
|
async function runBlockingLoaders(records, to, gen, ac) {
|
|
@@ -1073,14 +1158,15 @@ function createRouter(options) {
|
|
|
1073
1158
|
signal: ac.signal
|
|
1074
1159
|
};
|
|
1075
1160
|
const results = await Promise.allSettled(records.map((r) => executeLoader(r, loaderCtx)));
|
|
1076
|
-
if (gen !== _navGen) return
|
|
1161
|
+
if (gen !== _navGen) return { action: "cancel" };
|
|
1077
1162
|
for (let i = 0; i < records.length; i++) {
|
|
1078
1163
|
const result = results[i];
|
|
1079
1164
|
const record = records[i];
|
|
1080
1165
|
if (!result || !record) continue;
|
|
1081
|
-
|
|
1166
|
+
const outcome = processLoaderResult(result, record, ac, to);
|
|
1167
|
+
if (outcome.action !== "continue") return outcome;
|
|
1082
1168
|
}
|
|
1083
|
-
return
|
|
1169
|
+
return { action: "continue" };
|
|
1084
1170
|
}
|
|
1085
1171
|
/** Fire-and-forget background revalidation for stale-while-revalidate routes. */
|
|
1086
1172
|
function revalidateSwrLoaders(records, to, ac) {
|
|
@@ -1094,11 +1180,7 @@ function createRouter(options) {
|
|
|
1094
1180
|
r.loader(loaderCtx).then((data) => {
|
|
1095
1181
|
if (!ac.signal.aborted) {
|
|
1096
1182
|
router._loaderData.set(r, data);
|
|
1097
|
-
|
|
1098
|
-
router._loaderCache.set(key, {
|
|
1099
|
-
data,
|
|
1100
|
-
timestamp: Date.now()
|
|
1101
|
-
});
|
|
1183
|
+
loaderCacheSet(getCacheKey(r, loaderCtx), data);
|
|
1102
1184
|
loadingSignal.update((n) => n + 1);
|
|
1103
1185
|
loadingSignal.update((n) => n - 1);
|
|
1104
1186
|
}
|
|
@@ -1107,16 +1189,17 @@ function createRouter(options) {
|
|
|
1107
1189
|
}
|
|
1108
1190
|
async function runLoaders(to, gen, ac) {
|
|
1109
1191
|
const loadableRecords = to.matched.filter((r) => r.loader);
|
|
1110
|
-
if (loadableRecords.length === 0) return
|
|
1192
|
+
if (loadableRecords.length === 0) return { action: "continue" };
|
|
1111
1193
|
const blocking = [];
|
|
1112
1194
|
const swr = [];
|
|
1113
1195
|
for (const r of loadableRecords) if (r.staleWhileRevalidate && router._loaderData.has(r)) swr.push(r);
|
|
1114
1196
|
else blocking.push(r);
|
|
1115
1197
|
if (blocking.length > 0) {
|
|
1116
|
-
|
|
1198
|
+
const outcome = await runBlockingLoaders(blocking, to, gen, ac);
|
|
1199
|
+
if (outcome.action !== "continue") return outcome;
|
|
1117
1200
|
}
|
|
1118
1201
|
if (swr.length > 0) revalidateSwrLoaders(swr, to, ac);
|
|
1119
|
-
return
|
|
1202
|
+
return { action: "continue" };
|
|
1120
1203
|
}
|
|
1121
1204
|
async function commitNavigation(path, replace, to, from) {
|
|
1122
1205
|
scrollManager.save(from.path);
|
|
@@ -1211,8 +1294,10 @@ function createRouter(options) {
|
|
|
1211
1294
|
router._abortController?.abort();
|
|
1212
1295
|
const ac = new AbortController();
|
|
1213
1296
|
router._abortController = ac;
|
|
1214
|
-
|
|
1297
|
+
const loaderOutcome = await runLoaders(to, gen, ac);
|
|
1298
|
+
if (loaderOutcome.action !== "continue") {
|
|
1215
1299
|
loadingSignal.update((n) => n - 1);
|
|
1300
|
+
if (loaderOutcome.action === "redirect") return navigate(sanitizePath(loaderOutcome.target), replace, redirectDepth + 1);
|
|
1216
1301
|
return;
|
|
1217
1302
|
}
|
|
1218
1303
|
await commitNavigation(path, replace, to, from);
|
|
@@ -1280,7 +1365,7 @@ function createRouter(options) {
|
|
|
1280
1365
|
isReady() {
|
|
1281
1366
|
return router._readyPromise;
|
|
1282
1367
|
},
|
|
1283
|
-
async preload(path) {
|
|
1368
|
+
async preload(path, request) {
|
|
1284
1369
|
const resolved = resolveRoute(path, routes);
|
|
1285
1370
|
await Promise.all(resolved.matched.map(async (record) => {
|
|
1286
1371
|
if (componentCache.has(record)) return;
|
|
@@ -1295,11 +1380,12 @@ function createRouter(options) {
|
|
|
1295
1380
|
}));
|
|
1296
1381
|
const ac = new AbortController();
|
|
1297
1382
|
await Promise.all(resolved.matched.filter((r) => r.loader).map(async (r) => {
|
|
1298
|
-
const data = await r.loader
|
|
1383
|
+
const data = await Promise.resolve().then(() => r.loader({
|
|
1299
1384
|
params: resolved.params,
|
|
1300
1385
|
query: resolved.query,
|
|
1301
|
-
signal: ac.signal
|
|
1302
|
-
|
|
1386
|
+
signal: ac.signal,
|
|
1387
|
+
...request ? { request } : {}
|
|
1388
|
+
}));
|
|
1303
1389
|
router._loaderData.set(r, data);
|
|
1304
1390
|
}));
|
|
1305
1391
|
},
|
|
@@ -1454,20 +1540,51 @@ const RouterView = (props) => {
|
|
|
1454
1540
|
onUnmount(() => {
|
|
1455
1541
|
router._viewDepth--;
|
|
1456
1542
|
});
|
|
1457
|
-
const
|
|
1458
|
-
router._loadingSignal();
|
|
1543
|
+
const depthEntry = computed(() => {
|
|
1459
1544
|
const route = router.currentRoute();
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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;
|
|
1466
1567
|
if (!isLazy(raw)) {
|
|
1467
|
-
cacheSet(router,
|
|
1468
|
-
return
|
|
1568
|
+
cacheSet(router, rec, raw);
|
|
1569
|
+
return {
|
|
1570
|
+
rec,
|
|
1571
|
+
comp: raw,
|
|
1572
|
+
errored: false,
|
|
1573
|
+
route
|
|
1574
|
+
};
|
|
1469
1575
|
}
|
|
1470
|
-
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);
|
|
1471
1588
|
};
|
|
1472
1589
|
return h("div", { "data-pyreon-router-view": true }, child);
|
|
1473
1590
|
};
|
|
@@ -1491,7 +1608,7 @@ const RouterLink = (props) => {
|
|
|
1491
1608
|
if (prefetchMode === "intent") triggerPrefetch();
|
|
1492
1609
|
};
|
|
1493
1610
|
const inst = router;
|
|
1494
|
-
const href = inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`;
|
|
1611
|
+
const href = () => inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`;
|
|
1495
1612
|
const isExactMatch = () => {
|
|
1496
1613
|
if (!router) return false;
|
|
1497
1614
|
const target = props.to;
|
|
@@ -1525,12 +1642,15 @@ const RouterLink = (props) => {
|
|
|
1525
1642
|
});
|
|
1526
1643
|
onUnmount(() => observer.disconnect());
|
|
1527
1644
|
}
|
|
1528
|
-
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
|
+
};
|
|
1529
1649
|
return h("a", {
|
|
1530
1650
|
...rest,
|
|
1531
1651
|
ref,
|
|
1532
1652
|
href,
|
|
1533
|
-
class:
|
|
1653
|
+
class: mergedClass,
|
|
1534
1654
|
"aria-current": ariaCurrent,
|
|
1535
1655
|
onClick: handleClick,
|
|
1536
1656
|
onMouseEnter: handleMouseEnter,
|
|
@@ -1712,6 +1832,9 @@ function isStaleChunk(err) {
|
|
|
1712
1832
|
if (err instanceof SyntaxError) return true;
|
|
1713
1833
|
return false;
|
|
1714
1834
|
}
|
|
1835
|
+
nativeCompat(RouterProvider);
|
|
1836
|
+
nativeCompat(RouterView);
|
|
1837
|
+
nativeCompat(RouterLink);
|
|
1715
1838
|
|
|
1716
1839
|
//#endregion
|
|
1717
1840
|
//#region src/not-found.ts
|
|
@@ -1764,5 +1887,5 @@ const NotFoundBoundary = (props) => {
|
|
|
1764
1887
|
};
|
|
1765
1888
|
|
|
1766
1889
|
//#endregion
|
|
1767
|
-
export { NotFoundBoundary, RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, isNotFoundError, lazy, notFound, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams, useValidatedSearch };
|
|
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 };
|
|
1768
1891
|
//# sourceMappingURL=index.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as _pyreon_core0 from "@pyreon/core";
|
|
1
|
+
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
2
|
import { ComponentFn, ComponentFn as ComponentFn$1, Props, VNodeChild } from "@pyreon/core";
|
|
3
3
|
import { Computed, Signal } from "@pyreon/reactivity";
|
|
4
4
|
|
|
@@ -111,6 +111,21 @@ interface LoaderContext {
|
|
|
111
111
|
query: Record<string, string>;
|
|
112
112
|
/** Aborted when a newer navigation supersedes this one */
|
|
113
113
|
signal: AbortSignal;
|
|
114
|
+
/**
|
|
115
|
+
* The incoming HTTP `Request` — populated only when the loader runs during
|
|
116
|
+
* SSR (via `prefetchLoaderData`); `undefined` on every CSR navigation.
|
|
117
|
+
* Lets server-side loaders read cookies / auth headers and decide whether
|
|
118
|
+
* to `throw redirect('/login')` BEFORE the layout renders.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* loader: ({ request }) => {
|
|
122
|
+
* const cookie = request?.headers.get('cookie') ?? ''
|
|
123
|
+
* const sid = cookie.match(/sid=([^;]+)/)?.[1]
|
|
124
|
+
* if (!sid) redirect('/login')
|
|
125
|
+
* return { sid }
|
|
126
|
+
* }
|
|
127
|
+
*/
|
|
128
|
+
request?: Request;
|
|
114
129
|
}
|
|
115
130
|
type RouteLoaderFn = (ctx: LoaderContext) => Promise<unknown>;
|
|
116
131
|
interface RouteRecord<TPath extends string = string> {
|
|
@@ -313,7 +328,7 @@ interface Router<TNames extends string = string> {
|
|
|
313
328
|
* separately when creating the router (`createRouter({ url, ... })`) or
|
|
314
329
|
* call this for the same `url` you initialised the router with.
|
|
315
330
|
*/
|
|
316
|
-
preload(path: string): Promise<void>;
|
|
331
|
+
preload(path: string, request?: Request): Promise<void>;
|
|
317
332
|
/**
|
|
318
333
|
* Invalidate cached loader data. Forces loaders to re-run on next navigation.
|
|
319
334
|
* - No args: invalidate ALL cached loader data
|
|
@@ -363,8 +378,18 @@ interface RouterInstance extends Router {
|
|
|
363
378
|
data: unknown;
|
|
364
379
|
timestamp: number;
|
|
365
380
|
}>;
|
|
366
|
-
/**
|
|
367
|
-
|
|
381
|
+
/**
|
|
382
|
+
* In-flight loader dedup: cacheKey → { promise, signal }.
|
|
383
|
+
* Tracking the signal lets dedup skip an in-flight entry whose signal is
|
|
384
|
+
* already aborted — otherwise nav-2 would inherit nav-1's aborted promise
|
|
385
|
+
* (`router.push` aborts the previous nav's controller before starting the
|
|
386
|
+
* next, so back-to-back nav to the same path could resolve nav-2 against
|
|
387
|
+
* nav-1's aborted fetch).
|
|
388
|
+
*/
|
|
389
|
+
_loaderInflight: Map<string, {
|
|
390
|
+
promise: Promise<unknown>;
|
|
391
|
+
signal: AbortSignal;
|
|
392
|
+
}>;
|
|
368
393
|
}
|
|
369
394
|
//#endregion
|
|
370
395
|
//#region src/components.d.ts
|
|
@@ -460,6 +485,50 @@ interface NotFoundBoundaryProps extends Props {
|
|
|
460
485
|
*/
|
|
461
486
|
declare const NotFoundBoundary: ComponentFn<NotFoundBoundaryProps>;
|
|
462
487
|
//#endregion
|
|
488
|
+
//#region src/redirect.d.ts
|
|
489
|
+
/** Standard redirect status codes. 307/308 preserve the request method, 302/303 don't. */
|
|
490
|
+
type RedirectStatus = 301 | 302 | 303 | 307 | 308;
|
|
491
|
+
interface RedirectInfo {
|
|
492
|
+
url: string;
|
|
493
|
+
status: RedirectStatus;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Throw inside a route loader to redirect the navigation server-side
|
|
497
|
+
* (during SSR returns a 302/307 `Location:` response) and client-side
|
|
498
|
+
* (during CSR triggers `router.replace()` before the layout renders).
|
|
499
|
+
*
|
|
500
|
+
* The auth-gate use case: replaces the fragile `onMount + router.push()`
|
|
501
|
+
* workaround. `onMount` doesn't fire reliably under nested-layout dev SSR +
|
|
502
|
+
* hydration — so the layout renders briefly before the push happens, leaking
|
|
503
|
+
* authenticated UI to unauthenticated users. `redirect()` runs in the loader
|
|
504
|
+
* BEFORE the layout's component is invoked, so the unauthenticated UI never
|
|
505
|
+
* mounts in the first place.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* // src/routes/app/_layout.tsx
|
|
510
|
+
* export const loader = async ({ request }) => {
|
|
511
|
+
* const session = await getSession(request)
|
|
512
|
+
* if (!session) redirect('/login')
|
|
513
|
+
* return { user: session.user }
|
|
514
|
+
* }
|
|
515
|
+
* ```
|
|
516
|
+
*
|
|
517
|
+
* @param url - Target URL (typically a path like `/login` or absolute URL for cross-origin).
|
|
518
|
+
* @param status - HTTP redirect status. Default `307` (Temporary Redirect, method-preserving).
|
|
519
|
+
* Use `301`/`308` for permanent moves, `302`/`303` to force GET on the target.
|
|
520
|
+
*/
|
|
521
|
+
declare function redirect(url: string, status?: RedirectStatus): never;
|
|
522
|
+
/** Check if an error is a RedirectError thrown by `redirect()`. */
|
|
523
|
+
declare function isRedirectError(err: unknown): boolean;
|
|
524
|
+
/**
|
|
525
|
+
* Extract the redirect URL and status from a thrown RedirectError. Returns
|
|
526
|
+
* `null` if `err` isn't a RedirectError. Used by the router's loader-runner
|
|
527
|
+
* (CSR) and the SSR handler to convert the thrown error into the right kind
|
|
528
|
+
* of response (a `router.replace()` call or a `302`/`307` Response).
|
|
529
|
+
*/
|
|
530
|
+
declare function getRedirectInfo(err: unknown): RedirectInfo | null;
|
|
531
|
+
//#endregion
|
|
463
532
|
//#region src/loader.d.ts
|
|
464
533
|
/**
|
|
465
534
|
* Returns the data resolved by the current route's `loader` function.
|
|
@@ -478,12 +547,18 @@ declare function useLoaderData<T = unknown>(): T;
|
|
|
478
547
|
* SSR helper: pre-run all loaders for the given path before rendering.
|
|
479
548
|
* Call this before `renderToString` so route components can read data via `useLoaderData()`.
|
|
480
549
|
*
|
|
550
|
+
* The optional `request` is forwarded to each loader's `LoaderContext.request`,
|
|
551
|
+
* letting server-side loaders read cookies / auth headers and `throw redirect()`
|
|
552
|
+
* before the layout renders. A loader that throws `redirect()` propagates the
|
|
553
|
+
* thrown error here — the SSR handler's `catch` converts it into a 302/307
|
|
554
|
+
* `Location:` Response.
|
|
555
|
+
*
|
|
481
556
|
* @example
|
|
482
557
|
* const router = createRouter({ routes, url: req.url })
|
|
483
|
-
* await prefetchLoaderData(router, req.url)
|
|
558
|
+
* await prefetchLoaderData(router, req.url, request)
|
|
484
559
|
* const html = await renderToString(h(App, { router }))
|
|
485
560
|
*/
|
|
486
|
-
declare function prefetchLoaderData(router: RouterInstance, path: string): Promise<void>;
|
|
561
|
+
declare function prefetchLoaderData(router: RouterInstance, path: string, request?: Request): Promise<void>;
|
|
487
562
|
/**
|
|
488
563
|
* Serialize loader data to a JSON-safe plain object for embedding in SSR HTML.
|
|
489
564
|
* Keys are route path patterns (stable across server and client).
|
|
@@ -533,7 +608,7 @@ declare function buildPath(pattern: string, params: Record<string, string>): str
|
|
|
533
608
|
declare function findRouteByName(name: string, routes: RouteRecord[]): RouteRecord | null;
|
|
534
609
|
//#endregion
|
|
535
610
|
//#region src/router.d.ts
|
|
536
|
-
declare const RouterContext: _pyreon_core0.Context<RouterInstance | null>;
|
|
611
|
+
declare const RouterContext: _$_pyreon_core0.Context<RouterInstance | null>;
|
|
537
612
|
declare function useRouter(): Router;
|
|
538
613
|
declare function useRoute<TPath extends string = string>(): () => ResolvedRoute<ExtractParams<TPath> & Record<string, string>, Record<string, string>>;
|
|
539
614
|
/**
|
|
@@ -688,5 +763,5 @@ declare function useTransition(): () => boolean;
|
|
|
688
763
|
declare function useMiddlewareData(): () => Record<string, unknown>;
|
|
689
764
|
declare function createRouter<TNames extends string = string>(options: RouterOptions | RouteRecord[]): Router<TNames>;
|
|
690
765
|
//#endregion
|
|
691
|
-
export { type AfterEachHook, type Blocker, type BlockerFn, type ExtractParams, type LazyComponent, type LoaderContext, type NavigationGuard, type NavigationGuardResult, NotFoundBoundary, type NotFoundBoundaryProps, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteMiddleware, type RouteMiddlewareContext, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, RouterProvider, type RouterProviderProps, RouterView, type RouterViewProps, type ScrollBehaviorFn, buildPath, createRouter, findRouteByName, hydrateLoaderData, isNotFoundError, lazy, notFound, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useIsActive, useLoaderData, useMiddlewareData, useRoute, useRouter, useSearchParams, useTransition, useTypedSearchParams, useValidatedSearch };
|
|
766
|
+
export { type AfterEachHook, type Blocker, type BlockerFn, type ExtractParams, type LazyComponent, type LoaderContext, type NavigationGuard, type NavigationGuardResult, NotFoundBoundary, type NotFoundBoundaryProps, type RedirectStatus, type ResolvedRoute, type RouteComponent, type RouteLoaderFn, type RouteMeta, type RouteMiddleware, type RouteMiddlewareContext, type RouteRecord, type Router, RouterContext, RouterLink, type RouterLinkProps, type RouterOptions, RouterProvider, type RouterProviderProps, RouterView, type RouterViewProps, type ScrollBehaviorFn, 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 };
|
|
692
767
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Official router for Pyreon",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/router#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
+
"!lib/**/*.map",
|
|
17
18
|
"src",
|
|
18
19
|
"README.md",
|
|
19
20
|
"LICENSE"
|
|
@@ -43,9 +44,9 @@
|
|
|
43
44
|
"prepublishOnly": "bun run build"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@pyreon/core": "^0.
|
|
47
|
-
"@pyreon/reactivity": "^0.
|
|
48
|
-
"@pyreon/runtime-dom": "^0.
|
|
47
|
+
"@pyreon/core": "^0.15.0",
|
|
48
|
+
"@pyreon/reactivity": "^0.15.0",
|
|
49
|
+
"@pyreon/runtime-dom": "^0.15.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@happy-dom/global-registrator": "^20.8.9",
|