@devp0nt/route0 1.0.0-next.77 → 1.0.0-next.78
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/cjs/index.cjs +257 -195
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +26 -3
- package/dist/esm/index.d.ts +26 -3
- package/dist/esm/index.js +257 -195
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,112 +1,57 @@
|
|
|
1
1
|
import { parse as parseSearchQuery, stringify as stringifySearchQuery } from "@devp0nt/flat0";
|
|
2
2
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const segments = getRouteSegments(definition);
|
|
9
|
-
return segments.map((segment) => {
|
|
10
|
-
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
11
|
-
if (param) {
|
|
12
|
-
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
13
|
-
}
|
|
14
|
-
if (segment === "*" || segment === "*?") {
|
|
15
|
-
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
16
|
-
}
|
|
17
|
-
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
18
|
-
if (wildcard && !segment.includes("\\*")) {
|
|
19
|
-
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
20
|
-
}
|
|
21
|
-
return { kind: "static", value: segment };
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
const getRouteRegexBaseString = (definition) => {
|
|
25
|
-
const tokens = getRouteTokens(definition);
|
|
26
|
-
if (tokens.length === 0) return "";
|
|
27
|
-
let pattern = "";
|
|
28
|
-
for (const token of tokens) {
|
|
29
|
-
if (token.kind === "static") {
|
|
30
|
-
pattern += `/${escapeRegex(token.value)}`;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (token.kind === "param") {
|
|
34
|
-
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (token.prefix.length > 0) {
|
|
38
|
-
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
39
|
-
} else {
|
|
40
|
-
pattern += "(?:/(.*))?";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return pattern;
|
|
44
|
-
};
|
|
45
|
-
const getRouteCaptureKeys = (definition) => {
|
|
46
|
-
const keys = [];
|
|
47
|
-
for (const token of getRouteTokens(definition)) {
|
|
48
|
-
if (token.kind === "param") keys.push(token.name);
|
|
49
|
-
if (token.kind === "wildcard") keys.push("*");
|
|
50
|
-
}
|
|
51
|
-
return keys;
|
|
52
|
-
};
|
|
53
|
-
const getPathParamsDefinition = (definition) => {
|
|
54
|
-
const entries = getRouteTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
55
|
-
return Object.fromEntries(entries);
|
|
56
|
-
};
|
|
57
|
-
const validateRouteDefinition = (definition) => {
|
|
58
|
-
const segments = getRouteSegments(definition);
|
|
59
|
-
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
60
|
-
if (wildcardSegments.length === 0) return;
|
|
61
|
-
if (wildcardSegments.length > 1) {
|
|
62
|
-
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
63
|
-
}
|
|
64
|
-
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
65
|
-
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
66
|
-
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
67
|
-
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
68
|
-
}
|
|
69
|
-
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
70
|
-
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
const stripTrailingWildcard = (definition) => definition.replace(/\*\??$/, "");
|
|
74
|
-
const normalizeRouteDefinition = (definition) => {
|
|
75
|
-
const value = definition.replace(/\/{2,}/g, "/");
|
|
76
|
-
if (value === "" || value === "/") return "/";
|
|
77
|
-
const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
|
|
3
|
+
const collapseDuplicateSlashes = (value) => value.replace(/\/{2,}/g, "/");
|
|
4
|
+
const normalizeSlashPath = (value) => {
|
|
5
|
+
const collapsed = collapseDuplicateSlashes(value);
|
|
6
|
+
if (collapsed === "" || collapsed === "/") return "/";
|
|
7
|
+
const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
|
|
78
8
|
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
79
9
|
};
|
|
80
|
-
const normalizePathname = (pathname) => {
|
|
81
|
-
return normalizeRouteDefinition(pathname);
|
|
82
|
-
};
|
|
83
|
-
const getNormalizedPathnameFromInput = (hrefOrHrefRelOrLocation) => {
|
|
84
|
-
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
85
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
86
|
-
}
|
|
87
|
-
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
88
|
-
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
89
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
90
|
-
}
|
|
91
|
-
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
92
|
-
}
|
|
93
|
-
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
94
|
-
const base = abs ? void 0 : "http://example.com";
|
|
95
|
-
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
96
|
-
return normalizePathname(url.pathname);
|
|
97
|
-
};
|
|
98
10
|
class Route0 {
|
|
99
11
|
definition;
|
|
100
12
|
params;
|
|
101
13
|
_origin;
|
|
102
14
|
_callable;
|
|
15
|
+
_routeSegments;
|
|
16
|
+
_routeTokens;
|
|
17
|
+
_routePatternCandidates;
|
|
18
|
+
_pathParamsDefinition;
|
|
19
|
+
_definitionWithoutTrailingWildcard;
|
|
20
|
+
_routeRegexBaseStringRaw;
|
|
103
21
|
_regexBaseString;
|
|
104
22
|
_regexString;
|
|
105
23
|
_regex;
|
|
106
24
|
_regexAncestor;
|
|
25
|
+
_regexDescendantMatchers;
|
|
107
26
|
_captureKeys;
|
|
108
27
|
_normalizedDefinition;
|
|
109
28
|
_definitionParts;
|
|
29
|
+
static _getRouteSegments(definition) {
|
|
30
|
+
if (definition === "" || definition === "/") return [];
|
|
31
|
+
return definition.split("/").filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
static _normalizeRouteDefinition(definition) {
|
|
34
|
+
return normalizeSlashPath(definition);
|
|
35
|
+
}
|
|
36
|
+
static _normalizePathname(pathname) {
|
|
37
|
+
return Route0._normalizeRouteDefinition(pathname);
|
|
38
|
+
}
|
|
39
|
+
static _validateRouteDefinition(definition) {
|
|
40
|
+
const segments = Route0._getRouteSegments(definition);
|
|
41
|
+
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
42
|
+
if (wildcardSegments.length === 0) return;
|
|
43
|
+
if (wildcardSegments.length > 1) {
|
|
44
|
+
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
45
|
+
}
|
|
46
|
+
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
47
|
+
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
48
|
+
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
49
|
+
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
50
|
+
}
|
|
51
|
+
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
52
|
+
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
110
55
|
Infer = null;
|
|
111
56
|
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
112
57
|
get origin() {
|
|
@@ -121,10 +66,10 @@ class Route0 {
|
|
|
121
66
|
this._origin = origin;
|
|
122
67
|
}
|
|
123
68
|
constructor(definition, config = {}) {
|
|
124
|
-
const normalizedDefinition =
|
|
125
|
-
|
|
69
|
+
const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
|
|
70
|
+
Route0._validateRouteDefinition(normalizedDefinition);
|
|
126
71
|
this.definition = normalizedDefinition;
|
|
127
|
-
this.params =
|
|
72
|
+
this.params = this.pathParamsDefinition;
|
|
128
73
|
const { origin } = config;
|
|
129
74
|
if (origin && typeof origin === "string" && origin.length) {
|
|
130
75
|
this._origin = origin;
|
|
@@ -153,7 +98,7 @@ class Route0 {
|
|
|
153
98
|
return definition.clone(config);
|
|
154
99
|
}
|
|
155
100
|
const original = new Route0(
|
|
156
|
-
|
|
101
|
+
Route0._normalizeRouteDefinition(definition),
|
|
157
102
|
config
|
|
158
103
|
);
|
|
159
104
|
return original._callable;
|
|
@@ -168,23 +113,19 @@ class Route0 {
|
|
|
168
113
|
return definition;
|
|
169
114
|
}
|
|
170
115
|
const original = typeof definition === "object" ? definition : new Route0(
|
|
171
|
-
|
|
116
|
+
Route0._normalizeRouteDefinition(definition)
|
|
172
117
|
);
|
|
173
118
|
return original._callable;
|
|
174
119
|
}
|
|
175
120
|
static _getAbsPath(origin, url) {
|
|
176
121
|
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
177
122
|
}
|
|
178
|
-
static _getParamsDefinitionByDefinition(definition) {
|
|
179
|
-
return getPathParamsDefinition(definition);
|
|
180
|
-
}
|
|
181
123
|
search() {
|
|
182
124
|
return this._callable;
|
|
183
125
|
}
|
|
184
126
|
/** Extends the current route definition by appending a suffix route. */
|
|
185
127
|
extend(suffixDefinition) {
|
|
186
|
-
const
|
|
187
|
-
const definition = normalizeRouteDefinition(`${sourceDefinitionWithoutWildcard}/${suffixDefinition}`);
|
|
128
|
+
const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
|
|
188
129
|
return Route0.create(
|
|
189
130
|
definition,
|
|
190
131
|
{
|
|
@@ -261,7 +202,7 @@ class Route0 {
|
|
|
261
202
|
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
262
203
|
const searchString = stringifySearchQuery(searchInput);
|
|
263
204
|
url = [url, searchString].filter(Boolean).join("?");
|
|
264
|
-
url = url
|
|
205
|
+
url = collapseDuplicateSlashes(url);
|
|
265
206
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
266
207
|
if (hashInput !== void 0) {
|
|
267
208
|
url = `${url}#${hashInput}`;
|
|
@@ -273,7 +214,7 @@ class Route0 {
|
|
|
273
214
|
return Object.keys(this.params);
|
|
274
215
|
}
|
|
275
216
|
getTokens() {
|
|
276
|
-
return
|
|
217
|
+
return this.routeTokens.map((token) => ({ ...token }));
|
|
277
218
|
}
|
|
278
219
|
/** Clones route with optional config override. */
|
|
279
220
|
clone(config) {
|
|
@@ -281,7 +222,7 @@ class Route0 {
|
|
|
281
222
|
}
|
|
282
223
|
get regexBaseString() {
|
|
283
224
|
if (this._regexBaseString === void 0) {
|
|
284
|
-
this._regexBaseString =
|
|
225
|
+
this._regexBaseString = this.routeRegexBaseStringRaw.replace(/\/+$/, "") + "/?";
|
|
285
226
|
}
|
|
286
227
|
return this._regexBaseString;
|
|
287
228
|
}
|
|
@@ -303,12 +244,132 @@ class Route0 {
|
|
|
303
244
|
}
|
|
304
245
|
return this._regexAncestor;
|
|
305
246
|
}
|
|
247
|
+
get regexDescendantMatchers() {
|
|
248
|
+
if (this._regexDescendantMatchers === void 0) {
|
|
249
|
+
const matchers = [];
|
|
250
|
+
if (this.definitionParts[0] !== "/") {
|
|
251
|
+
let pattern = "";
|
|
252
|
+
const captureKeys = [];
|
|
253
|
+
for (const part of this.definitionParts) {
|
|
254
|
+
if (part.startsWith(":")) {
|
|
255
|
+
pattern += "/([^/]+)";
|
|
256
|
+
captureKeys.push(part.replace(/^:/, "").replace(/\?$/, ""));
|
|
257
|
+
} else if (part.includes("*")) {
|
|
258
|
+
const prefix = part.replace(/\*\??$/, "");
|
|
259
|
+
pattern += `/${escapeRegex(prefix)}[^/]*`;
|
|
260
|
+
captureKeys.push("*");
|
|
261
|
+
} else {
|
|
262
|
+
pattern += `/${escapeRegex(part)}`;
|
|
263
|
+
}
|
|
264
|
+
matchers.push({
|
|
265
|
+
regex: new RegExp(`^${pattern}/?$`),
|
|
266
|
+
captureKeys: [...captureKeys]
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this._regexDescendantMatchers = matchers;
|
|
271
|
+
}
|
|
272
|
+
return this._regexDescendantMatchers;
|
|
273
|
+
}
|
|
306
274
|
get captureKeys() {
|
|
307
275
|
if (this._captureKeys === void 0) {
|
|
308
|
-
this._captureKeys =
|
|
276
|
+
this._captureKeys = this.routeTokens.filter((token) => token.kind !== "static").map((token) => token.kind === "param" ? token.name : "*");
|
|
309
277
|
}
|
|
310
278
|
return this._captureKeys;
|
|
311
279
|
}
|
|
280
|
+
get routeSegments() {
|
|
281
|
+
if (this._routeSegments === void 0) {
|
|
282
|
+
this._routeSegments = Route0._getRouteSegments(this.definition);
|
|
283
|
+
}
|
|
284
|
+
return this._routeSegments;
|
|
285
|
+
}
|
|
286
|
+
get routeTokens() {
|
|
287
|
+
if (this._routeTokens === void 0) {
|
|
288
|
+
this._routeTokens = this.routeSegments.map((segment) => {
|
|
289
|
+
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
290
|
+
if (param) {
|
|
291
|
+
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
292
|
+
}
|
|
293
|
+
if (segment === "*" || segment === "*?") {
|
|
294
|
+
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
295
|
+
}
|
|
296
|
+
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
297
|
+
if (wildcard && !segment.includes("\\*")) {
|
|
298
|
+
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
299
|
+
}
|
|
300
|
+
return { kind: "static", value: segment };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return this._routeTokens;
|
|
304
|
+
}
|
|
305
|
+
get routePatternCandidates() {
|
|
306
|
+
if (this._routePatternCandidates === void 0) {
|
|
307
|
+
const values = (token) => {
|
|
308
|
+
if (token.kind === "static") return [token.value];
|
|
309
|
+
if (token.kind === "param") return token.optional ? ["", "x", "y"] : ["x", "y"];
|
|
310
|
+
if (token.prefix.length > 0)
|
|
311
|
+
return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x`, `${token.prefix}/y/z`];
|
|
312
|
+
return ["", "x", "y", "x/y"];
|
|
313
|
+
};
|
|
314
|
+
let acc = [""];
|
|
315
|
+
for (const token of this.routeTokens) {
|
|
316
|
+
const next = [];
|
|
317
|
+
for (const base of acc) {
|
|
318
|
+
for (const value of values(token)) {
|
|
319
|
+
if (value === "") {
|
|
320
|
+
next.push(base);
|
|
321
|
+
} else if (value.startsWith("/")) {
|
|
322
|
+
next.push(`${base}${value}`);
|
|
323
|
+
} else {
|
|
324
|
+
next.push(`${base}/${value}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
acc = next.length > 512 ? next.slice(0, 512) : next;
|
|
329
|
+
}
|
|
330
|
+
this._routePatternCandidates = acc.length === 0 ? ["/"] : Array.from(new Set(acc.map((x) => x === "" ? "/" : collapseDuplicateSlashes(x))));
|
|
331
|
+
}
|
|
332
|
+
return this._routePatternCandidates;
|
|
333
|
+
}
|
|
334
|
+
get pathParamsDefinition() {
|
|
335
|
+
if (this._pathParamsDefinition === void 0) {
|
|
336
|
+
const entries = this.routeTokens.filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
337
|
+
this._pathParamsDefinition = Object.fromEntries(entries);
|
|
338
|
+
}
|
|
339
|
+
return this._pathParamsDefinition;
|
|
340
|
+
}
|
|
341
|
+
get definitionWithoutTrailingWildcard() {
|
|
342
|
+
if (this._definitionWithoutTrailingWildcard === void 0) {
|
|
343
|
+
this._definitionWithoutTrailingWildcard = this.definition.replace(/\*\??$/, "");
|
|
344
|
+
}
|
|
345
|
+
return this._definitionWithoutTrailingWildcard;
|
|
346
|
+
}
|
|
347
|
+
get routeRegexBaseStringRaw() {
|
|
348
|
+
if (this._routeRegexBaseStringRaw === void 0) {
|
|
349
|
+
if (this.routeTokens.length === 0) {
|
|
350
|
+
this._routeRegexBaseStringRaw = "";
|
|
351
|
+
} else {
|
|
352
|
+
let pattern = "";
|
|
353
|
+
for (const token of this.routeTokens) {
|
|
354
|
+
if (token.kind === "static") {
|
|
355
|
+
pattern += `/${escapeRegex(token.value)}`;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (token.kind === "param") {
|
|
359
|
+
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (token.prefix.length > 0) {
|
|
363
|
+
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
364
|
+
} else {
|
|
365
|
+
pattern += "(?:/(.*))?";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this._routeRegexBaseStringRaw = pattern;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return this._routeRegexBaseStringRaw;
|
|
372
|
+
}
|
|
312
373
|
get normalizedDefinition() {
|
|
313
374
|
if (this._normalizedDefinition === void 0) {
|
|
314
375
|
this._normalizedDefinition = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
@@ -323,11 +384,11 @@ class Route0 {
|
|
|
323
384
|
}
|
|
324
385
|
/** Fast pathname exact match check without building a full location object. */
|
|
325
386
|
isExactPathnameMatch(pathname) {
|
|
326
|
-
return this.regex.test(
|
|
387
|
+
return this.regex.test(Route0._normalizePathname(pathname));
|
|
327
388
|
}
|
|
328
389
|
/** Fast pathname exact or ancestor match check without building a full location object. */
|
|
329
390
|
isExactOrAncestorPathnameMatch(pathname) {
|
|
330
|
-
const normalizedPathname =
|
|
391
|
+
const normalizedPathname = Route0._normalizePathname(pathname);
|
|
331
392
|
return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
|
|
332
393
|
}
|
|
333
394
|
/** Creates a grouped regex pattern string from many routes. */
|
|
@@ -377,10 +438,9 @@ class Route0 {
|
|
|
377
438
|
const base = abs ? void 0 : "http://example.com";
|
|
378
439
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
379
440
|
const search = parseSearchQuery(url.search);
|
|
380
|
-
const
|
|
381
|
-
const hrefRel = pathname + url.search + url.hash;
|
|
441
|
+
const hrefRel = url.pathname + url.search + url.hash;
|
|
382
442
|
const location = {
|
|
383
|
-
pathname,
|
|
443
|
+
pathname: url.pathname,
|
|
384
444
|
search,
|
|
385
445
|
searchString: url.search,
|
|
386
446
|
hash: url.hash,
|
|
@@ -413,9 +473,8 @@ class Route0 {
|
|
|
413
473
|
const location = Route0.getLocation(hrefOrHrefRelOrLocation);
|
|
414
474
|
location.route = this.definition;
|
|
415
475
|
location.params = {};
|
|
416
|
-
const pathname =
|
|
476
|
+
const pathname = Route0._normalizePathname(location.pathname);
|
|
417
477
|
const paramNames = this.captureKeys;
|
|
418
|
-
const defParts = this.definitionParts;
|
|
419
478
|
const exactRe = this.regex;
|
|
420
479
|
const ancestorRe = this.regexAncestor;
|
|
421
480
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -435,46 +494,25 @@ class Route0 {
|
|
|
435
494
|
} else {
|
|
436
495
|
location.params = {};
|
|
437
496
|
}
|
|
438
|
-
|
|
439
|
-
let
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
if (defPart.startsWith(":")) continue;
|
|
451
|
-
if (defPart.includes("*")) {
|
|
452
|
-
const prefix = defPart.replace(/\*\??$/, "");
|
|
453
|
-
if (pathPart.startsWith(prefix)) continue;
|
|
454
|
-
isPrefix = false;
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
if (defPart !== pathPart) {
|
|
458
|
-
isPrefix = false;
|
|
459
|
-
break;
|
|
460
|
-
}
|
|
497
|
+
let descendant = false;
|
|
498
|
+
let descendantMatch = null;
|
|
499
|
+
let descendantCaptureKeys = [];
|
|
500
|
+
if (!exact && !ancestor) {
|
|
501
|
+
for (const matcher of this.regexDescendantMatchers) {
|
|
502
|
+
const match = pathname.match(matcher.regex);
|
|
503
|
+
if (!match) continue;
|
|
504
|
+
descendant = true;
|
|
505
|
+
descendantMatch = match;
|
|
506
|
+
descendantCaptureKeys = matcher.captureKeys;
|
|
507
|
+
break;
|
|
461
508
|
}
|
|
462
509
|
}
|
|
463
|
-
const descendant = !exact && isPrefix;
|
|
464
510
|
const unmatched = !exact && !ancestor && !descendant;
|
|
465
|
-
if (descendant) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (!defPart || !pathPart) continue;
|
|
471
|
-
if (defPart.startsWith(":")) {
|
|
472
|
-
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
473
|
-
} else if (defPart.includes("*")) {
|
|
474
|
-
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
location.params = descendantParams;
|
|
511
|
+
if (descendant && descendantMatch) {
|
|
512
|
+
const values = descendantMatch.slice(1, 1 + descendantCaptureKeys.length);
|
|
513
|
+
location.params = Object.fromEntries(
|
|
514
|
+
descendantCaptureKeys.map((key, index) => [key, decodeURIComponent(values[index])])
|
|
515
|
+
);
|
|
478
516
|
}
|
|
479
517
|
return {
|
|
480
518
|
...location,
|
|
@@ -573,15 +611,18 @@ class Route0 {
|
|
|
573
611
|
};
|
|
574
612
|
/** True when path structure is equal (param names are ignored). */
|
|
575
613
|
isSame(other) {
|
|
576
|
-
|
|
614
|
+
const thisShape = this.routeTokens.map((t) => {
|
|
577
615
|
if (t.kind === "static") return `s:${t.value}`;
|
|
578
616
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
579
617
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
580
|
-
}).join("/")
|
|
618
|
+
}).join("/");
|
|
619
|
+
const otherRoute = Route0.from(other);
|
|
620
|
+
const otherShape = otherRoute.routeTokens.map((t) => {
|
|
581
621
|
if (t.kind === "static") return `s:${t.value}`;
|
|
582
622
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
583
623
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
584
624
|
}).join("/");
|
|
625
|
+
return thisShape === otherShape;
|
|
585
626
|
}
|
|
586
627
|
/** Static convenience wrapper for `isSame`. */
|
|
587
628
|
static isSame(a, b) {
|
|
@@ -605,11 +646,21 @@ class Route0 {
|
|
|
605
646
|
const thisParts = getParts(this.definition);
|
|
606
647
|
const otherParts = getParts(other.definition);
|
|
607
648
|
if (thisParts.length <= otherParts.length) return false;
|
|
649
|
+
const matchesPatternPart = (patternPart, valuePart) => {
|
|
650
|
+
if (patternPart.startsWith(":")) return { match: true, wildcard: false };
|
|
651
|
+
const wildcardIndex = patternPart.indexOf("*");
|
|
652
|
+
if (wildcardIndex >= 0) {
|
|
653
|
+
const prefix = patternPart.slice(0, wildcardIndex);
|
|
654
|
+
return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
|
|
655
|
+
}
|
|
656
|
+
return { match: patternPart === valuePart, wildcard: false };
|
|
657
|
+
};
|
|
608
658
|
for (let i = 0; i < otherParts.length; i++) {
|
|
609
659
|
const otherPart = otherParts[i];
|
|
610
660
|
const thisPart = thisParts[i];
|
|
611
|
-
|
|
612
|
-
if (
|
|
661
|
+
const result = matchesPatternPart(otherPart, thisPart);
|
|
662
|
+
if (!result.match) return false;
|
|
663
|
+
if (result.wildcard) return true;
|
|
613
664
|
}
|
|
614
665
|
return true;
|
|
615
666
|
}
|
|
@@ -624,58 +675,54 @@ class Route0 {
|
|
|
624
675
|
const thisParts = getParts(this.definition);
|
|
625
676
|
const otherParts = getParts(other.definition);
|
|
626
677
|
if (thisParts.length >= otherParts.length) return false;
|
|
678
|
+
const matchesPatternPart = (patternPart, valuePart) => {
|
|
679
|
+
if (patternPart.startsWith(":")) return { match: true, wildcard: false };
|
|
680
|
+
const wildcardIndex = patternPart.indexOf("*");
|
|
681
|
+
if (wildcardIndex >= 0) {
|
|
682
|
+
const prefix = patternPart.slice(0, wildcardIndex);
|
|
683
|
+
return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
|
|
684
|
+
}
|
|
685
|
+
return { match: patternPart === valuePart, wildcard: false };
|
|
686
|
+
};
|
|
627
687
|
for (let i = 0; i < thisParts.length; i++) {
|
|
628
688
|
const thisPart = thisParts[i];
|
|
629
689
|
const otherPart = otherParts[i];
|
|
630
|
-
|
|
631
|
-
if (
|
|
690
|
+
const result = matchesPatternPart(thisPart, otherPart);
|
|
691
|
+
if (!result.match) return false;
|
|
692
|
+
if (result.wildcard) return true;
|
|
632
693
|
}
|
|
633
694
|
return true;
|
|
634
695
|
}
|
|
635
696
|
/** True when two route patterns can match the same concrete URL. */
|
|
636
|
-
|
|
697
|
+
isOverlap(other) {
|
|
637
698
|
if (!other) return false;
|
|
638
|
-
|
|
699
|
+
const otherRoute = Route0.from(other);
|
|
639
700
|
const thisRegex = this.regex;
|
|
640
|
-
const otherRegex =
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
const values = (token) => {
|
|
644
|
-
if (token.kind === "static") return [token.value];
|
|
645
|
-
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
646
|
-
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
647
|
-
return ["", "x", "x/y"];
|
|
648
|
-
};
|
|
649
|
-
let acc = [""];
|
|
650
|
-
for (const token of tokens) {
|
|
651
|
-
const next = [];
|
|
652
|
-
for (const base of acc) {
|
|
653
|
-
for (const value of values(token)) {
|
|
654
|
-
if (value === "") {
|
|
655
|
-
next.push(base);
|
|
656
|
-
} else if (value.startsWith("/")) {
|
|
657
|
-
next.push(`${base}${value}`);
|
|
658
|
-
} else {
|
|
659
|
-
next.push(`${base}/${value}`);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
acc = next;
|
|
664
|
-
}
|
|
665
|
-
if (acc.length === 0) return ["/"];
|
|
666
|
-
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
667
|
-
};
|
|
668
|
-
const thisCandidates = makeCandidates(this.definition);
|
|
669
|
-
const otherCandidates = makeCandidates(other.definition);
|
|
701
|
+
const otherRegex = otherRoute.regex;
|
|
702
|
+
const thisCandidates = this.routePatternCandidates;
|
|
703
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
670
704
|
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
671
705
|
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
672
706
|
return false;
|
|
673
707
|
}
|
|
674
|
-
/**
|
|
675
|
-
|
|
708
|
+
/**
|
|
709
|
+
* True when overlap is not resolvable by route ordering inside one route set.
|
|
710
|
+
*
|
|
711
|
+
* Non-conflicting overlap means one route is a strict subset of another
|
|
712
|
+
* (e.g. `/x/y` is a strict subset of `/x/:id`) and can be safely ordered first.
|
|
713
|
+
*/
|
|
714
|
+
isConflict(other) {
|
|
676
715
|
if (!other) return false;
|
|
677
|
-
|
|
678
|
-
|
|
716
|
+
const otherRoute = Route0.from(other);
|
|
717
|
+
if (!this.isOverlap(otherRoute)) return false;
|
|
718
|
+
const thisRegex = this.regex;
|
|
719
|
+
const otherRegex = otherRoute.regex;
|
|
720
|
+
const thisCandidates = this.routePatternCandidates;
|
|
721
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
722
|
+
const thisExclusive = thisCandidates.some((path) => thisRegex.test(path) && !otherRegex.test(path));
|
|
723
|
+
const otherExclusive = otherCandidates.some((path) => otherRegex.test(path) && !thisRegex.test(path));
|
|
724
|
+
if (thisExclusive !== otherExclusive) return false;
|
|
725
|
+
return true;
|
|
679
726
|
}
|
|
680
727
|
/** Specificity comparator used for deterministic route ordering. */
|
|
681
728
|
isMoreSpecificThan(other) {
|
|
@@ -703,6 +750,21 @@ class Route0 {
|
|
|
703
750
|
}
|
|
704
751
|
}
|
|
705
752
|
class Routes {
|
|
753
|
+
static _getNormalizedPathnameFromInput(hrefOrHrefRelOrLocation) {
|
|
754
|
+
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
755
|
+
return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
|
|
756
|
+
}
|
|
757
|
+
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
758
|
+
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
759
|
+
return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
|
|
760
|
+
}
|
|
761
|
+
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
762
|
+
}
|
|
763
|
+
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
764
|
+
const base = abs ? void 0 : "http://example.com";
|
|
765
|
+
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
766
|
+
return normalizeSlashPath(url.pathname);
|
|
767
|
+
}
|
|
706
768
|
_routes;
|
|
707
769
|
_pathsOrdering;
|
|
708
770
|
_keysOrdering;
|
|
@@ -766,7 +828,7 @@ class Routes {
|
|
|
766
828
|
}
|
|
767
829
|
_getLocation(hrefOrHrefRelOrLocation) {
|
|
768
830
|
const input = hrefOrHrefRelOrLocation;
|
|
769
|
-
const pathname =
|
|
831
|
+
const pathname = Routes._getNormalizedPathnameFromInput(input);
|
|
770
832
|
for (const route of this._ordered) {
|
|
771
833
|
if (!route.isExactPathnameMatch(pathname)) {
|
|
772
834
|
continue;
|
|
@@ -788,7 +850,7 @@ class Routes {
|
|
|
788
850
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
789
851
|
const partsA = getParts(routeA.definition);
|
|
790
852
|
const partsB = getParts(routeB.definition);
|
|
791
|
-
if (routeA.
|
|
853
|
+
if (routeA.isOverlap(routeB)) {
|
|
792
854
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
793
855
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
794
856
|
}
|