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

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,85 @@ __export(index_exports, {
22
22
  Routes: () => Routes
23
23
  });
24
24
  module.exports = __toCommonJS(index_exports);
25
+ var import_flat0 = require("@devp0nt/flat0");
26
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ const getPathSegments = (definition) => {
28
+ if (definition === "" || definition === "/") return [];
29
+ return definition.split("/").filter(Boolean);
30
+ };
31
+ const getPathTokens = (definition) => {
32
+ const segments = getPathSegments(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 getPathRegexBaseStrictString = (definition) => {
49
+ const tokens = getPathTokens(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 getPathCaptureKeys = (definition) => {
70
+ const keys = [];
71
+ for (const token of getPathTokens(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 = getPathTokens(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 validatePathDefinition = (definition) => {
82
+ const segments = getPathSegments(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(/\*\??$/, "");
25
98
  class Route0 {
26
99
  definition;
27
- pathDefinition;
28
- paramsDefinition;
29
- searchDefinition;
30
- hasLooseSearch;
100
+ params;
31
101
  _origin;
102
+ _callable;
103
+ Infer = null;
32
104
  /** Base URL used when generating absolute URLs (`abs: true`). */
33
105
  get origin() {
34
106
  if (!this._origin) {
@@ -42,11 +114,9 @@ class Route0 {
42
114
  this._origin = origin;
43
115
  }
44
116
  constructor(definition, config = {}) {
117
+ validatePathDefinition(definition);
45
118
  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);
119
+ this.params = Route0._getParamsDefinitionByDefinition(definition);
50
120
  const { origin } = config;
51
121
  if (origin && typeof origin === "string" && origin.length) {
52
122
  this._origin = origin;
@@ -58,6 +128,12 @@ class Route0 {
58
128
  this._origin = void 0;
59
129
  }
60
130
  }
131
+ const callable = this.get.bind(this);
132
+ Object.setPrototypeOf(callable, this);
133
+ Object.defineProperty(callable, Symbol.toStringTag, {
134
+ value: this.definition
135
+ });
136
+ this._callable = callable;
61
137
  }
62
138
  /**
63
139
  * Creates a callable route instance.
@@ -65,19 +141,11 @@ class Route0 {
65
141
  * If an existing route/callable route is provided, it is cloned.
66
142
  */
67
143
  static create(definition, config) {
68
- if (typeof definition === "function") {
69
- return definition.clone(config);
70
- }
71
- if (typeof definition === "object") {
144
+ if (typeof definition === "function" || typeof definition === "object") {
72
145
  return definition.clone(config);
73
146
  }
74
147
  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;
148
+ return original._callable;
81
149
  }
82
150
  /**
83
151
  * Normalizes a definition/route into a callable route.
@@ -89,61 +157,25 @@ class Route0 {
89
157
  return definition;
90
158
  }
91
159
  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
- };
160
+ return original._callable;
106
161
  }
107
- static _getAbsPath(origin, pathWithSearch) {
108
- return new URL(pathWithSearch, origin).toString().replace(/\/$/, "");
162
+ static _getAbsPath(origin, url) {
163
+ return new URL(url, origin).toString().replace(/\/$/, "");
109
164
  }
110
- static _getPathDefinitionBydefinition(definition) {
111
- const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
112
- return pathDefinition;
113
- }
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;
165
+ static _getParamsDefinitionByDefinition(definition) {
166
+ return getPathParamsDefinition(definition);
136
167
  }
137
- static _hasLooseSearch(definition) {
138
- return definition.endsWith("&");
168
+ search() {
169
+ return this._callable;
139
170
  }
140
171
  /** Extends the current route definition by appending a suffix route. */
141
172
  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 });
173
+ const sourceDefinitionWithoutWildcard = stripTrailingWildcard(this.definition);
174
+ const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
175
+ return Route0.create(
176
+ definition,
177
+ { origin: this._origin }
178
+ );
147
179
  }
148
180
  // implementation
149
181
  get(...args) {
@@ -157,37 +189,63 @@ class Route0 {
157
189
  hashInput: void 0
158
190
  };
159
191
  }
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
- };
192
+ const [input, abs] = (() => {
193
+ if (typeof args[0] === "object" && args[0] !== null) {
194
+ return [args[0], args[1]];
195
+ }
196
+ if (typeof args[1] === "object" && args[1] !== null) {
197
+ return [args[1], args[0]];
198
+ }
199
+ if (typeof args[0] === "boolean" || typeof args[0] === "string") {
200
+ return [{}, args[0]];
201
+ }
202
+ if (typeof args[1] === "boolean" || typeof args[1] === "string") {
203
+ return [{}, args[1]];
204
+ }
205
+ return [{}, void 0];
206
+ })();
207
+ let searchInput2 = {};
208
+ let hashInput2 = void 0;
209
+ const paramsInput2 = {};
210
+ for (const [key, value] of Object.entries(input)) {
211
+ if (key === "?" && typeof value === "object" && value !== null) {
212
+ searchInput2 = value;
213
+ } else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
214
+ hashInput2 = String(value);
215
+ } else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
216
+ Object.assign(paramsInput2, { [key]: String(value) });
217
+ }
169
218
  }
170
- const { search, abs, hash, ...params } = input;
171
219
  const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
172
220
  return {
173
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
174
- searchInput: search || {},
175
- paramsInput: params,
221
+ searchInput: searchInput2,
222
+ paramsInput: paramsInput2,
176
223
  absInput: absOriginInput2 !== void 0 || abs === true,
177
224
  absOriginInput: absOriginInput2,
178
- hashInput: hash
225
+ hashInput: hashInput2
179
226
  };
180
227
  })();
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] ?? "")));
189
- const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]));
190
- url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join("?");
228
+ let url = this.definition;
229
+ url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
230
+ const value = paramsInput[k];
231
+ if (value === void 0) return "";
232
+ return `/${encodeURIComponent(String(value))}`;
233
+ });
234
+ url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
235
+ url = url.replace(/\/\*\?/g, () => {
236
+ const value = paramsInput["*"];
237
+ if (value === void 0) return "";
238
+ const stringValue = String(value);
239
+ return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
240
+ });
241
+ url = url.replace(/\/\*/g, () => {
242
+ const value = String(paramsInput["*"] ?? "");
243
+ return value.startsWith("/") ? value : `/${value}`;
244
+ });
245
+ url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
246
+ url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
247
+ const searchString = (0, import_flat0.stringify)(searchInput);
248
+ url = [url, searchString].filter(Boolean).join("?");
191
249
  url = url.replace(/\/{2,}/g, "/");
