@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/cjs/index.cjs
CHANGED
|
@@ -24,113 +24,58 @@ __export(index_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(index_exports);
|
|
25
25
|
var import_flat0 = require("@devp0nt/flat0");
|
|
26
26
|
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const segments = getRouteSegments(definition);
|
|
33
|
-
return segments.map((segment) => {
|
|
34
|
-
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
35
|
-
if (param) {
|
|
36
|
-
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
37
|
-
}
|
|
38
|
-
if (segment === "*" || segment === "*?") {
|
|
39
|
-
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
40
|
-
}
|
|
41
|
-
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
42
|
-
if (wildcard && !segment.includes("\\*")) {
|
|
43
|
-
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
44
|
-
}
|
|
45
|
-
return { kind: "static", value: segment };
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
const getRouteRegexBaseString = (definition) => {
|
|
49
|
-
const tokens = getRouteTokens(definition);
|
|
50
|
-
if (tokens.length === 0) return "";
|
|
51
|
-
let pattern = "";
|
|
52
|
-
for (const token of tokens) {
|
|
53
|
-
if (token.kind === "static") {
|
|
54
|
-
pattern += `/${escapeRegex(token.value)}`;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (token.kind === "param") {
|
|
58
|
-
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (token.prefix.length > 0) {
|
|
62
|
-
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
63
|
-
} else {
|
|
64
|
-
pattern += "(?:/(.*))?";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return pattern;
|
|
68
|
-
};
|
|
69
|
-
const getRouteCaptureKeys = (definition) => {
|
|
70
|
-
const keys = [];
|
|
71
|
-
for (const token of getRouteTokens(definition)) {
|
|
72
|
-
if (token.kind === "param") keys.push(token.name);
|
|
73
|
-
if (token.kind === "wildcard") keys.push("*");
|
|
74
|
-
}
|
|
75
|
-
return keys;
|
|
76
|
-
};
|
|
77
|
-
const getPathParamsDefinition = (definition) => {
|
|
78
|
-
const entries = getRouteTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
79
|
-
return Object.fromEntries(entries);
|
|
80
|
-
};
|
|
81
|
-
const validateRouteDefinition = (definition) => {
|
|
82
|
-
const segments = getRouteSegments(definition);
|
|
83
|
-
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
84
|
-
if (wildcardSegments.length === 0) return;
|
|
85
|
-
if (wildcardSegments.length > 1) {
|
|
86
|
-
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
87
|
-
}
|
|
88
|
-
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
89
|
-
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
90
|
-
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
91
|
-
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
92
|
-
}
|
|
93
|
-
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
94
|
-
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
const stripTrailingWildcard = (definition) => definition.replace(/\*\??$/, "");
|
|
98
|
-
const normalizeRouteDefinition = (definition) => {
|
|
99
|
-
const value = definition.replace(/\/{2,}/g, "/");
|
|
100
|
-
if (value === "" || value === "/") return "/";
|
|
101
|
-
const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
|
|
27
|
+
const collapseDuplicateSlashes = (value) => value.replace(/\/{2,}/g, "/");
|
|
28
|
+
const normalizeSlashPath = (value) => {
|
|
29
|
+
const collapsed = collapseDuplicateSlashes(value);
|
|
30
|
+
if (collapsed === "" || collapsed === "/") return "/";
|
|
31
|
+
const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
|
|
102
32
|
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
103
33
|
};
|
|
104
|
-
const normalizePathname = (pathname) => {
|
|
105
|
-
return normalizeRouteDefinition(pathname);
|
|
106
|
-
};
|
|
107
|
-
const getNormalizedPathnameFromInput = (hrefOrHrefRelOrLocation) => {
|
|
108
|
-
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
109
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
110
|
-
}
|
|
111
|
-
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
112
|
-
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
113
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
114
|
-
}
|
|
115
|
-
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
116
|
-
}
|
|
117
|
-
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
118
|
-
const base = abs ? void 0 : "http://example.com";
|
|
119
|
-
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
120
|
-
return normalizePathname(url.pathname);
|
|
121
|
-
};
|
|
122
34
|
class Route0 {
|
|
123
35
|
definition;
|
|
124
36
|
params;
|
|
125
37
|
_origin;
|
|
126
38
|
_callable;
|
|
39
|
+
_routeSegments;
|
|
40
|
+
_routeTokens;
|
|
41
|
+
_routePatternCandidates;
|
|
42
|
+
_pathParamsDefinition;
|
|
43
|
+
_definitionWithoutTrailingWildcard;
|
|
44
|
+
_routeRegexBaseStringRaw;
|
|
127
45
|
_regexBaseString;
|
|
128
46
|
_regexString;
|
|
129
47
|
_regex;
|
|
130
48
|
_regexAncestor;
|
|
49
|
+
_regexDescendantMatchers;
|
|
131
50
|
_captureKeys;
|
|
132
51
|
_normalizedDefinition;
|
|
133
52
|
_definitionParts;
|
|
53
|
+
static _getRouteSegments(definition) {
|
|
54
|
+
if (definition === "" || definition === "/") return [];
|
|
55
|
+
return definition.split("/").filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
static _normalizeRouteDefinition(definition) {
|
|
58
|
+
return normalizeSlashPath(definition);
|
|
59
|
+
}
|
|
60
|
+
static _normalizePathname(pathname) {
|
|
61
|
+
return Route0._normalizeRouteDefinition(pathname);
|
|
62
|
+
}
|
|
63
|
+
static _validateRouteDefinition(definition) {
|
|
64
|
+
const segments = Route0._getRouteSegments(definition);
|
|
65
|
+
const wildcardSegments = segments.filter((segment) => segment.includes("*"));
|
|
66
|
+
if (wildcardSegments.length === 0) return;
|
|
67
|
+
if (wildcardSegments.length > 1) {
|
|
68
|
+
throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
|
|
69
|
+
}
|
|
70
|
+
const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
|
|
71
|
+
const wildcardSegment = segments[wildcardSegmentIndex];
|
|
72
|
+
if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
|
|
73
|
+
throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
|
|
74
|
+
}
|
|
75
|
+
if (wildcardSegmentIndex !== segments.length - 1) {
|
|
76
|
+
throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
134
79
|
Infer = null;
|
|
135
80
|
/** Base URL used when generating absolute URLs (`abs: true`). */
|
|
136
81
|
get origin() {
|
|
@@ -145,10 +90,10 @@ class Route0 {
|
|
|
145
90
|
this._origin = origin;
|
|
146
91
|
}
|
|
147
92
|
constructor(definition, config = {}) {
|
|
148
|
-
const normalizedDefinition =
|
|
149
|
-
|
|
93
|
+
const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
|
|
94
|
+
Route0._validateRouteDefinition(normalizedDefinition);
|
|
150
95
|
this.definition = normalizedDefinition;
|
|
151
|
-
this.params =
|
|
96
|
+
this.params = this.pathParamsDefinition;
|
|
152
97
|
const { origin } = config;
|
|
153
98
|
if (origin && typeof origin === "string" && origin.length) {
|
|
154
99
|
this._origin = origin;
|
|
@@ -177,7 +122,7 @@ class Route0 {
|
|
|
177
122
|
return definition.clone(config);
|
|
178
123
|
}
|
|
179
124
|
const original = new Route0(
|
|
180
|
-
|
|
125
|
+
Route0._normalizeRouteDefinition(definition),
|
|
181
126
|
config
|
|
182
127
|
);
|
|
183
128
|
return original._callable;
|
|
@@ -192,23 +137,19 @@ class Route0 {
|
|
|
192
137
|
return definition;
|
|
193
138
|
}
|
|
194
139
|
const original = typeof definition === "object" ? definition : new Route0(
|
|
195
|
-
|
|
140
|
+
Route0._normalizeRouteDefinition(definition)
|
|
196
141
|
);
|
|
197
142
|
return original._callable;
|
|
198
143
|
}
|
|
199
144
|
static _getAbsPath(origin, url) {
|
|
200
145
|
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
201
146
|
}
|
|
202
|
-
static _getParamsDefinitionByDefinition(definition) {
|
|
203
|
-
return getPathParamsDefinition(definition);
|
|
204
|
-
}
|
|
205
147
|
search() {
|
|
206
148
|
return this._callable;
|
|
207
149
|
}
|
|
208
150
|
/** Extends the current route definition by appending a suffix route. */
|
|
209
151
|
extend(suffixDefinition) {
|
|
210
|
-
const
|
|
211
|
-
const definition = normalizeRouteDefinition(`${sourceDefinitionWithoutWildcard}/${suffixDefinition}`);
|
|
152
|
+
const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
|
|
212
153
|
return Route0.create(
|
|
213
154
|
definition,
|
|
214
155
|
{
|
|
@@ -285,7 +226,7 @@ class Route0 {
|
|
|
285
226
|
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
286
227
|
const searchString = (0, import_flat0.stringify)(searchInput);
|
|
287
228
|
url = [url, searchString].filter(Boolean).join("?");
|
|
288
|
-
url = url
|
|
229
|
+
url = collapseDuplicateSlashes(url);
|
|
289
230
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
290
231
|
if (hashInput !== void 0) {
|
|
291
232
|
url = `${url}#${hashInput}`;
|
|
@@ -297,7 +238,7 @@ class Route0 {
|
|
|
297
238
|
return Object.keys(this.params);
|
|
298
239
|
}
|
|
299
240
|
getTokens() {
|
|
300
|
-
return
|
|
241
|
+
return this.routeTokens.map((token) => ({ ...token }));
|
|
301
242
|
}
|
|
302
243
|
/** Clones route with optional config override. */
|
|
303
244
|
clone(config) {
|
|
@@ -305,7 +246,7 @@ class Route0 {
|
|
|
305
246
|
}
|
|
306
247
|
get regexBaseString() {
|
|
307
248
|
if (this._regexBaseString === void 0) {
|
|
308
|
-
this._regexBaseString =
|
|
249
|
+
this._regexBaseString = this.routeRegexBaseStringRaw.replace(/\/+$/, "") + "/?";
|
|
309
250
|
}
|
|
310
251
|
return this._regexBaseString;
|
|
311
252
|
}
|
|
@@ -327,12 +268,132 @@ class Route0 {
|
|
|
327
268
|
}
|
|
328
269
|
return this._regexAncestor;
|
|
329
270
|
}
|
|
271
|
+
get regexDescendantMatchers() {
|
|
272
|
+
if (this._regexDescendantMatchers === void 0) {
|
|
273
|
+
const matchers = [];
|
|
274
|
+
if (this.definitionParts[0] !== "/") {
|
|
275
|
+
let pattern = "";
|
|
276
|
+
const captureKeys = [];
|
|
277
|
+
for (const part of this.definitionParts) {
|
|
278
|
+
if (part.startsWith(":")) {
|
|
279
|
+
pattern += "/([^/]+)";
|
|
280
|
+
captureKeys.push(part.replace(/^:/, "").replace(/\?$/, ""));
|
|
281
|
+
} else if (part.includes("*")) {
|
|
282
|
+
const prefix = part.replace(/\*\??$/, "");
|
|
283
|
+
pattern += `/${escapeRegex(prefix)}[^/]*`;
|
|
284
|
+
captureKeys.push("*");
|
|
285
|
+
} else {
|
|
286
|
+
pattern += `/${escapeRegex(part)}`;
|
|
287
|
+
}
|
|
288
|
+
matchers.push({
|
|
289
|
+
regex: new RegExp(`^${pattern}/?$`),
|
|
290
|
+
captureKeys: [...captureKeys]
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
this._regexDescendantMatchers = matchers;
|
|
295
|
+
}
|
|
296
|
+
return this._regexDescendantMatchers;
|
|
297
|
+
}
|
|
330
298
|
get captureKeys() {
|
|
331
299
|
if (this._captureKeys === void 0) {
|
|
332
|
-
this._captureKeys =
|
|
300
|
+
this._captureKeys = this.routeTokens.filter((token) => token.kind !== "static").map((token) => token.kind === "param" ? token.name : "*");
|
|
333
301
|
}
|
|
334
302
|
return this._captureKeys;
|
|
335
303
|
}
|
|
304
|
+
get routeSegments() {
|
|
305
|
+
if (this._routeSegments === void 0) {
|
|
306
|
+
this._routeSegments = Route0._getRouteSegments(this.definition);
|
|
307
|
+
}
|
|
308
|
+
return this._routeSegments;
|
|
309
|
+
}
|
|
310
|
+
get routeTokens() {
|
|
311
|
+
if (this._routeTokens === void 0) {
|
|
312
|
+
this._routeTokens = this.routeSegments.map((segment) => {
|
|
313
|
+
const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
|
|
314
|
+
if (param) {
|
|
315
|
+
return { kind: "param", name: param[1], optional: param[2] === "?" };
|
|
316
|
+
}
|
|
317
|
+
if (segment === "*" || segment === "*?") {
|
|
318
|
+
return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
|
|
319
|
+
}
|
|
320
|
+
const wildcard = segment.match(/^(.*)\*(\?)?$/);
|
|
321
|
+
if (wildcard && !segment.includes("\\*")) {
|
|
322
|
+
return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
|
|
323
|
+
}
|
|
324
|
+
return { kind: "static", value: segment };
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return this._routeTokens;
|
|
328
|
+
}
|
|
329
|
+
get routePatternCandidates() {
|
|
330
|
+
if (this._routePatternCandidates === void 0) {
|
|
331
|
+
const values = (token) => {
|
|
332
|
+
if (token.kind === "static") return [token.value];
|
|
333
|
+
if (token.kind === "param") return token.optional ? ["", "x", "y"] : ["x", "y"];
|
|
334
|
+
if (token.prefix.length > 0)
|
|
335
|
+
return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x`, `${token.prefix}/y/z`];
|
|
336
|
+
return ["", "x", "y", "x/y"];
|
|
337
|
+
};
|
|
338
|
+
let acc = [""];
|
|
339
|
+
for (const token of this.routeTokens) {
|
|
340
|
+
const next = [];
|
|
341
|
+
for (const base of acc) {
|
|
342
|
+
for (const value of values(token)) {
|
|
343
|
+
if (value === "") {
|
|
344
|
+
next.push(base);
|
|
345
|
+
} else if (value.startsWith("/")) {
|
|
346
|
+
next.push(`${base}${value}`);
|
|
347
|
+
} else {
|
|
348
|
+
next.push(`${base}/${value}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
acc = next.length > 512 ? next.slice(0, 512) : next;
|
|
353
|
+
}
|
|
354
|
+
this._routePatternCandidates = acc.length === 0 ? ["/"] : Array.from(new Set(acc.map((x) => x === "" ? "/" : collapseDuplicateSlashes(x))));
|
|
355
|
+
}
|
|
356
|
+
return this._routePatternCandidates;
|
|
357
|
+
}
|
|
358
|
+
get pathParamsDefinition() {
|
|
359
|
+
if (this._pathParamsDefinition === void 0) {
|
|
360
|
+
const entries = this.routeTokens.filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
|
|
361
|
+
this._pathParamsDefinition = Object.fromEntries(entries);
|
|
362
|
+
}
|
|
363
|
+
return this._pathParamsDefinition;
|
|
364
|
+
}
|
|
365
|
+
get definitionWithoutTrailingWildcard() {
|
|
366
|
+
if (this._definitionWithoutTrailingWildcard === void 0) {
|
|
367
|
+
this._definitionWithoutTrailingWildcard = this.definition.replace(/\*\??$/, "");
|
|
368
|
+
}
|
|
369
|
+
return this._definitionWithoutTrailingWildcard;
|
|
370
|
+
}
|
|
371
|
+
get routeRegexBaseStringRaw() {
|
|
372
|
+
if (this._routeRegexBaseStringRaw === void 0) {
|
|
373
|
+
if (this.routeTokens.length === 0) {
|
|
374
|
+
this._routeRegexBaseStringRaw = "";
|
|
375
|
+
} else {
|
|
376
|
+
let pattern = "";
|
|
377
|
+
for (const token of this.routeTokens) {
|
|
378
|
+
if (token.kind === "static") {
|
|
379
|
+
pattern += `/${escapeRegex(token.value)}`;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (token.kind === "param") {
|
|
383
|
+
pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (token.prefix.length > 0) {
|
|
387
|
+
pattern += `/${escapeRegex(token.prefix)}(.*)`;
|
|
388
|
+
} else {
|
|
389
|
+
pattern += "(?:/(.*))?";
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
this._routeRegexBaseStringRaw = pattern;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return this._routeRegexBaseStringRaw;
|
|
396
|
+
}
|
|
336
397
|
get normalizedDefinition() {
|
|
337
398
|
if (this._normalizedDefinition === void 0) {
|
|
338
399
|
this._normalizedDefinition = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
@@ -347,11 +408,11 @@ class Route0 {
|
|
|
347
408
|
}
|
|
348
409
|
/** Fast pathname exact match check without building a full location object. */
|
|
349
410
|
isExactPathnameMatch(pathname) {
|
|
350
|
-
return this.regex.test(
|
|
411
|
+
return this.regex.test(Route0._normalizePathname(pathname));
|
|
351
412
|
}
|
|
352
413
|
/** Fast pathname exact or ancestor match check without building a full location object. */
|
|
353
414
|
isExactOrAncestorPathnameMatch(pathname) {
|
|
354
|
-
const normalizedPathname =
|
|
415
|
+
const normalizedPathname = Route0._normalizePathname(pathname);
|
|
355
416
|
return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
|
|
356
417
|
}
|
|
357
418
|
/** Creates a grouped regex pattern string from many routes. */
|
|
@@ -401,10 +462,9 @@ class Route0 {
|
|
|
401
462
|
const base = abs ? void 0 : "http://example.com";
|
|
402
463
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
403
464
|
const search = (0, import_flat0.parse)(url.search);
|
|
404
|
-
const
|
|
405
|
-
const hrefRel = pathname + url.search + url.hash;
|
|
465
|
+
const hrefRel = url.pathname + url.search + url.hash;
|
|
406
466
|
const location = {
|
|
407
|
-
pathname,
|
|
467
|
+
pathname: url.pathname,
|
|
408
468
|
search,
|
|
409
469
|
searchString: url.search,
|
|
410
470
|
hash: url.hash,
|
|
@@ -437,9 +497,8 @@ class Route0 {
|
|
|
437
497
|
const location = Route0.getLocation(hrefOrHrefRelOrLocation);
|
|
438
498
|
location.route = this.definition;
|
|
439
499
|
location.params = {};
|
|
440
|
-
const pathname =
|
|
500
|
+
const pathname = Route0._normalizePathname(location.pathname);
|
|
441
501
|
const paramNames = this.captureKeys;
|
|
442
|
-
const defParts = this.definitionParts;
|
|
443
502
|
const exactRe = this.regex;
|
|
444
503
|
const ancestorRe = this.regexAncestor;
|
|
445
504
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -459,46 +518,25 @@ class Route0 {
|
|
|
459
518
|
} else {
|
|
460
519
|
location.params = {};
|
|
461
520
|
}
|
|
462
|
-
|
|
463
|
-
let
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
if (defPart.startsWith(":")) continue;
|
|
475
|
-
if (defPart.includes("*")) {
|
|
476
|
-
const prefix = defPart.replace(/\*\??$/, "");
|
|
477
|
-
if (pathPart.startsWith(prefix)) continue;
|
|
478
|
-
isPrefix = false;
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
if (defPart !== pathPart) {
|
|
482
|
-
isPrefix = false;
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
521
|
+
let descendant = false;
|
|
522
|
+
let descendantMatch = null;
|
|
523
|
+
let descendantCaptureKeys = [];
|
|
524
|
+
if (!exact && !ancestor) {
|
|
525
|
+
for (const matcher of this.regexDescendantMatchers) {
|
|
526
|
+
const match = pathname.match(matcher.regex);
|
|
527
|
+
if (!match) continue;
|
|
528
|
+
descendant = true;
|
|
529
|
+
descendantMatch = match;
|
|
530
|
+
descendantCaptureKeys = matcher.captureKeys;
|
|
531
|
+
break;
|
|
485
532
|
}
|
|
486
533
|
}
|
|
487
|
-
const descendant = !exact && isPrefix;
|
|
488
534
|
const unmatched = !exact && !ancestor && !descendant;
|
|
489
|
-
if (descendant) {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (!defPart || !pathPart) continue;
|
|
495
|
-
if (defPart.startsWith(":")) {
|
|
496
|
-
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
497
|
-
} else if (defPart.includes("*")) {
|
|
498
|
-
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
location.params = descendantParams;
|
|
535
|
+
if (descendant && descendantMatch) {
|
|
536
|
+
const values = descendantMatch.slice(1, 1 + descendantCaptureKeys.length);
|
|
537
|
+
location.params = Object.fromEntries(
|
|
538
|
+
descendantCaptureKeys.map((key, index) => [key, decodeURIComponent(values[index])])
|
|
539
|
+
);
|
|
502
540
|
}
|
|
503
541
|
return {
|
|
504
542
|
...location,
|
|
@@ -597,15 +635,18 @@ class Route0 {
|
|
|
597
635
|
};
|
|
598
636
|
/** True when path structure is equal (param names are ignored). */
|
|
599
637
|
isSame(other) {
|
|
600
|
-
|
|
638
|
+
const thisShape = this.routeTokens.map((t) => {
|
|
601
639
|
if (t.kind === "static") return `s:${t.value}`;
|
|
602
640
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
603
641
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
604
|
-
}).join("/")
|
|
642
|
+
}).join("/");
|
|
643
|
+
const otherRoute = Route0.from(other);
|
|
644
|
+
const otherShape = otherRoute.routeTokens.map((t) => {
|
|
605
645
|
if (t.kind === "static") return `s:${t.value}`;
|
|
606
646
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
607
647
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
608
648
|
}).join("/");
|
|
649
|
+
return thisShape === otherShape;
|
|
609
650
|
}
|
|
610
651
|
/** Static convenience wrapper for `isSame`. */
|
|
611
652
|
static isSame(a, b) {
|
|
@@ -629,11 +670,21 @@ class Route0 {
|
|
|
629
670
|
const thisParts = getParts(this.definition);
|
|
630
671
|
const otherParts = getParts(other.definition);
|
|
631
672
|
if (thisParts.length <= otherParts.length) return false;
|
|
673
|
+
const matchesPatternPart = (patternPart, valuePart) => {
|
|
674
|
+
if (patternPart.startsWith(":")) return { match: true, wildcard: false };
|
|
675
|
+
const wildcardIndex = patternPart.indexOf("*");
|
|
676
|
+
if (wildcardIndex >= 0) {
|
|
677
|
+
const prefix = patternPart.slice(0, wildcardIndex);
|
|
678
|
+
return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
|
|
679
|
+
}
|
|
680
|
+
return { match: patternPart === valuePart, wildcard: false };
|
|
681
|
+
};
|
|
632
682
|
for (let i = 0; i < otherParts.length; i++) {
|
|
633
683
|
const otherPart = otherParts[i];
|
|
634
684
|
const thisPart = thisParts[i];
|
|
635
|
-
|
|
636
|
-
if (
|
|
685
|
+
const result = matchesPatternPart(otherPart, thisPart);
|
|
686
|
+
if (!result.match) return false;
|
|
687
|
+
if (result.wildcard) return true;
|
|
637
688
|
}
|
|
638
689
|
return true;
|
|
639
690
|
}
|
|
@@ -648,58 +699,54 @@ class Route0 {
|
|
|
648
699
|
const thisParts = getParts(this.definition);
|
|
649
700
|
const otherParts = getParts(other.definition);
|
|
650
701
|
if (thisParts.length >= otherParts.length) return false;
|
|
702
|
+
const matchesPatternPart = (patternPart, valuePart) => {
|
|
703
|
+
if (patternPart.startsWith(":")) return { match: true, wildcard: false };
|
|
704
|
+
const wildcardIndex = patternPart.indexOf("*");
|
|
705
|
+
if (wildcardIndex >= 0) {
|
|
706
|
+
const prefix = patternPart.slice(0, wildcardIndex);
|
|
707
|
+
return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
|
|
708
|
+
}
|
|
709
|
+
return { match: patternPart === valuePart, wildcard: false };
|
|
710
|
+
};
|
|
651
711
|
for (let i = 0; i < thisParts.length; i++) {
|
|
652
712
|
const thisPart = thisParts[i];
|
|
653
713
|
const otherPart = otherParts[i];
|
|
654
|
-
|
|
655
|
-
if (
|
|
714
|
+
const result = matchesPatternPart(thisPart, otherPart);
|
|
715
|
+
if (!result.match) return false;
|
|
716
|
+
if (result.wildcard) return true;
|
|
656
717
|
}
|
|
657
718
|
return true;
|
|
658
719
|
}
|
|
659
720
|
/** True when two route patterns can match the same concrete URL. */
|
|
660
|
-
|
|
721
|
+
isOverlap(other) {
|
|
661
722
|
if (!other) return false;
|
|
662
|
-
|
|
723
|
+
const otherRoute = Route0.from(other);
|
|
663
724
|
const thisRegex = this.regex;
|
|
664
|
-
const otherRegex =
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
const values = (token) => {
|
|
668
|
-
if (token.kind === "static") return [token.value];
|
|
669
|
-
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
670
|
-
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
671
|
-
return ["", "x", "x/y"];
|
|
672
|
-
};
|
|
673
|
-
let acc = [""];
|
|
674
|
-
for (const token of tokens) {
|
|
675
|
-
const next = [];
|
|
676
|
-
for (const base of acc) {
|
|
677
|
-
for (const value of values(token)) {
|
|
678
|
-
if (value === "") {
|
|
679
|
-
next.push(base);
|
|
680
|
-
} else if (value.startsWith("/")) {
|
|
681
|
-
next.push(`${base}${value}`);
|
|
682
|
-
} else {
|
|
683
|
-
next.push(`${base}/${value}`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
acc = next;
|
|
688
|
-
}
|
|
689
|
-
if (acc.length === 0) return ["/"];
|
|
690
|
-
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
691
|
-
};
|
|
692
|
-
const thisCandidates = makeCandidates(this.definition);
|
|
693
|
-
const otherCandidates = makeCandidates(other.definition);
|
|
725
|
+
const otherRegex = otherRoute.regex;
|
|
726
|
+
const thisCandidates = this.routePatternCandidates;
|
|
727
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
694
728
|
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
695
729
|
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
696
730
|
return false;
|
|
697
731
|
}
|
|
698
|
-
/**
|
|
699
|
-
|
|
732
|
+
/**
|
|
733
|
+
* True when overlap is not resolvable by route ordering inside one route set.
|
|
734
|
+
*
|
|
735
|
+
* Non-conflicting overlap means one route is a strict subset of another
|
|
736
|
+
* (e.g. `/x/y` is a strict subset of `/x/:id`) and can be safely ordered first.
|
|
737
|
+
*/
|
|
738
|
+
isConflict(other) {
|
|
700
739
|
if (!other) return false;
|
|
701
|
-
|
|
702
|
-
|
|
740
|
+
const otherRoute = Route0.from(other);
|
|
741
|
+
if (!this.isOverlap(otherRoute)) return false;
|
|
742
|
+
const thisRegex = this.regex;
|
|
743
|
+
const otherRegex = otherRoute.regex;
|
|
744
|
+
const thisCandidates = this.routePatternCandidates;
|
|
745
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
746
|
+
const thisExclusive = thisCandidates.some((path) => thisRegex.test(path) && !otherRegex.test(path));
|
|
747
|
+
const otherExclusive = otherCandidates.some((path) => otherRegex.test(path) && !thisRegex.test(path));
|
|
748
|
+
if (thisExclusive !== otherExclusive) return false;
|
|
749
|
+
return true;
|
|
703
750
|
}
|
|
704
751
|
/** Specificity comparator used for deterministic route ordering. */
|
|
705
752
|
isMoreSpecificThan(other) {
|
|
@@ -727,6 +774,21 @@ class Route0 {
|
|
|
727
774
|
}
|
|
728
775
|
}
|
|
729
776
|
class Routes {
|
|
777
|
+
static _getNormalizedPathnameFromInput(hrefOrHrefRelOrLocation) {
|
|
778
|
+
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
779
|
+
return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
|
|
780
|
+
}
|
|
781
|
+
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
782
|
+
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
783
|
+
return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
|
|
784
|
+
}
|
|
785
|
+
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
786
|
+
}
|
|
787
|
+
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
788
|
+
const base = abs ? void 0 : "http://example.com";
|
|
789
|
+
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
790
|
+
return normalizeSlashPath(url.pathname);
|
|
791
|
+
}
|
|
730
792
|
_routes;
|
|
731
793
|
_pathsOrdering;
|
|
732
794
|
_keysOrdering;
|
|
@@ -790,7 +852,7 @@ class Routes {
|
|
|
790
852
|
}
|
|
791
853
|
_getLocation(hrefOrHrefRelOrLocation) {
|
|
792
854
|
const input = hrefOrHrefRelOrLocation;
|
|
793
|
-
const pathname =
|
|
855
|
+
const pathname = Routes._getNormalizedPathnameFromInput(input);
|
|
794
856
|
for (const route of this._ordered) {
|
|
795
857
|
if (!route.isExactPathnameMatch(pathname)) {
|
|
796
858
|
continue;
|
|
@@ -812,7 +874,7 @@ class Routes {
|
|
|
812
874
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
813
875
|
const partsA = getParts(routeA.definition);
|
|
814
876
|
const partsB = getParts(routeB.definition);
|
|
815
|
-
if (routeA.
|
|
877
|
+
if (routeA.isOverlap(routeB)) {
|
|
816
878
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
817
879
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
818
880
|
}
|