@native-router/core 1.1.0 → 1.3.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/dist/index.cjs CHANGED
@@ -12,6 +12,43 @@ class NotFoundError extends NativeRouterError {
12
12
  super(`Can't find the path: ${pathname}`);
13
13
  }
14
14
  }
15
+ class RedirectLoopError extends NativeRouterError {
16
+ constructor(target) {
17
+ super(`Redirect loop detected${target ? ` after following redirects to: ${target}` : ''}`);
18
+ }
19
+ }
20
+
21
+ const DEFAULT_MAX_STACK_DEPTH = 100;
22
+
23
+ /** Max redirects followed by {@link resolveEntry} before giving up. */
24
+ const MAX_REDIRECTS = 10;
25
+
26
+ /** Default cache lifetime of {@link preload} results, in milliseconds. */
27
+ const DEFAULT_PRELOAD_TTL = 30_000;
28
+
29
+ /**
30
+ * A location resolved through the route guards, together with the view task
31
+ * of its final target. When guards redirected, `location` is the terminal
32
+ * location and `task` resolves the view of the target route.
33
+ */
34
+
35
+ /**
36
+ * Cache record of {@link preload}: the resolution promise of the
37
+ * prefetched target(every hit within the TTL awaits the very same
38
+ * promise, which also deduplicates concurrent callers) plus its
39
+ * expiry timestamp.
40
+ */
41
+
42
+ /**
43
+ * Bookkeeping every {@link create}d router carries on top of
44
+ * {@link RouterInstance}. Declared in this module(instead of types.ts)
45
+ * to keep the public instance type surface stable:
46
+ * - `baseIndex`: absolute history index of `locationStack[0]`. The
47
+ * physical(window-relative) stack slot of a history entry is
48
+ * `history index - baseIndex`; entries whose slot falls outside the
49
+ * memory window re-resolve lazily when landed on.
50
+ * - `preloadCache`: router-level cache of {@link preload} results.
51
+ */
15
52
 
16
53
  /**
17
54
  * Create a router instance.
@@ -25,28 +62,47 @@ class NotFoundError extends NativeRouterError {
25
62
  */