192
250
  url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
193
251
  if (hashInput !== void 0) {
@@ -195,92 +253,19 @@ class Route0 {
195
253
  }
196
254
  return url;
197
255
  }
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
256
  /** Returns path param keys extracted from route definition. */
264
257
  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 || {});
258
+ return Object.keys(this.params);
270
259
  }
271
- /** Returns all flat input keys (`search + params`). */
272
- getFlatKeys() {
273
- return [...this.getSearchKeys(), ...this.getParamsKeys()];
274
- }
275
- getDefinition() {
276
- return this.pathDefinition;
260
+ getPathTokens() {
261
+ return getPathTokens(this.definition);
277
262
  }
278
263
  /** Clones route with optional config override. */
279
264
  clone(config) {
280
265
  return Route0.create(this.definition, config);
281
266
  }
282
267
  getRegexBaseStrictString() {
283
- return this.pathDefinition.replace(/:(\w+)/g, "___PARAM___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/___PARAM___/g, "([^/]+)");
268
+ return getPathRegexBaseStrictString(this.definition);
284
269
  }
285
270
  getRegexBaseString() {
286
271
  return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
@@ -353,7 +338,7 @@ class Route0 {
353
338
  const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
354
339
  const base = abs ? void 0 : "http://example.com";
355
340
  const url = new URL(hrefOrHrefRelOrLocation, base);
356
- const searchParams = Object.fromEntries(url.searchParams.entries());
341
+ const search = (0, import_flat0.parse)(url.search);
357
342
  let pathname = url.pathname;
358
343
  if (pathname.length > 1 && pathname.endsWith("/")) {
359
344
  pathname = pathname.slice(0, -1);
@@ -361,7 +346,8 @@ class Route0 {
361
346
  const hrefRel = pathname + url.search + url.hash;
362
347
  const location = {
363
348
  pathname,
364
- search: url.search,
349
+ search,
350
+ searchString: url.search,
365
351
  hash: url.hash,
366
352
  origin: abs ? url.origin : void 0,
367
353
  href: abs ? url.href : void 0,
@@ -372,7 +358,6 @@ class Route0 {
372
358
  hostname: abs ? url.hostname : void 0,
373
359
  port: abs ? url.port || void 0 : void 0,
374
360
  // specific to UnknownLocation
375
- searchParams,
376
361
  params: void 0,
377
362
  route: void 0,
378
363
  known: false,
@@ -394,12 +379,8 @@ class Route0 {
394
379
  location.route = this.definition;
395
380
  location.params = {};
396
381
  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
- });
382
+ const paramNames = getPathCaptureKeys(this.definition);
383
+ const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
403
384
  const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
404
385
  const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
405
386
  const exactMatch = pathname.match(exactRe);
@@ -409,7 +390,12 @@ class Route0 {
409
390
  const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
410
391
  if (paramsMatch) {
411
392
  const values = paramsMatch.slice(1, 1 + paramNames.length);
412
- const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? "")]));
393
+ const params = Object.fromEntries(
394
+ paramNames.map((n, i) => {
395
+ const value = values[i];
396
+ return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
397
+ })
398
+ );
413
399
  location.params = params;
414
400
  } else {
415
401
  location.params = {};
@@ -429,6 +415,12 @@ class Route0 {
429
415
  break;
430
416
  }
431
417
  if (defPart.startsWith(":")) continue;
418
+ if (defPart.includes("*")) {
419
+ const prefix = defPart.replace(/\*\??$/, "");
420
+ if (pathPart.startsWith(prefix)) continue;
421
+ isPrefix = false;
422
+ break;
423
+ }
432
424
  if (defPart !== pathPart) {
433
425
  isPrefix = false;
434
426
  break;
@@ -444,7 +436,9 @@ class Route0 {
444
436
  const pathPart = pathParts[i];
445
437
  if (!defPart || !pathPart) continue;
446
438
  if (defPart.startsWith(":")) {
447
- descendantParams[defPart.slice(1)] = decodeURIComponent(pathPart);
439
+ descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
440
+ } else if (defPart.includes("*")) {
441
+ descendantParams["*"] = decodeURIComponent(pathPart);
448
442
  }
449
443
  }
450
444
  location.params = descendantParams;
@@ -459,13 +453,16 @@ class Route0 {
459
453
  };
460
454
  }
461
455
  _validateParamsInput(input) {
462
- const paramsKeys = this.getParamsKeys();
456
+ const paramsEntries = Object.entries(this.params);
457
+ const paramsMap = this.params;
458
+ const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
459
+ const paramsKeys = paramsEntries.map(([k]) => k);
463
460
  if (input === void 0) {
464
- if (paramsKeys.length) {
461
+ if (requiredParamsKeys.length) {
465
462
  return {
466
463
  issues: [
467
464
  {
468
- message: `Missing params: ${paramsKeys.map((k) => `"${k}"`).join(", ")}`
465
+ message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
469
466
  }
470
467
  ]
471
468
  };
@@ -474,12 +471,12 @@ class Route0 {
474
471
  }
475
472
  if (typeof input !== "object" || input === null) {
476
473
  return {
477
- issues: [{ message: "Invalid input: expected object" }]
474
+ issues: [{ message: "Invalid route params: expected object" }]
478
475
  };
479
476
  }
480
477
  const inputObj = input;
481
478
  const inputKeys = Object.keys(inputObj);
482
- const notDefinedKeys = paramsKeys.filter((k) => !inputKeys.includes(k));
479
+ const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
483
480
  if (notDefinedKeys.length) {
484
481
  return {
485
482
  issues: [
@@ -492,45 +489,16 @@ class Route0 {
492
489
  const data = {};
493
490
  for (const k of paramsKeys) {
494
491
  const v = inputObj[k];
495
- if (typeof v === "string") {
496
- data[k] = v;
497
- } else if (typeof v === "number") {
498
- data[k] = String(v);
499
- } else {
500
- return {
501
- issues: [{ message: `Invalid input: expected string, number, got ${typeof v} for "${k}"` }]
502
- };
503
- }
504
- }
505
- return {
506
- value: data
507
- };
508
- }
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") {
492
+ const required = paramsMap[k];
493
+ if (v === void 0 && !required) {
494
+ data[k] = void 0;
495
+ } else if (typeof v === "string") {
528
496
  data[k] = v;
529
497
  } else if (typeof v === "number") {
530
498
  data[k] = String(v);
531
499
  } else {
532
500
  return {
533
- issues: [{ message: `Invalid input: expected string, number, or undefined, got ${typeof v} for "${k}"` }]
501
+ issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
534
502
  };
535
503
  }
536
504
  }
@@ -538,26 +506,6 @@ class Route0 {
538
506
  value: data
539
507
  };
540
508
  }
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
509
  _safeParseSchemaResult(result) {
562
510
  if ("issues" in result) {
563
511
  return {
@@ -580,7 +528,7 @@ class Route0 {
580
528
  return safeResult.data;
581
529
  }
582
530
  /** Standard Schema for route params input. */
583
- paramsInputSchema = {
531
+ paramsSchema = {
584
532
  "~standard": {
585
533
  version: 1,
586
534
  vendor: "route0",
@@ -590,42 +538,17 @@ class Route0 {
590
538
  parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
591
539
  safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
592
540
  };
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
541
  /** True when path structure is equal (param names are ignored). */
627
542
  isSame(other) {
628
- return this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__") === other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__");
543
+ return getPathTokens(this.definition).map((t) => {
544
+ if (t.kind === "static") return `s:${t.value}`;
545
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
546
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
547
+ }).join("/") === getPathTokens(other.definition).map((t) => {
548
+ if (t.kind === "static") return `s:${t.value}`;
549
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
550
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
551
+ }).join("/");
629
552
  }
630
553
  /** Static convenience wrapper for `isSame`. */
631
554
  static isSame(a, b) {
@@ -643,11 +566,11 @@ class Route0 {
643
566
  if (!other) return false;
644
567
  other = Route0.create(other);
645
568
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
646
- if (other.pathDefinition === "/" && this.pathDefinition !== "/") {
569
+ if (other.definition === "/" && this.definition !== "/") {
647
570
  return true;
648
571
  }
649
- const thisParts = getParts(this.pathDefinition);
650
- const otherParts = getParts(other.pathDefinition);
572
+ const thisParts = getParts(this.definition);
573
+ const otherParts = getParts(other.definition);
651
574
  if (thisParts.length <= otherParts.length) return false;
652
575
  for (let i = 0; i < otherParts.length; i++) {
653
576
  const otherPart = otherParts[i];
@@ -662,11 +585,11 @@ class Route0 {
662
585
  if (!other) return false;
663
586
  other = Route0.create(other);
664
587
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
665
- if (this.pathDefinition === "/" && other.pathDefinition !== "/") {
588
+ if (this.definition === "/" && other.definition !== "/") {
666
589
  return true;
667
590
  }
668
- const thisParts = getParts(this.pathDefinition);
669
- const otherParts = getParts(other.pathDefinition);
591
+ const thisParts = getParts(this.definition);
592
+ const otherParts = getParts(other.definition);
670
593
  if (thisParts.length >= otherParts.length) return false;
671
594
  for (let i = 0; i < thisParts.length; i++) {
672
595
  const thisPart = thisParts[i];
@@ -680,29 +603,46 @@ class Route0 {
680
603
  isConflict(other) {
681
604
  if (!other) return false;
682
605
  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;
606
+ const thisRegex = this.getRegex();
607
+ const otherRegex = other.getRegex();
608
+ const makeCandidates = (definition) => {
609
+ const tokens = getPathTokens(definition);
610
+ const values = (token) => {
611
+ if (token.kind === "static") return [token.value];
612
+ if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
613
+ if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
614
+ return ["", "x", "x/y"];
615
+ };
616
+ let acc = [""];
617
+ for (const token of tokens) {
618
+ const next = [];
619
+ for (const base of acc) {
620
+ for (const value of values(token)) {
621
+ if (value === "") {
622
+ next.push(base);
623
+ } else if (value.startsWith("/")) {
624
+ next.push(`${base}${value}`);
625
+ } else {
626
+ next.push(`${base}/${value}`);
627
+ }
628
+ }
629
+ }
630
+ acc = next;
703
631
  }
704
- }
705
- return true;
632
+ if (acc.length === 0) return ["/"];
633
+ return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
634
+ };
635
+ const thisCandidates = makeCandidates(this.definition);
636
+ const otherCandidates = makeCandidates(other.definition);
637
+ if (thisCandidates.some((path) => otherRegex.test(path))) return true;
638
+ if (otherCandidates.some((path) => thisRegex.test(path))) return true;
639
+ return false;
640
+ }
641
+ /** True when paths are same or can overlap when optional parts are omitted. */
642
+ isMayBeSame(other) {
643
+ if (!other) return false;
644
+ other = Route0.create(other);
645
+ return this.isSame(other) || this.isConflict(other);
706
646
  }
707
647
  /** Specificity comparator used for deterministic route ordering. */
708
648
  isMoreSpecificThan(other) {
@@ -712,15 +652,21 @@ class Route0 {
712
652
  if (path === "/") return ["/"];
713
653
  return path.split("/").filter(Boolean);
714
654
  };
715
- const thisParts = getParts(this.pathDefinition);
716
- const otherParts = getParts(other.pathDefinition);
655
+ const rank = (part) => {
656
+ if (part.includes("*")) return -1;
657
+ if (part.startsWith(":") && part.endsWith("?")) return 0;
658
+ if (part.startsWith(":")) return 1;
659
+ return 2;
660
+ };
661
+ const thisParts = getParts(this.definition);
662
+ const otherParts = getParts(other.definition);
717
663
  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;
664
+ const thisRank = rank(thisParts[i]);
665
+ const otherRank = rank(otherParts[i]);
666
+ if (thisRank > otherRank) return true;
667
+ if (thisRank < otherRank) return false;
722
668
  }
723
- return this.pathDefinition < other.pathDefinition;
669
+ return this.definition < other.definition;
724
670
  }
725
671
  }
726
672
  class Routes {
@@ -803,16 +749,16 @@ class Routes {
803
749
  return path.split("/").filter(Boolean);
804
750
  };
805
751
  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)) {
752
+ const partsA = getParts(routeA.definition);
753
+ const partsB = getParts(routeB.definition);
754
+ if (routeA.isMayBeSame(routeB)) {
812
755
  if (routeA.isMoreSpecificThan(routeB)) return -1;
813
756
  if (routeB.isMoreSpecificThan(routeA)) return 1;
814
757
  }
815
- return routeA.pathDefinition.localeCompare(routeB.pathDefinition);
758
+ if (partsA.length !== partsB.length) {
759
+ return partsA.length - partsB.length;
760
+ }
761
+ return routeA.definition.localeCompare(routeB.definition);
816
762
  });
817
763
  const pathsOrdering = entries.map(([_key, route]) => route.definition);
818
764
  const keysOrdering = entries.map(([_key]) => _key);