@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.
package/dist/esm/index.js CHANGED
@@ -1,10 +1,82 @@
1
+ import { parse as parseSearchQuery, stringify as stringifySearchQuery } from "@devp0nt/flat0";
2
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
+ const getPathSegments = (definition) => {
4
+ if (definition === "" || definition === "/") return [];
5
+ return definition.split("/").filter(Boolean);
6
+ };
7
+ const getPathTokens = (definition) => {
8
+ const segments = getPathSegments(definition);
9
+ return segments.map((segment) => {
10
+ const param = segment.match(/^:([A-Za-z0-9_]+)(\?)?$/);
11
+ if (param) {
12
+ return { kind: "param", name: param[1], optional: param[2] === "?" };
13
+ }
14
+ if (segment === "*" || segment === "*?") {
15
+ return { kind: "wildcard", prefix: "", optional: segment.endsWith("?") };
16
+ }
17
+ const wildcard = segment.match(/^(.*)\*(\?)?$/);
18
+ if (wildcard && !segment.includes("\\*")) {
19
+ return { kind: "wildcard", prefix: wildcard[1], optional: wildcard[2] === "?" };
20
+ }
21
+ return { kind: "static", value: segment };
22
+ });
23
+ };
24
+ const getPathRegexBaseStrictString = (definition) => {
25
+ const tokens = getPathTokens(definition);
26
+ if (tokens.length === 0) return "";
27
+ let pattern = "";
28
+ for (const token of tokens) {
29
+ if (token.kind === "static") {
30
+ pattern += `/${escapeRegex(token.value)}`;
31
+ continue;
32
+ }
33
+ if (token.kind === "param") {
34
+ pattern += token.optional ? "(?:/([^/]+))?" : "/([^/]+)";
35
+ continue;
36
+ }
37
+ if (token.prefix.length > 0) {
38
+ pattern += `/${escapeRegex(token.prefix)}(.*)`;
39
+ } else {
40
+ pattern += "(?:/(.*))?";
41
+ }
42
+ }
43
+ return pattern;
44
+ };
45
+ const getPathCaptureKeys = (definition) => {
46
+ const keys = [];
47
+ for (const token of getPathTokens(definition)) {
48
+ if (token.kind === "param") keys.push(token.name);
49
+ if (token.kind === "wildcard") keys.push("*");
50
+ }
51
+ return keys;
52
+ };
53
+ const getPathParamsDefinition = (definition) => {
54
+ const entries = getPathTokens(definition).filter((t) => t.kind !== "static").map((t) => t.kind === "param" ? [t.name, !t.optional] : ["*", !t.optional]);
55
+ return Object.fromEntries(entries);
56
+ };
57
+ const validatePathDefinition = (definition) => {
58
+ const segments = getPathSegments(definition);
59
+ const wildcardSegments = segments.filter((segment) => segment.includes("*"));
60
+ if (wildcardSegments.length === 0) return;
61
+ if (wildcardSegments.length > 1) {
62
+ throw new Error(`Invalid route definition "${definition}": only one wildcard segment is allowed`);
63
+ }
64
+ const wildcardSegmentIndex = segments.findIndex((segment) => segment.includes("*"));
65
+ const wildcardSegment = segments[wildcardSegmentIndex];
66
+ if (!wildcardSegment.match(/^(?:\*|\*\?|[^*]+\*|\S+\*\?)$/)) {
67
+ throw new Error(`Invalid route definition "${definition}": wildcard must be trailing in its segment`);
68
+ }
69
+ if (wildcardSegmentIndex !== segments.length - 1) {
70
+ throw new Error(`Invalid route definition "${definition}": wildcard segment is allowed only at the end`);
71
+ }
72
+ };
73
+ const stripTrailingWildcard = (definition) => definition.replace(/\*\??$/, "");
1
74
  class Route0 {
2
75
  definition;
3
- pathDefinition;
4
- paramsDefinition;
5
- searchDefinition;
6
- hasLooseSearch;
76
+ params;
7
77
  _origin;
78
+ _callable;
79
+ Infer = null;
8
80
  /** Base URL used when generating absolute URLs (`abs: true`). */
9
81
  get origin() {
10
82
  if (!this._origin) {
@@ -18,11 +90,9 @@ class Route0 {
18
90
  this._origin = origin;
19
91
  }
20
92
  constructor(definition, config = {}) {
93
+ validatePathDefinition(definition);
21
94
  this.definition = definition;
22
- this.pathDefinition = Route0._getPathDefinitionBydefinition(definition);
23
- this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition);
24
- this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition);
25
- this.hasLooseSearch = Route0._hasLooseSearch(definition);
95
+ this.params = Route0._getParamsDefinitionByDefinition(definition);
26
96
  const { origin } = config;
27
97
  if (origin && typeof origin === "string" && origin.length) {
28
98
  this._origin = origin;
@@ -34,6 +104,12 @@ class Route0 {
34
104
  this._origin = void 0;
35
105
  }
36
106
  }
107
+ const callable = this.get.bind(this);
108
+ Object.setPrototypeOf(callable, this);
109
+ Object.defineProperty(callable, Symbol.toStringTag, {
110
+ value: this.definition
111
+ });
112
+ this._callable = callable;
37
113
  }
38
114
  /**
39
115
  * Creates a callable route instance.
@@ -41,19 +117,11 @@ class Route0 {
41
117
  * If an existing route/callable route is provided, it is cloned.
42
118
  */
43
119
  static create(definition, config) {
44
- if (typeof definition === "function") {
45
- return definition.clone(config);
46
- }
47
- if (typeof definition === "object") {
120
+ if (typeof definition === "function" || typeof definition === "object") {
48
121
  return definition.clone(config);
49
122
  }
50
123
  const original = new Route0(definition, config);
51
- const callable = original.get.bind(original);
52
- Object.setPrototypeOf(callable, original);
53
- Object.defineProperty(callable, Symbol.toStringTag, {
54
- value: original.definition
55
- });
56
- return callable;
124
+ return original._callable;
57
125
  }
58
126
  /**
59
127
  * Normalizes a definition/route into a callable route.
@@ -65,61 +133,25 @@ class Route0 {
65
133
  return definition;
66
134
  }
67
135
  const original = typeof definition === "object" ? definition : new Route0(definition);
68
- const callable = original.get.bind(original);
69
- Object.setPrototypeOf(callable, original);
70
- Object.defineProperty(callable, Symbol.toStringTag, {
71
- value: original.definition
72
- });
73
- return callable;
74
- }
75
- static _splitPathDefinitionAndSearchTailDefinition(definition) {
76
- const i = definition.indexOf("&");
77
- if (i === -1) return { pathDefinition: definition, searchTailDefinition: "" };
78
- return {
79
- pathDefinition: definition.slice(0, i),
80
- searchTailDefinition: definition.slice(i)
81
- };
136
+ return original._callable;
82
137
  }
83
- static _getAbsPath(origin, pathWithSearch) {
84
- return new URL(pathWithSearch, origin).toString().replace(/\/$/, "");
138
+ static _getAbsPath(origin, url) {
139
+ return new URL(url, origin).toString().replace(/\/$/, "");
85
140
  }
86
- static _getPathDefinitionBydefinition(definition) {
87
- const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
88
- return pathDefinition;
89
- }
90
- static _getParamsDefinitionBydefinition(definition) {
91
- const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
92
- const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g));
93
- const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]));
94
- const keysCount = Object.keys(paramsDefinition).length;
95
- if (keysCount === 0) {
96
- return void 0;
97
- }
98
- return paramsDefinition;
99
- }
100
- static _getSearchDefinitionBydefinition(definition) {
101
- const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
102
- if (!searchTailDefinition) {
103
- return void 0;
104
- }
105
- const keys = searchTailDefinition.split("&").filter(Boolean);
106
- const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]));
107
- const keysCount = Object.keys(searchDefinition).length;
108
- if (keysCount === 0) {
109
- return void 0;
110
- }
111
- return searchDefinition;
141
+ static _getParamsDefinitionByDefinition(definition) {
142
+ return getPathParamsDefinition(definition);
112
143
  }
113
- static _hasLooseSearch(definition) {
114
- return definition.endsWith("&");
144
+ search() {
145
+ return this._callable;
115
146
  }
116
147
  /** Extends the current route definition by appending a suffix route. */
117
148
  extend(suffixDefinition) {
118
- const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(this.definition);
119
- const { pathDefinition: suffixPathDefinition, searchTailDefinition: suffixSearchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(suffixDefinition);
120
- const pathDefinition = `${parentPathDefinition}/${suffixPathDefinition}`.replace(/\/{2,}/g, "/");
121
- const definition = `${pathDefinition}${suffixSearchTailDefinition}`;
122
- return Route0.create(definition, { origin: this._origin });
149
+ const sourceDefinitionWithoutWildcard = stripTrailingWildcard(this.definition);
150
+ const definition = `${sourceDefinitionWithoutWildcard}/${suffixDefinition}`.replace(/\/{2,}/g, "/");
151
+ return Route0.create(
152
+ definition,
153
+ { origin: this._origin }
154
+ );
123
155
  }
124
156
  // implementation
125
157
  get(...args) {
@@ -133,37 +165,63 @@ class Route0 {
133
165
  hashInput: void 0
134
166
  };
135
167
  }
136
- const input = args[0];
137
- if (typeof input !== "object" || input === null) {
138
- return {
139
- searchInput: {},
140
- paramsInput: {},
141
- absInput: false,
142
- absOriginInput: void 0,
143
- hashInput: void 0
144
- };
168
+ const [input, abs] = (() => {
169
+ if (typeof args[0] === "object" && args[0] !== null) {
170
+ return [args[0], args[1]];
171
+ }
172
+ if (typeof args[1] === "object" && args[1] !== null) {
173
+ return [args[1], args[0]];
174
+ }
175
+ if (typeof args[0] === "boolean" || typeof args[0] === "string") {
176
+ return [{}, args[0]];
177
+ }
178
+ if (typeof args[1] === "boolean" || typeof args[1] === "string") {
179
+ return [{}, args[1]];
180
+ }
181
+ return [{}, void 0];
182
+ })();
183
+ let searchInput2 = {};
184
+ let hashInput2 = void 0;
185
+ const paramsInput2 = {};
186
+ for (const [key, value] of Object.entries(input)) {
187
+ if (key === "?" && typeof value === "object" && value !== null) {
188
+ searchInput2 = value;
189
+ } else if (key === "#" && (typeof value === "string" || typeof value === "number")) {
190
+ hashInput2 = String(value);
191
+ } else if (key in this.params && (typeof value === "string" || typeof value === "number")) {
192
+ Object.assign(paramsInput2, { [key]: String(value) });
193
+ }
145
194
  }
146
- const { search, abs, hash, ...params } = input;
147
195
  const absOriginInput2 = typeof abs === "string" && abs.length > 0 ? abs : void 0;
148
196
  return {
149
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
150
- searchInput: search || {},
151
- paramsInput: params,
197
+ searchInput: searchInput2,
198
+ paramsInput: paramsInput2,
152
199
  absInput: absOriginInput2 !== void 0 || abs === true,
153
200
  absOriginInput: absOriginInput2,
154
- hashInput: hash
201
+ hashInput: hashInput2
155
202
  };
156
203
  })();
157
- const neededParamsKeys = this.paramsDefinition ? Object.keys(this.paramsDefinition) : [];
158
- const providedParamsKeys = Object.keys(paramsInput);
159
- const notProvidedKeys = neededParamsKeys.filter((k) => !providedParamsKeys.includes(k));
160
- if (notProvidedKeys.length) {
161
- Object.assign(paramsInput, Object.fromEntries(notProvidedKeys.map((k) => [k, "undefined"])));
162
- }
163
- let url = this.pathDefinition;
164
- url = url.replace(/:([A-Za-z0-9_]+)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "")));
165
- const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]));
166
- url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join("?");
204
+ let url = this.definition;
205
+ url = url.replace(/\/:([A-Za-z0-9_]+)\?/g, (_m, k) => {
206
+ const value = paramsInput[k];
207
+ if (value === void 0) return "";
208
+ return `/${encodeURIComponent(String(value))}`;
209
+ });
210
+ url = url.replace(/:([A-Za-z0-9_]+)(?!\?)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "undefined")));
211
+ url = url.replace(/\/\*\?/g, () => {
212
+ const value = paramsInput["*"];
213
+ if (value === void 0) return "";
214
+ const stringValue = String(value);
215
+ return stringValue.startsWith("/") ? stringValue : `/${stringValue}`;
216
+ });
217
+ url = url.replace(/\/\*/g, () => {
218
+ const value = String(paramsInput["*"] ?? "");
219
+ return value.startsWith("/") ? value : `/${value}`;
220
+ });
221
+ url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
222
+ url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
223
+ const searchString = stringifySearchQuery(searchInput);
224
+ url = [url, searchString].filter(Boolean).join("?");
167
225
  url = url.replace(/\/{2,}/g, "/");
