@real-router/route-utils 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -1,97 +1,58 @@
1
1
  # @real-router/route-utils
2
2
 
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)
3
+ [![npm](https://img.shields.io/npm/v/@real-router/route-utils.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/route-utils)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@real-router/route-utils.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/route-utils)
5
+ [![bundle size](https://deno.bundlejs.com/?q=@real-router/route-utils&treeshake=[*]&badge=detailed)](https://bundlejs.com/?q=@real-router/route-utils&treeshake=[*])
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
5
7
 
6
- Route tree queries and segment testing utilities for Real-Router. Pre-computed lookups for ancestor chains and siblings, plus regex-based segment matching with currying support.
8
+ > Cached read-only query API for [Real-Router](https://github.com/greydragon888/real-router) route tree structure. Pre-computed ancestor chains, sibling lookups, and regex-based segment testers.
7
9
 
8
10
  ## Installation
9
11
 
10
12
  ```bash
11
13
  npm install @real-router/route-utils
12
- # or
13
- pnpm add @real-router/route-utils
14
14
  ```
15
15
 
16
16
  ## Quick Start
17
17
 
18
18
  ```typescript
19
- import { createRouter, getPluginApi } from "@real-router/core";
19
+ import { createRouter } from "@real-router/core";
20
+ import { getPluginApi } from "@real-router/core/api";
20
21
  import { getRouteUtils, startsWithSegment } from "@real-router/route-utils";
21
22
 
22
23
  const router = createRouter([
23
24
  { name: "home", path: "/" },
24
- {
25
- name: "users",
26
- path: "/users",
27
- children: [
28
- {
29
- name: "profile",
30
- path: "/:id",
31
- children: [{ name: "edit", path: "/edit" }],
32
- },
33
- { name: "settings", path: "/settings" },
34
- ],
35
- },
25
+ { name: "users", path: "/users", children: [
26
+ { name: "profile", path: "/:id", children: [
27
+ { name: "edit", path: "/edit" },
28
+ ]},
29
+ { name: "settings", path: "/settings" },
30
+ ]},
36
31
  { name: "admin", path: "/admin" },
37
32
  ]);
38
33
 
39
34
  const utils = getRouteUtils(getPluginApi(router).getTree());
40
35
 
41
- utils.getChain("users.profile"); // ["users", "users.profile"]
42
- utils.getSiblings("users"); // ["home", "admin"]
43
- utils.isDescendantOf("users.profile", "users"); // true
36
+ utils.getChain("users.profile"); // ["users", "users.profile"]
37
+ utils.getSiblings("users"); // ["home", "admin"]
38
+ utils.isDescendantOf("users.profile", "users"); // true
44
39
 
45
- startsWithSegment("users.profile", "users"); // true
40
+ startsWithSegment("users.profile", "users"); // true
46
41
  ```
47
42
 
48
- ---
43
+ ## RouteUtils
49
44
 
50
- ## API
45
+ Pre-computes all route tree data in the constructor. Subsequent lookups are O(1) Map reads.
51
46
 
52
- ### `RouteUtils` Class
53
-
54
- [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils)
55
-
56
- Pre-computes all route tree data eagerly in the constructor. Subsequent lookups are O(1) Map reads.
57
-
58
- #### `new RouteUtils(root: RouteTreeNode)`
59
-
60
- Creates a new instance. All ancestor chains and sibling lists are frozen during construction.
61
-
62
- #### `utils.getChain(name): readonly string[] | undefined`
63
-
64
- Returns the cumulative ancestor chain for a route. Root returns `[""]`; other routes exclude root.
65
-
66
- ```typescript
67
- utils.getChain("users.profile.edit");
68
- // → ["users", "users.profile", "users.profile.edit"]
69
- ```
70
-
71
- #### `utils.getSiblings(name): readonly string[] | undefined`
72
-
73
- Returns non-absolute siblings of a route (excluding itself). Root returns `undefined`.
74
-
75
- #### `utils.isDescendantOf(child, parent): boolean`
76
-
77
- O(k) string prefix check. Does not perform tree lookup.
78
-
79
- #### Static Facade
80
-
81
- `RouteUtils` exposes segment testers as `static readonly` properties — delegates to standalone functions:
82
-
83
- ```typescript
84
- RouteUtils.startsWithSegment("users.list", "users"); // true
85
- RouteUtils.endsWithSegment("users.profile.edit", "edit"); // true
86
- RouteUtils.includesSegment("a.b.c.d", "b.c"); // true
87
- RouteUtils.areRoutesRelated("users", "users.profile"); // true
88
- ```
89
-
90
- ---
47
+ | Method | Returns | Description |
48
+ |--------|---------|-------------|
49
+ | `getChain(name)` | `readonly string[] \| undefined` | Ancestor chain (e.g., `["users", "users.profile", "users.profile.edit"]`) |
50
+ | `getSiblings(name)` | `readonly string[] \| undefined` | Sibling routes (excluding itself) |
51
+ | `isDescendantOf(child, parent)` | `boolean` | O(k) string prefix check |
91
52
 
92
53
  ### `getRouteUtils(root): RouteUtils`
93
54
 
94
- WeakMap-cached factory. Same `RouteTreeNode` reference same instance. [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils)
55
+ WeakMap-cached factory same tree reference returns the same instance:
95
56
 
96
57
  ```typescript
97
58
  const utils1 = getRouteUtils(tree);
@@ -99,77 +60,64 @@ const utils2 = getRouteUtils(tree);
99
60
  utils1 === utils2; // true
100
61
  ```
101
62
 
102
- ---
63
+ ## Segment Testers
103
64
 
104
- ### Segment Testers
65
+ Standalone functions for testing dot-separated route name segments. Each supports direct, curried, and `State` object calling patterns.
105
66
 
106
- Standalone functions for testing dot-separated route name segments. Each supports direct, curried, and `State` object calling patterns. [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils#startswithsegmentroute-segment)
107
-
108
- #### `startsWithSegment(route, segment?)`
109
-
110
- Tests if a route name starts with the given segment (respects dot boundaries).
67
+ | Function | Description |
68
+ |----------|-------------|
69
+ | `startsWithSegment(route, segment?)` | Route starts with segment (dot-bounded) |
70
+ | `endsWithSegment(route, segment?)` | Route ends with segment |
71
+ | `includesSegment(route, segment?)` | Route includes segment anywhere (contiguous) |
72
+ | `areRoutesRelated(route1, route2)` | Routes are same, parent-child, or child-parent |
111
73
 
112
74
  ```typescript
113
- startsWithSegment("users.list", "users"); // true
114
- startsWithSegment("users2.list", "users"); // false (dot boundary)
75
+ startsWithSegment("users.list", "users"); // true
76
+ startsWithSegment("users2.list", "users"); // false (dot boundary)
77
+ endsWithSegment("users.profile.edit", "edit"); // true
78
+ includesSegment("a.b.c.d", "b.c"); // true
79
+ areRoutesRelated("users", "users.profile"); // true
115
80
 
116
- // Curried form
81
+ // Curried form — first arg is route, returns tester for segments
117
82
  const tester = startsWithSegment("users.list");
118
- tester("users"); // true
119
- ```
120
-
121
- #### `endsWithSegment(route, segment?)`
122
-
123
- Tests if a route name ends with the given segment. [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils#endswithsegmentroute-segment)
124
-
125
- #### `includesSegment(route, segment?)`
126
-
127
- Tests if a route name includes the given segment anywhere (contiguous, dot-bounded). [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils#includessegmentroute-segment)
128
-
129
- #### `areRoutesRelated(route1, route2): boolean`
130
-
131
- Checks if two routes are related in the hierarchy (same, parent-child, or child-parent). [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils#areroutesrelatedroute1-route2)
132
-
133
- ---
83
+ tester("users"); // true
134
84
 
135
- ### Types
136
-
137
- ```typescript
138
- import type { SegmentTestFunction } from "@real-router/route-utils";
85
+ // Static access via RouteUtils
86
+ RouteUtils.startsWithSegment("users.list", "users"); // true
139
87
  ```
140
88
 
141
- See [Wiki](https://github.com/greydragon888/real-router/wiki/route-utils#segmenttestfunction) for the full interface definition.
142
-
143
- ---
144
-
145
- ## Segment Validation
146
-
147
- All segment testers validate input:
89
+ ### Input Validation
148
90
 
149
91
  - **Max length**: 10,000 characters (`RangeError`)
150
92
  - **Allowed characters**: `a-z`, `A-Z`, `0-9`, `.`, `-`, `_` (`TypeError`)
151
93
  - **Empty / null segment**: returns `false` (no error)
152
94
 
153
- ---
154
-
155
95
  ## Performance
156
96
 
157
- | Operation | Complexity | Notes |
158
- | --------------------------- | ---------- | -------------------------------- |
159
- | Construction | O(n) | Single DFS, n = number of routes |
160
- | `getChain` / `getSiblings` | O(1) | Frozen cached arrays |
161
- | `isDescendantOf` | O(k) | String prefix check |
162
- | `getRouteUtils` (cache hit) | O(1) | WeakMap lookup |
163
- | Segment tester (cached) | O(k) | Regex test |
97
+ | Operation | Complexity | Notes |
98
+ |-----------|------------|-------|
99
+ | Construction | O(n) | Single DFS, n = number of routes |
100
+ | `getChain` / `getSiblings` | O(1) | Frozen cached arrays |
101
+ | `isDescendantOf` | O(k) | String prefix check |
102
+ | `getRouteUtils` (cache hit) | O(1) | WeakMap lookup |
103
+ | Segment testers | O(k) | Regex test, cached |
104
+
105
+ ## Documentation
164
106
 
165
- ---
107
+ Full documentation: [Wiki — route-utils](https://github.com/greydragon888/real-router/wiki/route-utils)
166
108
 
167
109
  ## Related Packages
168
110
 
169
- - [@real-router/core](https://www.npmjs.com/package/@real-router/core) Core router
170
- - [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration (`useRouteUtils` hook)
171
- - [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) Browser history
111
+ | Package | Description |
112
+ |---------|-------------|
113
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router |
114
+ | [@real-router/react](https://www.npmjs.com/package/@real-router/react) | React integration (`useRouteUtils` hook) |
115
+ | [@real-router/sources](https://www.npmjs.com/package/@real-router/sources) | Subscription layer (uses route-utils internally) |
116
+
117
+ ## Contributing
118
+
119
+ See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
172
120
 
173
121
  ## License
174
122
 
175
- MIT © [Oleg Ivanov](https://github.com/greydragon888)
123
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- function e(e,t){return e===t||e.startsWith(`${t}.`)||t.startsWith(`${e}.`)}var t=/^[\w.-]+$/,n=e=>e.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`),s=(e,s)=>{const r=new Map,i=i=>{const a=r.get(i);if(a)return a;if(i.length>1e4)throw new RangeError("Segment exceeds maximum length of 10000 characters");if(!t.test(i))throw new TypeError("Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)");const l=new RegExp(e+n(i)+s);return r.set(i,l),l};return(e,t)=>{const n="string"==typeof e?e:e.name;if("string"!=typeof n)return!1;if(0===n.length)return!1;if(null===t)return!1;if(void 0===t)return e=>{if("string"!=typeof e)throw new TypeError("Segment must be a string, got "+typeof e);return 0!==e.length&&i(e).test(n)};if("string"!=typeof t)throw new TypeError("Segment must be a string, got "+typeof t);return 0!==t.length&&i(t).test(n)}},r=`(?:${n(".")}|$)`,i=s("^",r),a=s(`(?:^|${n(".")})`,"$"),l=s(`(?:^|${n(".")})`,r),o=class{static startsWithSegment=i;static endsWithSegment=a;static includesSegment=l;static areRoutesRelated=e;#e;#t;constructor(e){this.#e=new Map,this.#t=new Map,this.#n(e,[])}getChain(e){return this.#e.get(e)}getSiblings(e){return this.#t.get(e)}isDescendantOf(e,t){return e!==t&&e.startsWith(`${t}.`)}#n(e,t){const{fullName:n}=e;""!==n&&t.push(n),this.#e.set(n,Object.freeze(""===n?[""]:[...t]));const s=e.nonAbsoluteChildren.map(e=>e.fullName);for(const t of e.nonAbsoluteChildren)this.#t.set(t.fullName,Object.freeze(s.filter(e=>e!==t.fullName)));for(const t of e.children.values())this.#t.has(t.fullName)||""===t.fullName||this.#t.set(t.fullName,Object.freeze([...s]));for(const n of e.children.values())this.#n(n,t);""!==n&&t.pop()}},h=new WeakMap;exports.RouteUtils=o,exports.areRoutesRelated=e,exports.endsWithSegment=a,exports.getRouteUtils=function(e){let t=h.get(e);return void 0===t&&(t=new o(e),h.set(e,t)),t},exports.includesSegment=l,exports.startsWithSegment=i;//# sourceMappingURL=index.js.map
1
+ "use strict";function e(e,t){return e===t||e.startsWith(`${t}.`)||t.startsWith(`${e}.`)}var t=/^[\w.-]+$/,s=e=>e.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`),n=(e,n)=>{const r=new Map,i=i=>{const a=r.get(i);if(a)return a;if(i.length>1e4)throw new RangeError("Segment exceeds maximum length of 10000 characters");if(!t.test(i))throw new TypeError("Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)");const l=new RegExp(e+s(i)+n);return r.set(i,l),l};return(e,t)=>{const s="string"==typeof e?e:e.name;if("string"!=typeof s)return!1;if(0===s.length)return!1;if(null===t)return!1;if(void 0===t)return e=>{if("string"!=typeof e)throw new TypeError("Segment must be a string, got "+typeof e);return 0!==e.length&&i(e).test(s)};if("string"!=typeof t)throw new TypeError("Segment must be a string, got "+typeof t);return 0!==t.length&&i(t).test(s)}},r=`(?:${s(".")}|$)`,i=n("^",r),a=n(`(?:^|${s(".")})`,"$"),l=n(`(?:^|${s(".")})`,r),o=class{static startsWithSegment=i;static endsWithSegment=a;static includesSegment=l;static areRoutesRelated=e;#e;#t;constructor(e){this.#e=new Map,this.#t=new Map,this.#s(e,[])}getChain(e){return this.#e.get(e)}getSiblings(e){return this.#t.get(e)}isDescendantOf(e,t){return e!==t&&e.startsWith(`${t}.`)}#s(e,t){const{fullName:s}=e;""!==s&&t.push(s),this.#e.set(s,Object.freeze(""===s?[""]:[...t]));const n=e.nonAbsoluteChildren.map(e=>e.fullName);for(const t of e.nonAbsoluteChildren)this.#t.set(t.fullName,Object.freeze(n.filter(e=>e!==t.fullName)));for(const t of e.children.values())this.#t.has(t.fullName)||""===t.fullName||this.#t.set(t.fullName,Object.freeze([...n]));for(const s of e.children.values())this.#s(s,t);""!==s&&t.pop()}},h=new WeakMap;exports.RouteUtils=o,exports.areRoutesRelated=e,exports.endsWithSegment=a,exports.getRouteUtils=function(e){let t=h.get(e);return void 0===t&&(t=new o(e),h.set(e,t)),t},exports.includesSegment=l,exports.startsWithSegment=i;//# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
1
+ {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map(\n (child) => child.fullName,\n );\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(\n nonAbsoluteNames.filter((name) => name !== child.fullName),\n ),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
@@ -1 +1 @@
1
- {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/routeRelation.ts":{"bytes":994,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4807,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":19261},"dist/cjs/index.js":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3845},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6546}}}
1
+ {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/routeRelation.ts":{"bytes":994,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4855,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"/home/runner/work/real-router/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":19334},"dist/cjs/index.js":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3891},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6592}}}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":";AAqBO,SAAS,gBAAA,CAAiB,QAAgB,MAAA,EAAyB;AACxE,EAAA,OACE,MAAA,KAAW,MAAA,IACX,MAAA,CAAO,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,CAAA,CAAG,CAAA,IAC9B,MAAA,CAAO,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,CAAA,CAAG,CAAA;AAElC;;;ACtBO,IAAM,kBAAA,GAAqB,GAAA;AAO3B,IAAM,oBAAA,GAAuB,WAAA;AAK7B,IAAM,uBAAA,GAA0B,GAAA;;;ACEvC,IAAM,eAAe,CAAC,GAAA,KACpB,IAAI,UAAA,CAAW,sBAAA,EAAwB,OAAO,GAAA,CAAA,GAAA,CAAQ,CAAA;AAWxD,IAAM,iBAAA,GAAoB,CAAC,KAAA,EAAe,GAAA,KAAgB;AACxD,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoB;AAe3C,EAAA,MAAM,UAAA,GAAa,CAAC,OAAA,KAA4B;AAC9C,IAAA,MAAM,MAAA,GAAS,UAAA,CAAW,GAAA,CAAI,OAAO,CAAA;AAErC,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,OAAO,MAAA;AAAA,IACT;AAKA,IAAA,IAAI,OAAA,CAAQ,SAAS,kBAAA,EAAoB;AACvC,MAAA,MAAM,IAAI,UAAA;AAAA,QACR,qCAAqC,kBAAkB,CAAA,WAAA;AAAA,OACzD;AAAA,IACF;AAGA,IAAA,IAAI,CAAC,oBAAA,CAAqB,IAAA,CAAK,OAAO,CAAA,EAAG;AACvC,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,8FAAA;AAAA,OACF;AAAA,IACF;AAEA,IAAA,MAAM,QAAQ,IAAI,MAAA,CAAO,QAAQ,YAAA,CAAa,OAAO,IAAI,GAAG,CAAA;AAE5D,IAAA,UAAA,CAAW,GAAA,CAAI,SAAS,KAAK,CAAA;AAE7B,IAAA,OAAO,KAAA;AAAA,EACT,CAAA;AAMA,EAAA,OAAO,CAAC,OAAuB,OAAA,KAA4B;AAGzD,IAAA,MAAM,IAAA,GAAO,OAAO,KAAA,KAAU,QAAA,GAAW,QAAQ,KAAA,CAAM,IAAA;AAEvD,IAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrB,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,YAAY,IAAA,EAAM;AACpB,MAAA,OAAO,KAAA;AAAA,IACT;AAGA,IAAA,IAAI,YAAY,MAAA,EAAW;AACzB,MAAA,OAAO,CAAC,YAAA,KAAyB;AAE/B,QAAA,IAAI,OAAO,iBAAiB,QAAA,EAAU;AACpC,UAAA,MAAM,IAAI,SAAA;AAAA,YACR,CAAA,8BAAA,EAAiC,OAAO,YAAY,CAAA;AAAA,WACtD;AAAA,QACF;AAGA,QAAA,IAAI,YAAA,CAAa,WAAW,CAAA,EAAG;AAC7B,UAAA,OAAO,KAAA;AAAA,QACT;AAGA,QAAA,OAAO,UAAA,CAAW,YAAY,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAAA,MAC3C,CAAA;AAAA,IACF;AAEA,IAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAG/B,MAAA,MAAM,IAAI,SAAA,CAAU,CAAA,8BAAA,EAAiC,OAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACvE;AAGA,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,MAAA,OAAO,KAAA;AAAA,IACT;AAIA,IAAA,OAAO,UAAA,CAAW,OAAO,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAAA,EACtC,CAAA;AACF,CAAA;AAMA,IAAM,QAAA,GAAW,CAAA,GAAA,EAAM,YAAA,CAAa,uBAAuB,CAAC,CAAA,GAAA,CAAA;AAsCrD,IAAM,iBAAA,GAAoB,iBAAA;AAAA,EAC/B,GAAA;AAAA,EACA;AACF;AAiCO,IAAM,eAAA,GAAkB,iBAAA;AAAA,EAC7B,CAAA,KAAA,EAAQ,YAAA,CAAa,uBAAuB,CAAC,CAAA,CAAA,CAAA;AAAA,EAC7C;AACF;AA+BO,IAAM,eAAA,GAAkB,iBAAA;AAAA,EAC7B,CAAA,KAAA,EAAQ,YAAA,CAAa,uBAAuB,CAAC,CAAA,CAAA,CAAA;AAAA,EAC7C;AACF;;;ACnPO,IAAM,aAAN,MAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAStB,OAAgB,iBAAA,GAAyC,iBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQzD,OAAgB,eAAA,GAAuC,eAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQvD,OAAgB,eAAA,GAAuC,eAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQvD,OAAgB,gBAAA,GAAmB,gBAAA;AAAA;AAAA,EAI1B,WAAA;AAAA,EACA,cAAA;AAAA,EAET,YAAY,IAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,WAAA,uBAAkB,GAAA,EAAI;AAC3B,IAAA,IAAA,CAAK,cAAA,uBAAqB,GAAA,EAAI;AAC9B,IAAA,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,EAAE,CAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,SAAS,IAAA,EAA6C;AACpD,IAAA,OAAO,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,IAAI,CAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,YAAY,IAAA,EAA6C;AACvD,IAAA,OAAO,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,IAAI,CAAA;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,cAAA,CAAe,OAAe,MAAA,EAAyB;AACrD,IAAA,OAAO,UAAU,MAAA,IAAU,KAAA,CAAM,UAAA,CAAW,CAAA,EAAG,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,EAC1D;AAAA,EACA,SAAA,CAAU,MAAqB,KAAA,EAAuB;AACpD,IAAA,MAAM,EAAE,UAAS,GAAI,IAAA;AAGrB,IAAA,IAAI,aAAa,EAAA,EAAI;AACnB,MAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AAAA,IACrB;AAEA,IAAA,IAAA,CAAK,WAAA,CAAY,GAAA;AAAA,MACf,QAAA;AAAA,MACA,MAAA,CAAO,MAAA,CAAO,QAAA,KAAa,EAAA,GAAK,CAAC,EAAE,CAAA,GAAI,CAAC,GAAG,KAAK,CAAC;AAAA,KACnD;AAKA,IAAA,MAAM,mBAAmB,IAAA,CAAK,mBAAA,CAAoB,IAAI,CAAC,CAAA,KAAM,EAAE,QAAQ,CAAA;AAEvE,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,mBAAA,EAAqB;AAC5C,MAAA,IAAA,CAAK,cAAA,CAAe,GAAA;AAAA,QAClB,KAAA,CAAM,QAAA;AAAA,QACN,MAAA,CAAO,OAAO,gBAAA,CAAiB,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,KAAM,KAAA,CAAM,QAAQ,CAAC;AAAA,OACpE;AAAA,IACF;AAGA,IAAA,KAAA,MAAW,KAAA,IAAS,IAAA,CAAK,QAAA,CAAS,MAAA,EAAO,EAAG;AAC1C,MAAA,IAAI,CAAC,KAAK,cAAA,CAAe,GAAA,CAAI,MAAM,QAAQ,CAAA,IAAK,KAAA,CAAM,QAAA,KAAa,EAAA,EAAI;AACrE,QAAA,IAAA,CAAK,cAAA,CAAe,GAAA;AAAA,UAClB,KAAA,CAAM,QAAA;AAAA,UACN,MAAA,CAAO,MAAA,CAAO,CAAC,GAAG,gBAAgB,CAAC;AAAA,SACrC;AAAA,MACF;AAAA,IACF;AAGA,IAAA,KAAA,MAAW,KAAA,IAAS,IAAA,CAAK,QAAA,CAAS,MAAA,EAAO,EAAG;AAC1C,MAAA,IAAA,CAAK,SAAA,CAAU,OAAO,KAAK,CAAA;AAAA,IAC7B;AAGA,IAAA,IAAI,aAAa,EAAA,EAAI;AACnB,MAAA,KAAA,CAAM,GAAA,EAAI;AAAA,IACZ;AAAA,EACF;AACF;;;AChJA,IAAM,KAAA,uBAAY,OAAA,EAAmC;AAE9C,SAAS,cAAc,IAAA,EAAiC;AAC7D,EAAA,IAAI,KAAA,GAAQ,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA;AAE1B,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,KAAA,GAAQ,IAAI,WAAW,IAAI,CAAA;AAC3B,IAAA,KAAA,CAAM,GAAA,CAAI,MAAM,KAAK,CAAA;AAAA,EACvB;AAEA,EAAA,OAAO,KAAA;AACT","file":"index.mjs","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
1
+ {"version":3,"sources":["../../src/routeRelation.ts","../../src/constants.ts","../../src/segmentTesters.ts","../../src/RouteUtils.ts","../../src/getRouteUtils.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/route-utils/src/routeRelation.ts\n\n/**\n * Checks if two routes are related in the hierarchy.\n *\n * Routes are related if:\n * - They are exactly the same\n * - One is a parent of the other (e.g., \"users\" and \"users.list\")\n * - One is a child of the other (e.g., \"users.list\" and \"users\")\n *\n * @param route1 - First route name\n * @param route2 - Second route name\n * @returns True if routes are related, false otherwise\n *\n * @example\n * areRoutesRelated(\"users\", \"users.list\"); // true (parent-child)\n * areRoutesRelated(\"users.list\", \"users\"); // true (child-parent)\n * areRoutesRelated(\"users\", \"users\"); // true (same)\n * areRoutesRelated(\"users\", \"admin\"); // false (different branches)\n * areRoutesRelated(\"users.list\", \"users.view\"); // false (siblings)\n */\nexport function areRoutesRelated(route1: string, route2: string): boolean {\n return (\n route1 === route2 ||\n route1.startsWith(`${route2}.`) ||\n route2.startsWith(`${route1}.`)\n );\n}\n","// packages/route-utils/src/constants.ts\n\n/**\n * Maximum allowed segment length (10,000 characters)\n */\nexport const MAX_SEGMENT_LENGTH = 10_000;\n\n/**\n * Pattern for valid segment characters: alphanumeric + dot + dash + underscore\n * Uses explicit character ranges for clarity and portability.\n * Dash is placed at the end to avoid escaping (no range operator confusion).\n */\nexport const SAFE_SEGMENT_PATTERN = /^[\\w.-]+$/;\n\n/**\n * Route segment separator character\n */\nexport const ROUTE_SEGMENT_SEPARATOR = \".\";\n","// packages/route-utils/src/segmentTesters.ts\n\nimport {\n MAX_SEGMENT_LENGTH,\n ROUTE_SEGMENT_SEPARATOR,\n SAFE_SEGMENT_PATTERN,\n} from \"./constants\";\n\nimport type { SegmentTestFunction } from \"./types\";\nimport type { State } from \"@real-router/types\";\n\n/**\n * Escapes special RegExp characters in a string.\n * Handles all RegExp metacharacters including dash in character classes.\n *\n * @param str - String to escape\n * @returns Escaped string safe for RegExp construction\n * @internal\n */\nconst escapeRegExp = (str: string): string =>\n str.replaceAll(/[$()*+.?[\\\\\\]^{|}-]/g, String.raw`\\$&`);\n\n/**\n * Creates a segment tester function with specified start and end patterns.\n * This is a factory function that produces the actual test functions.\n *\n * @param start - RegExp pattern for start (e.g., \"^\" for startsWith)\n * @param end - RegExp pattern for end (e.g., \"$\" or dotOrEnd for specific matching)\n * @returns A test function that can check if routes match the segment pattern\n * @internal\n */\nconst makeSegmentTester = (start: string, end: string) => {\n const regexCache = new Map<string, RegExp>();\n\n /**\n * Builds a RegExp for testing segment matches.\n * Validates length and character pattern. Type and empty checks are done by caller.\n *\n * This optimizes performance by avoiding redundant checks - callers verify\n * type and empty before calling this function.\n *\n * @param segment - The segment to build a regex for (non-empty string, pre-validated)\n * @returns RegExp for testing\n * @throws {RangeError} If segment exceeds maximum length\n * @throws {TypeError} If segment contains invalid characters\n * @internal\n */\n const buildRegex = (segment: string): RegExp => {\n const cached = regexCache.get(segment);\n\n if (cached) {\n return cached;\n }\n\n // Type and empty checks are SKIPPED - caller already verified these\n\n // Length check\n if (segment.length > MAX_SEGMENT_LENGTH) {\n throw new RangeError(\n `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,\n );\n }\n\n // Character pattern check\n if (!SAFE_SEGMENT_PATTERN.test(segment)) {\n throw new TypeError(\n `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,\n );\n }\n\n const regex = new RegExp(start + escapeRegExp(segment) + end);\n\n regexCache.set(segment, regex);\n\n return regex;\n };\n\n // TypeScript cannot infer conditional return type for curried function with union return.\n // The function returns either boolean or a tester function based on whether segment is provided.\n // This is an intentional design pattern for API flexibility.\n // eslint-disable-next-line sonarjs/function-return-type\n return (route: State | string, segment?: string | null) => {\n // Extract route name, handling both string and State object inputs\n // State.name is always string by real-router type definition\n const name = typeof route === \"string\" ? route : route.name;\n\n if (typeof name !== \"string\") {\n return false;\n }\n\n // Empty route name always returns false\n if (name.length === 0) {\n return false;\n }\n\n // null always returns false (consistent behavior)\n if (segment === null) {\n return false;\n }\n\n // Currying: if no segment provided, return a tester function\n if (segment === undefined) {\n return (localSegment: string) => {\n // Type check for runtime safety (consistent with direct call)\n if (typeof localSegment !== \"string\") {\n throw new TypeError(\n `Segment must be a string, got ${typeof localSegment}`,\n );\n }\n\n // Empty string returns false (consistent with direct call)\n if (localSegment.length === 0) {\n return false;\n }\n\n // Use buildRegex (type and empty checks already done above)\n return buildRegex(localSegment).test(name);\n };\n }\n\n if (typeof segment !== \"string\") {\n // Runtime protection: TypeScript already narrows to 'string' here,\n // but we keep this check for defense against unexpected runtime values\n throw new TypeError(`Segment must be a string, got ${typeof segment}`);\n }\n\n // Empty string returns false (consistent behavior)\n if (segment.length === 0) {\n return false;\n }\n\n // Perform the actual regex test\n // buildRegex skips type and empty checks (already validated above)\n return buildRegex(segment).test(name);\n };\n};\n\n/**\n * Pattern that matches either a dot separator or end of string.\n * Used for prefix/suffix matching that respects segment boundaries.\n */\nconst dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;\n\n/**\n * Tests if a route name starts with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route starts with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * startsWithSegment('users.list', 'users'); // true\n * startsWithSegment('users.list', 'admin'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('admin'); // false\n *\n * @example\n * // With State object\n * const state: State = { name: 'users.list', params: {}, path: '/users/list' };\n * startsWithSegment(state, 'users'); // true\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see endsWithSegment for suffix matching\n * @see includesSegment for anywhere matching\n */\nexport const startsWithSegment = makeSegmentTester(\n \"^\",\n dotOrEnd,\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name ends with the given segment.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route ends with segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * endsWithSegment('users.list', 'list'); // true\n * endsWithSegment('users.profile.edit', 'edit'); // true\n *\n * @example\n * // Curried form\n * const tester = endsWithSegment('users.list');\n * tester('list'); // true\n * tester('users'); // false\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see includesSegment for anywhere matching\n */\nexport const endsWithSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n \"$\",\n) as SegmentTestFunction;\n\n/**\n * Tests if a route name includes the given segment anywhere in its path.\n *\n * Supports both direct calls and curried form for flexible usage patterns.\n * All segments are validated for safety (length and character constraints).\n *\n * @param route - Route state object or route name string\n * @param segment - Segment to test. If omitted, returns a tester function.\n *\n * @returns\n * - `boolean` if segment is provided (true if route includes segment)\n * - `(segment: string) => boolean` if segment is omitted (curried tester function)\n * - `false` if segment is null or empty string\n *\n * @example\n * // Direct call\n * includesSegment('users.profile.edit', 'profile'); // true\n *\n * @example\n * // Multi-segment inclusion\n * includesSegment('a.b.c.d', 'b.c'); // true\n * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)\n *\n * @throws {TypeError} If segment contains invalid characters or is not a string\n * @throws {RangeError} If segment exceeds maximum length (10,000 characters)\n *\n * @see startsWithSegment for prefix matching\n * @see endsWithSegment for suffix matching\n */\nexport const includesSegment = makeSegmentTester(\n `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,\n dotOrEnd,\n) as SegmentTestFunction;\n","import { areRoutesRelated } from \"./routeRelation.js\";\nimport {\n startsWithSegment,\n endsWithSegment,\n includesSegment,\n} from \"./segmentTesters.js\";\n\nimport type { RouteTreeNode, SegmentTestFunction } from \"./types.js\";\n\nexport class RouteUtils {\n // ===== Static facade: segment testing =====\n\n /**\n * Tests if a route name starts with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link startsWithSegment} standalone function for details\n */\n static readonly startsWithSegment: SegmentTestFunction = startsWithSegment;\n\n /**\n * Tests if a route name ends with the given segment.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link endsWithSegment} standalone function for details\n */\n static readonly endsWithSegment: SegmentTestFunction = endsWithSegment;\n\n /**\n * Tests if a route name includes the given segment anywhere in its path.\n * Supports direct calls, curried form, and `State` objects.\n *\n * @see {@link includesSegment} standalone function for details\n */\n static readonly includesSegment: SegmentTestFunction = includesSegment;\n\n /**\n * Checks if two routes are related in the hierarchy\n * (same, parent-child, or child-parent).\n *\n * @see {@link areRoutesRelated} standalone function for details\n */\n static readonly areRoutesRelated = areRoutesRelated;\n\n // ===== Instance fields =====\n\n readonly #chainCache: Map<string, readonly string[]>;\n readonly #siblingsCache: Map<string, readonly string[]>;\n\n constructor(root: RouteTreeNode) {\n this.#chainCache = new Map();\n this.#siblingsCache = new Map();\n this.#buildAll(root, []);\n }\n\n /**\n * Returns cumulative name segments for the given route (ancestor chain without root).\n *\n * All chains are pre-computed and frozen during construction.\n *\n * @param name - Full route name (e.g. `\"users.profile\"`)\n * @returns Frozen array of cumulative segments, or `undefined` if not in tree\n *\n * @example\n * ```ts\n * utils.getChain(\"users.profile.edit\");\n * // → [\"users\", \"users.profile\", \"users.profile.edit\"]\n * ```\n */\n getChain(name: string): readonly string[] | undefined {\n return this.#chainCache.get(name);\n }\n\n /**\n * Returns non-absolute siblings of the named node (excluding itself).\n *\n * Siblings are children of the same parent, filtered by `nonAbsoluteChildren`.\n * All siblings are pre-computed and frozen during construction.\n *\n * @param name - Full route name\n * @returns Frozen array of sibling full names, or `undefined` if not found or root\n */\n getSiblings(name: string): readonly string[] | undefined {\n return this.#siblingsCache.get(name);\n }\n\n /**\n * Checks if `child` is a descendant of `parent` via string prefix comparison.\n *\n * Does not perform tree lookup — O(k) where k is the name length.\n *\n * @param child - Full name of the potential descendant\n * @param parent - Full name of the potential ancestor\n * @returns `true` if `child` starts with `parent.` (dot-separated)\n *\n * @remarks\n * Does not work with root (`\"\"`) as parent — returns `false` because\n * `\"users\".startsWith(\".\")` is `false`. This is acceptable since\n * every route in the tree is trivially a descendant of root.\n */\n isDescendantOf(child: string, parent: string): boolean {\n return child !== parent && child.startsWith(`${parent}.`);\n }\n #buildAll(node: RouteTreeNode, chain: string[]): void {\n const { fullName } = node;\n\n // Build chain: root gets [\"\"], others get cumulative segments\n if (fullName !== \"\") {\n chain.push(fullName);\n }\n\n this.#chainCache.set(\n fullName,\n Object.freeze(fullName === \"\" ? [\"\"] : [...chain]),\n );\n\n // Build siblings for all children of this node\n // Siblings = nonAbsoluteChildren excluding the child itself\n // Absolute children also get siblings (all nonAbsoluteChildren)\n const nonAbsoluteNames = node.nonAbsoluteChildren.map(\n (child) => child.fullName,\n );\n\n for (const child of node.nonAbsoluteChildren) {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze(\n nonAbsoluteNames.filter((name) => name !== child.fullName),\n ),\n );\n }\n\n // Absolute children: their siblings are ALL nonAbsoluteChildren\n for (const child of node.children.values()) {\n if (!this.#siblingsCache.has(child.fullName) && child.fullName !== \"\") {\n this.#siblingsCache.set(\n child.fullName,\n Object.freeze([...nonAbsoluteNames]),\n );\n }\n }\n\n // Recurse into all children (including absolute)\n for (const child of node.children.values()) {\n this.#buildAll(child, chain);\n }\n\n // Restore chain for sibling traversal\n if (fullName !== \"\") {\n chain.pop();\n }\n }\n}\n","import { RouteUtils } from \"./RouteUtils.js\";\n\nimport type { RouteTreeNode } from \"./types.js\";\n\nconst cache = new WeakMap<RouteTreeNode, RouteUtils>();\n\nexport function getRouteUtils(root: RouteTreeNode): RouteUtils {\n let utils = cache.get(root);\n\n if (utils === undefined) {\n utils = new RouteUtils(root);\n cache.set(root, utils);\n }\n\n return utils;\n}\n"]}
@@ -1 +1 @@
1
- {"inputs":{"src/routeRelation.ts":{"bytes":994,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4807,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":19261},"dist/esm/index.mjs":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3845},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6546}}}
1
+ {"inputs":{"src/routeRelation.ts":{"bytes":994,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":515,"imports":[],"format":"esm"},"src/segmentTesters.ts":{"bytes":8570,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/RouteUtils.ts":{"bytes":4855,"imports":[{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"}],"format":"esm"},"src/getRouteUtils.ts":{"bytes":365,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"}],"format":"esm"},"src/index.ts":{"bytes":310,"imports":[{"path":"src/RouteUtils.ts","kind":"import-statement","original":"./RouteUtils.js"},{"path":"src/getRouteUtils.ts","kind":"import-statement","original":"./getRouteUtils.js"},{"path":"src/segmentTesters.ts","kind":"import-statement","original":"./segmentTesters.js"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation.js"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":19334},"dist/esm/index.mjs":{"imports":[],"exports":["RouteUtils","areRoutesRelated","endsWithSegment","getRouteUtils","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/routeRelation.ts":{"bytesInOutput":144},"src/constants.ts":{"bytesInOutput":105},"src/segmentTesters.ts":{"bytesInOutput":1998},"src/RouteUtils.ts":{"bytesInOutput":3891},"src/index.ts":{"bytesInOutput":0},"src/getRouteUtils.ts":{"bytesInOutput":215}},"bytes":6592}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/route-utils",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "commonjs",
5
5
  "description": "Cached read-only query API for route tree structure",
6
6
  "main": "./dist/cjs/index.js",
@@ -42,16 +42,17 @@
42
42
  "sideEffects": false,
43
43
  "devDependencies": {
44
44
  "mitata": "1.0.34",
45
- "route-tree": "^0.3.3"
45
+ "route-tree": "^0.3.4"
46
46
  },
47
47
  "dependencies": {
48
- "@real-router/types": "^0.23.0"
48
+ "@real-router/types": "^0.26.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup",
52
52
  "type-check": "tsc --noEmit",
53
53
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0 --no-error-on-unmatched-pattern",
54
54
  "test": "vitest run",
55
+ "test:properties": "vitest --config vitest.config.properties.mts --run",
55
56
  "bench": "NODE_OPTIONS='--expose-gc --max-old-space-size=4096' npx tsx tests/benchmarks/index.ts"
56
57
  }
57
58
  }
package/src/RouteUtils.ts CHANGED
@@ -117,12 +117,16 @@ export class RouteUtils {
117
117
  // Build siblings for all children of this node
118
118
  // Siblings = nonAbsoluteChildren excluding the child itself
119
119
  // Absolute children also get siblings (all nonAbsoluteChildren)
120
- const nonAbsoluteNames = node.nonAbsoluteChildren.map((c) => c.fullName);
120
+ const nonAbsoluteNames = node.nonAbsoluteChildren.map(
121
+ (child) => child.fullName,
122
+ );
121
123
 
122
124
  for (const child of node.nonAbsoluteChildren) {
123
125
  this.#siblingsCache.set(
124
126
  child.fullName,
125
- Object.freeze(nonAbsoluteNames.filter((n) => n !== child.fullName)),
127
+ Object.freeze(
128
+ nonAbsoluteNames.filter((name) => name !== child.fullName),
129
+ ),
126
130
  );
127
131
  }
128
132