@buzola/router 0.0.16 → 0.0.18
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/engine/matcher.d.ts.map +1 -1
- package/dist/engine/matcher.js +15 -12
- package/dist/engine/matcher.js.map +1 -1
- package/dist/engine/navigation-adapter.d.ts +2 -0
- package/dist/engine/navigation-adapter.d.ts.map +1 -1
- package/dist/engine/navigation-adapter.js +19 -5
- package/dist/engine/navigation-adapter.js.map +1 -1
- package/dist/engine/route-tree.js +9 -9
- package/dist/engine/route-tree.js.map +1 -1
- package/dist/engine/router.d.ts +36 -1
- package/dist/engine/router.d.ts.map +1 -1
- package/dist/engine/router.js +77 -1
- package/dist/engine/router.js.map +1 -1
- package/dist/engine/types.d.ts +31 -10
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/utils.d.ts +5 -0
- package/dist/engine/utils.d.ts.map +1 -1
- package/dist/engine/utils.js +13 -0
- package/dist/engine/utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/provider.d.ts +4 -1
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/provider.js +6 -0
- package/dist/react/provider.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/matcher.test.ts +33 -0
- package/src/__tests__/react.test.tsx +66 -0
- package/src/__tests__/router.test.ts +265 -2
- package/src/engine/matcher.ts +16 -17
- package/src/engine/navigation-adapter.ts +21 -6
- package/src/engine/route-tree.ts +9 -9
- package/src/engine/router.ts +89 -3
- package/src/engine/types.ts +33 -4
- package/src/engine/utils.ts +13 -0
- package/src/index.ts +2 -0
- package/src/react/provider.tsx +10 -2
package/dist/engine/utils.js
CHANGED
|
@@ -34,4 +34,17 @@ export function isSamePath(a, b) {
|
|
|
34
34
|
const urlB = typeof b === 'string' ? new URL(b) : b;
|
|
35
35
|
return urlA.origin === urlB.origin && urlA.pathname === urlB.pathname;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether going from `from` to `to` is a fragment navigation: the URLs differ at most in the hash and `to`
|
|
39
|
+
* has one. Browsers always perform those within the current document, whether intercepted or not.
|
|
40
|
+
*/
|
|
41
|
+
export function isFragmentNavigation(from, to) {
|
|
42
|
+
if (to.hash === '')
|
|
43
|
+
return false;
|
|
44
|
+
const fromWithoutHash = new URL(from);
|
|
45
|
+
fromWithoutHash.hash = '';
|
|
46
|
+
const toWithoutHash = new URL(to);
|
|
47
|
+
toWithoutHash.hash = '';
|
|
48
|
+
return fromWithoutHash.href === toWithoutHash.href;
|
|
49
|
+
}
|
|
37
50
|
//# sourceMappingURL=utils.js.map
|
package/dist/engine/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/engine/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACjE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAChD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAA;QAC5C,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,CAAe,EAAE,CAAe;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAA;AACtE,CAAC"}
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/engine/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACjE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;AAC5B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAChD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAA;QAC5C,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,CAAe,EAAE,CAAe;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAA;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAS,EAAE,EAAO;IACtD,IAAI,EAAE,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAA;IAChC,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IACrC,eAAe,CAAC,IAAI,GAAG,EAAE,CAAA;IACzB,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAA;IACjC,aAAa,CAAC,IAAI,GAAG,EAAE,CAAA;IACvB,OAAO,eAAe,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI,CAAA;AACnD,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type { CreateRouteNodeOptions } from './engine/route-tree.js';
|
|
|
6
6
|
export { matchRoutes } from './engine/matcher.js';
|
|
7
7
|
export { createBrowserNavigationAdapter, createMemoryNavigationAdapter } from './engine/navigation-adapter.js';
|
|
8
8
|
export type { MemoryNavigationAdapter, MemoryNavigationAdapterOptions } from './engine/navigation-adapter.js';
|
|
9
|
-
export type { BlockerFn, BuzolaNavigateEvent, BuzolaPageMap, BuzolaPersistentParams, EffectivePageParams, NavigateOptions, NavigationAdapter, PageParams, RegisteredPage, RouteComponent, RouteConfig, RouteMatch, RouteNode, RouterState, RouteTree, StandardSchema, } from './engine/types.js';
|
|
9
|
+
export type { BlockerFn, BuzolaNavigateEvent, BuzolaPageMap, BuzolaPersistentParams, EffectivePageParams, InterceptInfo, NavigateOptions, NavigationAdapter, PageParams, RegisteredPage, RouteComponent, RouteConfig, RouteMatch, RouteNode, RouterState, RouteTree, ShouldInterceptFn, StandardSchema, } from './engine/types.js';
|
|
10
10
|
export { s } from './engine/schema.js';
|
|
11
11
|
export { BuzolaRedirect, createPage, redirect } from './define/create-page.js';
|
|
12
12
|
export type { CatchContext, PageDefinition, PageProps, RedirectFn } from './define/create-page.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAC9G,YAAY,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAG7G,YAAY,EACX,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,cAAc,GACd,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAC9E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAGlG,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxE,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAC9G,YAAY,EAAE,uBAAuB,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAG7G,YAAY,EACX,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,EACT,iBAAiB,EACjB,cAAc,GACd,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAGtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAC9E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAGlG,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA;AACzB,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAGnE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAGnE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAGxE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,8BAA8B,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAyB9G,gFAAgF;AAChF,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAEtC,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAG9E,+EAA+E;AAC/E,OAAO,EACN,cAAc,EACd,aAAa,EACb,IAAI,EACJ,MAAM,EACN,YAAY,EACZ,aAAa,EACb,UAAU,EACV,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,SAAS,EACT,cAAc,EACd,eAAe,GACf,MAAM,kBAAkB,CAAA"}
|
package/dist/react/provider.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Router } from '../engine/router.js';
|
|
3
|
-
import type { RouteMatch, RouteTree } from '../engine/types.js';
|
|
3
|
+
import type { RouteMatch, RouteTree, ShouldInterceptFn } from '../engine/types.js';
|
|
4
4
|
/** Props passed to the middleware component. */
|
|
5
5
|
export interface MiddlewareProps {
|
|
6
6
|
/** Merged params from all matched routes. */
|
|
@@ -21,11 +21,14 @@ export type BuzolaProviderProps = {
|
|
|
21
21
|
routes?: never;
|
|
22
22
|
persistentParams?: never;
|
|
23
23
|
pageRegistry?: never;
|
|
24
|
+
shouldIntercept?: never;
|
|
24
25
|
} | {
|
|
25
26
|
router?: never;
|
|
26
27
|
routes: RouteTree;
|
|
27
28
|
persistentParams?: () => Record<string, string>;
|
|
28
29
|
pageRegistry?: Record<string, string>;
|
|
30
|
+
/** Decides per navigation whether the router handles it; see `RouterOptions.shouldIntercept`. */
|
|
31
|
+
shouldIntercept?: ShouldInterceptFn;
|
|
29
32
|
});
|
|
30
33
|
/**
|
|
31
34
|
* Root provider for Buzola router.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAA;AAElG,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8E,MAAM,OAAO,CAAA;AAElG,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAIlF,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC/B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,2CAA2C;IAC3C,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,uCAAuC;IACvC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CACzB;AAED,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,KAAK,CAAC,aAAa,CAAC,eAAe,CAAC,CAAA;AAEtE,MAAM,MAAM,mBAAmB,GAC5B;IACD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAA;CAChC,GACC,CACC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,gBAAgB,CAAC,EAAE,KAAK,CAAC;IAAC,YAAY,CAAC,EAAE,KAAK,CAAC;IAAC,eAAe,CAAC,EAAE,KAAK,CAAA;CAAE,GAC3G;IACD,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,MAAM,EAAE,SAAS,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,iGAAiG;IACjG,eAAe,CAAC,EAAE,iBAAiB,CAAA;CACnC,CACD,CAAA;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAqD9H"}
|
package/dist/react/provider.js
CHANGED
|
@@ -9,6 +9,11 @@ import { Outlet } from './outlet.js';
|
|
|
9
9
|
* Subscribes to router state and provides context to the React tree.
|
|
10
10
|
*/
|
|
11
11
|
export function BuzolaProvider({ routes, children, middleware: Middleware, ...props }) {
|
|
12
|
+
// The router is created once, so it reads the latest prop through a ref rather than the first render's closure.
|
|
13
|
+
const shouldInterceptRef = useRef(props.shouldIntercept);
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
shouldInterceptRef.current = props.shouldIntercept;
|
|
16
|
+
});
|
|
12
17
|
const routerRef = useRef(props.router);
|
|
13
18
|
if (!routerRef.current) {
|
|
14
19
|
routerRef.current = new Router({
|
|
@@ -16,6 +21,7 @@ export function BuzolaProvider({ routes, children, middleware: Middleware, ...pr
|
|
|
16
21
|
adapter: createBrowserNavigationAdapter(),
|
|
17
22
|
persistentParams: props.persistentParams,
|
|
18
23
|
pageRegistry: props.pageRegistry,
|
|
24
|
+
shouldIntercept: info => shouldInterceptRef.current?.(info) ?? true,
|
|
19
25
|
});
|
|
20
26
|
}
|
|
21
27
|
const router = routerRef.current;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAClG,OAAO,EAAE,8BAA8B,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAE5C,OAAO,EAAE,YAAY,EAA0B,aAAa,EAAE,MAAM,cAAc,CAAA;AAClF,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/react/provider.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -367,3 +367,36 @@ describe('matchRoutes', () => {
|
|
|
367
367
|
expect(matches).toBeNull()
|
|
368
368
|
})
|
|
369
369
|
})
|
|
370
|
+
|
|
371
|
+
describe('matchRoutes with differently named params at the same position', () => {
|
|
372
|
+
const tree = createTestTree([
|
|
373
|
+
{ path: '/app/programs/:programId/sessions/create', component: dummyComponent },
|
|
374
|
+
{ path: '/app/programs/:id/sessions', component: dummyComponent },
|
|
375
|
+
{ path: '/app/programs/:slug/files/:path+', component: dummyComponent },
|
|
376
|
+
])
|
|
377
|
+
|
|
378
|
+
it('names the value by the route that matched, not the one registered first', () => {
|
|
379
|
+
const matches = matchRoutes(tree, new URL('http://localhost/app/programs/42/sessions'))
|
|
380
|
+
expect(matches!.at(-1)!.node.fullPath).toBe('/app/programs/:id/sessions')
|
|
381
|
+
expect(matches!.at(-1)!.params).toEqual({ id: '42' })
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('still gives the first route its own name', () => {
|
|
385
|
+
const matches = matchRoutes(tree, new URL('http://localhost/app/programs/42/sessions/create'))
|
|
386
|
+
expect(matches!.at(-1)!.params).toEqual({ programId: '42' })
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('names values before a catch-all by the matched route too', () => {
|
|
390
|
+
const matches = matchRoutes(tree, new URL('http://localhost/app/programs/42/files/a/b'))
|
|
391
|
+
expect(matches!.at(-1)!.params).toEqual({ slug: '42', path: 'a/b' })
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('does not leak a value from a branch that failed to match', () => {
|
|
395
|
+
const tree = createTestTree([
|
|
396
|
+
{ path: '/a/:x/b', component: dummyComponent },
|
|
397
|
+
{ path: '/a/:y/:z/c', component: dummyComponent },
|
|
398
|
+
])
|
|
399
|
+
const matches = matchRoutes(tree, new URL('http://localhost/a/1/b/c'))
|
|
400
|
+
expect(matches!.at(-1)!.params).toEqual({ y: '1', z: 'b' })
|
|
401
|
+
})
|
|
402
|
+
})
|
|
@@ -1134,3 +1134,69 @@ describe('ErrorBoundary', () => {
|
|
|
1134
1134
|
spy.mockRestore()
|
|
1135
1135
|
})
|
|
1136
1136
|
})
|
|
1137
|
+
|
|
1138
|
+
// ─── BuzolaProvider shouldIntercept ──────────────────────────────────────────
|
|
1139
|
+
|
|
1140
|
+
describe('BuzolaProvider shouldIntercept', () => {
|
|
1141
|
+
/* The provider builds its router on the browser adapter, so stand in for the slice of the
|
|
1142
|
+
Navigation API it uses and raise navigate events by hand. */
|
|
1143
|
+
interface FakeNavigateEvent {
|
|
1144
|
+
destination: { url: string }
|
|
1145
|
+
canIntercept: boolean
|
|
1146
|
+
navigationType: 'push'
|
|
1147
|
+
userInitiated: boolean
|
|
1148
|
+
intercept: () => void
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function withNavigation() {
|
|
1152
|
+
const listeners = new Set<(event: FakeNavigateEvent) => void>()
|
|
1153
|
+
Object.defineProperty(window, 'navigation', {
|
|
1154
|
+
value: {
|
|
1155
|
+
currentEntry: { url: 'http://localhost/', getState: () => undefined },
|
|
1156
|
+
addEventListener: (type: string, listener: (event: FakeNavigateEvent) => void) => listeners.add(listener),
|
|
1157
|
+
removeEventListener: (type: string, listener: (event: FakeNavigateEvent) => void) => listeners.delete(listener),
|
|
1158
|
+
},
|
|
1159
|
+
writable: true,
|
|
1160
|
+
configurable: true,
|
|
1161
|
+
})
|
|
1162
|
+
return function push(url: string) {
|
|
1163
|
+
const intercept = mock(() => {})
|
|
1164
|
+
for (const listener of listeners) {
|
|
1165
|
+
listener({ destination: { url }, canIntercept: true, navigationType: 'push', userInitiated: true, intercept })
|
|
1166
|
+
}
|
|
1167
|
+
return intercept
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
afterEach(() => {
|
|
1172
|
+
Reflect.deleteProperty(window, 'navigation')
|
|
1173
|
+
})
|
|
1174
|
+
|
|
1175
|
+
const routes = buildRouteTree([
|
|
1176
|
+
{ path: '/', component: Home, isIndex: true },
|
|
1177
|
+
{ path: '/about', component: About },
|
|
1178
|
+
])
|
|
1179
|
+
|
|
1180
|
+
it('lets the browser take a navigation the prop declines', () => {
|
|
1181
|
+
const push = withNavigation()
|
|
1182
|
+
render(<BuzolaProvider routes={routes} shouldIntercept={() => false} />)
|
|
1183
|
+
|
|
1184
|
+
expect(push('http://localhost/about')).not.toHaveBeenCalled()
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
it('consults the prop from the latest render', () => {
|
|
1188
|
+
const push = withNavigation()
|
|
1189
|
+
const { rerender } = render(<BuzolaProvider routes={routes} shouldIntercept={() => true} />)
|
|
1190
|
+
|
|
1191
|
+
rerender(<BuzolaProvider routes={routes} shouldIntercept={() => false} />)
|
|
1192
|
+
|
|
1193
|
+
expect(push('http://localhost/about')).not.toHaveBeenCalled()
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
it('intercepts as before without the prop', () => {
|
|
1197
|
+
const push = withNavigation()
|
|
1198
|
+
render(<BuzolaProvider routes={routes} />)
|
|
1199
|
+
|
|
1200
|
+
expect(push('http://localhost/about')).toHaveBeenCalledTimes(1)
|
|
1201
|
+
})
|
|
1202
|
+
})
|
|
@@ -3,7 +3,7 @@ import { matchRoutes } from '../engine/matcher.js'
|
|
|
3
3
|
import { createMemoryNavigationAdapter } from '../engine/navigation-adapter.js'
|
|
4
4
|
import { buildRouteTree } from '../engine/route-tree.js'
|
|
5
5
|
import { Router } from '../engine/router.js'
|
|
6
|
-
import type { BuzolaNavigateEvent, NavigationAdapter, RouteConfig } from '../engine/types.js'
|
|
6
|
+
import type { BuzolaNavigateEvent, InterceptInfo, NavigationAdapter, RouteConfig, ShouldInterceptFn } from '../engine/types.js'
|
|
7
7
|
|
|
8
8
|
function dummyComponent() {
|
|
9
9
|
return null
|
|
@@ -12,7 +12,7 @@ function dummyComponent() {
|
|
|
12
12
|
function createRouter(
|
|
13
13
|
configs: RouteConfig[],
|
|
14
14
|
initialURL = 'http://localhost/',
|
|
15
|
-
options?: { persistentParams?: () => Record<string, string
|
|
15
|
+
options?: { persistentParams?: () => Record<string, string>; shouldIntercept?: ShouldInterceptFn },
|
|
16
16
|
) {
|
|
17
17
|
const routes = buildRouteTree(configs)
|
|
18
18
|
const adapter = createMemoryNavigationAdapter({ initialURL })
|
|
@@ -474,3 +474,266 @@ describe('Router.leaveApp', () => {
|
|
|
474
474
|
expect(router.getState().location.pathname).toBe('/')
|
|
475
475
|
})
|
|
476
476
|
})
|
|
477
|
+
|
|
478
|
+
describe('Router reload', () => {
|
|
479
|
+
const flush = () => new Promise(resolve => setTimeout(resolve, 0))
|
|
480
|
+
|
|
481
|
+
it('lets a reload through to the browser so it loads a fresh document', () => {
|
|
482
|
+
const { adapter } = createRouter([{ path: '/list', component: dummyComponent }], 'http://localhost/list')
|
|
483
|
+
|
|
484
|
+
adapter.reload()
|
|
485
|
+
|
|
486
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
it('asks the blockers first and reloads once they allow it', async () => {
|
|
490
|
+
const { router, adapter } = createRouter([{ path: '/list', component: dummyComponent }], 'http://localhost/list')
|
|
491
|
+
const blocker = mock(async () => true)
|
|
492
|
+
router.addBlocker(blocker)
|
|
493
|
+
|
|
494
|
+
adapter.reload()
|
|
495
|
+
await flush()
|
|
496
|
+
|
|
497
|
+
expect(blocker).toHaveBeenCalledTimes(1)
|
|
498
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('does not reload when a blocker refuses', async () => {
|
|
502
|
+
const { router, adapter } = createRouter([{ path: '/list', component: dummyComponent }], 'http://localhost/list')
|
|
503
|
+
router.addBlocker(async () => false)
|
|
504
|
+
|
|
505
|
+
adapter.reload()
|
|
506
|
+
await flush()
|
|
507
|
+
|
|
508
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('still intercepts a push to the current URL', () => {
|
|
512
|
+
const { router, adapter } = createRouter([{ path: '/list', component: dummyComponent }], 'http://localhost/list')
|
|
513
|
+
const before = router.getState().location
|
|
514
|
+
|
|
515
|
+
router.navigate('/list')
|
|
516
|
+
|
|
517
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
518
|
+
expect(router.getState().location).not.toBe(before)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('Router shouldIntercept', () => {
|
|
523
|
+
const flush = () => new Promise(resolve => setTimeout(resolve, 0))
|
|
524
|
+
const configs: RouteConfig[] = [
|
|
525
|
+
{ path: '/list', component: dummyComponent },
|
|
526
|
+
{ path: '/about', component: dummyComponent },
|
|
527
|
+
]
|
|
528
|
+
|
|
529
|
+
it('intercepts every navigation when the option is not set', () => {
|
|
530
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list')
|
|
531
|
+
|
|
532
|
+
router.navigate('/about')
|
|
533
|
+
router.back()
|
|
534
|
+
|
|
535
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
536
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('intercepts a navigation it allows', () => {
|
|
540
|
+
const { router } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => true })
|
|
541
|
+
|
|
542
|
+
router.navigate('/about')
|
|
543
|
+
|
|
544
|
+
expect(router.getState().location.pathname).toBe('/about')
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('describes the navigation it is asked about', () => {
|
|
548
|
+
const shouldIntercept = mock((info: InterceptInfo) => true)
|
|
549
|
+
const { router } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
550
|
+
|
|
551
|
+
router.navigate('/about?tab=team', { replace: true })
|
|
552
|
+
|
|
553
|
+
const info = shouldIntercept.mock.calls[0][0]
|
|
554
|
+
expect(info.destination.href).toBe('http://localhost/about?tab=team')
|
|
555
|
+
expect(info.from.href).toBe('http://localhost/list')
|
|
556
|
+
expect(info.navigationType).toBe('replace')
|
|
557
|
+
expect(info.userInitiated).toBe(false)
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('lets a declined push through to the browser as a document navigation', () => {
|
|
561
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => false })
|
|
562
|
+
|
|
563
|
+
router.navigate('/about')
|
|
564
|
+
|
|
565
|
+
expect(adapter.getCurrentURL().pathname).toBe('/about')
|
|
566
|
+
// The browser took it: the router neither rendered the destination nor turned it into a reload.
|
|
567
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
568
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
it('lets a declined replace through to the browser as a document navigation', () => {
|
|
572
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => false })
|
|
573
|
+
|
|
574
|
+
router.navigate('/about', { replace: true })
|
|
575
|
+
|
|
576
|
+
expect(adapter.getCurrentURL().pathname).toBe('/about')
|
|
577
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
578
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('reloads at the destination of a declined traversal', () => {
|
|
582
|
+
let intercept = true
|
|
583
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => intercept })
|
|
584
|
+
router.navigate('/about')
|
|
585
|
+
|
|
586
|
+
intercept = false
|
|
587
|
+
router.back()
|
|
588
|
+
|
|
589
|
+
expect(adapter.getCurrentURL().pathname).toBe('/list')
|
|
590
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
591
|
+
expect(router.getState().location.pathname).toBe('/about')
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('intercepts a traversal it allows', () => {
|
|
595
|
+
const shouldIntercept = mock((info: InterceptInfo) => true)
|
|
596
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
597
|
+
router.navigate('/about')
|
|
598
|
+
|
|
599
|
+
router.back()
|
|
600
|
+
|
|
601
|
+
expect(shouldIntercept.mock.calls.at(-1)?.[0].navigationType).toBe('traverse')
|
|
602
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
603
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('is not asked about a reload, which always goes to the browser', () => {
|
|
607
|
+
const shouldIntercept = mock(() => true)
|
|
608
|
+
const { adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
609
|
+
|
|
610
|
+
adapter.reload()
|
|
611
|
+
|
|
612
|
+
expect(shouldIntercept).not.toHaveBeenCalled()
|
|
613
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('is not asked about a fragment navigation, which the browser keeps in the document anyway', () => {
|
|
617
|
+
const shouldIntercept = mock(() => false)
|
|
618
|
+
const { router } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
619
|
+
|
|
620
|
+
router.navigate('/list#details')
|
|
621
|
+
|
|
622
|
+
expect(shouldIntercept).not.toHaveBeenCalled()
|
|
623
|
+
expect(router.getState().location.hash).toBe('#details')
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('is asked when the path changes along with the hash', () => {
|
|
627
|
+
const shouldIntercept = mock(() => false)
|
|
628
|
+
const { router } = createRouter(configs, 'http://localhost/list#details', { shouldIntercept })
|
|
629
|
+
|
|
630
|
+
router.navigate('/about#team')
|
|
631
|
+
|
|
632
|
+
expect(shouldIntercept).toHaveBeenCalledTimes(1)
|
|
633
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('is asked when the hash is dropped, since that loads a document', () => {
|
|
637
|
+
const shouldIntercept = mock(() => false)
|
|
638
|
+
const { router } = createRouter(configs, 'http://localhost/list#details', { shouldIntercept })
|
|
639
|
+
|
|
640
|
+
router.navigate('/list')
|
|
641
|
+
|
|
642
|
+
expect(shouldIntercept).toHaveBeenCalledTimes(1)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('is not asked about a URL handed to the browser with leaveApp', () => {
|
|
646
|
+
const shouldIntercept = mock(() => true)
|
|
647
|
+
const { router } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
648
|
+
|
|
649
|
+
router.leaveApp('/about')
|
|
650
|
+
|
|
651
|
+
expect(shouldIntercept).not.toHaveBeenCalled()
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
it('asks the blockers before a declined push and then loads the destination', async () => {
|
|
655
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => false })
|
|
656
|
+
const blocker = mock(async () => true)
|
|
657
|
+
router.addBlocker(blocker)
|
|
658
|
+
|
|
659
|
+
router.navigate('/about')
|
|
660
|
+
await flush()
|
|
661
|
+
|
|
662
|
+
expect(blocker).toHaveBeenCalledTimes(1)
|
|
663
|
+
expect(adapter.getCurrentURL().pathname).toBe('/about')
|
|
664
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
665
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('does not load the destination of a declined navigation when a blocker refuses', async () => {
|
|
669
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => false })
|
|
670
|
+
router.addBlocker(async () => false)
|
|
671
|
+
|
|
672
|
+
router.navigate('/about')
|
|
673
|
+
await flush()
|
|
674
|
+
|
|
675
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
676
|
+
expect(adapter.getCurrentURL().pathname).toBe('/about')
|
|
677
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('asks the blockers before a declined traversal and then loads the destination', async () => {
|
|
681
|
+
let intercept = true
|
|
682
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => intercept })
|
|
683
|
+
router.navigate('/about')
|
|
684
|
+
await flush()
|
|
685
|
+
const blocker = mock(async () => true)
|
|
686
|
+
router.addBlocker(blocker)
|
|
687
|
+
|
|
688
|
+
intercept = false
|
|
689
|
+
router.back()
|
|
690
|
+
await flush()
|
|
691
|
+
|
|
692
|
+
expect(blocker).toHaveBeenCalledTimes(1)
|
|
693
|
+
expect(adapter.getCurrentURL().pathname).toBe('/list')
|
|
694
|
+
expect(adapter.reloadCount()).toBe(1)
|
|
695
|
+
expect(router.getState().location.pathname).toBe('/about')
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
it('does not load the destination of a declined traversal when a blocker refuses', async () => {
|
|
699
|
+
let intercept = true
|
|
700
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept: () => intercept })
|
|
701
|
+
router.navigate('/about')
|
|
702
|
+
await flush()
|
|
703
|
+
router.addBlocker(async () => false)
|
|
704
|
+
|
|
705
|
+
intercept = false
|
|
706
|
+
router.back()
|
|
707
|
+
await flush()
|
|
708
|
+
|
|
709
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
710
|
+
expect(adapter.getCurrentURL().pathname).toBe('/list')
|
|
711
|
+
expect(router.getState().location.pathname).toBe('/about')
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('treats a fragment of the committed entry as a fragment navigation while a navigation is pending', async () => {
|
|
715
|
+
let intercept = true
|
|
716
|
+
const shouldIntercept = mock(() => intercept)
|
|
717
|
+
const { router, adapter } = createRouter(configs, 'http://localhost/list', { shouldIntercept })
|
|
718
|
+
let blockerCalls = 0
|
|
719
|
+
router.addBlocker(async () => {
|
|
720
|
+
blockerCalls++
|
|
721
|
+
// The first navigation stays pending, like one held by an open unsaved-changes dialog.
|
|
722
|
+
if (blockerCalls === 1) return new Promise<boolean>(() => {})
|
|
723
|
+
return true
|
|
724
|
+
})
|
|
725
|
+
router.navigate('/about')
|
|
726
|
+
await flush()
|
|
727
|
+
expect(adapter.getCurrentURL().pathname).toBe('/about')
|
|
728
|
+
expect(router.getState().location.pathname).toBe('/list')
|
|
729
|
+
|
|
730
|
+
intercept = false
|
|
731
|
+
router.navigate('/about#team')
|
|
732
|
+
await flush()
|
|
733
|
+
|
|
734
|
+
// The browser keeps `/about#team` in the document, so declining it would load nothing.
|
|
735
|
+
expect(shouldIntercept).toHaveBeenCalledTimes(1)
|
|
736
|
+
expect(adapter.reloadCount()).toBe(0)
|
|
737
|
+
expect(router.getState().location.href).toBe('http://localhost/about#team')
|
|
738
|
+
})
|
|
739
|
+
})
|
package/src/engine/matcher.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouteMatch, RouteTree, TrieNode } from './types.js'
|
|
1
|
+
import type { RouteMatch, RouteTree, TrieNode, TrieRoute } from './types.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Match a URL against the route trie.
|
|
@@ -10,20 +10,20 @@ import type { RouteMatch, RouteTree, TrieNode } from './types.js'
|
|
|
10
10
|
export function matchRoutes(tree: RouteTree, url: URL): RouteMatch[] | null {
|
|
11
11
|
const pathname = url.pathname
|
|
12
12
|
const segments = pathname.split('/').filter(Boolean)
|
|
13
|
-
return matchTrie(tree.root, segments, 0,
|
|
13
|
+
return matchTrie(tree.root, segments, 0, [], pathname)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function matchTrie(
|
|
17
17
|
node: TrieNode,
|
|
18
18
|
segments: string[],
|
|
19
19
|
index: number,
|
|
20
|
-
|
|
20
|
+
values: string[],
|
|
21
21
|
pathname: string,
|
|
22
22
|
): RouteMatch[] | null {
|
|
23
23
|
// All segments consumed — check if this trie node has a terminal route
|
|
24
24
|
if (index === segments.length) {
|
|
25
25
|
if (node.route) {
|
|
26
|
-
return buildMatchChain(node.route
|
|
26
|
+
return buildMatchChain(node.route, values, pathname)
|
|
27
27
|
}
|
|
28
28
|
return null
|
|
29
29
|
}
|
|
@@ -33,35 +33,34 @@ function matchTrie(
|
|
|
33
33
|
// 1. Try static child first — O(1) Map lookup, always most specific
|
|
34
34
|
const staticChild = node.staticChildren.get(segment)
|
|
35
35
|
if (staticChild) {
|
|
36
|
-
const result = matchTrie(staticChild, segments, index + 1,
|
|
36
|
+
const result = matchTrie(staticChild, segments, index + 1, values, pathname)
|
|
37
37
|
if (result) return result
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 2. Try dynamic child — matches any single segment
|
|
41
41
|
if (node.dynamicChild) {
|
|
42
|
-
const
|
|
43
|
-
const result = matchTrie(node.dynamicChild.node, segments, index + 1, newParams, pathname)
|
|
42
|
+
const result = matchTrie(node.dynamicChild, segments, index + 1, [...values, decodeURIComponent(segment)], pathname)
|
|
44
43
|
if (result) return result
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
// 3. Try catch-all child — consumes all remaining segments
|
|
48
47
|
if (node.catchAllChild) {
|
|
49
48
|
const remaining = segments.slice(index).map(s => decodeURIComponent(s)).join('/')
|
|
50
|
-
|
|
51
|
-
return buildMatchChain(node.catchAllChild.chain, newParams, pathname)
|
|
49
|
+
return buildMatchChain(node.catchAllChild, [...values, remaining], pathname)
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
return null
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
58
|
-
* Build a RouteMatch[] array
|
|
59
|
-
*
|
|
56
|
+
* Build a RouteMatch[] array for a matched route. Values are collected by position and named here,
|
|
57
|
+
* by the matched route's own pattern, since routes sharing a trie position may name it differently.
|
|
58
|
+
* Each match in the chain gets all params (they're merged by the React layer anyway).
|
|
60
59
|
*/
|
|
61
|
-
function buildMatchChain(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
return chain.map(node => ({ node, params, pathname }))
|
|
60
|
+
function buildMatchChain(route: TrieRoute, values: string[], pathname: string): RouteMatch[] {
|
|
61
|
+
const params: Record<string, string> = {}
|
|
62
|
+
route.paramNames.forEach((name, i) => {
|
|
63
|
+
params[name] = values[i]
|
|
64
|
+
})
|
|
65
|
+
return route.chain.map(node => ({ node, params, pathname }))
|
|
67
66
|
}
|