@devp0nt/route0 1.0.0-next.6 → 1.0.0-next.60

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,134 +1,724 @@
1
1
  class Route0 {
2
- pathOriginalDefinition;
2
+ definition;
3
3
  pathDefinition;
4
4
  paramsDefinition;
5
- queryDefinition;
6
- baseUrl;
5
+ searchDefinition;
6
+ hasLooseSearch;
7
+ _baseurl;
8
+ /** Base URL used when generating absolute URLs (`abs: true`). */
9
+ get baseurl() {
10
+ if (!this._baseurl) {
11
+ throw new Error(
12
+ "baseurl for route " + this.definition + ' is not set, please provide it like Route0.create(route, {baseurl: "https://example.com"}) in config or set via overrides like routes._.override({baseurl: "https://example.com"})'
13
+ );
14
+ }
15
+ return this._baseurl;
16
+ }
17
+ set baseurl(baseurl) {
18
+ this._baseurl = baseurl;
19
+ }
7
20
  constructor(definition, config = {}) {
8
- this.pathOriginalDefinition = definition;
9
- this.pathDefinition = Route0._getPathDefinitionByOriginalDefinition(definition);
10
- this.paramsDefinition = Route0._getParamsDefinitionByRouteDefinition(definition);
11
- this.queryDefinition = Route0._getQueryDefinitionByRouteDefinition(definition);
12
- const { baseUrl } = config;
13
- if (baseUrl && typeof baseUrl === "string" && baseUrl.length) {
14
- this.baseUrl = baseUrl;
21
+ 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);
26
+ const { baseurl } = config;
27
+ if (baseurl && typeof baseurl === "string" && baseurl.length) {
28
+ this._baseurl = baseurl;
15
29
  } else {
16
30
  const g = globalThis;
17
- if (g?.location?.origin) {
18
- this.baseUrl = g.location.origin;
31
+ if (typeof g?.location?.origin === "string" && g.location.origin.length > 0) {
32
+ this._baseurl = g.location.origin;
19
33
  } else {
20
- this.baseUrl = "https://example.com";
34
+ this._baseurl = void 0;
21
35
  }
22
36
  }
23
37
  }
38
+ /**
39
+ * Creates a callable route instance.
40
+ *
41
+ * If an existing route/callable route is provided, it is cloned.
42
+ */
24
43
  static create(definition, config) {
25
- const original = new Route0(
26
- definition,
27
- config
28
- );
44
+ if (typeof definition === "function") {
45
+ return definition.clone(config);
46
+ }
47
+ if (typeof definition === "object") {
48
+ return definition.clone(config);
49
+ }
50
+ const original = new Route0(definition, config);
29
51
  const callable = original.get.bind(original);
30
- const proxy = new Proxy(callable, {
31
- get(_target, prop, receiver) {
32
- const value = original[prop];
33
- if (typeof value === "function") {
34
- return value.bind(original);
35
- }
36
- return value;
37
- },
38
- set(_target, prop, value, receiver) {
39
- ;
40
- original[prop] = value;
41
- return true;
42
- },
43
- has(_target, prop) {
44
- return prop in original;
45
- }
52
+ Object.setPrototypeOf(callable, original);
53
+ Object.defineProperty(callable, Symbol.toStringTag, {
54
+ value: original.definition
46
55
  });
47
- Object.setPrototypeOf(proxy, Route0.prototype);
48
- return proxy;
56
+ return callable;
49
57
  }
50
- static _splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition) {
51
- const i = pathOriginalDefinition.indexOf("&");
52
- if (i === -1) return { pathDefinition: pathOriginalDefinition, queryTailDefinition: "" };
58
+ /**
59
+ * Normalizes a definition/route into a callable route.
60
+ *
61
+ * Unlike `create`, passing a callable route returns the same instance.
62
+ */
63
+ static from(definition) {
64
+ if (typeof definition === "function") {
65
+ return definition;
66
+ }
67
+ 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: "" };
53
78
  return {
54
- pathDefinition: pathOriginalDefinition.slice(0, i),
55
- queryTailDefinition: pathOriginalDefinition.slice(i)
79
+ pathDefinition: definition.slice(0, i),
80
+ searchTailDefinition: definition.slice(i)
56
81
  };
57
82
  }
58
- static _getAbsPath(baseUrl, pathWithQuery) {
59
- return new URL(pathWithQuery, baseUrl).toString().replace(/\/$/, "");
83
+ static _getAbsPath(baseurl, pathWithSearch) {
84
+ return new URL(pathWithSearch, baseurl).toString().replace(/\/$/, "");
60
85
  }
61
- static _getPathDefinitionByOriginalDefinition(pathOriginalDefinition) {
62
- const { pathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition);
86
+ static _getPathDefinitionBydefinition(definition) {
87
+ const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
63
88
  return pathDefinition;
64
89
  }
65
- static _getParamsDefinitionByRouteDefinition(pathOriginalDefinition) {
66
- const { pathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition);
90
+ static _getParamsDefinitionBydefinition(definition) {
91
+ const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
67
92
  const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g));
68
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
+ }
69
98
  return paramsDefinition;
70
99
  }
71
- static _getQueryDefinitionByRouteDefinition(pathOriginalDefinition) {
72
- const { queryTailDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition);
73
- if (!queryTailDefinition) {
74
- return {};
100
+ static _getSearchDefinitionBydefinition(definition) {
101
+ const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition);
102
+ if (!searchTailDefinition) {
103
+ return void 0;
75
104
  }
76
- const keys = queryTailDefinition.split("&").map(Boolean);
77
- const queryDefinition = Object.fromEntries(keys.map((k) => [k, true]));
78
- return queryDefinition;
79
- }
80
- static overrideMany(routes, config) {
81
- const result = {};
82
- for (const [key, value] of Object.entries(routes)) {
83
- ;
84
- result[key] = value.clone(config);
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;
85
110
  }
86
- return result;
111
+ return searchDefinition;
87
112
  }
113
+ static _hasLooseSearch(definition) {
114
+ return /&$/.test(definition);
115
+ }
116
+ /** Extends the current route definition by appending a suffix route. */
88
117
  extend(suffixDefinition) {
89
- const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(
90
- this.pathOriginalDefinition
91
- );
92
- const { pathDefinition: suffixPathDefinition, queryTailDefinition: suffixQueryTailDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(suffixDefinition);
118
+ const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(this.definition);
119
+ const { pathDefinition: suffixPathDefinition, searchTailDefinition: suffixSearchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(suffixDefinition);
93
120
  const pathDefinition = `${parentPathDefinition}/${suffixPathDefinition}`.replace(/\/{2,}/g, "/");
94
- const pathOriginalDefinition = `${pathDefinition}${suffixQueryTailDefinition}`;
95
- return Route0.create(pathOriginalDefinition, { baseUrl: this.baseUrl });
121
+ const definition = `${pathDefinition}${suffixSearchTailDefinition}`;
122
+ return Route0.create(definition, { baseurl: this._baseurl });
96
123
  }
97
124
  // implementation
98
125
  get(...args) {
99
- const { queryInput, paramsInput, absInput } = (() => {
126
+ const { searchInput, paramsInput, absInput, hashInput } = (() => {
100
127
  if (args.length === 0) {
101
- return { queryInput: {}, paramsInput: {}, absInput: false };
128
+ return {
129
+ searchInput: {},
130
+ paramsInput: {},
131
+ absInput: false,
132
+ hashInput: void 0
133
+ };
102
134
  }
103
135
  const input = args[0];
104
136
  if (typeof input !== "object" || input === null) {
105
- return { queryInput: {}, paramsInput: {}, absInput: false };
137
+ return {
138
+ searchInput: {},
139
+ paramsInput: {},
140
+ absInput: false,
141
+ hashInput: void 0
142
+ };
106
143
  }
107
- const { query, abs, ...params } = input;
108
- return { queryInput: query || {}, paramsInput: params, absInput: abs ?? false };
144
+ const { search, abs, hash, ...params } = input;
145
+ return {
146
+ searchInput: search || {},
147
+ paramsInput: params,
148
+ absInput: abs ?? false,
149
+ hashInput: hash
150
+ };
109
151
  })();
110
- const neededParamsKeys = Object.keys(this.paramsDefinition);
152
+ const neededParamsKeys = this.paramsDefinition ? Object.keys(this.paramsDefinition) : [];
111
153
  const providedParamsKeys = Object.keys(paramsInput);
112
154
  const notProvidedKeys = neededParamsKeys.filter((k) => !providedParamsKeys.includes(k));
113
155
  if (notProvidedKeys.length) {
114
156
  Object.assign(paramsInput, Object.fromEntries(notProvidedKeys.map((k) => [k, "undefined"])));
115
157
  }
116
- let url = String(this.pathDefinition);
158
+ let url = this.pathDefinition;
117
159
  url = url.replace(/:([A-Za-z0-9_]+)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? "")));
118
- const queryInputStringified = Object.fromEntries(Object.entries(queryInput).map(([k, v]) => [k, String(v)]));
119
- url = [url, new URLSearchParams(queryInputStringified).toString()].filter(Boolean).join("?");
160
+ const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]));
161
+ url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join("?");
120
162
  url = url.replace(/\/{2,}/g, "/");
121
- url = absInput ? Route0._getAbsPath(this.baseUrl, url) : url;
163
+ url = absInput ? Route0._getAbsPath(this.baseurl, url) : url;
164
+ if (hashInput !== void 0) {
165
+ url = `${url}#${hashInput}`;
166
+ }
122
167
  return url;
123
168
  }
169
+ // implementation
170
+ flat(...args) {
171
+ const { searchInput, paramsInput, absInput, hashInput } = (() => {
172
+ if (args.length === 0) {
173
+ return {
174
+ searchInput: {},
175
+ paramsInput: {},
176
+ absInput: false,
177
+ hashInput: void 0
178
+ };
179
+ }
180
+ const input = args[0];
181
+ if (typeof input !== "object" || input === null) {
182
+ return {
183
+ searchInput: {},
184
+ paramsInput: {},
185
+ absInput: args[1] ?? false,
186
+ hashInput: void 0
187
+ };
188
+ }
189
+ const loose = args[2] ?? this.hasLooseSearch;
190
+ const paramsKeys = this.getParamsKeys();
191
+ const paramsInput2 = paramsKeys.reduce((acc, key) => {
192
+ if (input[key] !== void 0) {
193
+ acc[key] = input[key];
194
+ }
195
+ return acc;
196
+ }, {});
197
+ const searchKeys = this.getSearchKeys();
198
+ const searchInput2 = Object.keys(input).filter((k) => {
199
+ if (k === "hash") {
200
+ return false;
201
+ }
202
+ if (searchKeys.includes(k)) {
203
+ return true;
204
+ }
205
+ if (paramsKeys.includes(k)) {
206
+ return false;
207
+ }
208
+ return !!loose;
209
+ }).reduce((acc, key) => {
210
+ acc[key] = input[key];
211
+ return acc;
212
+ }, {});
213
+ const hashInput2 = input.hash;
214
+ return {
215
+ searchInput: searchInput2,
216
+ paramsInput: paramsInput2,
217
+ absInput: args[1] ?? false,
218
+ hashInput: hashInput2
219
+ };
220
+ })();
221
+ return this.get({
222
+ ...paramsInput,
223
+ search: searchInput,
224
+ abs: absInput,
225
+ hash: hashInput
226
+ });
227
+ }
228
+ flatLoose(...args) {
229
+ return this.flat(args[0], args[1], true);
230
+ }
231
+ flatStrict(...args) {
232
+ return this.flat(args[0], args[1], false);
233
+ }
234
+ /** Returns path param keys extracted from route definition. */
235
+ getParamsKeys() {
236
+ return Object.keys(this.paramsDefinition || {});
237
+ }
238
+ /** Returns named search keys extracted from route definition. */
239
+ getSearchKeys() {
240
+ return Object.keys(this.searchDefinition || {});
241
+ }
242
+ /** Returns all flat input keys (`search + params`). */
243
+ getFlatKeys() {
244
+ return [...this.getSearchKeys(), ...this.getParamsKeys()];
245
+ }
124
246
  getDefinition() {
125
247
  return this.pathDefinition;
126
248
  }
249
+ /** Clones route with optional config override. */
127
250
  clone(config) {
128
- return new Route0(this.pathOriginalDefinition, config);
251
+ return Route0.create(this.definition, config);
252
+ }
253
+ getRegexBaseStrictString() {
254
+ return this.pathDefinition.replace(/:(\w+)/g, "___PARAM___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/___PARAM___/g, "([^/]+)");
255
+ }
256
+ getRegexBaseString() {
257
+ return this.getRegexBaseStrictString().replace(/\/+$/, "") + "/?";
258
+ }
259
+ getRegexStrictString() {
260
+ return `^${this.getRegexBaseStrictString()}$`;
261
+ }
262
+ getRegexString() {
263
+ return `^${this.getRegexBaseString()}$`;
264
+ }
265
+ getRegexStrict() {
266
+ return new RegExp(this.getRegexStrictString());
267
+ }
268
+ getRegex() {
269
+ return new RegExp(this.getRegexString());
270
+ }
271
+ /** Creates a grouped strict regex pattern string from many routes. */
272
+ static getRegexStrictStringGroup(routes) {
273
+ const patterns = routes.map((route) => route.getRegexStrictString()).join("|");
274
+ return `(${patterns})`;
275
+ }
276
+ /** Creates a strict grouped regex from many routes. */
277
+ static getRegexStrictGroup(routes) {
278
+ const patterns = Route0.getRegexStrictStringGroup(routes);
279
+ return new RegExp(`^(${patterns})$`);
280
+ }
281
+ /** Creates a grouped regex pattern string from many routes. */
282
+ static getRegexStringGroup(routes) {
283
+ const patterns = routes.map((route) => route.getRegexString()).join("|");
284
+ return `(${patterns})`;
285
+ }
286
+ /** Creates a grouped regex from many routes. */
287
+ static getRegexGroup(routes) {
288
+ const patterns = Route0.getRegexStringGroup(routes);
289
+ return new RegExp(`^(${patterns})$`);
290
+ }
291
+ /** Converts any location shape to relative form (removes host/origin fields). */
292
+ static toRelLocation(location) {
293
+ return {
294
+ ...location,
295
+ abs: false,
296
+ origin: void 0,
297
+ href: void 0,
298
+ port: void 0,
299
+ host: void 0,
300
+ hostname: void 0
301
+ };
302
+ }
303
+ /** Converts a location to absolute form using provided base URL. */
304
+ static toAbsLocation(location, baseurl) {
305
+ const relLoc = Route0.toRelLocation(location);
306
+ const url = new URL(relLoc.hrefRel, baseurl);
307
+ return {
308
+ ...location,
309
+ abs: true,
310
+ origin: url.origin,
311
+ href: url.href,
312
+ port: url.port,
313
+ host: url.host,
314
+ hostname: url.hostname
315
+ };
316
+ }
317
+ static getLocation(hrefOrHrefRelOrLocation) {
318
+ if (hrefOrHrefRelOrLocation instanceof URL) {
319
+ return Route0.getLocation(hrefOrHrefRelOrLocation.href);
320
+ }
321
+ if (typeof hrefOrHrefRelOrLocation !== "string") {
322
+ hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
323
+ }
324
+ const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
325
+ const base = abs ? void 0 : "http://example.com";
326
+ const url = new URL(hrefOrHrefRelOrLocation, base);
327
+ const searchParams = Object.fromEntries(url.searchParams.entries());
328
+ let pathname = url.pathname;
329
+ if (pathname.length > 1 && pathname.endsWith("/")) {
330
+ pathname = pathname.slice(0, -1);
331
+ }
332
+ const hrefRel = pathname + url.search + url.hash;
333
+ const location = {
334
+ pathname,
335
+ search: url.search,
336
+ hash: url.hash,
337
+ origin: abs ? url.origin : void 0,
338
+ href: abs ? url.href : void 0,
339
+ hrefRel,
340
+ abs,
341
+ // extra host-related fields (available even for relative with dummy base)
342
+ host: abs ? url.host : void 0,
343
+ hostname: abs ? url.hostname : void 0,
344
+ port: abs ? url.port || void 0 : void 0,
345
+ // specific to UnknownLocation
346
+ searchParams,
347
+ params: void 0,
348
+ route: void 0,
349
+ known: false,
350
+ exact: false,
351
+ ancestor: false,
352
+ descendant: false,
353
+ unmatched: false
354
+ };
355
+ return location;
356
+ }
357
+ getLocation(hrefOrHrefRelOrLocation) {
358
+ if (hrefOrHrefRelOrLocation instanceof URL) {
359
+ return this.getLocation(hrefOrHrefRelOrLocation.href);
360
+ }
361
+ if (typeof hrefOrHrefRelOrLocation !== "string") {
362
+ hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
363
+ }
364
+ const location = Route0.getLocation(hrefOrHrefRelOrLocation);
365
+ location.route = this.definition;
366
+ location.params = {};
367
+ const pathname = location.pathname.length > 1 && location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname;
368
+ const paramNames = [];
369
+ const def = this.pathDefinition.length > 1 && this.pathDefinition.endsWith("/") ? this.pathDefinition.slice(0, -1) : this.pathDefinition;
370
+ def.replace(/:([A-Za-z0-9_]+)/g, (_m, name) => {
371
+ paramNames.push(String(name));
372
+ return "";
373
+ });
374
+ const exactRe = new RegExp(`^${this.getRegexBaseString()}$`);
375
+ const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`);
376
+ const exactMatch = pathname.match(exactRe);
377
+ const ancestorMatch = pathname.match(ancestorRe);
378
+ const exact = !!exactMatch;
379
+ const ancestor = !exact && !!ancestorMatch;
380
+ const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
381
+ if (paramsMatch) {
382
+ const values = paramsMatch.slice(1, 1 + paramNames.length);
383
+ const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? "")]));
384
+ location.params = params;
385
+ } else {
386
+ location.params = {};
387
+ }
388
+ const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
389
+ const defParts = getParts(def);
390
+ const pathParts = getParts(pathname);
391
+ let isPrefix = true;
392
+ if (pathParts.length > defParts.length) {
393
+ isPrefix = false;
394
+ } else {
395
+ for (let i = 0; i < pathParts.length; i++) {
396
+ const defPart = defParts[i];
397
+ const pathPart = pathParts[i];
398
+ if (!defPart) {
399
+ isPrefix = false;
400
+ break;
401
+ }
402
+ if (defPart.startsWith(":")) continue;
403
+ if (defPart !== pathPart) {
404
+ isPrefix = false;
405
+ break;
406
+ }
407
+ }
408
+ }
409
+ const descendant = !exact && isPrefix;
410
+ const unmatched = !exact && !ancestor && !descendant;
411
+ if (descendant) {
412
+ const descendantParams = {};
413
+ for (let i = 0; i < pathParts.length; i++) {
414
+ const defPart = defParts[i];
415
+ const pathPart = pathParts[i];
416
+ if (!defPart || !pathPart) continue;
417
+ if (defPart.startsWith(":")) {
418
+ descendantParams[defPart.slice(1)] = decodeURIComponent(pathPart);
419
+ }
420
+ }
421
+ location.params = descendantParams;
422
+ }
423
+ return {
424
+ ...location,
425
+ known: true,
426
+ exact,
427
+ ancestor,
428
+ descendant,
429
+ unmatched
430
+ };
431
+ }
432
+ /**
433
+ * Safe parser for flat input objects.
434
+ *
435
+ * Returns structured success/error result instead of throwing.
436
+ */
437
+ safeParseFlatInput(input, loose) {
438
+ loose ??= this.hasLooseSearch;
439
+ const paramsKeys = this.getParamsKeys();
440
+ if (input === void 0) {
441
+ if (paramsKeys.length) {
442
+ return {
443
+ success: false,
444
+ data: void 0,
445
+ error: new Error(`Missing params: ${paramsKeys.map((k) => `"${k}"`).join(", ")}`)
446
+ };
447
+ }
448
+ input = {};
449
+ }
450
+ if (typeof input !== "object" || input === null) {
451
+ return {
452
+ success: false,
453
+ data: void 0,
454
+ error: new Error("Invalid input: expected object")
455
+ };
456
+ }
457
+ const inputKeys = Object.keys(input);
458
+ const notDefinedKeys = paramsKeys.filter((k) => !inputKeys.includes(k));
459
+ if (notDefinedKeys.length) {
460
+ return {
461
+ success: false,
462
+ data: void 0,
463
+ error: new Error(`Missing params: ${notDefinedKeys.map((k) => `"${k}"`).join(", ")}`)
464
+ };
465
+ }
466
+ const data = {};
467
+ const filterKeys = !loose ? [...paramsKeys, ...this.getSearchKeys()] : false;
468
+ for (const [k, v] of Object.entries(input)) {
469
+ if (filterKeys && !filterKeys.includes(k)) {
470
+ continue;
471
+ }
472
+ if (typeof v === "string") {
473
+ data[k] = v;
474
+ } else if (typeof v === "number") {
475
+ data[k] = String(v);
476
+ } else {
477
+ const isParamKey = paramsKeys.includes(k);
478
+ return {
479
+ success: false,
480
+ data: void 0,
481
+ error: new Error(
482
+ `Invalid input: expected string, number,${!isParamKey ? " or undefined," : ""} got ${typeof v} for "${k}"`
483
+ )
484
+ };
485
+ }
486
+ }
487
+ return {
488
+ success: true,
489
+ data,
490
+ error: void 0
491
+ };
492
+ }
493
+ /** Throwing variant of `safeParseFlatInput()`. */
494
+ parseFlatInput(input, loose) {
495
+ loose ??= this.hasLooseSearch;
496
+ const result = this.safeParseFlatInput(input, loose);
497
+ if (result.error) {
498
+ throw result.error;
499
+ }
500
+ return result.data;
501
+ }
502
+ /** True when path structure is equal (param names are ignored). */
503
+ isSame(other) {
504
+ return this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__") === other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, "__PARAM__");
505
+ }
506
+ /** Static convenience wrapper for `isSame`. */
507
+ static isSame(a, b) {
508
+ if (!a) {
509
+ if (!b) return true;
510
+ return false;
511
+ }
512
+ if (!b) {
513
+ return false;
514
+ }
515
+ return Route0.create(a).isSame(Route0.create(b));
516
+ }
517
+ /** True when current route is more specific/deeper than `other`. */
518
+ isDescendant(other) {
519
+ if (!other) return false;
520
+ other = Route0.create(other);
521
+ const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
522
+ if (other.pathDefinition === "/" && this.pathDefinition !== "/") {
523
+ return true;
524
+ }
525
+ const thisParts = getParts(this.pathDefinition);
526
+ const otherParts = getParts(other.pathDefinition);
527
+ if (thisParts.length <= otherParts.length) return false;
528
+ for (let i = 0; i < otherParts.length; i++) {
529
+ const otherPart = otherParts[i];
530
+ const thisPart = thisParts[i];
531
+ if (otherPart.startsWith(":")) continue;
532
+ if (otherPart !== thisPart) return false;
533
+ }
534
+ return true;
535
+ }
536
+ /** True when current route is broader/shallower than `other`. */
537
+ isAncestor(other) {
538
+ if (!other) return false;
539
+ other = Route0.create(other);
540
+ const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
541
+ if (this.pathDefinition === "/" && other.pathDefinition !== "/") {
542
+ return true;
543
+ }
544
+ const thisParts = getParts(this.pathDefinition);
545
+ const otherParts = getParts(other.pathDefinition);
546
+ if (thisParts.length >= otherParts.length) return false;
547
+ for (let i = 0; i < thisParts.length; i++) {
548
+ const thisPart = thisParts[i];
549
+ const otherPart = otherParts[i];
550
+ if (thisPart.startsWith(":")) continue;
551
+ if (thisPart !== otherPart) return false;
552
+ }
553
+ return true;
554
+ }
555
+ /** True when two route patterns can match the same concrete URL. */
556
+ isConflict(other) {
557
+ if (!other) return false;
558
+ other = Route0.create(other);
559
+ const getParts = (path) => {
560
+ if (path === "/") return ["/"];
561
+ return path.split("/").filter(Boolean);
562
+ };
563
+ const thisParts = getParts(this.pathDefinition);
564
+ const otherParts = getParts(other.pathDefinition);
565
+ if (thisParts.length !== otherParts.length) {
566
+ return false;
567
+ }
568
+ for (let i = 0; i < thisParts.length; i++) {
569
+ const thisPart = thisParts[i];
570
+ const otherPart = otherParts[i];
571
+ if (thisPart.startsWith(":") && otherPart.startsWith(":")) {
572
+ continue;
573
+ }
574
+ if (thisPart.startsWith(":") || otherPart.startsWith(":")) {
575
+ continue;
576
+ }
577
+ if (thisPart !== otherPart) {
578
+ return false;
579
+ }
580
+ }
581
+ return true;
582
+ }
583
+ /** Specificity comparator used for deterministic route ordering. */
584
+ isMoreSpecificThan(other) {
585
+ if (!other) return false;
586
+ other = Route0.create(other);
587
+ const getParts = (path) => {
588
+ if (path === "/") return ["/"];
589
+ return path.split("/").filter(Boolean);
590
+ };
591
+ const thisParts = getParts(this.pathDefinition);
592
+ const otherParts = getParts(other.pathDefinition);
593
+ for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
594
+ const thisIsStatic = !thisParts[i].startsWith(":");
595
+ const otherIsStatic = !otherParts[i].startsWith(":");
596
+ if (thisIsStatic && !otherIsStatic) return true;
597
+ if (!thisIsStatic && otherIsStatic) return false;
598
+ }
599
+ return this.pathDefinition < other.pathDefinition;
600
+ }
601
+ }
602
+ class Routes {
603
+ _routes;
604
+ _pathsOrdering;
605
+ _keysOrdering;
606
+ _ordered;
607
+ _;
608
+ constructor({
609
+ routes,
610
+ isHydrated = false,
611
+ pathsOrdering,
612
+ keysOrdering,
613
+ ordered
614
+ }) {
615
+ this._routes = isHydrated ? routes : Routes.hydrate(routes);
616
+ if (!pathsOrdering || !keysOrdering || !ordered) {
617
+ const ordering = Routes.makeOrdering(this._routes);
618
+ this._pathsOrdering = ordering.pathsOrdering;
619
+ this._keysOrdering = ordering.keysOrdering;
620
+ this._ordered = this._keysOrdering.map((key) => this._routes[key]);
621
+ } else {
622
+ this._pathsOrdering = pathsOrdering;
623
+ this._keysOrdering = keysOrdering;
624
+ this._ordered = ordered;
625
+ }
626
+ this._ = {
627
+ routes: this._routes,
628
+ getLocation: this._getLocation.bind(this),
629
+ override: this._override.bind(this),
630
+ pathsOrdering: this._pathsOrdering,
631
+ keysOrdering: this._keysOrdering,
632
+ ordered: this._ordered
633
+ };
634
+ }
635
+ /** Creates and hydrates a typed routes collection. */
636
+ static create(routes, override) {
637
+ const result = Routes.prettify(new Routes({ routes }));
638
+ if (!override) {
639
+ return result;
640
+ }
641
+ return result._.override(override);
642
+ }
643
+ static prettify(instance) {
644
+ Object.setPrototypeOf(instance, Routes.prototype);
645
+ Object.defineProperty(instance, Symbol.toStringTag, {
646
+ value: "Routes"
647
+ });
648
+ Object.assign(instance, {
649
+ override: instance._override.bind(instance)
650
+ });
651
+ Object.assign(instance, instance._routes);
652
+ return instance;
653
+ }
654
+ static hydrate(routes) {
655
+ const result = {};
656
+ for (const key in routes) {
657
+ if (Object.hasOwn(routes, key)) {
658
+ const value = routes[key];
659
+ result[key] = typeof value === "string" ? Route0.create(value) : value;
660
+ }
661
+ }
662
+ return result;
663
+ }
664
+ _getLocation(hrefOrHrefRelOrLocation) {
665
+ const input = hrefOrHrefRelOrLocation;
666
+ for (const route of this._ordered) {
667
+ const loc = route.getLocation(hrefOrHrefRelOrLocation);
668
+ if (loc.exact) {
669
+ return loc;
670
+ }
671
+ }
672
+ return typeof input === "string" ? Route0.getLocation(input) : Route0.getLocation(input);
673
+ }
674
+ static makeOrdering(routes) {
675
+ const hydrated = Routes.hydrate(routes);
676
+ const entries = Object.entries(hydrated);
677
+ const getParts = (path) => {
678
+ if (path === "/") return ["/"];
679
+ return path.split("/").filter(Boolean);
680
+ };
681
+ entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
682
+ const partsA = getParts(routeA.pathDefinition);
683
+ const partsB = getParts(routeB.pathDefinition);
684
+ if (partsA.length !== partsB.length) {
685
+ return partsA.length - partsB.length;
686
+ }
687
+ if (routeA.isConflict(routeB)) {
688
+ if (routeA.isMoreSpecificThan(routeB)) return -1;
689
+ if (routeB.isMoreSpecificThan(routeA)) return 1;
690
+ }
691
+ return routeA.pathDefinition.localeCompare(routeB.pathDefinition);
692
+ });
693
+ const pathsOrdering = entries.map(([_key, route]) => route.definition);
694
+ const keysOrdering = entries.map(([_key]) => _key);
695
+ return { pathsOrdering, keysOrdering };
696
+ }
697
+ /** Returns a cloned routes collection with config applied to each route. */
698
+ _override(config) {
699
+ const newRoutes = {};
700
+ for (const key in this._routes) {
701
+ if (Object.hasOwn(this._routes, key)) {
702
+ newRoutes[key] = this._routes[key].clone(config);
703
+ }
704
+ }
705
+ const instance = new Routes({
706
+ routes: newRoutes,
707
+ isHydrated: true,
708
+ pathsOrdering: this._pathsOrdering,
709
+ keysOrdering: this._keysOrdering,
710
+ ordered: this._keysOrdering.map((key) => newRoutes[key])
711
+ });
712
+ return Routes.prettify(instance);
129
713
  }
714
+ static _ = {
715
+ prettify: Routes.prettify.bind(Routes),
716
+ hydrate: Routes.hydrate.bind(Routes),
717
+ makeOrdering: Routes.makeOrdering.bind(Routes)
718
+ };
130
719
  }
131
720
  export {
132
- Route0
721
+ Route0,
722
+ Routes
133
723
  };
134
724
  //# sourceMappingURL=index.js.map