26
63
  function create(routes, history, resolveView, options) {
27
64
  const [currentGuard, cancelAll] = util.createCurrentGuard();
65
+ const instanceHistory = history;
66
+ const state = instanceHistory.location.state || {};
28
67
  const {
29
- index,
30
- locationStack
68
+ index
31
69
  } = getHistoryState({
32
- history: history
70
+ history: instanceHistory
33
71
  });
34
- const viewStack = new Array(locationStack.length).fill(null);
35
- if (options?.currentView) {
36
- viewStack[index] = options.currentView;
37
- }
38
- return {
72
+ // Restore the session window from the bounded location window in the
73
+ // current entry state. Window-less legacy(1.x index-only) states degrade
74
+ // to a single-entry window aligned with the landed position.
75
+ const locationStack = restoreLocationStack(instanceHistory);
76
+ const baseIndex = state.locationStack?.length ? state.base || 0 :
77
+ // Degraded window: its only entry IS the landed position.
78
+ index;
79
+ const router = {
39
80
  routes: Array.isArray(routes) ? routes : [routes],
40
81
  resolveView,
41
- history: history,
82
+ history: instanceHistory,
42
83
  locationStack,
43
- viewStack,
84
+ // The view stack is window-relative, so it is exactly as long as the
85
+ // location window and stays bounded by maxStackDepth with it.
86
+ viewStack: new Array(locationStack.length).fill(null),
87
+ baseIndex,
88
+ preloadCache: new Map(),
44
89
  currentGuard,
45
90
  cancelAll,
46
91
  errorHandler: util.reject,
47
92
  ...options,
48
- baseUrl: options?.baseUrl || ''
93
+ baseUrl: options?.baseUrl || '',
94
+ maxStackDepth: options?.maxStackDepth || DEFAULT_MAX_STACK_DEPTH
49
95
  };
96
+ if (options?.currentView) {
97
+ const physical = index - baseIndex;
98
+ // Hand-crafted or corrupted state may land the index outside the
99
+ // restored window; skip the write instead of creating a string-keyed
100
+ // property on the array(a negative index would).
101
+ if (physical >= 0 && physical < router.viewStack.length) {
102
+ router.viewStack[physical] = options.currentView;
103
+ }
104
+ }
105
+ return router;
50
106
  }
51
107
  function setOptions(router, options) {
52
108
  return Object.assign(router, options);
@@ -60,18 +116,56 @@ function getLocation({
60
116
  state: state.state
61
117
  };
62
118
  }
63
- function getHistoryState(router) {
119
+
120
+ /**
121
+ * Restore the bounded location window serialized in the current history
122
+ * entry state, as-is: entries before the window start are outside the
123
+ * memory window(see {@link RouterCore.baseIndex}) and re-resolve lazily
124
+ * when landed on. Window-less legacy(1.x index-only) state degrades to a
125
+ * single-entry window.
126
+ */
127
+ function restoreLocationStack(history) {
128
+ const state = history.location.state || {};
129
+ return state.locationStack?.length ? [...state.locationStack] : [getLocation({
130
+ history
131
+ })];
132
+ }
133
+
134
+ /**
135
+ * Serialize the memory window together with the absolute index of its
136
+ * first entry, so a refresh can restore it. The memory window is trimmed
137
+ * on every push, so it is already bounded by `maxStackDepth`; the cap
138
+ * only matters when `maxStackDepth` was lowered via
139
+ * {@link setOptions} after the fact.
140
+ */
141
+ function serializeStack(router) {
64
142
  const {
65
- location
66
- } = router.history;
67
- const state = location.state || {};
143
+ locationStack,
144
+ maxStackDepth
145
+ } = router;
146
+ const windowed = locationStack.length > maxStackDepth ? locationStack.slice(-maxStackDepth) : locationStack;
68
147
  return {
69
- index: state.index || 0,
70
- locationStack: state.locationStack || [getLocation(router)]
148
+ base: router.baseIndex + (locationStack.length - windowed.length),
149
+ locationStack: windowed
71
150
  };
72
151
  }
152
+ function getHistoryState(router) {
153
+ const state = router.history.location.state || {};
154
+ return {
155
+ index: state.index || 0
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Physical(window-relative) view slot of an absolute history index.
161
+ * Slots before the window start(negative) or past its end read as
162
+ * `undefined`, driving the lazy refresh fallback of {@link listen}.
163
+ */
164
+ function viewAt(router, index) {
165
+ return router.viewStack[index - router.baseIndex];
166
+ }
73
167
  function getCurrentView(router) {
74
- return router.viewStack[getHistoryState(router).index];
168
+ return viewAt(router, getHistoryState(router).index);
75
169
  }
76
170
 
77
171
  /**
@@ -171,6 +265,158 @@ function resolveTo(router, to, state) {
171
265
  return resolve(router, location);
172
266
  }
173
267
 
268
+ /**
269
+ * Resolve a location through the route guards(`redirect`/`beforeLoad`).
270
+ *
271
+ * Guards run per matched level from the shallowest to the deepest. A guard
272
+ * returning a path string(redirect) restarts the resolution at the new
273
+ * location — from the shallowest level again, so guards of shallower
274
+ * levels re-run on every hop(keep side-effectful guards idempotent) —
275
+ * carrying the original user state; at most
276
+ * {@link MAX_REDIRECTS 10} redirects are followed before a
277
+ * {@link RedirectLoopError} is thrown. An unmatched pathname keeps the
278
+ * {@link resolve resolve} behavior: the task rejects with a
279
+ * {@link NotFoundError} and is routed through `router.errorHandler`.
280
+ *
281
+ * @group Methods
282
+ * @category Router
283
+ * @param router router instance
284
+ * @param location the location to resolve; the object itself is never
285
+ * mutated — a redirect rebinds the resolution to a new location
286
+ * @returns the terminal location and its resolve task
287
+ */
288
+ async function resolveEntry(router, location) {
289
+ const {
290
+ resolveView,
291
+ errorHandler
292
+ } = router;
293
+ for (let redirects = 0;; redirects++) {
294
+ if (redirects > MAX_REDIRECTS) {
295
+ throw new RedirectLoopError(location.pathname);
296
+ }
297
+ const matched = match(router, location.pathname);
298
+ if (!matched) {
299
+ return {
300
+ location,
301
+ task: Promise.reject(new NotFoundError(location.pathname)).catch(errorHandler)
302
+ };
303
+ }
304
+ let redirected = false;
305
+ for (let i = 0; i < matched.length; i++) {
306
+ const {
307
+ route
308
+ } = matched[i];
309
+ // `redirect` wins over `beforeLoad`; a non-empty string target
310
+ // restarts the resolution at the redirected location.
311
+ const target = route.redirect ?? (
312
+ // eslint-disable-next-line no-await-in-loop -- guards must run in declaration order, sequentially
313
+ await route.beforeLoad?.({
314
+ router,
315
+ location,
316
+ params: mergeMatchedParams(matched, i)
317
+ }));
318
+ if (target) {
319
+ location = toLocation(router, target, location.state);
320
+ redirected = true;
321
+ break;
322
+ }
323
+ }
324
+ // eslint-disable-next-line no-continue -- the redirect loop restarts the outer resolution pass
325
+ if (redirected) continue;
326
+ return {
327
+ location,
328
+ task: resolveView(matched, {
329
+ router,
330
+ location
331
+ }).catch(errorHandler)
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Resolve a target through the route guards(`redirect`/`beforeLoad`) and
338
+ * cache the result at the router level, keyed by `pathname + search`.
339
+ *
340
+ * Within its TTL(`opts.ttl`, default 30s) repeated and concurrent calls
341
+ * return the very same entry promise, so concurrent callers share one
342
+ * resolution(in-flight dedup) and repeated prefetches reuse the resolved
343
+ * view task instead of re-running guards and `resolveView`. A rejected
344
+ * resolution(guard error, redirect loop) is evicted from the cache, so
345
+ * the next call retries it. Committing a navigation({@link commit} or
346
+ * {@link commitReplace}) consumes the entry and evicts its cache slot —
347
+ * a later preload re-resolves fresh state, while callers still holding
348
+ * the old entry keep their references.
349
+ *
350
+ * @group Methods
351
+ * @category Router
352
+ * @param router router instance
353
+ * @param to path string
354
+ * @param opts options; `ttl` is the cache lifetime in milliseconds
355
+ * @returns the terminal location and its resolve task
356
+ */
357
+ function preload(router, to, opts) {
358
+ const cache = preloadCacheOf(router);
359
+ const location = toLocation(router, to);
360
+ const key = preloadLocationKey(location);
361
+ const cached = cache.get(key);
362
+ if (cached && Date.now() < cached.expires) {
363
+ return cached.entry;
364
+ }
365
+ prunePreloadCache(cache);
366
+ const entry = resolveEntry(router, location);
367
+ const record = {
368
+ entry,
369
+ expires: Date.now() + (opts?.ttl ?? DEFAULT_PRELOAD_TTL)
370
+ };
371
+ cache.set(key, record);
372
+ entry.then(resolved => {
373
+ record.terminal = resolved.location;
374
+ }, () => {
375
+ // Never cache a failure: evict the slot(this record only, a newer
376
+ // preload may already have replaced it) so the next call retries.
377
+ if (cache.get(key)?.entry === entry) cache.delete(key);
378
+ });
379
+ return entry;
380
+ }
381
+ function preloadLocationKey(location) {
382
+ return location.pathname + location.search;
383
+ }
384
+
385
+ /**
386
+ * Drop expired records. Runs on every cache write, so distinct prefetched
387
+ * targets never accumulate beyond their TTL in a long session.
388
+ */
389
+ function prunePreloadCache(cache) {
390
+ const now = Date.now();
391
+ cache.forEach((record, key) => {
392
+ if (record.expires <= now) cache.delete(key);
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Evict the cache slots consumed by a committed navigation. A redirecting
398
+ * entry is cached under its pre-redirect key while its terminal location
399
+ * differs, so records whose terminal resolves to the committed location
400
+ * are dropped too; in-flight records(terminal not yet known) are left to
401
+ * the TTL.
402
+ */
403
+ function evictPreloadCache(router, location) {
404
+ const cache = preloadCacheOf(router);
405
+ const key = preloadLocationKey(location);
406
+ cache.delete(key);
407
+ cache.forEach((record, k) => {
408
+ if (record.terminal && preloadLocationKey(record.terminal) === key) {
409
+ cache.delete(k);
410
+ }
411
+ });
412
+ }
413
+ function preloadCacheOf(router) {
414
+ const core = router;
415
+ // create() always seeds the cache; the lazy path keeps hand-built
416
+ // router-shaped objects working.
417
+ return core.preloadCache ??= new Map();
418
+ }
419
+
174
420
  /**
175
421
  * Commit the resolve task and push history.
176
422
  * @group Methods
@@ -180,17 +426,47 @@ function resolveTo(router, to, state) {
180
426
  * @param location the location to resolved
181
427
  */
182
428
  function commit(router, resolvePromise, location) {
429
+ // Wrap the raw task so external callers share the guarded entry
430
+ // pipeline; the entry location is the given one, as-is.
431
+ return pushEntry(router, Promise.resolve({
432
+ location,
433
+ task: resolvePromise
434
+ }), location);
435
+ }
436
+ function pushEntry(router, entryPromise, fromLocation) {
183
437
  const {
184
438
  history
185
439
  } = router;
186
440
  const nextIndex = getHistoryState(router).index + 1;
187
- return commitBase(router, resolvePromise, location, resolvedView => {
188
- router.locationStack = [...router.locationStack.slice(0, nextIndex), location];
189
- router.viewStack = [...router.viewStack.slice(0, nextIndex), resolvedView];
441
+ return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
442
+ const {
443
+ location
444
+ } = entry;
445
+ let next = nextIndex - router.baseIndex;
446
+ if (next < 0 || next > router.locationStack.length) {
447
+ // The current entry sits outside the memory window(a push while an
448
+ // out-of-window lazy refresh is still pending): restart the window
449
+ // at the pushed position. Out-of-window neighbours re-resolve
450
+ // lazily when landed on.
451
+ router.baseIndex = nextIndex;
452
+ router.locationStack = [];
453
+ router.viewStack = [];
454
+ next = 0;
455
+ }
456
+ router.locationStack = [...router.locationStack.slice(0, next), location];
457
+ router.viewStack = [...router.viewStack.slice(0, next), resolvedView];
458
+ // Bound the memory window: evict the oldest entries once the stack
459
+ // outgrows maxStackDepth, shifting the window base along.
460
+ if (router.locationStack.length > router.maxStackDepth) {
461
+ const evicted = router.locationStack.length - router.maxStackDepth;
462
+ router.locationStack = router.locationStack.slice(evicted);
463
+ router.viewStack = router.viewStack.slice(evicted);
464
+ router.baseIndex += evicted;
465
+ }
190
466
  history.push(location, {
191
467
  index: nextIndex,
192
- locationStack: router.locationStack,
193
- state: location.state
468
+ state: location.state,
469
+ ...serializeStack(router)
194
470
  });
195
471
  });
196
472
  }
@@ -204,23 +480,45 @@ function commit(router, resolvePromise, location) {
204
480
  * @param location the location to resolved
205
481
  */
206
482
  function commitReplace(router, resolvePromise, location) {
483
+ return replaceEntry(router, Promise.resolve({
484
+ location,
485
+ task: resolvePromise
486
+ }), location);
487
+ }
488
+ function replaceEntry(router, entryPromise, fromLocation) {
207
489
  const {
208
490
  history
209
491
  } = router;
210
492
  const {
211
493
  index
212
494
  } = getHistoryState(router);
213
- return commitBase(router, resolvePromise, location, resolvedView => {
214
- router.locationStack[index] = location;
215
- router.viewStack[index] = resolvedView;
495
+ return commitBase(router, entryPromise, fromLocation, (resolvedView, entry) => {
496
+ const {
497
+ location
498
+ } = entry;
499
+ const physical = index - router.baseIndex;
500
+ if (physical < 0 || physical >= router.locationStack.length) {
501
+ // The landed entry is outside the memory window(the browser evicted
502
+ // older history past the window, or a window-less legacy state).
503
+ // Restart the window at the landed position — placeholders for the
504
+ // unknown gap slots are gone, so neighbouring out-of-window entries
505
+ // re-resolve lazily on every POP, consistent with the rare
506
+ // browser-evicted paths.
507
+ router.baseIndex = index;
508
+ router.locationStack = [location];
509
+ router.viewStack = [resolvedView];
510
+ } else {
511
+ router.locationStack[physical] = location;
512
+ router.viewStack[physical] = resolvedView;
513
+ }
216
514
  history.replace(location, {
217
515
  index,
218
- locationStack: router.locationStack,
219
- state: location.state
516
+ state: location.state,
517
+ ...serializeStack(router)
220
518
  });
221
519
  });
222
520
  }
223
- function commitBase(router, resolvePromise, location, onResolved) {
521
+ function commitBase(router, entryPromise, location, onResolved) {
224
522
  const {
225
523
  currentGuard,
226
524
  onLoadingChange = util.noop
@@ -231,16 +529,42 @@ function commitBase(router, resolvePromise, location, onResolved) {
231
529
  }
232
530
  router.resolving = location;
233
531
  onLoadingChange('pending');
234
- return currentGuard(resolvePromise).then(onResolved)
235
- // eslint-disable-next-line no-void
236
- .then(() => void onLoadingChange('resolved')).catch(e => {
237
- onLoadingChange('rejected');
238
- throw e;
239
- });
532
+ return (
533
+ // The whole chain — route guards AND the view task — is guarded from
534
+ // the very start: a superseding navigation or a cancel() while slow
535
+ // guards are still running parks this chain forever, exactly like
536
+ // during the view phase.
537
+ currentGuard(entryPromise.then(entry => entry.task.then(resolvedView => ({
538
+ entry,
539
+ resolvedView
540
+ })))).then(({
541
+ entry,
542
+ resolvedView
543
+ }) => {
544
+ onResolved(resolvedView, entry);
545
+ // The navigation consumed this resolution: drop its preload
546
+ // cache slots so a later preload re-resolves fresh state.
547
+ evictPreloadCache(router, entry.location);
548
+ }).then(() => {
549
+ // This chain settled, so it is no longer in flight. Superseded
550
+ // chains park forever, so only the latest chain can reach here:
551
+ // the mark is always ours to clear.
552
+ router.resolving = undefined;
553
+ onLoadingChange('resolved');
554
+ }).catch(e => {
555
+ router.resolving = undefined;
556
+ onLoadingChange('rejected');
557
+ throw e;
558
+ })
559
+ );
240
560
  }
241
561
 
242
562
  /**
243
- * Navigate to a new path.
563
+ * Navigate to a new path. Route guards(`redirect`/`beforeLoad`) run before
564
+ * the view resolves; the history entry is committed on the terminal
565
+ * location when guards redirected. The guard phase is part of the
566
+ * cancelable navigation: a superseding navigate or a `cancel()` while
567
+ * guards are still running discards this navigation.
244
568
  * @group Methods
245
569
  * @category Router
246
570
  * @param router router instance
@@ -249,20 +573,19 @@ function commitBase(router, resolvePromise, location, onResolved) {
249
573
  */
250
574
  function navigate(router, to, state) {
251
575
  const location = toLocation(router, to, state);
252
- const viewPromise = resolve(router, location);
253
- return commit(router, viewPromise, location);
576
+ return pushEntry(router, resolveEntry(router, location), location);
254
577
  }
255
578
 
256
579
  /**
257
- * Refresh the page.
580
+ * Refresh the page. Route guards run before the view resolves; a redirect
581
+ * replaces the current entry with the terminal location.
258
582
  * @group Methods
259
583
  * @category Router
260
584
  * @param router router instance
261
585
  */
262
586
  function refresh(router) {
263
587
  const location = getLocation(router);
264
- const viewPromise = resolve(router, location);
265
- return commitReplace(router, viewPromise, location);
588
+ return replaceEntry(router, resolveEntry(router, location), location);
266
589
  }
267
590
 
268
591
  /**
@@ -317,23 +640,29 @@ function createHref({
317
640
  * @category Router
318
641
  * @param router router instance
319
642
  */
320
- function cancel({
321
- cancelAll,
322
- onLoadingChange = util.noop
323
- }) {
324
- cancelAll();
325
- onLoadingChange();
643
+ function cancel(router) {
644
+ // The cancelled chain parks forever, so nothing else will clear the
645
+ // in-flight mark: drop it here, or a later navigation would fire a
646
+ // spurious cancel signal(`onLoadingChange()`) for a dead resolve.
647
+ router.resolving = undefined;
648
+ router.cancelAll();
649
+ router.onLoadingChange?.();
326
650
  }
651
+
652
+ /**
653
+ * Restore/warm up the view stack by re-resolving every reachable entry
654
+ * of the in-memory location stack. Call it after a refresh: in-window
655
+ * back/forward then switch views without new resolves.
656
+ *
657
+ * 恢复/预热内存栈中的可达条目(窗口内有 location 的槽位),刷新后调用
658
+ * 可让窗口内前进/后退零请求。
659
+ * @group Methods
660
+ * @category Router
661
+ * @param router router instance
662
+ */
327
663
  function initHistoryStack(router) {
328
- const {
329
- history: history$1
330
- } = router;
331
- const {
332
- locationStack
333
- } = getHistoryState(router);
334
- return Promise.all(locationStack.map(l => resolve(router, l))).then(views => {
664
+ return Promise.all(router.locationStack.map(location => resolve(router, location))).then(views => {
335
665
  router.viewStack = views;
336
- history$1.replace(history.createPath(history$1.location), history$1.location.state);
337
666
  });
338
667
  }
339
668
 
@@ -356,13 +685,22 @@ function listen(router, onViewChange) {
356
685
  cancel(router);
357
686
  const state = location.state;
358
687
  const index = state?.index || 0;
359
- const view = router.viewStack[index];
688
+ const view = viewAt(router, index);
360
689
  onViewChange(view);
361
- if (!view) refresh(router);
362
- if (action === 'POP') {
690
+ if (!view) {
691
+ // Lazy fallback for out-of-window slots and window-less legacy
692
+ // state: re-resolving the landed entry also re-serializes the
693
+ // window into it via the replace commit. A guard failure here
694
+ // must not surface as an unhandled rejection — the landed entry
695
+ // simply keeps its(unknown) view.
696
+ refresh(router).catch(util.noop);
697
+ } else if (action === 'POP') {
698
+ // Sync the current window into the landed entry so a later
699
+ // refresh("refresh → back → refresh again") still restores it.
363
700
  history$1.replace(history.createPath(history$1.location), {
364
701
  ...state,
365
- locationStack: router.locationStack
702
+ index,
703
+ ...serializeStack(router)
366
704
  });
367
705
  }
368
706
  });
@@ -374,7 +712,33 @@ function listen(router, onViewChange) {
374
712
  }
375
713
 
376
714
  /**
377
- * Get current route params from router.
715
+ * Merge params of the matched levels. Params of deeper levels override
716
+ * the same keys of shallower ones.
717
+ *
718
+ * When `end` is given, only the levels up to and including `end` are
719
+ * merged — the accumulated params a level at index `end` sees(shallow →
720
+ * current level). Omitting `end` merges every level.
721
+ * @group Methods
722
+ * @category Router
723
+ * @param matched matched route levels, see {@link match}
724
+ * @param end the index of the last level to merge, defaults to the deepest
725
+ * @returns the merged params object
726
+ */
727
+ function mergeMatchedParams(matched, end) {
728
+ return matched.slice(0, end === undefined ? matched.length : end + 1).reduce((params, {
729
+ params: levelParams
730
+ }) => ({
731
+ ...params,
732
+ ...levelParams
733
+ }), {});
734
+ }
735
+
736
+ /**
737
+ * Get current route params from router. The params are re-derived by
738
+ * matching the current entry of {@link RouterInstance.locationStack} so
739
+ * they stay correct even when the view stack holds resolved views
740
+ * (e.g. React elements) instead of match results. Merges params of all
741
+ * matched levels; deeper levels override shallower ones.
378
742
  * @group Methods
379
743
  * @category Router
380
744
  * @param router router instance
@@ -384,13 +748,14 @@ function getParams(router) {
384
748
  const {
385
749
  index
386
750
  } = getHistoryState(router);
387
- const matched = router.viewStack[index];
388
- if (!matched || !matched.length) return {};
389
- return matched[matched.length - 1]?.params ?? {};
751
+ const location = router.locationStack[index - router.baseIndex];
752
+ if (!location) return {};
753
+ return mergeMatchedParams(match(router, location.pathname) ?? []);
390
754
  }
391
755
 
392
756
  exports.NativeRouterError = NativeRouterError;
393
757
  exports.NotFoundError = NotFoundError;
758
+ exports.RedirectLoopError = RedirectLoopError;
394
759
  exports.back = back;
395
760
  exports.cancel = cancel;
396
761
  exports.commit = commit;
@@ -405,9 +770,12 @@ exports.go = go;
405
770
  exports.initHistoryStack = initHistoryStack;
406
771
  exports.listen = listen;
407
772
  exports.match = match;
773
+ exports.mergeMatchedParams = mergeMatchedParams;
408
774
  exports.navigate = navigate;
775
+ exports.preload = preload;
409
776
  exports.refresh = refresh;
410
777
  exports.resolve = resolve;
778
+ exports.resolveEntry = resolveEntry;
411
779
  exports.resolveTo = resolveTo;
412
780
  exports.setOptions = setOptions;
413
781
  exports.toLocation = toLocation;