@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.
@@ -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 getRouteSegments = (definition) => {
28
- if (definition === "" || definition === "/") return [];
29
- return definition.split("/").filter(Boolean);
30
- };
31
- const getRouteTokens = (definition) => {
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 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
- _regexBaseStrictString;
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
- validateRouteDefinition(definition);
149
- this.definition = definition;
150
- this.params = Route0._getParamsDefinitionByDefinition(definition);
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(definition, config);
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(definition);
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 sourceDefinitionWithoutWildcard = stripTrailingWildcard(this.definition);
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
- { origin: this._origin }
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.replace(/\/{2,}/g, "/");
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 getRouteTokens(this.definition);
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.regexBaseStrictString.replace(/\/+$/, "") + "/?";
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 = getRouteCaptureKeys(this.definition);
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(normalizePathname(pathname));
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 = normalizePathname(pathname);
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 pathname = normalizePathname(url.pathname);
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 = normalizePathname(location.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
- const pathParts = pathname === "/" ? ["/"] : pathname.split("/").filter(Boolean);
483
- let isPrefix = true;
484
- if (pathParts.length > defParts.length) {
485
- isPrefix = false;
486
- } else {
487
- for (let i = 0; i < pathParts.length; i++) {
488
- const defPart = defParts[i];
489
- const pathPart = pathParts[i];
490
- if (!defPart) {
491
- isPrefix = false;
492
- break;
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 descendantParams = {};
511
- for (let i = 0; i < pathParts.length; i++) {
512
- const defPart = defParts[i];
513
- const pathPart = pathParts[i];
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
- return getRouteTokens(this.definition).map((t) => {
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("/") === getRouteTokens(other.definition).map((t) => {
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
- if (otherPart.startsWith(":")) continue;
656
- if (otherPart !== thisPart) return false;
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
- if (thisPart.startsWith(":")) continue;
675
- if (thisPart !== otherPart) return false;
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
- isConflict(other) {
721
+ isOverlap(other) {
681
722
  if (!other) return false;
682
- other = Route0.create(other);
723
+ const otherRoute = Route0.from(other);
683
724
  const thisRegex = this.regex;
684
- const otherRegex = other.regex;
685
- const makeCandidates = (definition) => {
686
- const tokens = getRouteTokens(definition);
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
- /** True when paths are same or can overlap when optional parts are omitted. */
719
- isMayBeSame(other) {
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
- other = Route0.create(other);
722
- return this.isSame(other) || this.isConflict(other);
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 = getNormalizedPathnameFromInput(input);
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.isMayBeSame(routeB)) {
877
+ if (routeA.isOverlap(routeB)) {
836
878
  if (routeA.isMoreSpecificThan(routeB)) return -1;
837
879
  if (routeB.isMoreSpecificThan(routeA)) return 1;
838
880
  }