168
226
  url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
169
227
  if (hashInput !== void 0) {
@@ -171,92 +229,19 @@ class Route0 {
171
229
  }
172
230
  return url;
173
231
  }
174
- // implementation
175
- flat(...args) {
176
- const { searchInput, paramsInput, absInput, hashInput } = (() => {
177
- if (args.length === 0) {
178
- return {
179
- searchInput: {},
180
- paramsInput: {},
181
- absInput: false,
182
- hashInput: void 0
183
- };
184
- }
185
- const input = args[0];
186
- if (typeof input !== "object" || input === null) {
187
- return {
188
- searchInput: {},
189
- paramsInput: {},
190
- absInput: args[1] ?? false,
191
- hashInput: void 0
192
- };
193
- }
194
- const loose = args[2] ?? this.hasLooseSearch;
195
- const paramsKeys = this.getParamsKeys();
196
- const paramsInput2 = paramsKeys.reduce((acc, key) => {
197
- if (input[key] !== void 0) {
198
- acc[key] = input[key];
199
- }
200
- return acc;
201
- }, {});
202
- const searchKeys = this.getSearchKeys();
203
- const searchInput2 = Object.keys(input).filter((k) => {
204
- if (k === "hash") {
205
- return false;
206
- }
207
- if (searchKeys.includes(k)) {
208
- return true;
209
- }
210
- if (paramsKeys.includes(k)) {
211
- return false;
212
- }
213
- return loose;
214
- }).reduce((acc, key) => {
215
- acc[key] = input[key];
216
- return acc;
217
- }, {});
218
- const hashInput2 = input.hash;
219
- return {
220
- searchInput: searchInput2,
221
- paramsInput: paramsInput2,
222
- absInput: args[1] ?? false,
223
- hashInput: hashInput2
224
- };
225
- })();
226
- return this.get({
227
- ...paramsInput,
228
- search: searchInput,
229
- abs: absInput,
230
- hash: hashInput
231
- });
232
- }
233
- flatLoose(...args) {
234
- return this.flat(args[0], args[1], true);
235
- }
236
- flatStrict(...args) {
237
- return this.flat(args[0], args[1], false);
238
- }
239
232
  /** Returns path param keys extracted from route definition. */
