@uxf/router 11.126.0 → 11.128.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/create-router.js +30 -12
- package/create-router.test.js +52 -0
- package/package.json +3 -3
- package/utils/path-specificity.d.ts +16 -0
- package/utils/path-specificity.js +39 -0
- package/utils/path-specificity.test.d.ts +1 -0
- package/utils/path-specificity.test.js +72 -0
package/README.md
CHANGED
|
@@ -108,6 +108,61 @@ Add configuration to `tsconfig.json`
|
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## routeToUrl
|
|
112
|
+
|
|
113
|
+
Builds a URL from a route name and its params. Path params are substituted into the bracketed segments; anything left over becomes the query string.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
routeToUrl("blog/detail", { id: 12 }); /* "/blog/[id]" -> "/blog/12" */
|
|
117
|
+
routeToUrl("admin/index", { param1: 3 }); /* "/admin?param1=3" */
|
|
118
|
+
routeToUrl("localized-route", { term: "x" }, { locale: "cs" }); /* "/cs/domu?term=x" */
|
|
119
|
+
routeToUrl("index", {}, { shouldBeAbsolute: true }); /* "https://www.uxf.cz/" */
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**A missing required param throws.** Any bracketed segment still present after substitution means the URL would be broken, so it fails loudly rather than shipping a href containing `[id]`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
routeToUrl("blog/detail", {});
|
|
126
|
+
// Error: Missing parameter '[id]' for route 'blog/detail'.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Optional catch-all segments may be omitted entirely.** For a path like `/catch-all-optional/[[...pathParams]]`, the segment is stripped whether the key is absent, `null`, `undefined`, `""` or `[]`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
routeToUrl("optionalCatchAll", {}); /* "/catch-all-optional" */
|
|
133
|
+
routeToUrl("optionalCatchAll", { pathParams: null }); /* "/catch-all-optional" */
|
|
134
|
+
routeToUrl("optionalCatchAll", { pathParams: ["a", "b"] }); /* "/catch-all-optional/a/b" */
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A **required** catch-all (`[...pathParams]`) is not optional: an empty array throws `Parameter 'pathParams' can not be empty array for route '…'`, and omitting the key throws the missing-parameter error above.
|
|
138
|
+
|
|
139
|
+
## getRouteInfo
|
|
140
|
+
|
|
141
|
+
Resolves a pathname back to the route that declared it, or `null` when nothing matches. The result is `RouteInfo` = `{ pathname: string; routeName: string; routeDefinition: RouteDefinition }` — note it carries the pathname, not parsed params.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
getRouteInfo(
|
|
145
|
+
"/blog/12",
|
|
146
|
+
); /* { pathname: "/blog/12", routeName: "blog/detail", routeDefinition: { path: "/blog/[id]", … } } */
|
|
147
|
+
getRouteInfo("/not-a-route"); /* null */
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Candidates are ordered by specificity, not by declaration order** — fewest catch-all segments first, then fewest dynamic segments, then most static segments. A localized route is ranked by its most specific variant. So a static path is never shadowed by a dynamic one declared before it:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const routes = {
|
|
154
|
+
hotelDetail: { path: "/hotel/[hotel-id]" },
|
|
155
|
+
hotelCreate: { path: "/hotel/create-hotel" },
|
|
156
|
+
hotelCatchAll: { path: "/hotel/[...rest]" },
|
|
157
|
+
} as const;
|
|
158
|
+
|
|
159
|
+
getRouteInfo("/hotel/create-hotel")?.routeName; /* "hotelCreate", not "hotelDetail" */
|
|
160
|
+
getRouteInfo("/hotel/12")?.routeName; /* "hotelDetail" */
|
|
161
|
+
getRouteInfo("/hotel/12/a/b")?.routeName; /* "hotelCatchAll" - only when nothing more specific matches */
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`createRouteMatcher` delegates to `getRouteInfo`, so it follows the same ordering — which is what makes active-route detection in navigation and layouts agree with the resolved route.
|
|
165
|
+
|
|
111
166
|
## useQueryParams
|
|
112
167
|
|
|
113
168
|
Hooks live in the client entry (`@app-routes/client`):
|
package/create-router.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRouter = createRouter;
|
|
4
4
|
const empty_object_1 = require("@uxf/core/constants/empty-object");
|
|
5
|
+
const is_empty_1 = require("@uxf/core/utils/is-empty");
|
|
6
|
+
const is_not_empty_1 = require("@uxf/core/utils/is-not-empty");
|
|
5
7
|
const qs_1 = require("@uxf/core/utils/qs");
|
|
6
8
|
const throw_error_1 = require("@uxf/core/utils/throw-error");
|
|
7
9
|
const sitemap_generator_1 = require("./sitemap-generator");
|
|
10
|
+
const path_specificity_1 = require("./utils/path-specificity");
|
|
8
11
|
const path_to_regex_1 = require("./utils/path-to-regex");
|
|
9
12
|
/**
|
|
10
13
|
* Arguments can be:
|
|
@@ -32,8 +35,15 @@ function createRouter(routes, routerOptions) {
|
|
|
32
35
|
: Object.values(routeDefinition.path).map((path) => (0, path_to_regex_1.pathToRegex)(path)),
|
|
33
36
|
},
|
|
34
37
|
}), {});
|
|
38
|
+
const routeNamesBySpecificity = Object.entries(routes)
|
|
39
|
+
.map(([routeName, routeDefinition]) => ({
|
|
40
|
+
routeName,
|
|
41
|
+
specificity: (0, path_specificity_1.getRouteSpecificity)(routeDefinition.path),
|
|
42
|
+
}))
|
|
43
|
+
.sort((a, b) => (0, path_specificity_1.comparePathSpecificity)(a.specificity, b.specificity))
|
|
44
|
+
.map((entry) => entry.routeName);
|
|
35
45
|
const routeToUrl = (...args) => {
|
|
36
|
-
var _a;
|
|
46
|
+
var _a, _b;
|
|
37
47
|
const { route, params, options } = decodeArgs(args);
|
|
38
48
|
let pathname = typeof routes[route].path === "string"
|
|
39
49
|
? routes[route].path
|
|
@@ -51,10 +61,7 @@ function createRouter(routes, routerOptions) {
|
|
|
51
61
|
pathname = pathname.replace(segment, value);
|
|
52
62
|
}
|
|
53
63
|
else if (pathname.includes(catchAllSegmentsOptional)) {
|
|
54
|
-
if ((Array.isArray(value) && value
|
|
55
|
-
value === null ||
|
|
56
|
-
value === undefined ||
|
|
57
|
-
value === "") {
|
|
64
|
+
if ((Array.isArray(value) && (0, is_empty_1.isEmpty)(value)) || value === null || value === undefined || value === "") {
|
|
58
65
|
pathname = pathname.replace(`/${catchAllSegmentsOptional}`, "");
|
|
59
66
|
}
|
|
60
67
|
else if (Array.isArray(value)) {
|
|
@@ -68,7 +75,7 @@ function createRouter(routes, routerOptions) {
|
|
|
68
75
|
else if (pathname.includes(catchAllSegments)) {
|
|
69
76
|
// catch all segments
|
|
70
77
|
if (Array.isArray(value)) {
|
|
71
|
-
if (
|
|
78
|
+
if ((0, is_empty_1.isEmpty)(value)) {
|
|
72
79
|
throw new Error(`Parameter '${key}' can not be empty array for route '${String(route)}'`);
|
|
73
80
|
}
|
|
74
81
|
pathname = pathname.replace(catchAllSegments, value.join("/"));
|
|
@@ -81,20 +88,31 @@ function createRouter(routes, routerOptions) {
|
|
|
81
88
|
restParams[key] = value;
|
|
82
89
|
}
|
|
83
90
|
});
|
|
84
|
-
|
|
91
|
+
// Optional catch-all segments must be stripped even when their key is absent from `params`
|
|
92
|
+
// (the loop above only visits provided keys). Otherwise a literal `/[[...xxx]]` leaks into the
|
|
93
|
+
// URL and Next.js — unable to know the brackets were an unsubstituted placeholder — falls back
|
|
94
|
+
// to a full-document navigation.
|
|
95
|
+
pathname = pathname.replace(/\/\[\[\.\.\.[^\]]+\]\]/g, "");
|
|
96
|
+
// Any bracketed segment still present means a required param was never provided; the resulting
|
|
97
|
+
// URL would be broken, so fail loudly instead of shipping it.
|
|
98
|
+
const firstUnresolved = (_b = pathname.match(/\[\[?\.\.\.[^\]]+\]\]?|\[[^\]]+\]/)) === null || _b === void 0 ? void 0 : _b.at(0);
|
|
99
|
+
if (firstUnresolved) {
|
|
100
|
+
throw new Error(`Missing parameter '${firstUnresolved}' for route '${String(route)}'.`);
|
|
101
|
+
}
|
|
102
|
+
if ((0, is_not_empty_1.isNotEmpty)(Object.keys(restParams))) {
|
|
85
103
|
pathname = `${pathname}?${(0, qs_1.stringify)(restParams, { arrayFormat: "repeat" })}`;
|
|
86
104
|
}
|
|
87
105
|
return (options === null || options === void 0 ? void 0 : options.shouldBeAbsolute) ? `${routerOptions.baseUrl}${pathname}` : pathname;
|
|
88
106
|
};
|
|
89
107
|
const getRouteInfo = (pathname) => {
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
108
|
+
const routeName = routeNamesBySpecificity.find((name) => routesWithRegex[name].regex.some((regex) => regex.test(pathname)));
|
|
109
|
+
if (!routeName) {
|
|
92
110
|
return null;
|
|
93
111
|
}
|
|
94
112
|
return {
|
|
95
113
|
pathname,
|
|
96
|
-
routeName
|
|
97
|
-
routeDefinition:
|
|
114
|
+
routeName,
|
|
115
|
+
routeDefinition: routesWithRegex[routeName],
|
|
98
116
|
};
|
|
99
117
|
};
|
|
100
118
|
return {
|
|
@@ -121,7 +139,7 @@ function createRouter(routes, routerOptions) {
|
|
|
121
139
|
if ((routeInfo === null || routeInfo === void 0 ? void 0 : routeInfo.routeName) !== requiredRouteName) {
|
|
122
140
|
return false;
|
|
123
141
|
}
|
|
124
|
-
if (!requiredParams || Object.keys(requiredParams)
|
|
142
|
+
if (!requiredParams || (0, is_empty_1.isEmpty)(Object.keys(requiredParams))) {
|
|
125
143
|
return true;
|
|
126
144
|
}
|
|
127
145
|
for (const [paramName, paramValue] of Object.entries(requiredParams)) {
|
package/create-router.test.js
CHANGED
|
@@ -99,6 +99,16 @@ const DATA = [
|
|
|
99
99
|
actual: routeToUrl("optionalCatchAllSegments", { pathParams: "", queryParam: "value" }),
|
|
100
100
|
expected: "/catch-all-optional?queryParam=value",
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
// key omitted entirely — the optional segment must still be stripped
|
|
104
|
+
actual: routeToUrl("optionalCatchAllSegments", {}),
|
|
105
|
+
expected: "/catch-all-optional",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
// key omitted but a query param is present
|
|
109
|
+
actual: routeToUrl("optionalCatchAllSegments", { queryParam: "value" }),
|
|
110
|
+
expected: "/catch-all-optional?queryParam=value",
|
|
111
|
+
},
|
|
102
112
|
{
|
|
103
113
|
actual: routeToUrl("routeWithoutParams"),
|
|
104
114
|
expected: "/route-without-params",
|
|
@@ -139,6 +149,14 @@ const DATA = [
|
|
|
139
149
|
test("routeToUrl", () => {
|
|
140
150
|
DATA.map(({ actual, expected }) => expect(actual).toBe(expected));
|
|
141
151
|
});
|
|
152
|
+
test("routeToUrl throws when a required param is missing", () => {
|
|
153
|
+
expect(() =>
|
|
154
|
+
// @ts-expect-error param2 is required, so this call is invalid on purpose — the point is the runtime guard
|
|
155
|
+
routeToUrl("manyParameters", { param1: "value-1" })).toThrow("Missing parameter '[param2]' for route 'manyParameters'.");
|
|
156
|
+
expect(() =>
|
|
157
|
+
// @ts-expect-error pathParams is required, so this call is invalid on purpose — the point is the runtime guard
|
|
158
|
+
routeToUrl("catchAllSegments", {})).toThrow("Missing parameter '[...pathParams]' for route 'catchAllSegments'.");
|
|
159
|
+
});
|
|
142
160
|
test("create route matcher", () => {
|
|
143
161
|
expect(createRouteMatcher("manyParameters", { param1: "value-1" })("/many-parameters/value-1/form/any", {
|
|
144
162
|
param1: "value-1",
|
|
@@ -178,3 +196,37 @@ test("create-router has no client-only imports", () => {
|
|
|
178
196
|
expect(source).not.toMatch(/from "react"/);
|
|
179
197
|
expect(source).not.toMatch(/from "react-dom"/);
|
|
180
198
|
});
|
|
199
|
+
describe("getRouteInfo", () => {
|
|
200
|
+
const { getRouteInfo, createRouteMatcher: matcher } = (0, create_router_1.createRouter)({
|
|
201
|
+
hotelDetail: { path: "/hotel/[hotel-id]" },
|
|
202
|
+
hotelEdit: { path: "/hotel/[hotel-id]/edit" },
|
|
203
|
+
hotelTab: { path: "/hotel/[hotel-id]/[tab]" },
|
|
204
|
+
hotelCreate: { path: "/hotel/create-hotel" },
|
|
205
|
+
hotelCatchAll: { path: "/hotel/[...rest]" },
|
|
206
|
+
}, {});
|
|
207
|
+
it("resolves a static path declared after a matching dynamic one", () => {
|
|
208
|
+
var _a;
|
|
209
|
+
expect((_a = getRouteInfo("/hotel/create-hotel")) === null || _a === void 0 ? void 0 : _a.routeName).toBe("hotelCreate");
|
|
210
|
+
});
|
|
211
|
+
it("resolves dynamic paths", () => {
|
|
212
|
+
var _a, _b, _c;
|
|
213
|
+
expect((_a = getRouteInfo("/hotel/12")) === null || _a === void 0 ? void 0 : _a.routeName).toBe("hotelDetail");
|
|
214
|
+
expect((_b = getRouteInfo("/hotel/12/edit")) === null || _b === void 0 ? void 0 : _b.routeName).toBe("hotelEdit");
|
|
215
|
+
expect((_c = getRouteInfo("/hotel/12/rooms")) === null || _c === void 0 ? void 0 : _c.routeName).toBe("hotelTab");
|
|
216
|
+
});
|
|
217
|
+
it("falls back to a catch-all only when nothing more specific matches", () => {
|
|
218
|
+
var _a;
|
|
219
|
+
expect((_a = getRouteInfo("/hotel/12/a/b")) === null || _a === void 0 ? void 0 : _a.routeName).toBe("hotelCatchAll");
|
|
220
|
+
});
|
|
221
|
+
it("returns null when nothing matches", () => {
|
|
222
|
+
expect(getRouteInfo("/not-a-route")).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
it("exposes the matched route definition", () => {
|
|
225
|
+
var _a;
|
|
226
|
+
expect((_a = getRouteInfo("/hotel/create-hotel")) === null || _a === void 0 ? void 0 : _a.routeDefinition.path).toBe("/hotel/create-hotel");
|
|
227
|
+
});
|
|
228
|
+
it("makes createRouteMatcher agree with getRouteInfo", () => {
|
|
229
|
+
expect(matcher("hotelCreate")("/hotel/create-hotel")).toBe(true);
|
|
230
|
+
expect(matcher("hotelDetail")("/hotel/create-hotel")).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uxf/router",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.128.0",
|
|
4
4
|
"description": "UXF Router",
|
|
5
5
|
"author": "UXFans <dev@uxf.cz>",
|
|
6
6
|
"homepage": "https://gitlab.com/uxf-npm/router#readme",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"true-case-path": "2.2.1"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@uxf/core": "11.
|
|
29
|
+
"@uxf/core": "11.128.0",
|
|
30
30
|
"next": ">= 12",
|
|
31
31
|
"superstruct": "^2.0.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@uxf/core": "11.
|
|
34
|
+
"@uxf/core": "11.128.0",
|
|
35
35
|
"next": "16.3.0",
|
|
36
36
|
"superstruct": "^2.0.2"
|
|
37
37
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PathSpecificity = {
|
|
2
|
+
catchAllSegmentCount: number;
|
|
3
|
+
dynamicSegmentCount: number;
|
|
4
|
+
staticSegmentCount: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function getPathSpecificity(path: string): PathSpecificity;
|
|
7
|
+
/**
|
|
8
|
+
* Orders route candidates from the most to the least specific: fewest catch-all segments first, then
|
|
9
|
+
* fewest dynamic segments, then most static segments. Used so that a pathname resolves to the most
|
|
10
|
+
* specific matching route instead of whichever route happens to be declared first.
|
|
11
|
+
*/
|
|
12
|
+
export declare function comparePathSpecificity(a: PathSpecificity, b: PathSpecificity): number;
|
|
13
|
+
/**
|
|
14
|
+
* Specificity of a route definition's path. A localized route is ranked by its most specific variant.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getRouteSpecificity(path: string | Partial<Record<string, string>>): PathSpecificity;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPathSpecificity = getPathSpecificity;
|
|
4
|
+
exports.comparePathSpecificity = comparePathSpecificity;
|
|
5
|
+
exports.getRouteSpecificity = getRouteSpecificity;
|
|
6
|
+
const is_not_nil_1 = require("@uxf/core/utils/is-not-nil");
|
|
7
|
+
const LEAST_SPECIFIC = {
|
|
8
|
+
catchAllSegmentCount: Number.MAX_SAFE_INTEGER,
|
|
9
|
+
dynamicSegmentCount: Number.MAX_SAFE_INTEGER,
|
|
10
|
+
staticSegmentCount: 0,
|
|
11
|
+
};
|
|
12
|
+
function getPathSpecificity(path) {
|
|
13
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
14
|
+
const dynamicSegments = segments.filter((segment) => segment.startsWith("["));
|
|
15
|
+
const catchAllSegments = dynamicSegments.filter((segment) => segment.startsWith("[...") || segment.startsWith("[[..."));
|
|
16
|
+
return {
|
|
17
|
+
catchAllSegmentCount: catchAllSegments.length,
|
|
18
|
+
dynamicSegmentCount: dynamicSegments.length,
|
|
19
|
+
staticSegmentCount: segments.length - dynamicSegments.length,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Orders route candidates from the most to the least specific: fewest catch-all segments first, then
|
|
24
|
+
* fewest dynamic segments, then most static segments. Used so that a pathname resolves to the most
|
|
25
|
+
* specific matching route instead of whichever route happens to be declared first.
|
|
26
|
+
*/
|
|
27
|
+
function comparePathSpecificity(a, b) {
|
|
28
|
+
return (a.catchAllSegmentCount - b.catchAllSegmentCount ||
|
|
29
|
+
a.dynamicSegmentCount - b.dynamicSegmentCount ||
|
|
30
|
+
b.staticSegmentCount - a.staticSegmentCount);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Specificity of a route definition's path. A localized route is ranked by its most specific variant.
|
|
34
|
+
*/
|
|
35
|
+
function getRouteSpecificity(path) {
|
|
36
|
+
var _a;
|
|
37
|
+
const paths = typeof path === "string" ? [path] : Object.values(path).filter(is_not_nil_1.isNotNil);
|
|
38
|
+
return (_a = paths.map(getPathSpecificity).sort(comparePathSpecificity).at(0)) !== null && _a !== void 0 ? _a : LEAST_SPECIFIC;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_specificity_1 = require("./path-specificity");
|
|
4
|
+
describe("getPathSpecificity", () => {
|
|
5
|
+
it("counts static, dynamic and catch-all segments", () => {
|
|
6
|
+
expect((0, path_specificity_1.getPathSpecificity)("/hotel/create-hotel")).toEqual({
|
|
7
|
+
catchAllSegmentCount: 0,
|
|
8
|
+
dynamicSegmentCount: 0,
|
|
9
|
+
staticSegmentCount: 2,
|
|
10
|
+
});
|
|
11
|
+
expect((0, path_specificity_1.getPathSpecificity)("/hotel/[hotel-id]/edit")).toEqual({
|
|
12
|
+
catchAllSegmentCount: 0,
|
|
13
|
+
dynamicSegmentCount: 1,
|
|
14
|
+
staticSegmentCount: 2,
|
|
15
|
+
});
|
|
16
|
+
expect((0, path_specificity_1.getPathSpecificity)("/hotel/[...rest]")).toEqual({
|
|
17
|
+
catchAllSegmentCount: 1,
|
|
18
|
+
dynamicSegmentCount: 1,
|
|
19
|
+
staticSegmentCount: 1,
|
|
20
|
+
});
|
|
21
|
+
expect((0, path_specificity_1.getPathSpecificity)("/hotel/[[...rest]]")).toEqual({
|
|
22
|
+
catchAllSegmentCount: 1,
|
|
23
|
+
dynamicSegmentCount: 1,
|
|
24
|
+
staticSegmentCount: 1,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it("handles the root path", () => {
|
|
28
|
+
expect((0, path_specificity_1.getPathSpecificity)("/")).toEqual({
|
|
29
|
+
catchAllSegmentCount: 0,
|
|
30
|
+
dynamicSegmentCount: 0,
|
|
31
|
+
staticSegmentCount: 0,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("comparePathSpecificity", () => {
|
|
36
|
+
const sortPaths = (paths) => [...paths].sort((a, b) => (0, path_specificity_1.comparePathSpecificity)((0, path_specificity_1.getPathSpecificity)(a), (0, path_specificity_1.getPathSpecificity)(b)));
|
|
37
|
+
it("puts a static path before a dynamic one", () => {
|
|
38
|
+
expect(sortPaths(["/hotel/[hotel-id]", "/hotel/create-hotel"])).toEqual([
|
|
39
|
+
"/hotel/create-hotel",
|
|
40
|
+
"/hotel/[hotel-id]",
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it("puts catch-all segments last", () => {
|
|
44
|
+
expect(sortPaths(["/hotel/[...rest]", "/hotel/[hotel-id]", "/hotel/detail"])).toEqual([
|
|
45
|
+
"/hotel/detail",
|
|
46
|
+
"/hotel/[hotel-id]",
|
|
47
|
+
"/hotel/[...rest]",
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it("prefers more static segments when the dynamic count is equal", () => {
|
|
51
|
+
expect(sortPaths(["/hotel/[hotel-id]/[tab]", "/hotel/[hotel-id]/edit"])).toEqual([
|
|
52
|
+
"/hotel/[hotel-id]/edit",
|
|
53
|
+
"/hotel/[hotel-id]/[tab]",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("getRouteSpecificity", () => {
|
|
58
|
+
it("accepts a plain path", () => {
|
|
59
|
+
expect((0, path_specificity_1.getRouteSpecificity)("/hotel/[hotel-id]")).toEqual({
|
|
60
|
+
catchAllSegmentCount: 0,
|
|
61
|
+
dynamicSegmentCount: 1,
|
|
62
|
+
staticSegmentCount: 1,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("ranks a localized route by its most specific variant", () => {
|
|
66
|
+
expect((0, path_specificity_1.getRouteSpecificity)({ cs: "/hotel/[hotel-id]", en: "/hotel/detail" })).toEqual({
|
|
67
|
+
catchAllSegmentCount: 0,
|
|
68
|
+
dynamicSegmentCount: 0,
|
|
69
|
+
staticSegmentCount: 2,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|