@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.
@@ -25,12 +25,6 @@ module.exports = __toCommonJS(index_exports);
25
25
  var import_flat0 = require("@devp0nt/flat0");
26
26
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
27
  const collapseDuplicateSlashes = (value) => value.replace(/\/{2,}/g, "/");
28
- const normalizeSlashPath = (value) => {
29
- const collapsed = collapseDuplicateSlashes(value);
30
- if (collapsed === "" || collapsed === "/") return "/";
31
- const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
32
- return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
33
- };
34
28
  class Route0 {
35
29
  definition;
36
30
  params;
@@ -50,16 +44,16 @@ class Route0 {
50
44
  _captureKeys;
51
45
  _normalizedDefinition;
52
46
  _definitionParts;
47
+ static normalizeSlash = (value) => {
48
+ const collapsed = collapseDuplicateSlashes(value);
49
+ if (collapsed === "" || collapsed === "/") return "/";
50
+ const withLeadingSlash = collapsed.startsWith("/") ? collapsed : `/${collapsed}`;
51
+ return withLeadingSlash.length > 1 && withLeadingSlash.endsWith("/") ? withLeadingSlash.slice(0, -1) : withLeadingSlash;
52
+ };
53
53
  static _getRouteSegments(definition) {
54
54
  if (definition === "" || definition === "/") return [];
55
55
  return definition.split("/").filter(Boolean);
56
56
  }
57
- static _normalizeRouteDefinition(definition) {
58
- return normalizeSlashPath(definition);
59
- }
60
- static _normalizePathname(pathname) {
61
- return Route0._normalizeRouteDefinition(pathname);
62
- }
63
57
  static _validateRouteDefinition(definition) {
64
58
  const segments = Route0._getRouteSegments(definition);
65
59
  const wildcardSegments = segments.filter((segment) => segment.includes("*"));
@@ -90,7 +84,7 @@ class Route0 {
90
84
  this._origin = origin;
91
85
  }
92
86
  constructor(definition, config = {}) {
93
- const normalizedDefinition = Route0._normalizeRouteDefinition(definition);
87
+ const normalizedDefinition = Route0.normalizeSlash(definition);
94
88
  Route0._validateRouteDefinition(normalizedDefinition);
95
89
  this.definition = normalizedDefinition;
96
90
  this.params = this.pathParamsDefinition;
@@ -122,7 +116,7 @@ class Route0 {
122
116
  return definition.clone(config);
123
117
  }
124
118
  const original = new Route0(
125
- Route0._normalizeRouteDefinition(definition),
119
+ Route0.normalizeSlash(definition),
126
120
  config
127
121
  );
128
122
  return original._callable;
@@ -137,7 +131,7 @@ class Route0 {
137
131
  return definition;
138
132
  }
139
133
  const original = typeof definition === "object" ? definition : new Route0(
140
- Route0._normalizeRouteDefinition(definition)
134
+ Route0.normalizeSlash(definition)
141
135
  );
142
136
  return original._callable;
143
137
  }
@@ -149,7 +143,7 @@ class Route0 {
149
143
  }
150
144
  /** Extends the current route definition by appending a suffix route. */
151
145
  extend(suffixDefinition) {
152
- const definition = Route0._normalizeRouteDefinition(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
146
+ const definition = Route0.normalizeSlash(`${this.definitionWithoutTrailingWildcard}/${suffixDefinition}`);
153
147
  return Route0.create(
154
148
  definition,
155
149
  {
@@ -224,7 +218,7 @@ class Route0 {
224
218
  });
225
219
  url = url.replace(/\*\?/g, () => String(paramsInput["*"] ?? ""));
226
220
  url = url.replace(/\*/g, () => String(paramsInput["*"] ?? ""));
227
- const searchString = (0, import_flat0.stringify)(searchInput);
221
+ const searchString = (0, import_flat0.stringify)(searchInput, { arrayIndexes: false });
228
222
  url = [url, searchString].filter(Boolean).join("?");
229
223
  url = collapseDuplicateSlashes(url);
230
224
  url = absInput ? Route0._getAbsPath(absOriginInput || this.origin, url) : url;
@@ -406,15 +400,34 @@ class Route0 {
406
400
  }
407
401
  return this._definitionParts;
408
402
  }
409
- /** Fast pathname exact match check without building a full location object. */
410
- isExactPathnameMatch(pathname) {
411
- return this.regex.test(Route0._normalizePathname(pathname));
403
+ /** Fast pathname exact match check without building a full relation object. */
404
+ isExact(pathname, normalize = true) {
405
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
406
+ return this.regex.test(normalizedPathname);
412
407
  }
413
- /** Fast pathname exact or ancestor match check without building a full location object. */
414
- isExactOrAncestorPathnameMatch(pathname) {
415
- const normalizedPathname = Route0._normalizePathname(pathname);
408
+ /** Fast pathname exact or ancestor match check without building a full relation object. */
409
+ isExactOrAncestor(pathname, normalize = true) {
410
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
416
411
  return this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname);
417
412
  }
413
+ /** True when route is ancestor of pathname (pathname is deeper). */
414
+ isAncestor(pathname, normalize = true) {
415
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
416
+ return !this.regex.test(normalizedPathname) && this.regexAncestor.test(normalizedPathname);
417
+ }
418
+ /** True when route is descendant of pathname (pathname is shallower). */
419
+ isDescendant(pathname, normalize = true) {
420
+ const normalizedPathname = normalize ? Route0.normalizeSlash(pathname) : pathname;
421
+ if (this.regex.test(normalizedPathname) || this.regexAncestor.test(normalizedPathname)) {
422
+ return false;
423
+ }
424
+ for (const matcher of this.regexDescendantMatchers) {
425
+ if (normalizedPathname.match(matcher.regex)) {
426
+ return true;
427
+ }
428
+ }
429
+ return false;
430
+ }
418
431
  /** Creates a grouped regex pattern string from many routes. */
419
432
  static getRegexStringGroup(routes) {
420
433
  const patterns = routes.map((route) => route.regexString).join("|");
@@ -461,11 +474,16 @@ class Route0 {
461
474
  const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
462
475
  const base = abs ? void 0 : "http://example.com";
463
476
  const url = new URL(hrefOrHrefRelOrLocation, base);
464
- const search = (0, import_flat0.parse)(url.search);
465
477
  const hrefRel = url.pathname + url.search + url.hash;
478
+ let _search;
466
479
  const location = {
467
480
  pathname: url.pathname,
468
- search,
481
+ get search() {
482
+ if (_search === void 0) {
483
+ _search = (0, import_flat0.parse)(url.search);
484
+ }
485
+ return _search;
486
+ },
469
487
  searchString: url.search,
470
488
  hash: url.hash,
471
489
  origin: abs ? url.origin : void 0,
@@ -478,73 +496,91 @@ class Route0 {
478
496
  port: abs ? url.port || void 0 : void 0,
479
497
  // specific to UnknownLocation
480
498
  params: void 0,
481
- route: void 0,
482
- known: false,
483
- exact: false,
484
- ancestor: false,
485
- descendant: false,
486
- unmatched: false
499
+ route: void 0
487
500
  };
488
501
  return location;
489
502
  }
490
- getLocation(hrefOrHrefRelOrLocation) {
503
+ getRelation(hrefOrHrefRelOrLocation) {
491
504
  if (hrefOrHrefRelOrLocation instanceof URL) {
492
- return this.getLocation(hrefOrHrefRelOrLocation.href);
505
+ return this.getRelation(hrefOrHrefRelOrLocation.href);
493
506
  }
494
507
  if (typeof hrefOrHrefRelOrLocation !== "string") {
495
508
  hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
496
509
  }
497
- const location = Route0.getLocation(hrefOrHrefRelOrLocation);
498
- location.route = this.definition;
499
- location.params = {};
500
- const pathname = Route0._normalizePathname(location.pathname);
510
+ const pathname = Route0.normalizeSlash(new URL(hrefOrHrefRelOrLocation, "http://example.com").pathname);
501
511
  const paramNames = this.captureKeys;
502
512
  const exactRe = this.regex;
503
- const ancestorRe = this.regexAncestor;
504
513
  const exactMatch = pathname.match(exactRe);
514
+ if (exactMatch) {
515
+ const values = exactMatch.slice(1, 1 + paramNames.length);
516
+ const params = Object.fromEntries(
517
+ paramNames.map((n, i) => {
518
+ const value = values[i];
519
+ return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
520
+ })
521
+ );
522
+ return {
523
+ type: "exact",
524
+ route: this.definition,
525
+ params,
526
+ exact: true,
527
+ ancestor: false,
528
+ descendant: false,
529
+ unmatched: false
530
+ };
531
+ }
532
+ const ancestorRe = this.regexAncestor;
505
533
  const ancestorMatch = pathname.match(ancestorRe);
506
- const exact = !!exactMatch;
507
- const ancestor = !exact && !!ancestorMatch;
508
- const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null);
509
- if (paramsMatch) {
510
- const values = paramsMatch.slice(1, 1 + paramNames.length);
534
+ if (ancestorMatch) {
535
+ const values = ancestorMatch.slice(1, 1 + paramNames.length);
511
536
  const params = Object.fromEntries(
512
537
  paramNames.map((n, i) => {
513
538
  const value = values[i];
514
539
  return [n, value === void 0 ? void 0 : decodeURIComponent(value)];
515
540
  })
516
541
  );
517
- location.params = params;
518
- } else {
519
- location.params = {};
542
+ return {
543
+ type: "ancestor",
544
+ route: this.definition,
545
+ params,
546
+ exact: false,
547
+ ancestor: true,
548
+ descendant: false,
549
+ unmatched: false
550
+ };
520
551
  }
521
- let descendant = false;
522
552
  let descendantMatch = null;
523
553
  let descendantCaptureKeys = [];
524
- if (!exact && !ancestor) {
525
- for (const matcher of this.regexDescendantMatchers) {
526
- const match = pathname.match(matcher.regex);
527
- if (!match) continue;
528
- descendant = true;
529
- descendantMatch = match;
530
- descendantCaptureKeys = matcher.captureKeys;
531
- break;
532
- }
533
- }
534
- const unmatched = !exact && !ancestor && !descendant;
535
- if (descendant && descendantMatch) {
554
+ for (const matcher of this.regexDescendantMatchers) {
555
+ const match = pathname.match(matcher.regex);
556
+ if (!match) continue;
557
+ descendantMatch = match;
558
+ descendantCaptureKeys = matcher.captureKeys;
559
+ break;
560
+ }
561
+ if (descendantMatch) {
536
562
  const values = descendantMatch.slice(1, 1 + descendantCaptureKeys.length);
537
- location.params = Object.fromEntries(
563
+ const params = Object.fromEntries(
538
564
  descendantCaptureKeys.map((key, index) => [key, decodeURIComponent(values[index])])
539
565
  );
566
+ return {
567
+ type: "descendant",
568
+ route: this.definition,
569
+ params,
570
+ exact: false,
571
+ ancestor: false,
572
+ descendant: true,
573
+ unmatched: false
574
+ };
540
575
  }
541
576
  return {
542
- ...location,
543
- known: true,
544
- exact,
545
- ancestor,
546
- descendant,
547
- unmatched
577
+ type: "unmatched",
578
+ route: this.definition,
579
+ params: {},
580
+ exact: false,
581
+ ancestor: false,
582
+ descendant: false,
583
+ unmatched: true
548
584
  };
549
585
  }
550
586
  _validateParamsInput(input) {
@@ -633,90 +669,106 @@ class Route0 {
633
669
  parse: (value) => this._parseSchemaResult(this._validateParamsInput(value)),
634
670
  safeParse: (value) => this._safeParseSchemaResult(this._validateParamsInput(value))
635
671
  };
636
- /** True when path structure is equal (param names are ignored). */
637
- isSame(other) {
638
- const thisShape = this.routeTokens.map((t) => {
639
- if (t.kind === "static") return `s:${t.value}`;
640
- if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
641
- return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
642
- }).join("/");
643
- const otherRoute = Route0.from(other);
644
- const otherShape = otherRoute.routeTokens.map((t) => {
645
- if (t.kind === "static") return `s:${t.value}`;
646
- if (t.kind === "param") return `p:${t.optional ? "o" : "r"}`;
647
- return `w:${t.prefix}:${t.optional ? "o" : "r"}`;
648
- }).join("/");
649
- return thisShape === otherShape;
650
- }
651
- /** Static convenience wrapper for `isSame`. */
652
- static isSame(a, b) {
653
- if (!a) {
654
- if (!b) return true;
655
- return false;
656
- }
657
- if (!b) {
658
- return false;
659
- }
660
- return Route0.create(a).isSame(Route0.create(b));
661
- }
662
- /** True when current route is more specific/deeper than `other`. */
663
- isDescendant(other) {
664
- if (!other) return false;
665
- other = Route0.create(other);
666
- const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
667
- if (other.definition === "/" && this.definition !== "/") {
668
- return true;
669
- }
670
- const thisParts = getParts(this.definition);
671
- const otherParts = getParts(other.definition);
672
- if (thisParts.length <= otherParts.length) return false;
673
- const matchesPatternPart = (patternPart, valuePart) => {
674
- if (patternPart.startsWith(":")) return { match: true, wildcard: false };
675
- const wildcardIndex = patternPart.indexOf("*");
676
- if (wildcardIndex >= 0) {
677
- const prefix = patternPart.slice(0, wildcardIndex);
678
- return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
679
- }
680
- return { match: patternPart === valuePart, wildcard: false };
681
- };
682
- for (let i = 0; i < otherParts.length; i++) {
683
- const otherPart = otherParts[i];
684
- const thisPart = thisParts[i];
685
- const result = matchesPatternPart(otherPart, thisPart);
686
- if (!result.match) return false;
687
- if (result.wildcard) return true;
688
- }
689
- return true;
690
- }
691
- /** True when current route is broader/shallower than `other`. */
692
- isAncestor(other) {
693
- if (!other) return false;
694
- other = Route0.create(other);
695
- const getParts = (path) => path === "/" ? ["/"] : path.split("/").filter(Boolean);
696
- if (this.definition === "/" && other.definition !== "/") {
697
- return true;
698
- }
699
- const thisParts = getParts(this.definition);
700
- const otherParts = getParts(other.definition);
701
- if (thisParts.length >= otherParts.length) return false;
702
- const matchesPatternPart = (patternPart, valuePart) => {
703
- if (patternPart.startsWith(":")) return { match: true, wildcard: false };
704
- const wildcardIndex = patternPart.indexOf("*");
705
- if (wildcardIndex >= 0) {
706
- const prefix = patternPart.slice(0, wildcardIndex);
707
- return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true };
708
- }
709
- return { match: patternPart === valuePart, wildcard: false };
710
- };
711
- for (let i = 0; i < thisParts.length; i++) {
712
- const thisPart = thisParts[i];
713
- const otherPart = otherParts[i];
714
- const result = matchesPatternPart(thisPart, otherPart);
715
- if (!result.match) return false;
716
- if (result.wildcard) return true;
717
- }
718
- return true;
719
- }
672
+ // /** True when path structure is equal (param names are ignored). */
673
+ // isSame(other: AnyRoute): boolean {
674
+ // const thisShape = this.routeTokens
675
+ // .map((t) => {
676
+ // if (t.kind === 'static') return `s:${t.value}`
677
+ // if (t.kind === 'param') return `p:${t.optional ? 'o' : 'r'}`
678
+ // return `w:${t.prefix}:${t.optional ? 'o' : 'r'}`
679
+ // })
680
+ // .join('/')
681
+ // const otherRoute = Route0.from(other) as Route0<string, UnknownSearchInput>
682
+ // const otherShape = otherRoute.routeTokens
683
+ // .map((t) => {
684
+ // if (t.kind === 'static') return `s:${t.value}`
685
+ // if (t.kind === 'param') return `p:${t.optional ? 'o' : 'r'}`
686
+ // return `w:${t.prefix}:${t.optional ? 'o' : 'r'}`
687
+ // })
688
+ // .join('/')
689
+ // return thisShape === otherShape
690
+ // }
691
+ // /** Static convenience wrapper for `isSame`. */
692
+ // static isSame(a: AnyRoute | string | undefined, b: AnyRoute | string | undefined): boolean {
693
+ // if (!a) {
694
+ // if (!b) return true
695
+ // return false
696
+ // }
697
+ // if (!b) {
698
+ // return false
699
+ // }
700
+ // return Route0.create(a).isSame(Route0.create(b))
701
+ // }
702
+ // /** True when current route is more specific/deeper than `other`. */
703
+ // isDescendant(other: AnyRoute | string | undefined): boolean {
704
+ // if (!other) return false
705
+ // other = Route0.create(other)
706
+ // // this is a descendant of other if:
707
+ // // - paths are not exactly the same
708
+ // // - other's path is a prefix of this path, matching params as wildcards
709
+ // const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
710
+ // // Root is ancestor of any non-root; thus any non-root is a descendant of root
711
+ // if (other.definition === '/' && this.definition !== '/') {
712
+ // return true
713
+ // }
714
+ // const thisParts = getParts(this.definition)
715
+ // const otherParts = getParts(other.definition)
716
+ // // A descendant must be deeper
717
+ // if (thisParts.length <= otherParts.length) return false
718
+ // const matchesPatternPart = (patternPart: string, valuePart: string): { match: boolean; wildcard: boolean } => {
719
+ // if (patternPart.startsWith(':')) return { match: true, wildcard: false }
720
+ // const wildcardIndex = patternPart.indexOf('*')
721
+ // if (wildcardIndex >= 0) {
722
+ // const prefix = patternPart.slice(0, wildcardIndex)
723
+ // return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true }
724
+ // }
725
+ // return { match: patternPart === valuePart, wildcard: false }
726
+ // }
727
+ // for (let i = 0; i < otherParts.length; i++) {
728
+ // const otherPart = otherParts[i]
729
+ // const thisPart = thisParts[i]
730
+ // const result = matchesPatternPart(otherPart, thisPart)
731
+ // if (!result.match) return false
732
+ // if (result.wildcard) return true
733
+ // }
734
+ // // Not equal (depth already ensures not equal)
735
+ // return true
736
+ // }
737
+ // /** True when current route is broader/shallower than `other`. */
738
+ // isAncestor(other: AnyRoute | string | undefined): boolean {
739
+ // if (!other) return false
740
+ // other = Route0.create(other)
741
+ // // this is an ancestor of other if:
742
+ // // - paths are not exactly the same
743
+ // // - this path is a prefix of other path, matching params as wildcards
744
+ // const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
745
+ // // Root is ancestor of any non-root path
746
+ // if (this.definition === '/' && other.definition !== '/') {
747
+ // return true
748
+ // }
749
+ // const thisParts = getParts(this.definition)
750
+ // const otherParts = getParts(other.definition)
751
+ // // An ancestor must be shallower
752
+ // if (thisParts.length >= otherParts.length) return false
753
+ // const matchesPatternPart = (patternPart: string, valuePart: string): { match: boolean; wildcard: boolean } => {
754
+ // if (patternPart.startsWith(':')) return { match: true, wildcard: false }
755
+ // const wildcardIndex = patternPart.indexOf('*')
756
+ // if (wildcardIndex >= 0) {
757
+ // const prefix = patternPart.slice(0, wildcardIndex)
758
+ // return { match: prefix.length === 0 || valuePart.startsWith(prefix), wildcard: true }
759
+ // }
760
+ // return { match: patternPart === valuePart, wildcard: false }
761
+ // }
762
+ // for (let i = 0; i < thisParts.length; i++) {
763
+ // const thisPart = thisParts[i]
764
+ // const otherPart = otherParts[i]
765
+ // const result = matchesPatternPart(thisPart, otherPart)
766
+ // if (!result.match) return false
767
+ // if (result.wildcard) return true
768
+ // }
769
+ // // Not equal (depth already ensures not equal)
770
+ // return true
771
+ // }
720
772
  /** True when two route patterns can match the same concrete URL. */
721
773
  isOverlap(other) {
722
774
  if (!other) return false;
@@ -774,21 +826,6 @@ class Route0 {
774
826
  }
775
827
  }
776
828
  class Routes {
777
- static _getNormalizedPathnameFromInput(hrefOrHrefRelOrLocation) {
778
- if (hrefOrHrefRelOrLocation instanceof URL) {
779
- return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
780
- }
781
- if (typeof hrefOrHrefRelOrLocation !== "string") {
782
- if (typeof hrefOrHrefRelOrLocation.pathname === "string") {
783
- return normalizeSlashPath(hrefOrHrefRelOrLocation.pathname);
784
- }
785
- hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel;
786
- }
787
- const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation);
788
- const base = abs ? void 0 : "http://example.com";
789
- const url = new URL(hrefOrHrefRelOrLocation, base);
790
- return normalizeSlashPath(url.pathname);
791
- }
792
829
  _routes;
793
830
  _pathsOrdering;
794
831
  _keysOrdering;
@@ -852,17 +889,17 @@ class Routes {
852
889
  }
853
890
  _getLocation(hrefOrHrefRelOrLocation) {
854
891
  const input = hrefOrHrefRelOrLocation;
855
- const pathname = Routes._getNormalizedPathnameFromInput(input);
892
+ const location = Route0.getLocation(input);
856
893
  for (const route of this._ordered) {
857
- if (!route.isExactPathnameMatch(pathname)) {
858
- continue;
859
- }
860
- const loc = route.getLocation(input);
861
- if (loc.exact) {
862
- return loc;
894
+ if (route.isExact(location.pathname, false)) {
895
+ const relation = route.getRelation(input);
896
+ return Object.assign(location, {
897
+ route: route.definition,
898
+ params: relation.params
899
+ });
863
900
  }
864
901
  }
865
- return typeof input === "string" ? Route0.getLocation(input) : Route0.getLocation(input);
902
+ return location;
866
903
  }
867
904
  static makeOrdering(routes) {
868
905
  const hydrated = Routes.hydrate(routes);