@pyreon/router 0.3.1 → 0.5.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 +283 -30
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +285 -29
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +118 -5
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components.tsx +2 -1
- package/src/index.ts +12 -1
- package/src/match.ts +94 -28
- package/src/router.ts +336 -25
- package/src/tests/router.test.ts +629 -2
- package/src/types.ts +79 -7
|
@@ -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":"216d545d-1","name":"loader.ts"},{"uid":"216d545d-3","name":"match.ts"},{"uid":"216d545d-5","name":"scroll.ts"},{"uid":"216d545d-7","name":"types.ts"},{"uid":"216d545d-9","name":"router.ts"},{"uid":"216d545d-11","name":"components.tsx"},{"uid":"216d545d-13","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"216d545d-1":{"renderedLength":2855,"gzipLength":1243,"brotliLength":0,"metaUid":"216d545d-0"},"216d545d-3":{"renderedLength":12203,"gzipLength":3691,"brotliLength":0,"metaUid":"216d545d-2"},"216d545d-5":{"renderedLength":1367,"gzipLength":576,"brotliLength":0,"metaUid":"216d545d-4"},"216d545d-7":{"renderedLength":385,"gzipLength":246,"brotliLength":0,"metaUid":"216d545d-6"},"216d545d-9":{"renderedLength":16575,"gzipLength":4649,"brotliLength":0,"metaUid":"216d545d-8"},"216d545d-11":{"renderedLength":6669,"gzipLength":2506,"brotliLength":0,"metaUid":"216d545d-10"},"216d545d-13":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"216d545d-12"}},"nodeMetas":{"216d545d-0":{"id":"/src/loader.ts","moduleParts":{"index.js":"216d545d-1"},"imported":[{"uid":"216d545d-14"}],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-10"}]},"216d545d-2":{"id":"/src/match.ts","moduleParts":{"index.js":"216d545d-3"},"imported":[],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-8"}]},"216d545d-4":{"id":"/src/scroll.ts","moduleParts":{"index.js":"216d545d-5"},"imported":[],"importedBy":[{"uid":"216d545d-8"}]},"216d545d-6":{"id":"/src/types.ts","moduleParts":{"index.js":"216d545d-7"},"imported":[],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-8"}]},"216d545d-8":{"id":"/src/router.ts","moduleParts":{"index.js":"216d545d-9"},"imported":[{"uid":"216d545d-14"},{"uid":"216d545d-15"},{"uid":"216d545d-2"},{"uid":"216d545d-4"},{"uid":"216d545d-6"}],"importedBy":[{"uid":"216d545d-12"},{"uid":"216d545d-10"}]},"216d545d-10":{"id":"/src/components.tsx","moduleParts":{"index.js":"216d545d-11"},"imported":[{"uid":"216d545d-14"},{"uid":"216d545d-0"},{"uid":"216d545d-8"}],"importedBy":[{"uid":"216d545d-12"}]},"216d545d-12":{"id":"/src/index.ts","moduleParts":{"index.js":"216d545d-13"},"imported":[{"uid":"216d545d-10"},{"uid":"216d545d-0"},{"uid":"216d545d-2"},{"uid":"216d545d-8"},{"uid":"216d545d-6"}],"importedBy":[],"isEntry":true},"216d545d-14":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"216d545d-10"},{"uid":"216d545d-0"},{"uid":"216d545d-8"}]},"216d545d-15":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"216d545d-8"}]}},"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
|
@@ -142,18 +142,28 @@ function compileSegment(raw) {
|
|
|
142
142
|
raw,
|
|
143
143
|
isParam: true,
|
|
144
144
|
isSplat: true,
|
|
145
|
+
isOptional: false,
|
|
146
|
+
paramName: raw.slice(1, -1)
|
|
147
|
+
};
|
|
148
|
+
if (raw.endsWith("?") && raw.startsWith(":")) return {
|
|
149
|
+
raw,
|
|
150
|
+
isParam: true,
|
|
151
|
+
isSplat: false,
|
|
152
|
+
isOptional: true,
|
|
145
153
|
paramName: raw.slice(1, -1)
|
|
146
154
|
};
|
|
147
155
|
if (raw.startsWith(":")) return {
|
|
148
156
|
raw,
|
|
149
157
|
isParam: true,
|
|
150
158
|
isSplat: false,
|
|
159
|
+
isOptional: false,
|
|
151
160
|
paramName: raw.slice(1)
|
|
152
161
|
};
|
|
153
162
|
return {
|
|
154
163
|
raw,
|
|
155
164
|
isParam: false,
|
|
156
165
|
isSplat: false,
|
|
166
|
+
isOptional: false,
|
|
157
167
|
paramName: ""
|
|
158
168
|
};
|
|
159
169
|
}
|
|
@@ -185,14 +195,30 @@ function compileRoute(route) {
|
|
|
185
195
|
firstSegment
|
|
186
196
|
};
|
|
187
197
|
}
|
|
198
|
+
/** Expand alias paths into additional compiled entries sharing the original RouteRecord */
|
|
199
|
+
function expandAliases(r, c) {
|
|
200
|
+
if (!r.alias) return [];
|
|
201
|
+
return (Array.isArray(r.alias) ? r.alias : [r.alias]).map((aliasPath) => {
|
|
202
|
+
const { alias: _, ...withoutAlias } = r;
|
|
203
|
+
const ac = compileRoute({
|
|
204
|
+
...withoutAlias,
|
|
205
|
+
path: aliasPath
|
|
206
|
+
});
|
|
207
|
+
ac.children = c.children;
|
|
208
|
+
ac.route = r;
|
|
209
|
+
return ac;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
188
212
|
function compileRoutes(routes) {
|
|
189
213
|
const cached = _compiledCache.get(routes);
|
|
190
214
|
if (cached) return cached;
|
|
191
|
-
const compiled =
|
|
215
|
+
const compiled = [];
|
|
216
|
+
for (const r of routes) {
|
|
192
217
|
const c = compileRoute(r);
|
|
193
218
|
if (r.children && r.children.length > 0) c.children = compileRoutes(r.children);
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
compiled.push(c);
|
|
220
|
+
compiled.push(...expandAliases(r, c));
|
|
221
|
+
}
|
|
196
222
|
_compiledCache.set(routes, compiled);
|
|
197
223
|
return compiled;
|
|
198
224
|
}
|
|
@@ -205,6 +231,9 @@ function getFirstSegment(segments) {
|
|
|
205
231
|
/** Build a FlattenedRoute from segments + metadata */
|
|
206
232
|
function makeFlatEntry(segments, chain, meta, isWildcard) {
|
|
207
233
|
const isStatic = !isWildcard && segments.every((s) => !s.isParam);
|
|
234
|
+
const hasOptional = segments.some((s) => s.isOptional);
|
|
235
|
+
let minSegs = segments.length;
|
|
236
|
+
if (hasOptional) while (minSegs > 0 && segments[minSegs - 1]?.isOptional) minSegs--;
|
|
208
237
|
return {
|
|
209
238
|
segments,
|
|
210
239
|
segmentCount: segments.length,
|
|
@@ -214,7 +243,9 @@ function makeFlatEntry(segments, chain, meta, isWildcard) {
|
|
|
214
243
|
meta,
|
|
215
244
|
firstSegment: getFirstSegment(segments),
|
|
216
245
|
hasSplat: segments.some((s) => s.isSplat),
|
|
217
|
-
isWildcard
|
|
246
|
+
isWildcard,
|
|
247
|
+
hasOptional,
|
|
248
|
+
minSegments: minSegs
|
|
218
249
|
};
|
|
219
250
|
}
|
|
220
251
|
/**
|
|
@@ -305,22 +336,31 @@ function captureSplat(pathParts, from, pathLen) {
|
|
|
305
336
|
}
|
|
306
337
|
return remaining.join("/");
|
|
307
338
|
}
|
|
339
|
+
/** Check whether a flattened route's segment count is compatible with the path length */
|
|
340
|
+
function isSegmentCountCompatible(f, pathLen) {
|
|
341
|
+
if (f.segmentCount === pathLen) return true;
|
|
342
|
+
if (f.hasSplat && pathLen >= f.segmentCount) return true;
|
|
343
|
+
if (f.hasOptional && pathLen >= f.minSegments && pathLen <= f.segmentCount) return true;
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
308
346
|
/** Try to match a flattened route against path parts */
|
|
309
347
|
function matchFlattened(f, pathParts, pathLen) {
|
|
310
|
-
if (f
|
|
311
|
-
if (!f.hasSplat || pathLen < f.segmentCount) return null;
|
|
312
|
-
}
|
|
348
|
+
if (!isSegmentCountCompatible(f, pathLen)) return null;
|
|
313
349
|
const params = {};
|
|
314
350
|
const segments = f.segments;
|
|
315
351
|
const count = f.segmentCount;
|
|
316
352
|
for (let i = 0; i < count; i++) {
|
|
317
353
|
const seg = segments[i];
|
|
318
354
|
const pt = pathParts[i];
|
|
319
|
-
if (!seg
|
|
355
|
+
if (!seg) return null;
|
|
320
356
|
if (seg.isSplat) {
|
|
321
357
|
params[seg.paramName] = captureSplat(pathParts, i, pathLen);
|
|
322
358
|
return params;
|
|
323
359
|
}
|
|
360
|
+
if (pt === void 0) {
|
|
361
|
+
if (!seg.isOptional) return null;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
324
364
|
if (seg.isParam) params[seg.paramName] = decodeSafe(pt);
|
|
325
365
|
else if (seg.raw !== pt) return null;
|
|
326
366
|
}
|
|
@@ -413,7 +453,11 @@ function mergeMeta(matched) {
|
|
|
413
453
|
}
|
|
414
454
|
/** Build a path string from a named route's pattern and params */
|
|
415
455
|
function buildPath(pattern, params) {
|
|
416
|
-
return pattern.replace(
|
|
456
|
+
return pattern.replace(/\/:([^/]+)\?/g, (_match, key) => {
|
|
457
|
+
const val = params[key];
|
|
458
|
+
if (!val) return "";
|
|
459
|
+
return `/${encodeURIComponent(val)}`;
|
|
460
|
+
}).replace(/:([^/]+)\*?/g, (match, key) => {
|
|
417
461
|
const val = params[key] ?? "";
|
|
418
462
|
if (match.endsWith("*")) return val.split("/").map(encodeURIComponent).join("/");
|
|
419
463
|
return encodeURIComponent(val);
|
|
@@ -535,28 +579,134 @@ function useRoute() {
|
|
|
535
579
|
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
536
580
|
return router.currentRoute;
|
|
537
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* In-component guard: called before the component's route is left.
|
|
584
|
+
* Return `false` to cancel, a string to redirect, or `undefined`/`true` to proceed.
|
|
585
|
+
* Automatically removed on component unmount.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* onBeforeRouteLeave((to, from) => {
|
|
589
|
+
* if (hasUnsavedChanges()) return false
|
|
590
|
+
* })
|
|
591
|
+
*/
|
|
592
|
+
function onBeforeRouteLeave(guard) {
|
|
593
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
594
|
+
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
595
|
+
const currentMatched = router.currentRoute().matched;
|
|
596
|
+
const wrappedGuard = (to, from) => {
|
|
597
|
+
if (!from.matched.some((r) => currentMatched.includes(r))) return void 0;
|
|
598
|
+
return guard(to, from);
|
|
599
|
+
};
|
|
600
|
+
const remove = router.beforeEach(wrappedGuard);
|
|
601
|
+
onUnmount(() => remove());
|
|
602
|
+
return remove;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* In-component guard: called when the route changes but the component is reused
|
|
606
|
+
* (e.g. `/user/1` → `/user/2`). Useful for reacting to param changes.
|
|
607
|
+
* Automatically removed on component unmount.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* onBeforeRouteUpdate((to, from) => {
|
|
611
|
+
* if (!isValidId(to.params.id)) return false
|
|
612
|
+
* })
|
|
613
|
+
*/
|
|
614
|
+
function onBeforeRouteUpdate(guard) {
|
|
615
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
616
|
+
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
617
|
+
const currentMatched = router.currentRoute().matched;
|
|
618
|
+
const wrappedGuard = (to, from) => {
|
|
619
|
+
if (!to.matched.some((r) => currentMatched.includes(r))) return void 0;
|
|
620
|
+
return guard(to, from);
|
|
621
|
+
};
|
|
622
|
+
const remove = router.beforeEach(wrappedGuard);
|
|
623
|
+
onUnmount(() => remove());
|
|
624
|
+
return remove;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Register a navigation blocker. The `fn` callback is called before each
|
|
628
|
+
* navigation — return `true` (or resolve to `true`) to block it.
|
|
629
|
+
*
|
|
630
|
+
* Automatically removed on component unmount if called during component setup.
|
|
631
|
+
* Also installs a `beforeunload` handler so the browser shows a confirmation
|
|
632
|
+
* dialog when the user tries to close the tab while a blocker is active.
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* const blocker = useBlocker((to, from) => {
|
|
636
|
+
* return hasUnsavedChanges() && !confirm("Discard changes?")
|
|
637
|
+
* })
|
|
638
|
+
* // later: blocker.remove()
|
|
639
|
+
*/
|
|
640
|
+
function useBlocker(fn) {
|
|
641
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
642
|
+
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
643
|
+
router._blockers.add(fn);
|
|
644
|
+
const beforeUnloadHandler = _isBrowser ? (e) => {
|
|
645
|
+
e.preventDefault();
|
|
646
|
+
} : null;
|
|
647
|
+
if (beforeUnloadHandler) window.addEventListener("beforeunload", beforeUnloadHandler);
|
|
648
|
+
const remove = () => {
|
|
649
|
+
router._blockers.delete(fn);
|
|
650
|
+
if (beforeUnloadHandler) window.removeEventListener("beforeunload", beforeUnloadHandler);
|
|
651
|
+
};
|
|
652
|
+
onUnmount(() => remove());
|
|
653
|
+
return { remove };
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Reactive read/write access to the current route's query parameters.
|
|
657
|
+
*
|
|
658
|
+
* Returns `[get, set]` where `get` is a reactive signal producing the merged
|
|
659
|
+
* query object and `set` navigates to the current path with updated params.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* const [params, setParams] = useSearchParams({ page: "1", sort: "name" })
|
|
663
|
+
* params().page // "1" if not in URL
|
|
664
|
+
* setParams({ page: "2" }) // navigates to ?page=2&sort=name
|
|
665
|
+
*/
|
|
666
|
+
function useSearchParams(defaults) {
|
|
667
|
+
const router = useContext(RouterContext) ?? _activeRouter;
|
|
668
|
+
if (!router) throw new Error("[pyreon-router] No router installed. Wrap your app in <RouterProvider router={router}>.");
|
|
669
|
+
const get = () => {
|
|
670
|
+
const query = router.currentRoute().query;
|
|
671
|
+
if (!defaults) return query;
|
|
672
|
+
return {
|
|
673
|
+
...defaults,
|
|
674
|
+
...query
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
const set = (updates) => {
|
|
678
|
+
const merged = {
|
|
679
|
+
...get(),
|
|
680
|
+
...updates
|
|
681
|
+
};
|
|
682
|
+
const path = router.currentRoute().path + stringifyQuery(merged);
|
|
683
|
+
return router.replace(path);
|
|
684
|
+
};
|
|
685
|
+
return [get, set];
|
|
686
|
+
}
|
|
538
687
|
function createRouter(options) {
|
|
539
688
|
const opts = Array.isArray(options) ? { routes: options } : options;
|
|
540
|
-
const { routes, mode = "hash", scrollBehavior, onError, maxCacheSize = 100 } = opts;
|
|
689
|
+
const { routes, mode = "hash", scrollBehavior, onError, maxCacheSize = 100, trailingSlash = "strip" } = opts;
|
|
690
|
+
const base = mode === "history" ? normalizeBase(opts.base ?? "") : "";
|
|
541
691
|
const nameIndex = buildNameIndex(routes);
|
|
542
692
|
const guards = [];
|
|
543
693
|
const afterHooks = [];
|
|
544
694
|
const scrollManager = new ScrollManager(scrollBehavior);
|
|
545
695
|
let _navGen = 0;
|
|
546
696
|
const getInitialLocation = () => {
|
|
547
|
-
if (opts.url) return opts.url;
|
|
697
|
+
if (opts.url) return stripBase(opts.url, base);
|
|
548
698
|
if (!_isBrowser) return "/";
|
|
549
|
-
if (mode === "history") return window.location.pathname + window.location.search;
|
|
699
|
+
if (mode === "history") return stripBase(window.location.pathname, base) + window.location.search;
|
|
550
700
|
const hash = window.location.hash;
|
|
551
701
|
return hash.startsWith("#") ? hash.slice(1) || "/" : "/";
|
|
552
702
|
};
|
|
553
703
|
const getCurrentLocation = () => {
|
|
554
704
|
if (!_isBrowser) return currentPath();
|
|
555
|
-
if (mode === "history") return window.location.pathname + window.location.search;
|
|
705
|
+
if (mode === "history") return stripBase(window.location.pathname, base) + window.location.search;
|
|
556
706
|
const hash = window.location.hash;
|
|
557
707
|
return hash.startsWith("#") ? hash.slice(1) || "/" : "/";
|
|
558
708
|
};
|
|
559
|
-
const currentPath = signal(getInitialLocation());
|
|
709
|
+
const currentPath = signal(normalizeTrailingSlash(getInitialLocation(), trailingSlash));
|
|
560
710
|
const currentRoute = computed(() => resolveRoute(currentPath(), routes));
|
|
561
711
|
let _popstateHandler = null;
|
|
562
712
|
let _hashchangeHandler = null;
|
|
@@ -612,7 +762,7 @@ function createRouter(options) {
|
|
|
612
762
|
}
|
|
613
763
|
function syncBrowserUrl(path, replace) {
|
|
614
764
|
if (!_isBrowser) return;
|
|
615
|
-
const url = mode === "history" ? path : `#${path}`;
|
|
765
|
+
const url = mode === "history" ? `${base}${path}` : `#${path}`;
|
|
616
766
|
if (replace) window.history.replaceState(null, "", url);
|
|
617
767
|
else window.history.pushState(null, "", url);
|
|
618
768
|
}
|
|
@@ -628,27 +778,53 @@ function createRouter(options) {
|
|
|
628
778
|
if (enterOutcome.action !== "continue") return enterOutcome;
|
|
629
779
|
return runGlobalGuards(guards, to, from, gen);
|
|
630
780
|
}
|
|
631
|
-
async function
|
|
632
|
-
const loadableRecords = to.matched.filter((r) => r.loader);
|
|
633
|
-
if (loadableRecords.length === 0) return true;
|
|
781
|
+
async function runBlockingLoaders(records, to, gen, ac) {
|
|
634
782
|
const loaderCtx = {
|
|
635
783
|
params: to.params,
|
|
636
784
|
query: to.query,
|
|
637
785
|
signal: ac.signal
|
|
638
786
|
};
|
|
639
|
-
const results = await Promise.allSettled(
|
|
640
|
-
if (!r.loader) return Promise.resolve(void 0);
|
|
641
|
-
return r.loader(loaderCtx);
|
|
642
|
-
}));
|
|
787
|
+
const results = await Promise.allSettled(records.map((r) => r.loader ? r.loader(loaderCtx) : Promise.resolve(void 0)));
|
|
643
788
|
if (gen !== _navGen) return false;
|
|
644
|
-
for (let i = 0; i <
|
|
789
|
+
for (let i = 0; i < records.length; i++) {
|
|
645
790
|
const result = results[i];
|
|
646
|
-
const record =
|
|
791
|
+
const record = records[i];
|
|
647
792
|
if (!result || !record) continue;
|
|
648
793
|
if (!processLoaderResult(result, record, ac, to)) return false;
|
|
649
794
|
}
|
|
650
795
|
return true;
|
|
651
796
|
}
|
|
797
|
+
/** Fire-and-forget background revalidation for stale-while-revalidate routes. */
|
|
798
|
+
function revalidateSwrLoaders(records, to, ac) {
|
|
799
|
+
const loaderCtx = {
|
|
800
|
+
params: to.params,
|
|
801
|
+
query: to.query,
|
|
802
|
+
signal: ac.signal
|
|
803
|
+
};
|
|
804
|
+
for (const r of records) {
|
|
805
|
+
if (!r.loader) continue;
|
|
806
|
+
r.loader(loaderCtx).then((data) => {
|
|
807
|
+
if (!ac.signal.aborted) {
|
|
808
|
+
router._loaderData.set(r, data);
|
|
809
|
+
loadingSignal.update((n) => n + 1);
|
|
810
|
+
loadingSignal.update((n) => n - 1);
|
|
811
|
+
}
|
|
812
|
+
}).catch(() => {});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function runLoaders(to, gen, ac) {
|
|
816
|
+
const loadableRecords = to.matched.filter((r) => r.loader);
|
|
817
|
+
if (loadableRecords.length === 0) return true;
|
|
818
|
+
const blocking = [];
|
|
819
|
+
const swr = [];
|
|
820
|
+
for (const r of loadableRecords) if (r.staleWhileRevalidate && router._loaderData.has(r)) swr.push(r);
|
|
821
|
+
else blocking.push(r);
|
|
822
|
+
if (blocking.length > 0) {
|
|
823
|
+
if (!await runBlockingLoaders(blocking, to, gen, ac)) return false;
|
|
824
|
+
}
|
|
825
|
+
if (swr.length > 0) revalidateSwrLoaders(swr, to, ac);
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
652
828
|
function commitNavigation(path, replace, to, from) {
|
|
653
829
|
scrollManager.save(from.path);
|
|
654
830
|
currentPath.set(path);
|
|
@@ -660,8 +836,16 @@ function createRouter(options) {
|
|
|
660
836
|
} catch (_err) {}
|
|
661
837
|
if (_isBrowser) queueMicrotask(() => scrollManager.restore(to, from));
|
|
662
838
|
}
|
|
663
|
-
async function
|
|
839
|
+
async function checkBlockers(to, from, gen) {
|
|
840
|
+
for (const blocker of router._blockers) {
|
|
841
|
+
const blocked = await blocker(to, from);
|
|
842
|
+
if (gen !== _navGen || blocked) return "cancel";
|
|
843
|
+
}
|
|
844
|
+
return "continue";
|
|
845
|
+
}
|
|
846
|
+
async function navigate(rawPath, replace, redirectDepth = 0) {
|
|
664
847
|
if (redirectDepth > 10) return;
|
|
848
|
+
const path = normalizeTrailingSlash(rawPath, trailingSlash);
|
|
665
849
|
const gen = ++_navGen;
|
|
666
850
|
loadingSignal.update((n) => n + 1);
|
|
667
851
|
const to = resolveRoute(path, routes);
|
|
@@ -671,6 +855,10 @@ function createRouter(options) {
|
|
|
671
855
|
loadingSignal.update((n) => n - 1);
|
|
672
856
|
return navigate(redirectTarget, replace, redirectDepth + 1);
|
|
673
857
|
}
|
|
858
|
+
if (await checkBlockers(to, from, gen) !== "continue") {
|
|
859
|
+
loadingSignal.update((n) => n - 1);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
674
862
|
const guardOutcome = await runAllGuards(to, from, gen);
|
|
675
863
|
if (guardOutcome.action !== "continue") {
|
|
676
864
|
loadingSignal.update((n) => n - 1);
|
|
@@ -687,9 +875,14 @@ function createRouter(options) {
|
|
|
687
875
|
commitNavigation(path, replace, to, from);
|
|
688
876
|
loadingSignal.update((n) => n - 1);
|
|
689
877
|
}
|
|
878
|
+
let _readyResolve = null;
|
|
879
|
+
const _readyPromise = new Promise((resolve) => {
|
|
880
|
+
_readyResolve = resolve;
|
|
881
|
+
});
|
|
690
882
|
const router = {
|
|
691
883
|
routes,
|
|
692
884
|
mode,
|
|
885
|
+
_base: base,
|
|
693
886
|
currentRoute,
|
|
694
887
|
_currentPath: currentPath,
|
|
695
888
|
_currentRoute: currentRoute,
|
|
@@ -701,18 +894,28 @@ function createRouter(options) {
|
|
|
701
894
|
_erroredChunks: /* @__PURE__ */ new Set(),
|
|
702
895
|
_loaderData: /* @__PURE__ */ new Map(),
|
|
703
896
|
_abortController: null,
|
|
897
|
+
_blockers: /* @__PURE__ */ new Set(),
|
|
898
|
+
_readyResolve,
|
|
899
|
+
_readyPromise,
|
|
704
900
|
_onError: onError,
|
|
705
901
|
_maxCacheSize: maxCacheSize,
|
|
706
902
|
async push(location) {
|
|
707
|
-
if (typeof location === "string") return navigate(sanitizePath(location), false);
|
|
903
|
+
if (typeof location === "string") return navigate(sanitizePath(resolveRelativePath(location, currentPath())), false);
|
|
708
904
|
return navigate(resolveNamedPath(location.name, location.params ?? {}, location.query ?? {}, nameIndex), false);
|
|
709
905
|
},
|
|
710
|
-
async replace(
|
|
711
|
-
return navigate(sanitizePath(
|
|
906
|
+
async replace(location) {
|
|
907
|
+
if (typeof location === "string") return navigate(sanitizePath(resolveRelativePath(location, currentPath())), true);
|
|
908
|
+
return navigate(resolveNamedPath(location.name, location.params ?? {}, location.query ?? {}, nameIndex), true);
|
|
712
909
|
},
|
|
713
910
|
back() {
|
|
714
911
|
if (_isBrowser) window.history.back();
|
|
715
912
|
},
|
|
913
|
+
forward() {
|
|
914
|
+
if (_isBrowser) window.history.forward();
|
|
915
|
+
},
|
|
916
|
+
go(delta) {
|
|
917
|
+
if (_isBrowser) window.history.go(delta);
|
|
918
|
+
},
|
|
716
919
|
beforeEach(guard) {
|
|
717
920
|
guards.push(guard);
|
|
718
921
|
return () => {
|
|
@@ -728,6 +931,9 @@ function createRouter(options) {
|
|
|
728
931
|
};
|
|
729
932
|
},
|
|
730
933
|
loading: () => loadingSignal() > 0,
|
|
934
|
+
isReady() {
|
|
935
|
+
return router._readyPromise;
|
|
936
|
+
},
|
|
731
937
|
destroy() {
|
|
732
938
|
if (_popstateHandler) {
|
|
733
939
|
window.removeEventListener("popstate", _popstateHandler);
|
|
@@ -739,6 +945,7 @@ function createRouter(options) {
|
|
|
739
945
|
}
|
|
740
946
|
guards.length = 0;
|
|
741
947
|
afterHooks.length = 0;
|
|
948
|
+
router._blockers.clear();
|
|
742
949
|
componentCache.clear();
|
|
743
950
|
router._loaderData.clear();
|
|
744
951
|
router._abortController?.abort();
|
|
@@ -746,6 +953,12 @@ function createRouter(options) {
|
|
|
746
953
|
},
|
|
747
954
|
_resolve: (rawPath) => resolveRoute(rawPath, routes)
|
|
748
955
|
};
|
|
956
|
+
queueMicrotask(() => {
|
|
957
|
+
if (router._readyResolve) {
|
|
958
|
+
router._readyResolve();
|
|
959
|
+
router._readyResolve = null;
|
|
960
|
+
}
|
|
961
|
+
});
|
|
749
962
|
return router;
|
|
750
963
|
}
|
|
751
964
|
async function runGuard(guard, to, from) {
|
|
@@ -763,6 +976,45 @@ function resolveNamedPath(name, params, query, index) {
|
|
|
763
976
|
if (qs) path += `?${qs}`;
|
|
764
977
|
return path;
|
|
765
978
|
}
|
|
979
|
+
/** Normalize a base path: ensure leading `/`, strip trailing `/`. */
|
|
980
|
+
function normalizeBase(raw) {
|
|
981
|
+
if (!raw) return "";
|
|
982
|
+
let b = raw;
|
|
983
|
+
if (!b.startsWith("/")) b = `/${b}`;
|
|
984
|
+
if (b.endsWith("/")) b = b.slice(0, -1);
|
|
985
|
+
return b;
|
|
986
|
+
}
|
|
987
|
+
/** Strip the base prefix from a full URL path. Returns the app-relative path. */
|
|
988
|
+
function stripBase(path, base) {
|
|
989
|
+
if (!base) return path;
|
|
990
|
+
if (path === base || path === `${base}/`) return "/";
|
|
991
|
+
if (path.startsWith(`${base}/`)) return path.slice(base.length);
|
|
992
|
+
return path;
|
|
993
|
+
}
|
|
994
|
+
/** Normalize trailing slash on a path according to the configured strategy. */
|
|
995
|
+
function normalizeTrailingSlash(path, strategy) {
|
|
996
|
+
if (strategy === "ignore" || path === "/") return path;
|
|
997
|
+
const qIdx = path.indexOf("?");
|
|
998
|
+
const hIdx = path.indexOf("#");
|
|
999
|
+
const endIdx = qIdx >= 0 ? qIdx : hIdx >= 0 ? hIdx : path.length;
|
|
1000
|
+
const pathPart = path.slice(0, endIdx);
|
|
1001
|
+
const suffix = path.slice(endIdx);
|
|
1002
|
+
if (strategy === "strip") return pathPart.length > 1 && pathPart.endsWith("/") ? pathPart.slice(0, -1) + suffix : path;
|
|
1003
|
+
return !pathPart.endsWith("/") ? `${pathPart}/${suffix}` : path;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Resolve a relative path (starting with `.` or `..`) against the current path.
|
|
1007
|
+
* Non-relative paths are returned as-is.
|
|
1008
|
+
*/
|
|
1009
|
+
function resolveRelativePath(to, from) {
|
|
1010
|
+
if (!to.startsWith("./") && !to.startsWith("../") && to !== "." && to !== "..") return to;
|
|
1011
|
+
const fromSegments = from.split("/").filter(Boolean);
|
|
1012
|
+
fromSegments.pop();
|
|
1013
|
+
const toSegments = to.split("/").filter(Boolean);
|
|
1014
|
+
for (const seg of toSegments) if (seg === "..") fromSegments.pop();
|
|
1015
|
+
else if (seg !== ".") fromSegments.push(seg);
|
|
1016
|
+
return `/${fromSegments.join("/")}`;
|
|
1017
|
+
}
|
|
766
1018
|
/** Block unsafe navigation targets: javascript/data/vbscript URIs and absolute URLs. */
|
|
767
1019
|
function sanitizePath(path) {
|
|
768
1020
|
const trimmed = path.trim();
|
|
@@ -846,7 +1098,8 @@ const RouterLink = (props) => {
|
|
|
846
1098
|
if (prefetchMode !== "hover" || !router) return;
|
|
847
1099
|
prefetchRoute(router, props.to);
|
|
848
1100
|
};
|
|
849
|
-
const
|
|
1101
|
+
const inst = router;
|
|
1102
|
+
const href = inst?.mode === "history" ? `${inst._base}${props.to}` : `#${props.to}`;
|
|
850
1103
|
const activeClass = () => {
|
|
851
1104
|
if (!router) return "";
|
|
852
1105
|
const current = router.currentRoute().path;
|
|
@@ -968,5 +1221,5 @@ function isStaleChunk(err) {
|
|
|
968
1221
|
}
|
|
969
1222
|
|
|
970
1223
|
//#endregion
|
|
971
|
-
export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useLoaderData, useRoute, useRouter };
|
|
1224
|
+
export { RouterContext, RouterLink, RouterProvider, RouterView, buildPath, createRouter, findRouteByName, hydrateLoaderData, lazy, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, parseQueryMulti, prefetchLoaderData, resolveRoute, serializeLoaderData, stringifyQuery, useBlocker, useLoaderData, useRoute, useRouter, useSearchParams };
|
|
972
1225
|
//# sourceMappingURL=index.js.map
|