@devp0nt/route0 1.0.0-next.80 → 1.0.0-next.82

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,12 +1,6 @@
1
1
  import { parse as parseSearchQuery, stringify as stringifySearchQuery } from "@devp0nt/flat0";
2
2
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
3
  const collapseDuplicateSlashes = (value) => value.replace(/\/{2,}/g, "/");
4
- const normalizeSlashPath = (value) => {
5
- const collapsed = collapseDuplicateSlashes(value);
6
- if (collapsed === "" || collapsed === "/") return "/";
7
- const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
8
- return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
9
- };
10
4
  class Route0 {
11
5
  definition;
12
6
  params;
@@ -26,16 +20,16 @@ class Route0 {
26
20
  _captureKeys;
27
21
  _normalizedDefinition;
28
22
  _definitionParts;
23
+ static normalizeSlash = (value) => {
24
+ const collapsed = collapseDuplicateSlashes(value);
25
+ if (collapsed === "" || collapsed === "/") return "/";
26
+ const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
27
+ return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
28
+ };
29
29
  static _getRouteSegments(definition) {
30
30
  if (definition === "" || definition === "/") return [];
31
31
  return definition.split("/").filter(Boolean);
32
32
  }
33
- static _normalizeRouteDefinition(definition) {
34
- return normalizeSlashPath(definition);
35
- }
36
- static _normalizePathname(pathname) {
37
- return Route0._normalizeRouteDefinition(pathname);
38
- }
39
33
  static _validateRouteDefinition(definition) {
40
34
  const segments = Route0._getRouteSegments(definition);
41
35
  const wildcardSegments = segments.filter((segment) => segment.includes("*"));
@@ -66,7 +60,7 @@ class Route0 {
66
60
  this._origin = origin;
67
61
  }
68
62
  constructor(definition, config = {}) {
69
- const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
63
+ const normalizedDefinition = Route0.normalizeSlash(definition);
70
64
  Route0._validateRouteDefinition(normalizedDefinition);
71
65
  this.definition = normalizedDefinition;
72
66
  this.params = this.pathParamsDefinition;
@@ -98,7 +92,7 @@ class Route0 {
98
92
  return definition.clone(config);
99
93
  }
100
94
  const original = new Route0(
101
- Route0._normalizeRouteDefinition(definition),
95
+ Route0.normalizeSlash(definition),
102
96
  config
103
97
  );
104
98
  return original._callable;
@@ -113,7 +107,7 @@ class Route0 {
113
107
  return definition;
114
108
  }
115
109
  const original = typeof definition === "object" ? definition : new Route0(
116
- Route0._normalizeRouteDefinition(definition)
110
+ Route0.normalizeSlash(definition)
117
111
  );
118
112
  return original._callable;
119
113
  }
@@ -125,7 +119,7 @@ class Route0 {
125
119
  }
126
120
  /** Extends the current route definition by appending a suffix route. */
127
121
  extend(suffixDefinition) {
128
- const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
122
+ const definition = Route0.normalizeSlash(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
129
123
  return Route0.create(
130
124
  definition,
131
125
  {
@@ -200,7 +194,7 @@ class Route0 {
200
194
  });
201
195
  url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
202
196
  url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
203
- const searchString = stringifySearchQuery(searchInput);
197
+ const searchString = stringifySearchQuery(searchInput, { arrayIndexes: false });
204
198
  url = [url, searchString].filter(Boolean).join("?");
205
199
  url = collapseDuplicateSlashes(url);
206
200
  url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
@@ -382,15 +376,34 @@ class Route0 {
382
376
  }
383
377
  return this._definitionParts;
384
378
  }
385
- /** Fast pathname exact match check without building a full location object. */
386
- isExactPathnameMatch(pathname) {
387
- return this.regex.test(Route0._normalizePathname(pathname));
379
+ /** Fast pathname exact match check without building a full relation object. */
380
+ isExact(pathname, normalize = true) {
381
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
382
+ return this.regex.test(normalizedPathname);
388
383
  }
389
- /** Fast pathname exact or ancestor match check without building a full location object. */
390
- isExactOrAncestorPathnameMatch(pathname) {
391
- const normalizedPathname = Route0._normalizePathname(pathname);
384
+ /** Fast pathname exact or ancestor match check without building a full relation object. */
385
+ isExactOrAncestor(pathname, normalize = true) {
386
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
392
387
  return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
393
388
  }
389
+ /** True when route is ancestor of pathname (pathname is deeper). */
390
+ isAncestor(pathname, normalize = true) {
391
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
392
+ return !this.regex.test(normalizedPathname) && this.regexAncestor.test(normalizedPathname);
393
+ }
394
+ /** True when route is descendant of pathname (pathname is shallower). */
395
+ isDescendant(pathname, normalize = true) {
396
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
397
+ if (this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname)) {
398
+ return false;
399
+ }
400
+ for (const matcher of this.regexDescendantMatchers) {
401
+ if (normalizedPathname.match(matcher.regex)) {
402
+ return true;
403
+ }
404
+ }
405
+ return false;
406
+ }
394
407
  /** Creates a grouped regex pattern string from many routes. */
395
408
  static getRegexStringGroup(routes) {
396
409
  const patterns = routes.map((route) => route.regexString).join("|");
@@ -437,11 +450,16 @@ class Route0 {
437
450
  const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
438
451
  const base = abs ? void 0 : "http://example.com";
439
452
  const url = new URL(hrefOrHrefRelOrLocation, base);
440
- const search = parseSearchQuery(url.search);
441
453
  const hrefRel = url.pathname + url.search + url.hash;
454
+ let _search;
442
455
  const location = {
443
456
  pathname: url.pathname,
444
- search,
457
+ get search() {
458
+ if (_search === void 0) {
459
+ _search = parseSearchQuery(url.search);
460
+ }
461
+ return _search;
462
+ },
445
463
  searchString: url.search,
446
464
  hash: url.hash,
447
465
  origin: abs ? url.origin : void 0,
@@ -454,73 +472,91 @@ class Route0 {
454
472
  port: abs ? url.port || void 0 : void 0,
455
473
  // specific to UnknownLocation
456
474
  params: void 0,
457
- route: void 0,
458
- known: false,
459
- exact: false,
460
- ancestor: false,
461
- descendant: false,
462
- unmatched: false
475
+ route: void 0
463
476
  };
464
477
  return location;
465
478
  }
466
- getLocation(hrefOrHrefRelOrLocation) {
479
+ getRelation(hrefOrHrefRelOrLocation) {
467
480
  if (hrefOrHrefRelOrLocation instanceof URL) {
468
- return this.getLocation(hrefOrHrefRelOrLocation.href);
481
+ return this.getRelation(hrefOrHrefRelOrLocation.href);
469
482
  }
470
483
  if (typeof hrefOrHrefRelOrLocation !== "string") {
471
484
  hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
472
485
  }
473
- const location = Route0.getLocation(hrefOrHrefRelOrLocation);
474
- location.route = this.definition;
475
- location.params = {};
476
- const pathname = Route0._normalizePathname(location.pathname);
486
+ const pathname = Route0.normalizeSlash(new URL(hrefOrHrefRelOrLocation, "http://example.com").pathname);
477
487
  const paramNames = this.captureKeys;
478
488
  const exactRe = this.regex;
479
- const ancestorRe = this.regexAncestor;
480
489
  const exactMatch = pathname.match(exactRe);
490
+ if (exactMatch) {
491
+ const values = exactMatch.slice(1, 1 + paramNames.length);
492
+ const params = Object.fromEntries(
493
+ paramNames.map((n, i) => {
494
+ const value = values[i];
495
+ return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
496
+ })
497
+ );
498
+ return {
499
+ type: "exact",
500
+ route: this.definition,
501
+ params,
502
+ exact: true,
503
+ ancestor: false,
504
+ descendant: false,
505
+ unmatched: false
506
+ };
507
+ }
508
+ const ancestorRe = this.regexAncestor;
481
509
  const ancestorMatch = pathname.match(ancestorRe);
482
- const exact = !!exactMatch;
483
- const ancestor = !exact && !!ancestorMatch;
484
- const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
485
- if (paramsMatch) {
486
- const values = paramsMatch.slice(1, 1 + paramNames.length);
510
+ if (ancestorMatch) {
511
+ const values = ancestorMatch.slice(1, 1 + paramNames.length);
487
512
  const params = Object.fromEntries(
488
513
  paramNames.map((n, i) => {
489
514
  const value = values[i];
490
515
  return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
491
516
  })
492
517
  );
493
- location.params = params;
494
- } else {
495
- location.params = {};
518
+ return {
519
+ type: "ancestor",
520
+ route: this.definition,
521
+ params,
522
+ exact: false,
523
+ ancestor: true,
524
+ descendant: false,
525
+ unmatched: false
526
+ };
496
527
  }
497
- let descendant = false;
498
528
  let descendantMatch = null;
499
529
  let descendantCaptureKeys = [];
500
- if (!exact && !ancestor) {
501
- for (const matcher of this.regexDescendantMatchers) {
502
- const match = pathname.match(matcher.regex);
503
- if (!match) continue;
504
- descendant = true;
505
- descendantMatch = match;
506
- descendantCaptureKeys = matcher.captureKeys;
507
- break;
508
- }
509
- }
510
- const unmatched = !exact && !ancestor && !descendant;
511
- if (descendant && descendantMatch) {
530
+ for (const matcher of this.regexDescendantMatchers) {
531
+ const match = pathname.match(matcher.regex);
532
+ if (!match) continue;
533
+ descendantMatch = match;
534
+ descendantCaptureKeys = matcher.captureKeys;
535
+ break;
536
+ }
537
+ if (descendantMatch) {
512
538
  const values = descendantMatch.slice(1, 1 + descendantCaptureKeys.length);
513
- location.params = Object.fromEntries(
539
+ const params = Object.fromEntries(
514
540
  descendantCaptureKeys.map((key, index) => [key, decodeURIComponent(values[index])])
515
541
  );
542
+ return {
543
+ type: "descendant",
544
+ route: this.definition,
545
+ params,
546
+ exact: false,
547
+ ancestor: false,
548
+ descendant: true,
549
+ unmatched: false
550
+ };
516
551
  }
517
552
  return {
518
- ...location,
519
- known: true,
520
- exact,
521
- ancestor,
522
- descendant,
523
- unmatched
553
+ type: "unmatched",
554
+ route: this.definition,
555
+ params: {},
556
+ exact: false,
557
+ ancestor: false,
558
+ descendant: false,
559
+ unmatched: true
524
560
  };
525
561
  }
526
562
  _validateParamsInput(input) {
@@ -609,90 +645,106 @@ class Route0 {
609
645
  parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
610
646
  safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
611
647
  };
612
- /** True when path structure is equal (param names are ignored). */
613
- isSame(other) {
614
- const thisShape = this.routeTokens.map((t) => {
615
- if (t.kind === "static") return `s:${t.value}`;
616
- if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
617
- return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
618
- }).join("/");
619
- const otherRoute = Route0.from(other);
620
- const otherShape = otherRoute.routeTokens.map((t) => {
621
- if (t.kind === "static") return `s:${t.value}`;
622
- if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
623
- return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
624
- }).join("/");
625
- return thisShape === otherShape;
626
- }
627
- /** Static convenience wrapper for `isSame`. */
628
- static isSame(a, b) {
629
- if (!a) {
630
- if (!b) return true;
631
- return false;
632
- }
633
- if (!b) {
634
- return false;
635
- }
636
- return Route0.create(a).isSame(Route0.create(b));
637
- }
638
- /** True when current route is more specific/deeper than `other`. */
639
- isDescendant(other) {
640
- if (!other) return false;
641
- other = Route0.create(other);
642
- const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
643
- if (other.definition === "/" && this.definition !== "/") {
644
- return true;
645
- }
646
- const thisParts = getParts(this.definition);
647
- const otherParts = getParts(other.definition);
648
- if (thisParts.length <= otherParts.length) return false;
649
- const matchesPatternPart = (patternPart, valuePart) => {
650
- if (patternPart.startsWith(":")) return { match: true, wildcard: false };
651
- const wildcardIndex = patternPart.indexOf("*");
652
- if (wildcardIndex >= 0) {
653
- const prefix = patternPart.slice(0, wildcardIndex);
654
- return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
655
- }
656
- return { match: patternPart === valuePart, wildcard: false };
657
- };
658
- for (let i = 0; i < otherParts.length; i++) {
659
- const otherPart = otherParts[i];
660
- const thisPart = thisParts[i];
661
- const result = matchesPatternPart(otherPart, thisPart);
662
- if (!result.match) return false;
663
- if (result.wildcard) return true;
664
- }
665
- return true;
666
- }
667
- /** True when current route is broader/shallower than `other`. */
668
- isAncestor(other) {
669
- if (!other) return false;
670
- other = Route0.create(other);
671
- const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
672
- if (this.definition === "/" && other.definition !== "/") {
673
- return true;
674
- }
675
- const thisParts = getParts(this.definition);
676
- const otherParts = getParts(other.definition);
677
- if (thisParts.length >= otherParts.length) return false;
678
- const matchesPatternPart = (patternPart, valuePart) => {
679
- if (patternPart.startsWith(":")) return { match: true, wildcard: false };
680
- const wildcardIndex = patternPart.indexOf("*");
681
- if (wildcardIndex >= 0) {
682
- const prefix = patternPart.slice(0, wildcardIndex);
683
- return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
684
- }
685
- return { match: patternPart === valuePart, wildcard: false };
686
- };
687
- for (let i = 0; i < thisParts.length; i++) {
688
- const thisPart = thisParts[i];
689
- const otherPart = otherParts[i];
690
- const result = matchesPatternPart(thisPart, otherPart);
691
- if (!result.match) return false;
692
- if (result.wildcard) return true;
693
- }
694
- return true;
695
- }
648
+ // /** True when path structure is equal (param names are ignored). */
649
+ // isSame(other: AnyRoute): boolean {
650
+ // const thisShape = this.routeTokens
651
+ // .map((t) => {
652
+ // if (t.kind === 'static') return `s:${t.value}`
653
+ // if (t.kind === 'param') return `p:${t.optional ? 'o' : 'r'}`
654
+ // return `w:${t.prefix}:${t.optional ? 'o' : 'r'}`
655
+ // })
656
+ // .join('/')
657
+ // const otherRoute = Route0.from(other) as Route0<string, UnknownSearchInput>
658
+ // const otherShape = otherRoute.routeTokens
659
+ // .map((t) => {
660
+ // if (t.kind === 'static') return `s:${t.value}`
661
+ // if (t.kind === 'param') return `p:${t.optional ? 'o' : 'r'}`
662
+ // return `w:${t.prefix}:${t.optional ? 'o' : 'r'}`
663
+ // })
664
+ // .join('/')
665
+ // return thisShape === otherShape
666
+ // }
667
+ // /** Static convenience wrapper for `isSame`. */
668
+ // static isSame(a: AnyRoute | string | undefined, b: AnyRoute | string | undefined): boolean {
669
+ // if (!a) {
670
+ // if (!b) return true
671
+ // return false
672
+ // }
673
+ // if (!b) {
674
+ // return false
675
+ // }
676
+ // return Route0.create(a).isSame(Route0.create(b))
677
+ // }
678
+ // /** True when current route is more specific/deeper than `other`. */
679
+ // isDescendant(other: AnyRoute | string | undefined): boolean {
680
+ // if (!other) return false
681
+ // other = Route0.create(other)
682
+ // // this is a descendant of other if:
683
+ // // - paths are not exactly the same
684
+ // // - other's path is a prefix of this path, matching params as wildcards
685
+ // const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
686
+ // // Root is ancestor of any non-root; thus any non-root is a descendant of root
687
+ // if (other.definition === '/' && this.definition !== '/') {
688
+ // return true
689
+ // }
690
+ // const thisParts = getParts(this.definition)
691
+ // const otherParts = getParts(other.definition)
692
+ // // A descendant must be deeper
693
+ // if (thisParts.length <= otherParts.length) return false
694
+ // const matchesPatternPart = (patternPart: string, valuePart: string): { match: boolean; wildcard: boolean } => {
695
+ // if (patternPart.startsWith(':')) return { match: true, wildcard: false }
696
+ // const wildcardIndex = patternPart.indexOf('*')
697
+ // if (wildcardIndex >= 0) {
698
+ // const prefix = patternPart.slice(0, wildcardIndex)
699
+ // return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true }
700
+ // }
701
+ // return { match: patternPart === valuePart, wildcard: false }
702
+ // }
703
+ // for (let i = 0; i < otherParts.length; i++) {
704
+ // const otherPart = otherParts[i]
705
+ // const thisPart = thisParts[i]
706
+ // const result = matchesPatternPart(otherPart, thisPart)
707
+ // if (!result.match) return false
708
+ // if (result.wildcard) return true
709
+ // }
710
+ // // Not equal (depth already ensures not equal)
711
+ // return true
712
+ // }
713
+ // /** True when current route is broader/shallower than `other`. */
714
+ // isAncestor(other: AnyRoute | string | undefined): boolean {
715
+ // if (!other) return false
716
+ // other = Route0.create(other)
717
+ // // this is an ancestor of other if:
718
+ // // - paths are not exactly the same
719
+ // // - this path is a prefix of other path, matching params as wildcards
720
+ // const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
721
+ // // Root is ancestor of any non-root path
722
+ // if (this.definition === '/' && other.definition !== '/') {
723
+ // return true
724
+ // }
725
+ // const thisParts = getParts(this.definition)
726
+ // const otherParts = getParts(other.definition)
727
+ // // An ancestor must be shallower
728
+ // if (thisParts.length >= otherParts.length) return false
729
+ // const matchesPatternPart = (patternPart: string, valuePart: string): { match: boolean; wildcard: boolean } => {
730
+ // if (patternPart.startsWith(':')) return { match: true, wildcard: false }
731
+ // const wildcardIndex = patternPart.indexOf('*')
732
+ // if (wildcardIndex >= 0) {
733
+ // const prefix = patternPart.slice(0, wildcardIndex)
734
+ // return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true }
735
+ // }
736
+ // return { match: patternPart === valuePart, wildcard: false }
737
+ // }
738
+ // for (let i = 0; i < thisParts.length; i++) {
739
+ // const thisPart = thisParts[i]
740
+ // const otherPart = otherParts[i]
741
+ // const result = matchesPatternPart(thisPart, otherPart)
742
+ // if (!result.match) return false
743
+ // if (result.wildcard) return true
744
+ // }
745
+ // // Not equal (depth already ensures not equal)
746
+ // return true
747
+ // }
696
748
  /** True when two route patterns can match the same concrete URL. */
697
749
  isOverlap(other) {
698
750
  if (!other) return false;
@@ -750,21 +802,6 @@ class Route0 {
750
802
  }
751
803
  }
752
804
  class Routes {
753
- static _getNormalizedPathnameFromInput(hrefOrHrefRelOrLocation) {
754
- if (hrefOrHrefRelOrLocation instanceof URL) {
755
- return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
756
- }
757
- if (typeof hrefOrHrefRelOrLocation !== "string") {
758
- if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
759
- return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
760
- }
761
- hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
762
- }
763
- const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
764
- const base = abs ? void 0 : "http://example.com";
765
- const url = new URL(hrefOrHrefRelOrLocation, base);
766
- return normalizeSlashPath(url.pathname);
767
- }
768
805
  _routes;
769
806
  _pathsOrdering;
770
807
  _keysOrdering;
@@ -828,17 +865,17 @@ class Routes {
828
865
  }
829
866
  _getLocation(hrefOrHrefRelOrLocation) {
830
867
  const input = hrefOrHrefRelOrLocation;
831
- const pathname = Routes._getNormalizedPathnameFromInput(input);
868
+ const location = Route0.getLocation(input);
832
869
  for (const route of this._ordered) {
833
- if (!route.isExactPathnameMatch(pathname)) {
834
- continue;
835
- }
836
- const loc = route.getLocation(input);
837
- if (loc.exact) {
838
- return loc;
870
+ if (route.isExact(location.pathname, false)) {
871
+ const relation = route.getRelation(input);
872
+ return Object.assign(location, {
873
+ route: route.definition,
874
+ params: relation.params
875
+ });
839
876
  }
840
877
  }
841
- return typeof input === "string" ? Route0.getLocation(input) : Route0.getLocation(input);
878
+ return location;
842
879
  }
843
880
  static makeOrdering(routes) {
844
881
  const hydrated = Routes.hydrate(routes);