@devp0nt/route0 1.0.0-next.76 → 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 +267 -225
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +35 -19
- package/dist/esm/index.d.ts +35 -19
- package/dist/esm/index.js +267 -225
- 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
|
-
|
|
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 getRouteRegexBaseStrictString = (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 normalizePathname = (pathname) => {
|
|
75
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
76
|
-
return pathname.slice(0, -1);
|
|
77
|
-
}
|
|
78
|
-
return pathname;
|
|
79
|
-
};
|
|
80
|
-
const getNormalizedPathnameFromInput = (hrefOrHrefRelOrLocation) => {
|
|
81
|
-
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
82
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
83
|
-
}
|
|
84
|
-
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
85
|
-
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
86
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
87
|
-
}
|
|
88
|
-
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
89
|
-
}
|
|
90
|
-
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
91
|
-
const base = abs ? void 0 : "http://example.com";
|
|
92
|
-
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
93
|
-
return normalizePathname(url.pathname);
|
|
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}`;
|
|
8
|
+
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
94
9
|
};
|
|
95
10
|
class Route0 {
|
|
96
11
|
definition;
|
|
97
12
|
params;
|
|
98
13
|
_origin;
|
|
99
14
|
_callable;
|
|
100
|
-
|
|
15
|
+
_routeSegments;
|
|
16
|
+
_routeTokens;
|
|
17
|
+
_routePatternCandidates;
|
|
18
|
+
_pathParamsDefinition;
|
|
19
|
+
_definitionWithoutTrailingWildcard;
|
|
20
|
+
_routeRegexBaseStringRaw;
|
|
101
21
|
_regexBaseString;
|
|
102
|
-
_regexStrictString;
|
|
103
22
|
_regexString;
|
|
104
|
-
_regexStrict;
|
|
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,9 +66,10 @@ class Route0 {
|
|
|
121
66
|
this._origin = origin;
|
|
122
67
|
}
|
|
123
68
|
constructor(definition, config = {}) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.
|
|
69
|
+
const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
|
|
70
|
+
Route0._validateRouteDefinition(normalizedDefinition);
|
|
71
|
+
this.definition = normalizedDefinition;
|
|
72
|
+
this.params = this.pathParamsDefinition;
|
|
127
73
|
const { origin } = config;
|
|
128
74
|
if (origin && typeof origin === "string" && origin.length) {
|
|
129
75
|
this._origin = origin;
|
|
@@ -151,7 +97,10 @@ class Route0 {
|
|
|
151
97
|
if (typeof definition === "function" || typeof definition === "object") {
|
|
152
98
|
return definition.clone(config);
|
|
153
99
|
}
|
|
154
|
-
const original = new Route0(
|
|
100
|
+
const original = new Route0(
|
|
101
|
+
Route0._normalizeRouteDefinition(definition),
|
|
102
|
+
config
|
|
103
|
+
);
|
|
155
104
|
return original._callable;
|
|
156
105
|
}
|
|
157
106
|
/**
|
|
@@ -163,25 +112,25 @@ class Route0 {
|
|
|
163
112
|
if (typeof definition === "function") {
|
|
164
113
|
return definition;
|
|
165
114
|
}
|
|
166
|
-
const original = typeof definition === "object" ? definition : new Route0(
|
|
115
|
+
const original = typeof definition === "object" ? definition : new Route0(
|
|
116
|
+
Route0._normalizeRouteDefinition(definition)
|
|
117
|
+
);
|
|
167
118
|
return original._callable;
|
|
168
119
|
}
|
|
169
120
|
static _getAbsPath(origin, url) {
|
|
170
121
|
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
171
122
|
}
|
|
172
|
-
static _getParamsDefinitionByDefinition(definition) {
|
|
173
|
-
return getPathParamsDefinition(definition);
|
|
174
|
-
}
|
|
175
123
|
search() {
|
|
176
124
|
return this._callable;
|
|
177
125
|
}
|
|
178
126
|
/** Extends the current route definition by appending a suffix route. */
|
|
179
127
|
extend(suffixDefinition) {
|
|
180
|
-
const
|
|
181
|
-
const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
|
|
128
|
+
const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
|
|
182
129
|
return Route0.create(
|
|
183
130
|
definition,
|
|
184
|
-
{
|
|
131
|
+
{
|
|
132
|
+
origin: this._origin
|
|
133
|
+
}
|
|
185
134
|
);
|
|
186
135
|
}
|
|
187
136
|
// implementation
|
|
@@ -253,7 +202,7 @@ class Route0 {
|
|
|
253
202
|
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
254
203
|
const searchString = stringifySearchQuery(searchInput);
|
|
255
204
|
url = [url, searchString].filter(Boolean).join("?");
|
|
256
|
-
url = url
|
|
205
|
+
url = collapseDuplicateSlashes(url);
|
|
257
206
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
258
207
|
if (hashInput !== void 0) {
|
|
259
208
|
url = `${url}#${hashInput}`;
|
|
@@ -265,42 +214,24 @@ class Route0 {
|
|
|
265
214
|
return Object.keys(this.params);
|
|
266
215
|
}
|
|
267
216
|
getTokens() {
|
|
268
|
-
return
|
|
217
|
+
return this.routeTokens.map((token) => ({ ...token }));
|
|
269
218
|
}
|
|
270
219
|
/** Clones route with optional config override. */
|
|
271
220
|
clone(config) {
|
|
272
221
|
return Route0.create(this.definition, config);
|
|
273
222
|
}
|
|
274
|
-
get regexBaseStrictString() {
|
|
275
|
-
if (this._regexBaseStrictString === void 0) {
|
|
276
|
-
this._regexBaseStrictString = getRouteRegexBaseStrictString(this.definition);
|
|
277
|
-
}
|
|
278
|
-
return this._regexBaseStrictString;
|
|
279
|
-
}
|
|
280
223
|
get regexBaseString() {
|
|
281
224
|
if (this._regexBaseString === void 0) {
|
|
282
|
-
this._regexBaseString = this.
|
|
225
|
+
this._regexBaseString = this.routeRegexBaseStringRaw.replace(/\/+$/, "") + "/?";
|
|
283
226
|
}
|
|
284
227
|
return this._regexBaseString;
|
|
285
228
|
}
|
|
286
|
-
get regexStrictString() {
|
|
287
|
-
if (this._regexStrictString === void 0) {
|
|
288
|
-
this._regexStrictString = `^${this.regexBaseStrictString}$`;
|
|
289
|
-
}
|
|
290
|
-
return this._regexStrictString;
|
|
291
|
-
}
|
|
292
229
|
get regexString() {
|
|
293
230
|
if (this._regexString === void 0) {
|
|
294
231
|
this._regexString = `^${this.regexBaseString}$`;
|
|
295
232
|
}
|
|
296
233
|
return this._regexString;
|
|
297
234
|
}
|
|
298
|
-
get regexStrict() {
|
|
299
|
-
if (this._regexStrict === void 0) {
|
|
300
|
-
this._regexStrict = new RegExp(this.regexStrictString);
|
|
301
|
-
}
|
|
302
|
-
return this._regexStrict;
|
|
303
|
-
}
|
|
304
235
|
get regex() {
|
|
305
236
|
if (this._regex === void 0) {
|
|
306
237
|
this._regex = new RegExp(this.regexString);
|
|
@@ -313,12 +244,132 @@ class Route0 {
|
|
|
313
244
|
}
|
|
314
245
|
return this._regexAncestor;
|
|
315
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
|
+
}
|
|
316
274
|
get captureKeys() {
|
|
317
275
|
if (this._captureKeys === void 0) {
|
|
318
|
-
this._captureKeys =
|
|
276
|
+
this._captureKeys = this.routeTokens.filter((token) => token.kind !== "static").map((token) => token.kind === "param" ? token.name : "*");
|
|
319
277
|
}
|
|
320
278
|
return this._captureKeys;
|
|
321
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
|
+
}
|
|
322
373
|
get normalizedDefinition() {
|
|
323
374
|
if (this._normalizedDefinition === void 0) {
|
|
324
375
|
this._normalizedDefinition = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
@@ -333,23 +384,13 @@ class Route0 {
|
|
|
333
384
|
}
|
|
334
385
|
/** Fast pathname exact match check without building a full location object. */
|
|
335
386
|
isExactPathnameMatch(pathname) {
|
|
336
|
-
return this.regex.test(
|
|
387
|
+
return this.regex.test(Route0._normalizePathname(pathname));
|
|
337
388
|
}
|
|
338
389
|
/** Fast pathname exact or ancestor match check without building a full location object. */
|
|
339
390
|
isExactOrAncestorPathnameMatch(pathname) {
|
|
340
|
-
const normalizedPathname =
|
|
391
|
+
const normalizedPathname = Route0._normalizePathname(pathname);
|
|
341
392
|
return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
|
|
342
393
|
}
|
|
343
|
-
/** Creates a grouped strict regex pattern string from many routes. */
|
|
344
|
-
static getRegexStrictStringGroup(routes) {
|
|
345
|
-
const patterns = routes.map((route) => route.regexStrictString).join("|");
|
|
346
|
-
return `(${patterns})`;
|
|
347
|
-
}
|
|
348
|
-
/** Creates a strict grouped regex from many routes. */
|
|
349
|
-
static getRegexStrictGroup(routes) {
|
|
350
|
-
const patterns = Route0.getRegexStrictStringGroup(routes);
|
|
351
|
-
return new RegExp(`^(${patterns})$`);
|
|
352
|
-
}
|
|
353
394
|
/** Creates a grouped regex pattern string from many routes. */
|
|
354
395
|
static getRegexStringGroup(routes) {
|
|
355
396
|
const patterns = routes.map((route) => route.regexString).join("|");
|
|
@@ -397,10 +438,9 @@ class Route0 {
|
|
|
397
438
|
const base = abs ? void 0 : "http://example.com";
|
|
398
439
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
399
440
|
const search = parseSearchQuery(url.search);
|
|
400
|
-
const
|
|
401
|
-
const hrefRel = pathname + url.search + url.hash;
|
|
441
|
+
const hrefRel = url.pathname + url.search + url.hash;
|
|
402
442
|
const location = {
|
|
403
|
-
pathname,
|
|
443
|
+
pathname: url.pathname,
|
|
404
444
|
search,
|
|
405
445
|
searchString: url.search,
|
|
406
446
|
hash: url.hash,
|
|
@@ -433,9 +473,8 @@ class Route0 {
|
|
|
433
473
|
const location = Route0.getLocation(hrefOrHrefRelOrLocation);
|
|
434
474
|
location.route = this.definition;
|
|
435
475
|
location.params = {};
|
|
436
|
-
const pathname =
|
|
476
|
+
const pathname = Route0._normalizePathname(location.pathname);
|
|
437
477
|
const paramNames = this.captureKeys;
|
|
438
|
-
const defParts = this.definitionParts;
|
|
439
478
|
const exactRe = this.regex;
|
|
440
479
|
const ancestorRe = this.regexAncestor;
|
|
441
480
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -455,46 +494,25 @@ class Route0 {
|
|
|
455
494
|
} else {
|
|
456
495
|
location.params = {};
|
|
457
496
|
}
|
|
458
|
-
|
|
459
|
-
let
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
if (defPart.startsWith(":")) continue;
|
|
471
|
-
if (defPart.includes("*")) {
|
|
472
|
-
const prefix = defPart.replace(/\*\??$/, "");
|
|
473
|
-
if (pathPart.startsWith(prefix)) continue;
|
|
474
|
-
isPrefix = false;
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
if (defPart !== pathPart) {
|
|
478
|
-
isPrefix = false;
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
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;
|
|
481
508
|
}
|
|
482
509
|
}
|
|
483
|
-
const descendant = !exact && isPrefix;
|
|
484
510
|
const unmatched = !exact && !ancestor && !descendant;
|
|
485
|
-
if (descendant) {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (!defPart || !pathPart) continue;
|
|
491
|
-
if (defPart.startsWith(":")) {
|
|
492
|
-
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
493
|
-
} else if (defPart.includes("*")) {
|
|
494
|
-
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
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
|
+
);
|
|
498
516
|
}
|
|
499
517
|
return {
|
|
500
518
|
...location,
|
|
@@ -593,15 +611,18 @@ class Route0 {
|
|
|
593
611
|
};
|
|
594
612
|
/** True when path structure is equal (param names are ignored). */
|
|
595
613
|
isSame(other) {
|
|
596
|
-
|
|
614
|
+
const thisShape = this.routeTokens.map((t) => {
|
|
597
615
|
if (t.kind === "static") return `s:${t.value}`;
|
|
598
616
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
599
617
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
600
|
-
}).join("/")
|
|
618
|
+
}).join("/");
|
|
619
|
+
const otherRoute = Route0.from(other);
|
|
620
|
+
const otherShape = otherRoute.routeTokens.map((t) => {
|
|
601
621
|
if (t.kind === "static") return `s:${t.value}`;
|
|
602
622
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
603
623
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
604
624
|
}).join("/");
|
|
625
|
+
return thisShape === otherShape;
|
|
605
626
|
}
|
|
606
627
|
/** Static convenience wrapper for `isSame`. */
|
|
607
628
|
static isSame(a, b) {
|
|
@@ -625,11 +646,21 @@ class Route0 {
|
|
|
625
646
|
const thisParts = getParts(this.definition);
|
|
626
647
|
const otherParts = getParts(other.definition);
|
|
627
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
|
+
};
|
|
628
658
|
for (let i = 0; i < otherParts.length; i++) {
|
|
629
659
|
const otherPart = otherParts[i];
|
|
630
660
|
const thisPart = thisParts[i];
|
|
631
|
-
|
|
632
|
-
if (
|
|
661
|
+
const result = matchesPatternPart(otherPart, thisPart);
|
|
662
|
+
if (!result.match) return false;
|
|
663
|
+
if (result.wildcard) return true;
|
|
633
664
|
}
|
|
634
665
|
return true;
|
|
635
666
|
}
|
|
@@ -644,58 +675,54 @@ class Route0 {
|
|
|
644
675
|
const thisParts = getParts(this.definition);
|
|
645
676
|
const otherParts = getParts(other.definition);
|
|
646
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
|
+
};
|
|
647
687
|
for (let i = 0; i < thisParts.length; i++) {
|
|
648
688
|
const thisPart = thisParts[i];
|
|
649
689
|
const otherPart = otherParts[i];
|
|
650
|
-
|
|
651
|
-
if (
|
|
690
|
+
const result = matchesPatternPart(thisPart, otherPart);
|
|
691
|
+
if (!result.match) return false;
|
|
692
|
+
if (result.wildcard) return true;
|
|
652
693
|
}
|
|
653
694
|
return true;
|
|
654
695
|
}
|
|
655
696
|
/** True when two route patterns can match the same concrete URL. */
|
|
656
|
-
|
|
697
|
+
isOverlap(other) {
|
|
657
698
|
if (!other) return false;
|
|
658
|
-
|
|
699
|
+
const otherRoute = Route0.from(other);
|
|
659
700
|
const thisRegex = this.regex;
|
|
660
|
-
const otherRegex =
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
const values = (token) => {
|
|
664
|
-
if (token.kind === "static") return [token.value];
|
|
665
|
-
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
666
|
-
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
667
|
-
return ["", "x", "x/y"];
|
|
668
|
-
};
|
|
669
|
-
let acc = [""];
|
|
670
|
-
for (const token of tokens) {
|
|
671
|
-
const next = [];
|
|
672
|
-
for (const base of acc) {
|
|
673
|
-
for (const value of values(token)) {
|
|
674
|
-
if (value === "") {
|
|
675
|
-
next.push(base);
|
|
676
|
-
} else if (value.startsWith("/")) {
|
|
677
|
-
next.push(`${base}${value}`);
|
|
678
|
-
} else {
|
|
679
|
-
next.push(`${base}/${value}`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
acc = next;
|
|
684
|
-
}
|
|
685
|
-
if (acc.length === 0) return ["/"];
|
|
686
|
-
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
687
|
-
};
|
|
688
|
-
const thisCandidates = makeCandidates(this.definition);
|
|
689
|
-
const otherCandidates = makeCandidates(other.definition);
|
|
701
|
+
const otherRegex = otherRoute.regex;
|
|
702
|
+
const thisCandidates = this.routePatternCandidates;
|
|
703
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
690
704
|
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
691
705
|
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
692
706
|
return false;
|
|
693
707
|
}
|
|
694
|
-
/**
|
|
695
|
-
|
|
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) {
|
|
696
715
|
if (!other) return false;
|
|
697
|
-
|
|
698
|
-
|
|
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;
|
|
699
726
|
}
|
|
700
727
|
/** Specificity comparator used for deterministic route ordering. */
|
|
701
728
|
isMoreSpecificThan(other) {
|
|
@@ -723,6 +750,21 @@ class Route0 {
|
|
|
723
750
|
}
|
|
724
751
|
}
|
|
725
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
|
+
}
|
|
726
768
|
_routes;
|
|
727
769
|
_pathsOrdering;
|
|
728
770
|
_keysOrdering;
|
|
@@ -786,7 +828,7 @@ class Routes {
|
|
|
786
828
|
}
|
|
787
829
|
_getLocation(hrefOrHrefRelOrLocation) {
|
|
788
830
|
const input = hrefOrHrefRelOrLocation;
|
|
789
|
-
const pathname =
|
|
831
|
+
const pathname = Routes._getNormalizedPathnameFromInput(input);
|
|
790
832
|
for (const route of this._ordered) {
|
|
791
833
|
if (!route.isExactPathnameMatch(pathname)) {
|
|
792
834
|
continue;
|
|
@@ -808,7 +850,7 @@ class Routes {
|
|
|
808
850
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
809
851
|
const partsA = getParts(routeA.definition);
|
|
810
852
|
const partsB = getParts(routeB.definition);
|
|
811
|
-
if (routeA.
|
|
853
|
+
if (routeA.isOverlap(routeB)) {
|
|
812
854
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
813
855
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
814
856
|
}
|