@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/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
|
-
|
|
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 getRouteRegexBaseStrictString = (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 normalizePathname = (pathname) => {
|
|
99
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
100
|
-
return pathname.slice(0, -1);
|
|
101
|
-
}
|
|
102
|
-
return pathname;
|
|
103
|
-
};
|
|
104
|
-
const getNormalizedPathnameFromInput = (hrefOrHrefRelOrLocation) => {
|
|
105
|
-
if (hrefOrHrefRelOrLocation instanceof URL) {
|
|
106
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
107
|
-
}
|
|
108
|
-
if (typeof hrefOrHrefRelOrLocation !== "string") {
|
|
109
|
-
if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
|
|
110
|
-
return normalizePathname(hrefOrHrefRelOrLocation.pathname);
|
|
111
|
-
}
|
|
112
|
-
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
|
|
113
|
-
}
|
|
114
|
-
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
|
|
115
|
-
const base = abs ? void 0 : "http://example.com";
|
|
116
|
-
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
117
|
-
return normalizePathname(url.pathname);
|
|
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}`;
|
|
32
|
+
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
|
|
118
33
|
};
|
|
119
34
|
class Route0 {
|
|
120
35
|
definition;
|
|
121
36
|
params;
|
|
122
37
|
_origin;
|
|
123
38
|
_callable;
|
|
124
|
-
|
|
39
|
+
_routeSegments;
|
|
40
|
+
_routeTokens;
|
|
41
|
+
_routePatternCandidates;
|
|
42
|
+
_pathParamsDefinition;
|
|
43
|
+
_definitionWithoutTrailingWildcard;
|
|
44
|
+
_routeRegexBaseStringRaw;
|
|
125
45
|
_regexBaseString;
|
|
126
|
-
_regexStrictString;
|
|
127
46
|
_regexString;
|
|
128
|
-
_regexStrict;
|
|
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,9 +90,10 @@ class Route0 {
|
|
|
145
90
|
this._origin = origin;
|
|
146
91
|
}
|
|
147
92
|
constructor(definition, config = {}) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.
|
|
93
|
+
const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
|
|
94
|
+
Route0._validateRouteDefinition(normalizedDefinition);
|
|
95
|
+
this.definition = normalizedDefinition;
|
|
96
|
+
this.params = this.pathParamsDefinition;
|
|
151
97
|
const { origin } = config;
|
|
152
98
|
if (origin && typeof origin === "string" && origin.length) {
|
|
153
99
|
this._origin = origin;
|
|
@@ -175,7 +121,10 @@ class Route0 {
|
|
|
175
121
|
if (typeof definition === "function" || typeof definition === "object") {
|
|
176
122
|
return definition.clone(config);
|
|
177
123
|
}
|
|
178
|
-
const original = new Route0(
|
|
124
|
+
const original = new Route0(
|
|
125
|
+
Route0._normalizeRouteDefinition(definition),
|
|
126
|
+
config
|
|
127
|
+
);
|
|
179
128
|
return original._callable;
|
|
180
129
|
}
|
|
181
130
|
/**
|
|
@@ -187,25 +136,25 @@ class Route0 {
|
|
|
187
136
|
if (typeof definition === "function") {
|
|
188
137
|
return definition;
|
|
189
138
|
}
|
|
190
|
-
const original = typeof definition === "object" ? definition : new Route0(
|
|
139
|
+
const original = typeof definition === "object" ? definition : new Route0(
|
|
140
|
+
Route0._normalizeRouteDefinition(definition)
|
|
141
|
+
);
|
|
191
142
|
return original._callable;
|
|
192
143
|
}
|
|
193
144
|
static _getAbsPath(origin, url) {
|
|
194
145
|
return new URL(url, origin).toString().replace(/\/$/, "");
|
|
195
146
|
}
|
|
196
|
-
static _getParamsDefinitionByDefinition(definition) {
|
|
197
|
-
return getPathParamsDefinition(definition);
|
|
198
|
-
}
|
|
199
147
|
search() {
|
|
200
148
|
return this._callable;
|
|
201
149
|
}
|
|
202
150
|
/** Extends the current route definition by appending a suffix route. */
|
|
203
151
|
extend(suffixDefinition) {
|
|
204
|
-
const
|
|
205
|
-
const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
|
|
152
|
+
const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
|
|
206
153
|
return Route0.create(
|
|
207
154
|
definition,
|
|
208
|
-
{
|
|
155
|
+
{
|
|
156
|
+
origin: this._origin
|
|
157
|
+
}
|
|
209
158
|
);
|
|
210
159
|
}
|
|
211
160
|
// implementation
|
|
@@ -277,7 +226,7 @@ class Route0 {
|
|
|
277
226
|
url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
|
|
278
227
|
const searchString = (0, import_flat0.stringify)(searchInput);
|
|
279
228
|
url = [url, searchString].filter(Boolean).join("?");
|
|
280
|
-
url = url
|
|
229
|
+
url = collapseDuplicateSlashes(url);
|
|
281
230
|
url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
|
|
282
231
|
if (hashInput !== void 0) {
|
|
283
232
|
url = `${url}#${hashInput}`;
|
|
@@ -289,42 +238,24 @@ class Route0 {
|
|
|
289
238
|
return Object.keys(this.params);
|
|
290
239
|
}
|
|
291
240
|
getTokens() {
|
|
292
|
-
return
|
|
241
|
+
return this.routeTokens.map((token) => ({ ...token }));
|
|
293
242
|
}
|
|
294
243
|
/** Clones route with optional config override. */
|
|
295
244
|
clone(config) {
|
|
296
245
|
return Route0.create(this.definition, config);
|
|
297
246
|
}
|
|
298
|
-
get regexBaseStrictString() {
|
|
299
|
-
if (this._regexBaseStrictString === void 0) {
|
|
300
|
-
this._regexBaseStrictString = getRouteRegexBaseStrictString(this.definition);
|
|
301
|
-
}
|
|
302
|
-
return this._regexBaseStrictString;
|
|
303
|
-
}
|
|
304
247
|
get regexBaseString() {
|
|
305
248
|
if (this._regexBaseString === void 0) {
|
|
306
|
-
this._regexBaseString = this.
|
|
249
|
+
this._regexBaseString = this.routeRegexBaseStringRaw.replace(/\/+$/, "") + "/?";
|
|
307
250
|
}
|
|
308
251
|
return this._regexBaseString;
|
|
309
252
|
}
|
|
310
|
-
get regexStrictString() {
|
|
311
|
-
if (this._regexStrictString === void 0) {
|
|
312
|
-
this._regexStrictString = `^${this.regexBaseStrictString}$`;
|
|
313
|
-
}
|
|
314
|
-
return this._regexStrictString;
|
|
315
|
-
}
|
|
316
253
|
get regexString() {
|
|
317
254
|
if (this._regexString === void 0) {
|
|
318
255
|
this._regexString = `^${this.regexBaseString}$`;
|
|
319
256
|
}
|
|
320
257
|
return this._regexString;
|
|
321
258
|
}
|
|
322
|
-
get regexStrict() {
|
|
323
|
-
if (this._regexStrict === void 0) {
|
|
324
|
-
this._regexStrict = new RegExp(this.regexStrictString);
|
|
325
|
-
}
|
|
326
|
-
return this._regexStrict;
|
|
327
|
-
}
|
|
328
259
|
get regex() {
|
|
329
260
|
if (this._regex === void 0) {
|
|
330
261
|
this._regex = new RegExp(this.regexString);
|
|
@@ -337,12 +268,132 @@ class Route0 {
|
|
|
337
268
|
}
|
|
338
269
|
return this._regexAncestor;
|
|
339
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
|
+
}
|
|
340
298
|
get captureKeys() {
|
|
341
299
|
if (this._captureKeys === void 0) {
|
|
342
|
-
this._captureKeys =
|
|
300
|
+
this._captureKeys = this.routeTokens.filter((token) => token.kind !== "static").map((token) => token.kind === "param" ? token.name : "*");
|
|
343
301
|
}
|
|
344
302
|
return this._captureKeys;
|
|
345
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
|
+
}
|
|
346
397
|
get normalizedDefinition() {
|
|
347
398
|
if (this._normalizedDefinition === void 0) {
|
|
348
399
|
this._normalizedDefinition = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
|
|
@@ -357,23 +408,13 @@ class Route0 {
|
|
|
357
408
|
}
|
|
358
409
|
/** Fast pathname exact match check without building a full location object. */
|
|
359
410
|
isExactPathnameMatch(pathname) {
|
|
360
|
-
return this.regex.test(
|
|
411
|
+
return this.regex.test(Route0._normalizePathname(pathname));
|
|
361
412
|
}
|
|
362
413
|
/** Fast pathname exact or ancestor match check without building a full location object. */
|
|
363
414
|
isExactOrAncestorPathnameMatch(pathname) {
|
|
364
|
-
const normalizedPathname =
|
|
415
|
+
const normalizedPathname = Route0._normalizePathname(pathname);
|
|
365
416
|
return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
|
|
366
417
|
}
|
|
367
|
-
/** Creates a grouped strict regex pattern string from many routes. */
|
|
368
|
-
static getRegexStrictStringGroup(routes) {
|
|
369
|
-
const patterns = routes.map((route) => route.regexStrictString).join("|");
|
|
370
|
-
return `(${patterns})`;
|
|
371
|
-
}
|
|
372
|
-
/** Creates a strict grouped regex from many routes. */
|
|
373
|
-
static getRegexStrictGroup(routes) {
|
|
374
|
-
const patterns = Route0.getRegexStrictStringGroup(routes);
|
|
375
|
-
return new RegExp(`^(${patterns})$`);
|
|
376
|
-
}
|
|
377
418
|
/** Creates a grouped regex pattern string from many routes. */
|
|
378
419
|
static getRegexStringGroup(routes) {
|
|
379
420
|
const patterns = routes.map((route) => route.regexString).join("|");
|
|
@@ -421,10 +462,9 @@ class Route0 {
|
|
|
421
462
|
const base = abs ? void 0 : "http://example.com";
|
|
422
463
|
const url = new URL(hrefOrHrefRelOrLocation, base);
|
|
423
464
|
const search = (0, import_flat0.parse)(url.search);
|
|
424
|
-
const
|
|
425
|
-
const hrefRel = pathname + url.search + url.hash;
|
|
465
|
+
const hrefRel = url.pathname + url.search + url.hash;
|
|
426
466
|
const location = {
|
|
427
|
-
pathname,
|
|
467
|
+
pathname: url.pathname,
|
|
428
468
|
search,
|
|
429
469
|
searchString: url.search,
|
|
430
470
|
hash: url.hash,
|
|
@@ -457,9 +497,8 @@ class Route0 {
|
|
|
457
497
|
const location = Route0.getLocation(hrefOrHrefRelOrLocation);
|
|
458
498
|
location.route = this.definition;
|
|
459
499
|
location.params = {};
|
|
460
|
-
const pathname =
|
|
500
|
+
const pathname = Route0._normalizePathname(location.pathname);
|
|
461
501
|
const paramNames = this.captureKeys;
|
|
462
|
-
const defParts = this.definitionParts;
|
|
463
502
|
const exactRe = this.regex;
|
|
464
503
|
const ancestorRe = this.regexAncestor;
|
|
465
504
|
const exactMatch = pathname.match(exactRe);
|
|
@@ -479,46 +518,25 @@ class Route0 {
|
|
|
479
518
|
} else {
|
|
480
519
|
location.params = {};
|
|
481
520
|
}
|
|
482
|
-
|
|
483
|
-
let
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
if (defPart.startsWith(":")) continue;
|
|
495
|
-
if (defPart.includes("*")) {
|
|
496
|
-
const prefix = defPart.replace(/\*\??$/, "");
|
|
497
|
-
if (pathPart.startsWith(prefix)) continue;
|
|
498
|
-
isPrefix = false;
|
|
499
|
-
break;
|
|
500
|
-
}
|
|
501
|
-
if (defPart !== pathPart) {
|
|
502
|
-
isPrefix = false;
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
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;
|
|
505
532
|
}
|
|
506
533
|
}
|
|
507
|
-
const descendant = !exact && isPrefix;
|
|
508
534
|
const unmatched = !exact && !ancestor && !descendant;
|
|
509
|
-
if (descendant) {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (!defPart || !pathPart) continue;
|
|
515
|
-
if (defPart.startsWith(":")) {
|
|
516
|
-
descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
|
|
517
|
-
} else if (defPart.includes("*")) {
|
|
518
|
-
descendantParams["*"] = decodeURIComponent(pathPart);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
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
|
+
);
|
|
522
540
|
}
|
|
523
541
|
return {
|
|
524
542
|
...location,
|
|
@@ -617,15 +635,18 @@ class Route0 {
|
|
|
617
635
|
};
|
|
618
636
|
/** True when path structure is equal (param names are ignored). */
|
|
619
637
|
isSame(other) {
|
|
620
|
-
|
|
638
|
+
const thisShape = this.routeTokens.map((t) => {
|
|
621
639
|
if (t.kind === "static") return `s:${t.value}`;
|
|
622
640
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
623
641
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
624
|
-
}).join("/")
|
|
642
|
+
}).join("/");
|
|
643
|
+
const otherRoute = Route0.from(other);
|
|
644
|
+
const otherShape = otherRoute.routeTokens.map((t) => {
|
|
625
645
|
if (t.kind === "static") return `s:${t.value}`;
|
|
626
646
|
if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
|
|
627
647
|
return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
|
|
628
648
|
}).join("/");
|
|
649
|
+
return thisShape === otherShape;
|
|
629
650
|
}
|
|
630
651
|
/** Static convenience wrapper for `isSame`. */
|
|
631
652
|
static isSame(a, b) {
|
|
@@ -649,11 +670,21 @@ class Route0 {
|
|
|
649
670
|
const thisParts = getParts(this.definition);
|
|
650
671
|
const otherParts = getParts(other.definition);
|
|
651
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
|
+
};
|
|
652
682
|
for (let i = 0; i < otherParts.length; i++) {
|
|
653
683
|
const otherPart = otherParts[i];
|
|
654
684
|
const thisPart = thisParts[i];
|
|
655
|
-
|
|
656
|
-
if (
|
|
685
|
+
const result = matchesPatternPart(otherPart, thisPart);
|
|
686
|
+
if (!result.match) return false;
|
|
687
|
+
if (result.wildcard) return true;
|
|
657
688
|
}
|
|
658
689
|
return true;
|
|
659
690
|
}
|
|
@@ -668,58 +699,54 @@ class Route0 {
|
|
|
668
699
|
const thisParts = getParts(this.definition);
|
|
669
700
|
const otherParts = getParts(other.definition);
|
|
670
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
|
+
};
|
|
671
711
|
for (let i = 0; i < thisParts.length; i++) {
|
|
672
712
|
const thisPart = thisParts[i];
|
|
673
713
|
const otherPart = otherParts[i];
|
|
674
|
-
|
|
675
|
-
if (
|
|
714
|
+
const result = matchesPatternPart(thisPart, otherPart);
|
|
715
|
+
if (!result.match) return false;
|
|
716
|
+
if (result.wildcard) return true;
|
|
676
717
|
}
|
|
677
718
|
return true;
|
|
678
719
|
}
|
|
679
720
|
/** True when two route patterns can match the same concrete URL. */
|
|
680
|
-
|
|
721
|
+
isOverlap(other) {
|
|
681
722
|
if (!other) return false;
|
|
682
|
-
|
|
723
|
+
const otherRoute = Route0.from(other);
|
|
683
724
|
const thisRegex = this.regex;
|
|
684
|
-
const otherRegex =
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
const values = (token) => {
|
|
688
|
-
if (token.kind === "static") return [token.value];
|
|
689
|
-
if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
|
|
690
|
-
if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
|
|
691
|
-
return ["", "x", "x/y"];
|
|
692
|
-
};
|
|
693
|
-
let acc = [""];
|
|
694
|
-
for (const token of tokens) {
|
|
695
|
-
const next = [];
|
|
696
|
-
for (const base of acc) {
|
|
697
|
-
for (const value of values(token)) {
|
|
698
|
-
if (value === "") {
|
|
699
|
-
next.push(base);
|
|
700
|
-
} else if (value.startsWith("/")) {
|
|
701
|
-
next.push(`${base}${value}`);
|
|
702
|
-
} else {
|
|
703
|
-
next.push(`${base}/${value}`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
acc = next;
|
|
708
|
-
}
|
|
709
|
-
if (acc.length === 0) return ["/"];
|
|
710
|
-
return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
|
|
711
|
-
};
|
|
712
|
-
const thisCandidates = makeCandidates(this.definition);
|
|
713
|
-
const otherCandidates = makeCandidates(other.definition);
|
|
725
|
+
const otherRegex = otherRoute.regex;
|
|
726
|
+
const thisCandidates = this.routePatternCandidates;
|
|
727
|
+
const otherCandidates = otherRoute.routePatternCandidates;
|
|
714
728
|
if (thisCandidates.some((path) => otherRegex.test(path))) return true;
|
|
715
729
|
if (otherCandidates.some((path) => thisRegex.test(path))) return true;
|
|
716
730
|
return false;
|
|
717
731
|
}
|
|
718
|
-
/**
|
|
719
|
-
|
|
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) {
|
|
720
739
|
if (!other) return false;
|
|
721
|
-
|
|
722
|
-
|
|
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;
|
|
723
750
|
}
|
|
724
751
|
/** Specificity comparator used for deterministic route ordering. */
|
|
725
752
|
isMoreSpecificThan(other) {
|
|
@@ -747,6 +774,21 @@ class Route0 {
|
|
|
747
774
|
}
|
|
748
775
|
}
|
|
749
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
|
+
}
|
|
750
792
|
_routes;
|
|
751
793
|
_pathsOrdering;
|
|
752
794
|
_keysOrdering;
|
|
@@ -810,7 +852,7 @@ class Routes {
|
|
|
810
852
|
}
|
|
811
853
|
_getLocation(hrefOrHrefRelOrLocation) {
|
|
812
854
|
const input = hrefOrHrefRelOrLocation;
|
|
813
|
-
const pathname =
|
|
855
|
+
const pathname = Routes._getNormalizedPathnameFromInput(input);
|
|
814
856
|
for (const route of this._ordered) {
|
|
815
857
|
if (!route.isExactPathnameMatch(pathname)) {
|
|
816
858
|
continue;
|
|
@@ -832,7 +874,7 @@ class Routes {
|
|
|
832
874
|
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
833
875
|
const partsA = getParts(routeA.definition);
|
|
834
876
|
const partsB = getParts(routeB.definition);
|
|
835
|
-
if (routeA.
|
|
877
|
+
if (routeA.isOverlap(routeB)) {
|
|
836
878
|
if (routeA.isMoreSpecificThan(routeB)) return -1;
|
|
837
879
|
if (routeB.isMoreSpecificThan(routeA)) return 1;
|
|
838
880
|
}
|