240
233
  getParamsKeys() {
241
- return Object.keys(this.paramsDefinition || {});
242
- }
243
- /** Returns named search keys extracted from route definition. */
244
- getSearchKeys() {
245
- return Object.keys(this.searchDefinition || {});
234
+ return Object.keys(this.params);
246
235
  }
247
- /** Returns all flat input keys (`search + params`). */
248
- getFlatKeys() {
249
- return [...this.getSearchKeys(), ...this.getParamsKeys()];
250
- }
251
- getDefinition() {
252
- return this.pathDefinition;
236
+ getPathTokens() {
237
+ return getPathTokens(this.definition);
253
238
  }
254
239
  /** Clones route with optional config override. */
255
240
  clone(config) {
256
241
  return Route0.create(this.definition, config);
257
242
  }
258
243
  getRegexBaseStrictString() {
259
- return this.pathDefinition.replace(/:(\w+)/g, "___PARAM___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/___PARAM___/g, "([^/]+)");
244
+ return getPathRegexBaseStrictString(this.definition);
260
245
  }
261
246
  getRegexBaseString() {
262
247
  return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
@@ -329,7 +314,7 @@ class Route0 {
329
314
  const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
330
315
  const base = abs ? void 0 : "http://example.com";
331
316
  const url = new URL(hrefOrHrefRelOrLocation, base);
332
- const searchParams = Object.fromEntries(url.searchParams.entries());
317
+ const search = parseSearchQuery(url.search);
333
318
  let pathname = url.pathname;
334
319
  if (pathname.length > 1 && pathname.endsWith("/")) {
335
320
  pathname = pathname.slice(0, -1);
@@ -337,7 +322,8 @@ class Route0 {
337
322
  const hrefRel = pathname + url.search + url.hash;
338
323
  const location = {
339
324
  pathname,
340
- search: url.search,
325
+ search,
326
+ searchString: url.search,
341
327
  hash: url.hash,
342
328
  origin: abs ? url.origin : void 0,
343
329
  href: abs ? url.href : void 0,
@@ -348,7 +334,6 @@ class Route0 {
348
334
  hostname: abs ? url.hostname : void 0,
349
335
  port: abs ? url.port || void 0 : void 0,
350
336
  // specific to UnknownLocation
351
- searchParams,
352
337
  params: void 0,
353
338
  route: void 0,
354
339
  known: false,
@@ -370,12 +355,8 @@ class Route0 {
370
355
  location.route = this.definition;
371
356
  location.params = {};
372
357
  const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
373
- const paramNames = [];
374
- const def = this.pathDefinition.length > 1 && this.pathDefinition.endsWith("/") ? this.pathDefinition.slice(0, -1) : this.pathDefinition;
375
- def.replace(/:([A-Za-z0-9_]+)/g, (_m, name) => {
376
- paramNames.push(String(name));
377
- return "";
378
- });
358
+ const paramNames = getPathCaptureKeys(this.definition);
359
+ const def = this.definition.length > 1 && this.definition.endsWith("/") ? this.definition.slice(0, -1) : this.definition;
379
360
  const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
380
361
  const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
381
362
  const exactMatch = pathname.match(exactRe);
@@ -385,7 +366,12 @@ class Route0 {
385
366
  const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
386
367
  if (paramsMatch) {
387
368
  const values = paramsMatch.slice(1, 1 + paramNames.length);
388
- const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? "")]));
369
+ const params = Object.fromEntries(
370
+ paramNames.map((n, i) => {
371
+ const value = values[i];
372
+ return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
373
+ })
374
+ );
389
375
  location.params = params;
390
376
  } else {
391
377
  location.params = {};
@@ -405,6 +391,12 @@ class Route0 {
405
391
  break;
406
392
  }
407
393
  if (defPart.startsWith(":")) continue;
394
+ if (defPart.includes("*")) {
395
+ const prefix = defPart.replace(/\*\??$/, "");
396
+ if (pathPart.startsWith(prefix)) continue;
397
+ isPrefix = false;
398
+ break;
399
+ }
408
400
  if (defPart !== pathPart) {
409
401
  isPrefix = false;
410
402
  break;
@@ -420,7 +412,9 @@ class Route0 {
420
412
  const pathPart = pathParts[i];
421
413
  if (!defPart || !pathPart) continue;
422
414
  if (defPart.startsWith(":")) {
423
- descendantParams[defPart.slice(1)] = decodeURIComponent(pathPart);
415
+ descendantParams[defPart.replace(/^:/, "").replace(/\?$/, "")] = decodeURIComponent(pathPart);
416
+ } else if (defPart.includes("*")) {
417
+ descendantParams["*"] = decodeURIComponent(pathPart);
424
418
  }
425
419
  }
426
420
  location.params = descendantParams;
@@ -435,13 +429,16 @@ class Route0 {
435
429
  };
436
430
  }
437
431
  _validateParamsInput(input) {
438
- const paramsKeys = this.getParamsKeys();
432
+ const paramsEntries = Object.entries(this.params);
433
+ const paramsMap = this.params;
434
+ const requiredParamsKeys = paramsEntries.filter(([, required]) => required).map(([k]) => k);
435
+ const paramsKeys = paramsEntries.map(([k]) => k);
439
436
  if (input === void 0) {
440
- if (paramsKeys.length) {
437
+ if (requiredParamsKeys.length) {
441
438
  return {
442
439
  issues: [
443
440
  {
444
- message: `Missing params: ${paramsKeys.map((k) => `"${k}"`).join(", ")}`
441
+ message: `Missing params: ${requiredParamsKeys.map((k) => `"${k}"`).join(", ")}`
445
442
  }
446
443
  ]
447
444
  };
@@ -450,12 +447,12 @@ class Route0 {
450
447
  }
451
448
  if (typeof input !== "object" || input === null) {
452
449
  return {
453
- issues: [{ message: "Invalid input: expected object" }]
450
+ issues: [{ message: "Invalid route params: expected object" }]
454
451
  };
455
452
  }
456
453
  const inputObj = input;
457
454
  const inputKeys = Object.keys(inputObj);
458
- const notDefinedKeys = paramsKeys.filter((k) => !inputKeys.includes(k));
455
+ const notDefinedKeys = requiredParamsKeys.filter((k) => !inputKeys.includes(k));
459
456
  if (notDefinedKeys.length) {
460
457
  return {
461
458
  issues: [
@@ -468,45 +465,16 @@ class Route0 {
468
465
  const data = {};
469
466
  for (const k of paramsKeys) {
470
467
  const v = inputObj[k];
471
- if (typeof v === "string") {
472
- data[k] = v;
473
- } else if (typeof v === "number") {
474
- data[k] = String(v);
475
- } else {
476
- return {
477
- issues: [{ message: `Invalid input: expected string, number, got ${typeof v} for "${k}"` }]
478
- };
479
- }
480
- }
481
- return {
482
- value: data
483
- };
484
- }
485
- _validateSearchInput(input, loose) {
486
- if (input === void 0) {
487
- input = {};
488
- }
489
- if (typeof input !== "object" || input === null) {
490
- return {
491
- issues: [{ message: "Invalid input: expected object" }]
492
- };
493
- }
494
- const inputObj = input;
495
- const paramsKeys = this.getParamsKeys();
496
- const searchKeys = this.getSearchKeys();
497
- const data = {};
498
- for (const [k, v] of Object.entries(inputObj)) {
499
- if (k === "hash") continue;
500
- if (paramsKeys.includes(k)) continue;
501
- if (!loose && !searchKeys.includes(k)) continue;
502
- if (v === void 0) continue;
503
- if (typeof v === "string") {
468
+ const required = paramsMap[k];
469
+ if (v === void 0 && !required) {
470
+ data[k] = void 0;
471
+ } else if (typeof v === "string") {
504
472
  data[k] = v;
505
473
  } else if (typeof v === "number") {
506
474
  data[k] = String(v);
507
475
  } else {
508
476
  return {
509
- issues: [{ message: `Invalid input: expected string, number, or undefined, got ${typeof v} for "${k}"` }]
477
+ issues: [{ message: `Invalid route params: expected string, number, got ${typeof v} for "${k}"` }]
510
478
  };
511
479
  }
512
480
  }
@@ -514,26 +482,6 @@ class Route0 {
514
482
  value: data
515
483
  };
516
484
  }
517
- _validateFlatInput(input, loose) {
518
- const paramsResult = this._validateParamsInput(input);
519
- if ("issues" in paramsResult) {
520
- return {
521
- issues: paramsResult.issues ?? []
522
- };
523
- }
524
- const searchResult = this._validateSearchInput(input, loose);
525
- if ("issues" in searchResult) {
526
- return {
527
- issues: searchResult.issues ?? []
528
- };
529
- }
530
- return {
531
- value: {
532
- ...searchResult.value,
533
- ...paramsResult.value
534
- }
535
- };
536
- }
537
485
  _safeParseSchemaResult(result) {
538
486
  if ("issues" in result) {
539
487
  return {
@@ -556,7 +504,7 @@ class Route0 {
556
504
  return safeResult.data;
557
505
  }
558
506
  /** Standard Schema for route params input. */
559
- paramsInputSchema = {
507
+ paramsSchema = {
560
508
  "~standard": {
561
509
  version: 1,
562
510
  vendor: "route0",
@@ -566,42 +514,17 @@ class Route0 {
566
514
  parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
567
515
  safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
568
516
  };
569
- /** Standard Schema for strict search input. */
570
- strictSearchInputSchema = {
571
- "~standard": {
572
- version: 1,
573
- vendor: "route0",
574
- validate: (value) => this._validateSearchInput(value, false),
575
- types: void 0
576
- },
577
- parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, false)),
578
- safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, false))
579
- };
580
- /** Standard Schema for loose search input. */
581
- looseSearchInputSchema = {
582
- "~standard": {
583
- version: 1,
584
- vendor: "route0",
585
- validate: (value) => this._validateSearchInput(value, true),
586
- types: void 0
587
- },
588
- parse: (value) => this._parseSchemaResult(this._validateSearchInput(value, true)),
589
- safeParse: (value) => this._safeParseSchemaResult(this._validateSearchInput(value, true))
590
- };
591
- /** Standard Schema for route flat input (uses route default strict/loose mode). */
592
- flatInputSchema = {
593
- "~standard": {
594
- version: 1,
595
- vendor: "route0",
596
- validate: (value) => this._validateFlatInput(value, this.hasLooseSearch),
597
- types: void 0
598
- },
599
- parse: (value) => this._parseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch)),
600
- safeParse: (value) => this._safeParseSchemaResult(this._validateFlatInput(value, this.hasLooseSearch))
601
- };
602
517
  /** True when path structure is equal (param names are ignored). */
603
518
  isSame(other) {
604
- return this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__") === other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__");
519
+ return getPathTokens(this.definition).map((t) => {
520
+ if (t.kind === "static") return `s:${t.value}`;
521
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
522
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
523
+ }).join("/") === getPathTokens(other.definition).map((t) => {
524
+ if (t.kind === "static") return `s:${t.value}`;
525
+ if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
526
+ return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
527
+ }).join("/");
605
528
  }
606
529
  /** Static convenience wrapper for `isSame`. */
607
530
  static isSame(a, b) {
@@ -619,11 +542,11 @@ class Route0 {
619
542
  if (!other) return false;
620
543
  other = Route0.create(other);
621
544
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
622
- if (other.pathDefinition === "/" && this.pathDefinition !== "/") {
545
+ if (other.definition === "/" && this.definition !== "/") {
623
546
  return true;
624
547
  }
625
- const thisParts = getParts(this.pathDefinition);
626
- const otherParts = getParts(other.pathDefinition);
548
+ const thisParts = getParts(this.definition);
549
+ const otherParts = getParts(other.definition);
627
550
  if (thisParts.length <= otherParts.length) return false;
628
551
  for (let i = 0; i < otherParts.length; i++) {
629
552
  const otherPart = otherParts[i];
@@ -638,11 +561,11 @@ class Route0 {
638
561
  if (!other) return false;
639
562
  other = Route0.create(other);
640
563
  const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
641
- if (this.pathDefinition === "/" && other.pathDefinition !== "/") {
564
+ if (this.definition === "/" && other.definition !== "/") {
642
565
  return true;
643
566
  }
644
- const thisParts = getParts(this.pathDefinition);
645
- const otherParts = getParts(other.pathDefinition);
567
+ const thisParts = getParts(this.definition);
568
+ const otherParts = getParts(other.definition);
646
569
  if (thisParts.length >= otherParts.length) return false;
647
570
  for (let i = 0; i < thisParts.length; i++) {
648
571
  const thisPart = thisParts[i];
@@ -656,29 +579,46 @@ class Route0 {
656
579
  isConflict(other) {
657
580
  if (!other) return false;
658
581
  other = Route0.create(other);
659
- const getParts = (path) => {
660
- if (path === "/") return ["/"];
661
- return path.split("/").filter(Boolean);
662
- };
663
- const thisParts = getParts(this.pathDefinition);
664
- const otherParts = getParts(other.pathDefinition);
665
- if (thisParts.length !== otherParts.length) {
666
- return false;
667
- }
668
- for (let i = 0; i < thisParts.length; i++) {
669
- const thisPart = thisParts[i];
670
- const otherPart = otherParts[i];
671
- if (thisPart.startsWith(":") && otherPart.startsWith(":")) {
672
- continue;
673
- }
674
- if (thisPart.startsWith(":") || otherPart.startsWith(":")) {
675
- continue;
676
- }
677
- if (thisPart !== otherPart) {
678
- return false;
582
+ const thisRegex = this.getRegex();
583
+ const otherRegex = other.getRegex();
584
+ const makeCandidates = (definition) => {
585
+ const tokens = getPathTokens(definition);
586
+ const values = (token) => {
587
+ if (token.kind === "static") return [token.value];
588
+ if (token.kind === "param") return token.optional ? ["", "x"] : ["x"];
589
+ if (token.prefix.length > 0) return [token.prefix, `${token.prefix}-x`, `${token.prefix}/x/y`];
590
+ return ["", "x", "x/y"];
591
+ };
592
+ let acc = [""];
593
+ for (const token of tokens) {
594
+ const next = [];
595
+ for (const base of acc) {
596
+ for (const value of values(token)) {
597
+ if (value === "") {
598
+ next.push(base);
599
+ } else if (value.startsWith("/")) {
600
+ next.push(`${base}${value}`);
601
+ } else {
602
+ next.push(`${base}/${value}`);
603
+ }
604
+ }
605
+ }
606
+ acc = next;
679
607
  }
680
- }
681
- return true;
608
+ if (acc.length === 0) return ["/"];
609
+ return Array.from(new Set(acc.map((x) => x === "" ? "/" : x.replace(/\/{2,}/g, "/"))));
610
+ };
611
+ const thisCandidates = makeCandidates(this.definition);
612
+ const otherCandidates = makeCandidates(other.definition);
613
+ if (thisCandidates.some((path) => otherRegex.test(path))) return true;
614
+ if (otherCandidates.some((path) => thisRegex.test(path))) return true;
615
+ return false;
616
+ }
617
+ /** True when paths are same or can overlap when optional parts are omitted. */
618
+ isMayBeSame(other) {
619
+ if (!other) return false;
620
+ other = Route0.create(other);
621
+ return this.isSame(other) || this.isConflict(other);
682
622
  }
683
623
  /** Specificity comparator used for deterministic route ordering. */
684
624
  isMoreSpecificThan(other) {
@@ -688,15 +628,21 @@ class Route0 {
688
628
  if (path === "/") return ["/"];
689
629
  return path.split("/").filter(Boolean);
690
630
  };
691
- const thisParts = getParts(this.pathDefinition);
692
- const otherParts = getParts(other.pathDefinition);
631
+ const rank = (part) => {
632
+ if (part.includes("*")) return -1;
633
+ if (part.startsWith(":") && part.endsWith("?")) return 0;
634
+ if (part.startsWith(":")) return 1;
635
+ return 2;
636
+ };
637
+ const thisParts = getParts(this.definition);
638
+ const otherParts = getParts(other.definition);
693
639
  for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
694
- const thisIsStatic = !thisParts[i].startsWith(":");
695
- const otherIsStatic = !otherParts[i].startsWith(":");
696
- if (thisIsStatic && !otherIsStatic) return true;
697
- if (!thisIsStatic && otherIsStatic) return false;
640
+ const thisRank = rank(thisParts[i]);
641
+ const otherRank = rank(otherParts[i]);
642
+ if (thisRank > otherRank) return true;
643
+ if (thisRank < otherRank) return false;
698
644
  }
699
- return this.pathDefinition < other.pathDefinition;
645
+ return this.definition < other.definition;
700
646
  }
701
647
  }
702
648
  class Routes {
@@ -779,16 +725,16 @@ class Routes {
779
725
  return path.split("/").filter(Boolean);
780
726
  };
781
727
  entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
782
- const partsA = getParts(routeA.pathDefinition);
783
- const partsB = getParts(routeB.pathDefinition);
784
- if (partsA.length !== partsB.length) {
785
- return partsA.length - partsB.length;
786
- }
787
- if (routeA.isConflict(routeB)) {
728
+ const partsA = getParts(routeA.definition);
729
+ const partsB = getParts(routeB.definition);
730
+ if (routeA.isMayBeSame(routeB)) {
788
731
  if (routeA.isMoreSpecificThan(routeB)) return -1;
789
732
  if (routeB.isMoreSpecificThan(routeA)) return 1;
790
733
  }
791
- return routeA.pathDefinition.localeCompare(routeB.pathDefinition);
734
+ if (partsA.length !== partsB.length) {
735
+ return partsA.length - partsB.length;
736
+ }
737
+ return routeA.definition.localeCompare(routeB.definition);
792
738
  });
793
739
  const pathsOrdering = entries.map(([_key, route]) => route.definition);
794
740
  const keysOrdering = entries.map(([_key]) => _key);