@real-router/route-utils 0.0.1
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 +175 -0
- package/dist/cjs/index.d.ts +223 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/metafile-cjs.json +1 -0
- package/dist/esm/index.d.mts +223 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/metafile-esm.json +1 -0
- package/package.json +57 -0
- package/src/RouteUtils.ts +149 -0
- package/src/constants.ts +18 -0
- package/src/getRouteUtils.ts +16 -0
- package/src/index.ts +13 -0
- package/src/routeRelation.ts +28 -0
- package/src/segmentTesters.ts +253 -0
- package/src/types.ts +33 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// packages/route-utils/src/segmentTesters.ts
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MAX_SEGMENT_LENGTH,
|
|
5
|
+
ROUTE_SEGMENT_SEPARATOR,
|
|
6
|
+
SAFE_SEGMENT_PATTERN,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
|
|
9
|
+
import type { SegmentTestFunction } from "./types";
|
|
10
|
+
import type { State } from "@real-router/types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Escapes special RegExp characters in a string.
|
|
14
|
+
* Handles all RegExp metacharacters including dash in character classes.
|
|
15
|
+
*
|
|
16
|
+
* @param str - String to escape
|
|
17
|
+
* @returns Escaped string safe for RegExp construction
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const escapeRegExp = (str: string): string =>
|
|
21
|
+
str.replaceAll(/[$()*+.?[\\\]^{|}-]/g, String.raw`\$&`);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a segment tester function with specified start and end patterns.
|
|
25
|
+
* This is a factory function that produces the actual test functions.
|
|
26
|
+
*
|
|
27
|
+
* @param start - RegExp pattern for start (e.g., "^" for startsWith)
|
|
28
|
+
* @param end - RegExp pattern for end (e.g., "$" or dotOrEnd for specific matching)
|
|
29
|
+
* @returns A test function that can check if routes match the segment pattern
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
const makeSegmentTester = (start: string, end: string) => {
|
|
33
|
+
const regexCache = new Map<string, RegExp>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Builds a RegExp for testing segment matches.
|
|
37
|
+
* Validates length and character pattern. Type and empty checks are done by caller.
|
|
38
|
+
*
|
|
39
|
+
* This optimizes performance by avoiding redundant checks - callers verify
|
|
40
|
+
* type and empty before calling this function.
|
|
41
|
+
*
|
|
42
|
+
* @param segment - The segment to build a regex for (non-empty string, pre-validated)
|
|
43
|
+
* @returns RegExp for testing
|
|
44
|
+
* @throws {RangeError} If segment exceeds maximum length
|
|
45
|
+
* @throws {TypeError} If segment contains invalid characters
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
const buildRegex = (segment: string): RegExp => {
|
|
49
|
+
const cached = regexCache.get(segment);
|
|
50
|
+
|
|
51
|
+
if (cached) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Type and empty checks are SKIPPED - caller already verified these
|
|
56
|
+
|
|
57
|
+
// Length check
|
|
58
|
+
if (segment.length > MAX_SEGMENT_LENGTH) {
|
|
59
|
+
throw new RangeError(
|
|
60
|
+
`Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Character pattern check
|
|
65
|
+
if (!SAFE_SEGMENT_PATTERN.test(segment)) {
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
`Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const regex = new RegExp(start + escapeRegExp(segment) + end);
|
|
72
|
+
|
|
73
|
+
regexCache.set(segment, regex);
|
|
74
|
+
|
|
75
|
+
return regex;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// TypeScript cannot infer conditional return type for curried function with union return.
|
|
79
|
+
// The function returns either boolean or a tester function based on whether segment is provided.
|
|
80
|
+
// This is an intentional design pattern for API flexibility.
|
|
81
|
+
// eslint-disable-next-line sonarjs/function-return-type
|
|
82
|
+
return (route: State | string, segment?: string | null) => {
|
|
83
|
+
// Extract route name, handling both string and State object inputs
|
|
84
|
+
// State.name is always string by real-router type definition
|
|
85
|
+
const name = typeof route === "string" ? route : route.name;
|
|
86
|
+
|
|
87
|
+
if (typeof name !== "string") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Empty route name always returns false
|
|
92
|
+
if (name.length === 0) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// null always returns false (consistent behavior)
|
|
97
|
+
if (segment === null) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Currying: if no segment provided, return a tester function
|
|
102
|
+
if (segment === undefined) {
|
|
103
|
+
return (localSegment: string) => {
|
|
104
|
+
// Type check for runtime safety (consistent with direct call)
|
|
105
|
+
if (typeof localSegment !== "string") {
|
|
106
|
+
throw new TypeError(
|
|
107
|
+
`Segment must be a string, got ${typeof localSegment}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Empty string returns false (consistent with direct call)
|
|
112
|
+
if (localSegment.length === 0) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Use buildRegex (type and empty checks already done above)
|
|
117
|
+
return buildRegex(localSegment).test(name);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof segment !== "string") {
|
|
122
|
+
// Runtime protection: TypeScript already narrows to 'string' here,
|
|
123
|
+
// but we keep this check for defense against unexpected runtime values
|
|
124
|
+
throw new TypeError(`Segment must be a string, got ${typeof segment}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Empty string returns false (consistent behavior)
|
|
128
|
+
if (segment.length === 0) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Perform the actual regex test
|
|
133
|
+
// buildRegex skips type and empty checks (already validated above)
|
|
134
|
+
return buildRegex(segment).test(name);
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pattern that matches either a dot separator or end of string.
|
|
140
|
+
* Used for prefix/suffix matching that respects segment boundaries.
|
|
141
|
+
*/
|
|
142
|
+
const dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Tests if a route name starts with the given segment.
|
|
146
|
+
*
|
|
147
|
+
* Supports both direct calls and curried form for flexible usage patterns.
|
|
148
|
+
* All segments are validated for safety (length and character constraints).
|
|
149
|
+
*
|
|
150
|
+
* @param route - Route state object or route name string
|
|
151
|
+
* @param segment - Segment to test. If omitted, returns a tester function.
|
|
152
|
+
*
|
|
153
|
+
* @returns
|
|
154
|
+
* - `boolean` if segment is provided (true if route starts with segment)
|
|
155
|
+
* - `(segment: string) => boolean` if segment is omitted (curried tester function)
|
|
156
|
+
* - `false` if segment is null or empty string
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* // Direct call
|
|
160
|
+
* startsWithSegment('users.list', 'users'); // true
|
|
161
|
+
* startsWithSegment('users.list', 'admin'); // false
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // Curried form
|
|
165
|
+
* const tester = startsWithSegment('users.list');
|
|
166
|
+
* tester('users'); // true
|
|
167
|
+
* tester('admin'); // false
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // With State object
|
|
171
|
+
* const state: State = { name: 'users.list', params: {}, path: '/users/list' };
|
|
172
|
+
* startsWithSegment(state, 'users'); // true
|
|
173
|
+
*
|
|
174
|
+
* @throws {TypeError} If segment contains invalid characters or is not a string
|
|
175
|
+
* @throws {RangeError} If segment exceeds maximum length (10,000 characters)
|
|
176
|
+
*
|
|
177
|
+
* @see endsWithSegment for suffix matching
|
|
178
|
+
* @see includesSegment for anywhere matching
|
|
179
|
+
*/
|
|
180
|
+
export const startsWithSegment = makeSegmentTester(
|
|
181
|
+
"^",
|
|
182
|
+
dotOrEnd,
|
|
183
|
+
) as SegmentTestFunction;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Tests if a route name ends with the given segment.
|
|
187
|
+
*
|
|
188
|
+
* Supports both direct calls and curried form for flexible usage patterns.
|
|
189
|
+
* All segments are validated for safety (length and character constraints).
|
|
190
|
+
*
|
|
191
|
+
* @param route - Route state object or route name string
|
|
192
|
+
* @param segment - Segment to test. If omitted, returns a tester function.
|
|
193
|
+
*
|
|
194
|
+
* @returns
|
|
195
|
+
* - `boolean` if segment is provided (true if route ends with segment)
|
|
196
|
+
* - `(segment: string) => boolean` if segment is omitted (curried tester function)
|
|
197
|
+
* - `false` if segment is null or empty string
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* // Direct call
|
|
201
|
+
* endsWithSegment('users.list', 'list'); // true
|
|
202
|
+
* endsWithSegment('users.profile.edit', 'edit'); // true
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* // Curried form
|
|
206
|
+
* const tester = endsWithSegment('users.list');
|
|
207
|
+
* tester('list'); // true
|
|
208
|
+
* tester('users'); // false
|
|
209
|
+
*
|
|
210
|
+
* @throws {TypeError} If segment contains invalid characters or is not a string
|
|
211
|
+
* @throws {RangeError} If segment exceeds maximum length (10,000 characters)
|
|
212
|
+
*
|
|
213
|
+
* @see startsWithSegment for prefix matching
|
|
214
|
+
* @see includesSegment for anywhere matching
|
|
215
|
+
*/
|
|
216
|
+
export const endsWithSegment = makeSegmentTester(
|
|
217
|
+
`(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,
|
|
218
|
+
"$",
|
|
219
|
+
) as SegmentTestFunction;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Tests if a route name includes the given segment anywhere in its path.
|
|
223
|
+
*
|
|
224
|
+
* Supports both direct calls and curried form for flexible usage patterns.
|
|
225
|
+
* All segments are validated for safety (length and character constraints).
|
|
226
|
+
*
|
|
227
|
+
* @param route - Route state object or route name string
|
|
228
|
+
* @param segment - Segment to test. If omitted, returns a tester function.
|
|
229
|
+
*
|
|
230
|
+
* @returns
|
|
231
|
+
* - `boolean` if segment is provided (true if route includes segment)
|
|
232
|
+
* - `(segment: string) => boolean` if segment is omitted (curried tester function)
|
|
233
|
+
* - `false` if segment is null or empty string
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Direct call
|
|
237
|
+
* includesSegment('users.profile.edit', 'profile'); // true
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* // Multi-segment inclusion
|
|
241
|
+
* includesSegment('a.b.c.d', 'b.c'); // true
|
|
242
|
+
* includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)
|
|
243
|
+
*
|
|
244
|
+
* @throws {TypeError} If segment contains invalid characters or is not a string
|
|
245
|
+
* @throws {RangeError} If segment exceeds maximum length (10,000 characters)
|
|
246
|
+
*
|
|
247
|
+
* @see startsWithSegment for prefix matching
|
|
248
|
+
* @see endsWithSegment for suffix matching
|
|
249
|
+
*/
|
|
250
|
+
export const includesSegment = makeSegmentTester(
|
|
251
|
+
`(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,
|
|
252
|
+
dotOrEnd,
|
|
253
|
+
) as SegmentTestFunction;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// packages/route-utils/src/types.ts
|
|
2
|
+
|
|
3
|
+
import type { State } from "@real-router/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal interface for route tree nodes used by RouteUtils.
|
|
7
|
+
* Structurally compatible with `RouteTree` from `route-tree` package.
|
|
8
|
+
* Defined locally to avoid a runtime dependency on the internal `route-tree` package.
|
|
9
|
+
*/
|
|
10
|
+
export interface RouteTreeNode {
|
|
11
|
+
/** Pre-computed full name (e.g. `"users.profile"`, `""` for root) */
|
|
12
|
+
readonly fullName: string;
|
|
13
|
+
|
|
14
|
+
/** Child route nodes */
|
|
15
|
+
readonly children: ReadonlyMap<string, RouteTreeNode>;
|
|
16
|
+
|
|
17
|
+
/** Children without absolute paths */
|
|
18
|
+
readonly nonAbsoluteChildren: readonly RouteTreeNode[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type definition for segment test functions.
|
|
23
|
+
* These functions can be called directly with a segment, or curried for later use.
|
|
24
|
+
*/
|
|
25
|
+
export interface SegmentTestFunction {
|
|
26
|
+
(route: State | string): (segment: string) => boolean;
|
|
27
|
+
(route: State | string, segment: string): boolean;
|
|
28
|
+
(route: State | string, segment: null): false;
|
|
29
|
+
(
|
|
30
|
+
route: State | string,
|
|
31
|
+
segment?: string | null,
|
|
32
|
+
): boolean | ((segment: string) => boolean);
|
|
33
|
+
}
|