@devp0nt/route0 1.0.0-next.67 → 1.0.0-next.68

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.
@@ -22,13 +22,67 @@ __export(index_exports, {
22
22
  Routes: () => Routes
23
23
  });
24
24
  module.exports = __toCommonJS(index_exports);
25
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ const getPathSegments = (definition) => {
27
+ if (definition === "" || definition === "/") return [];
28
+ return definition.split("/").filter(Boolean);
29
+ };
30
+ const getRuntimePathTokens = (definition) => {
31
+ const segments = getPathSegments(definition);
32
+ return segments.map((segment) => {
33
+ const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
34
+ if (param) {
35
+ return { kind: "param", name: param[1], optional: param[2] === "?" };
36
+ }
37
+ if (segment === "*" || segment === "*?") {
38
+ return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
39
+ }
40
+ const wildcard = segment.match(/^(.*)\*(\?)?$/);
41
+ if (wildcard && !segment.includes("\\*")) {
42
+ return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
43
+ }
44
+ return { kind: "static", value: segment };
45
+ });
46
+ };
47
+ const getPathRegexBaseStrictString = (definition) => {
48
+ const tokens = getRuntimePathTokens(definition);
49
+ if (tokens.length === 0) return "";
50
+ let pattern = "";
51
+ for (const token of tokens) {
52
+ if (token.kind === "static") {
53
+ pattern += `/${escapeRegex(token.value)}`;
54
+ continue;
55
+ }
56
+ if (token.kind === "param") {
57
+ pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
58
+ continue;
59
+ }
60
+ if (token.prefix.length > 0) {
61
+ pattern += `/${escapeRegex(token.prefix)}(.*)`;
62
+ } else {
63
+ pattern += "(?:/(.*))?";
64
+ }
65
+ }
66
+ return pattern;
67
+ };
68
+ const getPathCaptureKeys = (definition) => {
69
+ const keys = [];
70
+ for (const token of getRuntimePathTokens(definition)) {
71
+ if (token.kind === "param") keys.push(token.name);
72
+ if (token.kind === "wildcard") keys.push("*");
73
+ }
74
+ return keys;
75
+ };
76
+ const getPathParamsDefinition = (definition) => {
77
+ const entries = getRuntimePathTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
78
+ return Object.fromEntries(entries);
79
+ };
25
80
  class Route0 {
26
81
  definition;
27
- pathDefinition;
28
- paramsDefinition;
29
- searchDefinition;
30
- hasLooseSearch;
82
+ params;
31
83
  _origin;
84
+ _callable;
85
+ Infer = null;
32
86
  /** Base URL used when generating absolute URLs (`abs: true`). */
33
87
  get origin() {
34
88
  if (!this._origin) {
@@ -43,10 +97,7 @@ class Route0 {
43
97
  }
44
98
  constructor(definition, config = {}) {
45
99
  this.definition = definition;
46
- this.pathDefinition = Route0._getPathDefinitionBydefinition(definition);
47
- this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition);
48
- this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition);
49
- this.hasLooseSearch = Route0._hasLooseSearch(definition);
100
+ this.params = Route0._getParamsDefinitionByDefinition(definition);
50
101
  const { origin } = config;
51
102
  if (origin && typeof origin === "string" && origin.length) {
52
103
  this._origin = origin;
@@ -58,6 +109,12 @@ class Route0 {
58
109
  this._origin = void 0;
59
110
  }
60
111
  }
112
+ const callable = this.get.bind(this);
113
+ Object.setPrototypeOf(callable, this);
114
+ Object.defineProperty(callable, Symbol.toStringTag, {
115
+ value: this.definition
116
+ });
117
+ this._callable = callable;
61
118
  }
62
119
  /**
63
120
  * Creates a callable route instance.
@@ -65,19 +122,11 @@ class Route0 {
65
122
  * If an existing route/callable route is provided, it is cloned.
66
123
  */
67
124
  static create(definition, config) {
68
- if (typeof definition === "function") {
69
- return definition.clone(config);
70
- }
71
- if (typeof definition === "object") {
125
+ if (typeof definition === "function" || typeof definition === "object") {
72
126
  return definition.clone(config);
73
127
  }
74
128
  const original = new Route0(definition, config);
75
- const callable = original.get.bind(original);
76
- Object.setPrototypeOf(callable, original);
77
- Object.defineProperty(callable, Symbol.toStringTag, {
78
- value: original.definition
79
- });
80
- return callable;
129
+ return original._callable;
81
130
  }
82
131
  /**
83
132
  * Normalizes a definition/route into a callable route.
@@ -89,61 +138,24 @@ class Route0 {
89
138
  return definition;
90
139
  }
91
140
  const original = typeof definition === "object" ? definition : new Route0(definition);
92
- const callable = original.get.bind(original);
93
- Object.setPrototypeOf(callable, original);
94
- Object.defineProperty(callable, Symbol.toStringTag, {
95
- value: original.definition
96
- });
97
- return callable;
98
- }
99
- static _splitPathDefinitionAndSearchTailDefinition(definition) {
100
- const i = definition.indexOf("&");
101
- if (i === -1) return { pathDefinition: definition, searchTailDefinition: "" };
102
- return {
103
- pathDefinition: definition.slice(0, i),
104
- searchTailDefinition: definition.slice(i)
105
- };
106
- }
107
- static _getAbsPath(origin, pathWithSearch) {
108
- return new URL(pathWithSearch, origin).toString().replace(/\/$/, "");
141
+ return original._callable;
109
142
  }
110
- static _getPathDefinitionBydefinition(definition) {
111
- const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
112
- return pathDefinition;
143
+ static _getAbsPath(origin, url) {
144
+ return new URL(url, origin).toString().replace(/\/$/, "");
113
145
  }
114
- static _getParamsDefinitionBydefinition(definition) {
115
- const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
116
- const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g));
117
- const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]));
118
- const keysCount = Object.keys(paramsDefinition).length;
119
- if (keysCount === 0) {
120
- return void 0;
121
- }
122
- return paramsDefinition;
123
- }
124
- static _getSearchDefinitionBydefinition(definition) {
125
- const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
126
- if (!searchTailDefinition) {
127
- return void 0;
128
- }
129
- const keys = searchTailDefinition.split("&").filter(Boolean);
130
- const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]));
131
- const keysCount = Object.keys(searchDefinition).length;
132
- if (keysCount === 0) {
133
- return void 0;
134
- }
135
- return searchDefinition;
146
+ static _getParamsDefinitionByDefinition(definition) {
147
+ return getPathParamsDefinition(definition);
136
148
  }
137
- static _hasLooseSearch(definition) {
138
- return definition.endsWith("&");
149
+ search() {
150
+ return this._callable;
139
151
  }
140
152
  /** Extends the current route definition by appending a suffix route. */
141
153
  extend(suffixDefinition) {
142
- const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(this.definition);
143
- const { pathDefinition: suffixPathDefinition, searchTailDefinition: suffixSearchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(suffixDefinition);
144
- const pathDefinition = `${parentPathDefinition}/${suffixPathDefinition}`.replace(/\/{2,}/g, "/");
145
- const definition = `${pathDefinition}${suffixSearchTailDefinition}`;
146
- return Route0.create(definition, { origin: this._origin });
154
+ const definition = `${this.definition}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
155
+ return Route0.create(
156
+ definition,
157
+ { origin: this._origin }
158
+ );
147
159
  }
148
160
  // implementation
149
161
  get(...args) {
@@ -157,35 +169,61 @@ class Route0 {
157
169
  hashInput: void 0
158
170
  };
159
171
  }
160
- const input = args[0];
161
- if (typeof input !== "object" || input === null) {
162
- return {
163
- searchInput: {},
164
- paramsInput: {},
165
- absInput: false,
166
- absOriginInput: void 0,
167
- hashInput: void 0
168
- };
172
+ const [input, abs] = (() => {
173
+ if (typeof args[0] === "object" && args[0] !== null) {
174
+ return [args[0], args[1]];
175
+ }
176
+ if (typeof args[1] === "object" && args[1] !== null) {
177
+ return [args[1], args[0]];
178
+ }
179
+ if (typeof args[0] === "boolean" || typeof args[0] === "string") {
180
+ return [{}, args[0]];
181
+ }
182
+ if (typeof args[1] === "boolean" || typeof args[1] === "string") {
183
+ return [{}, args[1]];
184
+ }
185
+ return [{}, void 0];
186
+ })();
187
+ let searchInput2 = {};
188
+ let hashInput2 = void 0;
189
+ const paramsInput2 = {};
190
+ for (const [key, value] of Object.entries(input)) {
191
+ if (key === "?" && typeof value === "object" && value !== null) {
192
+ searchInput2 = value;
193
+ } else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
194
+ hashInput2 = String(value);
195
+ } else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
196
+ Object.assign(paramsInput2, { [key]: String(value) });
197
+ }
169
198
  }
170
- const { search, abs, hash, ...params } = input;
171
199
  const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
172
200
  return {
173
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
174
- searchInput: search || {},
175
- paramsInput: params,
201
+ searchInput: searchInput2,
202
+ paramsInput: paramsInput2,
176
203
  absInput: absOriginInput2 !== void 0 || abs === true,
177
204
  absOriginInput: absOriginInput2,
178
- hashInput: hash
205
+ hashInput: hashInput2
179
206
  };
180
207
  })();
181
- const neededParamsKeys = this.paramsDefinition ? Object.keys(this.paramsDefinition) : [];
182
- const providedParamsKeys = Object.keys(paramsInput);
183
- const notProvidedKeys = neededParamsKeys.filter((k) => !providedParamsKeys.includes(k));
184
- if (notProvidedKeys.length) {
185
- Object.assign(paramsInput, Object.fromEntries(notProvidedKeys.map((k) => [k, "undefined"])));
186
- }
187
- let url = this.pathDefinition;
188
- url = url.replace(/:([A-Za-z0-9_]+)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "")));
208
+ let url = this.definition;
209
+ url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
210
+ const value = paramsInput[k];
211
+ if (value === void 0) return "";
212
+ return `/${encodeURIComponent(String(value))}`;
213
+ });
214
+ url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
215
+ url = url.replace(/\/\*\?/g, () => {
216
+ const value = paramsInput["*"];
217
+ if (value === void 0) return "";
218
+ const stringValue = String(value);
219
+ return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
220
+ });
221
+ url = url.replace(/\/\*/g, () => {
222
+ const value = String(paramsInput["*"] ?? "");
223
+ return value.startsWith("/") ? value : `/${value}`;
224
+ });
225
+ url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
226
+ url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
189
227
  const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]));
190
228
  url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join("?");
191
229
  url = url.replace(/\/{2,}/g, "/");
@@ -195,92 +233,16 @@ class Route0 {
195
233
  }
196
234
  return url;
197
235
  }
198
- // implementation
199
- flat(...args) {
200
- const { searchInput, paramsInput, absInput, hashInput } = (() => {
201
- if (args.length === 0) {
202
- return {
203
- searchInput: {},
204
- paramsInput: {},
205
- absInput: false,
206
- hashInput: void 0
207
- };
208
- }
209
- const input = args[0];
210
- if (typeof input !== "object" || input === null) {
211
- return {
212
- searchInput: {},
213
- paramsInput: {},
214
- absInput: args[1] ?? false,
215
- hashInput: void 0
216
- };
217
- }
218
- const loose = args[2] ?? this.hasLooseSearch;
219
- const paramsKeys = this.getParamsKeys();
220
- const paramsInput2 = paramsKeys.reduce((acc, key) => {
221
- if (input[key] !== void 0) {
222
- acc[key] = input[key];
223
- }
224
- return acc;
225
- }, {});
226
- const searchKeys = this.getSearchKeys();
227
- const searchInput2 = Object.keys(input).filter((k) => {
228
- if (k === "hash") {
229
- return false;
230
- }
231
- if (searchKeys.includes(k)) {
232
- return true;
233
- }
234
- if (paramsKeys.includes(k)) {
235
- return false;
236
- }
237
- return loose;
238
- }).reduce((acc, key) => {
239
- acc[key] = input[key];
240
- return acc;
241
- }, {});
242
- const hashInput2 = input.hash;
243
- return {
244
- searchInput: searchInput2,
245
- paramsInput: paramsInput2,
246
- absInput: args[1] ?? false,
247
- hashInput: hashInput2
248
- };
249
- })();
250
- return this.get({
251
- ...paramsInput,
252
- search: searchInput,
253
- abs: absInput,
254
- hash: hashInput
255
- });
256
- }
257
- flatLoose(...args) {
258
- return this.flat(args[0], args[1], true);
259
- }
260
- flatStrict(...args) {
261
- return this.flat(args[0], args[1], false);
262
- }
263
236
  /** Returns path param keys extracted from route definition. */
264
237
  getParamsKeys() {
265
- return Object.keys(this.paramsDefinition || {});
266
- }
267
- /** Returns named search keys extracted from route definition. */
268
- getSearchKeys() {
269
- return Object.keys(this.searchDefinition || {});
270
- }
271
- /** Returns all flat input keys (`search + params`). */
272
- getFlatKeys() {
273
- return [...this.getSearchKeys(), ...this.getParamsKeys()];
274
- }
275
- getDefinition() {
276
- return this.pathDefinition;
238
+ return Object.keys(this.params);
277
239
  }
278
240
  /** Clones route with optional config override. */
279
241
  clone(config) {
280
242
  return Route0.create(this.definition, config);
281
243
  }
282
244
  getRegexBaseStrictString() {
283
- return this.pathDefinition.replace(/:(\w+)/g, "___PARAM___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/___PARAM___/g, "([^/]+)");
245
+ return getPathRegexBaseStrictString(this.definition);
284
246
  }
285
247
  getRegexBaseString() {
286
248
  return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
@@ -394,12 +356,8 @@ class Route0 {
394
356
  location.route = this.definition;
395
357
  location.params = {};
396
358
  const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
397
- const paramNames = [];
398
- const def = this.pathDefinition.length > 1 && this.pathDefinition.endsWith("/") ? this.pathDefinition.slice(0, -1) : this.pathDefinition;
399
- def.replace(/:([A-Za-z0-9_]+)/g, (_m, name) => {
400
- paramNames.push(String(name));
401
- return "";
402
- });
359
+ const paramNames = getPathCaptureKeys(this.definition);
360
+ const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
403
361
  const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
404
362
  const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
405
363
  const exactMatch = pathname.match(exactRe);
@@ -409,7 +367,12 @@ class Route0 {
409
367
  const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
410
368
  if (paramsMatch) {
411
369
  const values = paramsMatch.slice(1, 1 + paramNames.length);
412
- const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? "")]));
370
+ const params = Object.fromEntries(
371
+ paramNames.map((n, i) => {
372
+ const value = values[i];
373
+ return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
374
+ })
375
+ );
413
376
  location.params = params;
414
377
  } else {
415
378
  location.params = {};
@@ -429,6 +392,12 @@ class Route0 {
429
392
  break;
430
393
  }
431
394
  if (defPart.startsWith(":")) continue;
395
+ if (defPart.includes("*")) {
396
+ const prefix = defPart.replace(/\*\??$/, "");
397
+ if (pathPart.startsWith(prefix)) continue;
398
+ isPrefix = false;
399
+ break;
400
+ }
432
401
  if (defPart !== pathPart) {
433
402
  isPrefix = false;
434
403
  break;
@@ -444,7 +413,9 @@ class Route0 {
444
413
  const pathPart = pathParts[i];
445
414
  if (!defPart || !pathPart) continue;
446
415
  if (defPart.startsWith(":")) {
447
- descendantParams[defPart.slice(1)] = decodeURIComponent(pathPart);
416
+ descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
417
+ } else if (defPart.includes("*")) {
418
+ descendantParams["*"] = decodeURIComponent(pathPart);
448
419
  }
449
420
  }
450
421
  location.params = descendantParams;
@@ -459,13 +430,16 @@ class Route0 {
459
430
  };
460
431
  }
461
432
  _validateParamsInput(input) {
462
- const paramsKeys = this.getParamsKeys();
433
+ const paramsEntries = Object.entries(this.params);
434
+ const paramsMap = this.params;
435
+ const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
436
+ const paramsKeys = paramsEntries.map(([k]) => k);
463
437
  if (input === void 0) {
464
- if (paramsKeys.length) {
438
+ if (requiredParamsKeys.length) {
465
439
  return {
466
440
  issues: [
467
441
  {
468
- message: `Missing params: ${paramsKeys.map((k) => `"${k}"`).join(", ")}`
442
+ message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
469
443
  }
470
444
  ]
471
445
  };
@@ -474,12 +448,12 @@ class Route0 {
474
448
  }
475
449
  if (typeof input !== "object" || input === null) {
476
450
  return {
477
- issues: [{ message: "Invalid input: expected object" }]
451
+ issues: [{ message: "Invalid route params: expected object" }]
478
452
  };
479
453
  }
480
454
  const inputObj = input;
481
455
  const inputKeys = Object.keys(inputObj);
482
- const notDefinedKeys = paramsKeys.filter((k) => !inputKeys.includes(k));
456
+ const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
483
457
  if (notDefinedKeys.length) {
484
458
  return {
485
459
  issues: [
@@ -492,13 +466,16 @@ class Route0 {
492
466
  const data = {};
493
467
  for (const k of paramsKeys) {
494
468
  const v = inputObj[k];
495
- if (typeof v === "string") {
469
+ const required = paramsMap[k];
470
+ if (v === void 0 && !required) {
471
+ data[k] = void 0;
472
+ } else if (typeof v === "string") {
496
473
  data[k] = v;
497
474
  } else if (typeof v === "number") {
498
475
  data[k] = String(v);
499
476
  } else {
500
477
  return {
501
- issues: [{ message: `Invalid input: expected string, number, got ${typeof v} for "${k}"` }]
478
+ issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
502
479
  };
503
480
  }
504
481
  }
@@ -506,58 +483,6 @@ class Route0 {
506
483
  value: data
507
484
  };
508
485
  }
509
- _validateSearchInput(input, loose) {
510
- if (input === void 0) {
511
- input = {};
512
- }
513
- if (typeof input !== "object" || input === null) {
514
- return {
515
- issues: [{ message: "Invalid input: expected object" }]
516
- };
517
- }
518
- const inputObj = input;
519
- const paramsKeys = this.getParamsKeys();
520
- const searchKeys = this.getSearchKeys();
521
- const data = {};
522
- for (const [k, v] of Object.entries(inputObj)) {
523
- if (k === "hash") continue;
524
- if (paramsKeys.includes(k)) continue;
525
- if (!loose && !searchKeys.includes(k)) continue;
526
- if (v === void 0) continue;
527
- if (typeof v === "string") {
528
- data[k] = v;
529
- } else if (typeof v === "number") {
530
- data[k] = String(v);
531
- } else {
532
- return {
533
- issues: [{ message: `Invalid input: expected string, number, or undefined, got ${typeof v} for "${k}"` }]
534
- };
535
- }
536
- }
537
- return {
538
- value: data
539
- };
540
- }
541
- _validateFlatInput(input, loose) {
542
- const paramsResult = this._validateParamsInput(input);
543
- if ("issues" in paramsResult) {
544
- return {
545
- issues: paramsResult.issues ?? []
546
- };
547
- }
548
- const searchResult = this._validateSearchInput(input, loose);
549
- if ("issues" in searchResult) {
550
- return {
551
- issues: searchResult.issues ?? []
552
- };
553
- }
554
- return {
555
- value: {
556
- ...searchResult.value,
557
- ...paramsResult.value
558
- }
559
- };
560
- }
561
486
  _safeParseSchemaResult(result) {
562
487
  if ("issues" in result) {
563
488
  return {
@@ -580,7 +505,7 @@ class Route0 {
580
505
  return safeResult.data;
581
506
  }
582
507
  /** Standard Schema for route params input. */
583
- paramsInputSchema = {
508
+ paramsSchema = {
584
509
  "~standard": {
585
510
  version: 1,
586
511
  vendor: "route0",
@@ -590,42 +515,17 @@ class Route0 {
590
515
  parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
591
516
  safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
592
517
  };
593
- /** Standard Schema for strict search input. */
594
- strictSearchInputSchema = {
595
- "~standard": {
596
- version: 1,
597
- vendor: "route0",
598
- validate: (value) => this._validateSearchInput(value, false),
599
- types: void 0
600
- },
601
- parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, false)),
602
- safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, false))
603
- };
604
- /** Standard Schema for loose search input. */
605
- looseSearchInputSchema = {
606
- "~standard": {
607
- version: 1,
608
- vendor: "route0",
609
- validate: (value) => this._validateSearchInput(value, true),
610
- types: void 0
611
- },
612
- parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, true)),
613
- safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, true))
614
- };
615
- /** Standard Schema for route flat input (uses route default strict/loose mode). */
616
- flatInputSchema = {
617
- "~standard": {
618
- version: 1,
619
- vendor: "route0",
620
- validate: (value) => this._validateFlatInput(value, this.hasLooseSearch),
621
- types: void 0
622
- },
623
- parse: (value) => this._parseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch)),
624
- safeParse: (value) => this._safeParseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch))
625
- };
626
518
  /** True when path structure is equal (param names are ignored). */
627
519
  isSame(other) {
628
- return this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__") === other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__");
520
+ return getRuntimePathTokens(this.definition).map((t) => {
521
+ if (t.kind === "static") return `s:${t.value}`;
522
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
523
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
524
+ }).join("/") === getRuntimePathTokens(other.definition).map((t) => {
525
+ if (t.kind === "static") return `s:${t.value}`;
526
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
527
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
528
+ }).join("/");
629
529
  }
630
530
  /** Static convenience wrapper for `isSame`. */
631
531
  static isSame(a, b) {
@@ -643,11 +543,11 @@ class Route0 {
643
543
  if (!other) return false;
644
544
  other = Route0.create(other);
645
545
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
646
- if (other.pathDefinition === "/" && this.pathDefinition !== "/") {
546
+ if (other.definition === "/" && this.definition !== "/") {
647
547
  return true;
648
548
  }
649
- const thisParts = getParts(this.pathDefinition);
650
- const otherParts = getParts(other.pathDefinition);
549
+ const thisParts = getParts(this.definition);
550
+ const otherParts = getParts(other.definition);
651
551
  if (thisParts.length <= otherParts.length) return false;
652
552
  for (let i = 0; i < otherParts.length; i++) {
653
553
  const otherPart = otherParts[i];
@@ -662,11 +562,11 @@ class Route0 {
662
562
  if (!other) return false;
663
563
  other = Route0.create(other);
664
564
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
665
- if (this.pathDefinition === "/" && other.pathDefinition !== "/") {
565
+ if (this.definition === "/" && other.definition !== "/") {
666
566
  return true;
667
567
  }
668
- const thisParts = getParts(this.pathDefinition);
669
- const otherParts = getParts(other.pathDefinition);
568
+ const thisParts = getParts(this.definition);
569
+ const otherParts = getParts(other.definition);
670
570
  if (thisParts.length >= otherParts.length) return false;
671
571
  for (let i = 0; i < thisParts.length; i++) {
672
572
  const thisPart = thisParts[i];
@@ -680,29 +580,46 @@ class Route0 {
680
580
  isConflict(other) {
681
581
  if (!other) return false;
682
582
  other = Route0.create(other);
683
- const getParts = (path) => {
684
- if (path === "/") return ["/"];
685
- return path.split("/").filter(Boolean);
686
- };
687
- const thisParts = getParts(this.pathDefinition);
688
- const otherParts = getParts(other.pathDefinition);
689
- if (thisParts.length !== otherParts.length) {
690
- return false;
691
- }
692
- for (let i = 0; i < thisParts.length; i++) {
693
- const thisPart = thisParts[i];
694
- const otherPart = otherParts[i];
695
- if (thisPart.startsWith(":") && otherPart.startsWith(":")) {
696
- continue;
697
- }
698
- if (thisPart.startsWith(":") || otherPart.startsWith(":")) {
699
- continue;
700
- }
701
- if (thisPart !== otherPart) {
702
- return false;
583
+ const thisRegex = this.getRegex();
584
+ const otherRegex = other.getRegex();
585
+ const makeCandidates = (definition) => {
586
+ const tokens = getRuntimePathTokens(definition);
587
+ const values = (token) => {
588
+ if (token.kind === "static") return [token.value];
589
+ if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
590
+ if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
591
+ return ["", "x", "x/y"];
592
+ };
593
+ let acc = [""];
594
+ for (const token of tokens) {
595
+ const next = [];
596
+ for (const base of acc) {
597
+ for (const value of values(token)) {
598
+ if (value === "") {
599
+ next.push(base);
600
+ } else if (value.startsWith("/")) {
601
+ next.push(`${base}${value}`);
602
+ } else {
603
+ next.push(`${base}/${value}`);
604
+ }
605
+ }
606
+ }
607
+ acc = next;
703
608
  }
704
- }
705
- return true;
609
+ if (acc.length === 0) return ["/"];
610
+ return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
611
+ };
612
+ const thisCandidates = makeCandidates(this.definition);
613
+ const otherCandidates = makeCandidates(other.definition);
614
+ if (thisCandidates.some((path) => otherRegex.test(path))) return true;
615
+ if (otherCandidates.some((path) => thisRegex.test(path))) return true;
616
+ return false;
617
+ }
618
+ /** True when paths are same or can overlap when optional parts are omitted. */
619
+ isMayBeSame(other) {
620
+ if (!other) return false;
621
+ other = Route0.create(other);
622
+ return this.isSame(other) || this.isConflict(other);
706
623
  }
707
624
  /** Specificity comparator used for deterministic route ordering. */
708
625
  isMoreSpecificThan(other) {
@@ -712,15 +629,21 @@ class Route0 {
712
629
  if (path === "/") return ["/"];
713
630
  return path.split("/").filter(Boolean);
714
631
  };
715
- const thisParts = getParts(this.pathDefinition);
716
- const otherParts = getParts(other.pathDefinition);
632
+ const rank = (part) => {
633
+ if (part.includes("*")) return -1;
634
+ if (part.startsWith(":") && part.endsWith("?")) return 0;
635
+ if (part.startsWith(":")) return 1;
636
+ return 2;
637
+ };
638
+ const thisParts = getParts(this.definition);
639
+ const otherParts = getParts(other.definition);
717
640
  for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
718
- const thisIsStatic = !thisParts[i].startsWith(":");
719
- const otherIsStatic = !otherParts[i].startsWith(":");
720
- if (thisIsStatic && !otherIsStatic) return true;
721
- if (!thisIsStatic && otherIsStatic) return false;
641
+ const thisRank = rank(thisParts[i]);
642
+ const otherRank = rank(otherParts[i]);
643
+ if (thisRank > otherRank) return true;
644
+ if (thisRank < otherRank) return false;
722
645
  }
723
- return this.pathDefinition < other.pathDefinition;
646
+ return this.definition < other.definition;
724
647
  }
725
648
  }
726
649
  class Routes {
@@ -803,16 +726,16 @@ class Routes {
803
726
  return path.split("/").filter(Boolean);
804
727
  };
805
728
  entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
806
- const partsA = getParts(routeA.pathDefinition);
807
- const partsB = getParts(routeB.pathDefinition);
808
- if (partsA.length !== partsB.length) {
809
- return partsA.length - partsB.length;
810
- }
811
- if (routeA.isConflict(routeB)) {
729
+ const partsA = getParts(routeA.definition);
730
+ const partsB = getParts(routeB.definition);
731
+ if (routeA.isMayBeSame(routeB)) {
812
732
  if (routeA.isMoreSpecificThan(routeB)) return -1;
813
733
  if (routeB.isMoreSpecificThan(routeA)) return 1;
814
734
  }
815
- return routeA.pathDefinition.localeCompare(routeB.pathDefinition);
735
+ if (partsA.length !== partsB.length) {
736
+ return partsA.length - partsB.length;
737
+ }
738
+ return routeA.definition.localeCompare(routeB.definition);
816
739
  });
817
740
  const pathsOrdering = entries.map(([_key, route]) => route.definition);
818
741
  const keysOrdering = entries.map(([_key]) => _key);