@real-router/helpers 0.1.25 → 0.1.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/helpers",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "type": "commonjs",
5
5
  "description": "Helper utilities for comparing and checking routes",
6
6
  "main": "./dist/cjs/index.js",
@@ -8,6 +8,7 @@
8
8
  "types": "./dist/esm/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
+ "development": "./src/index.ts",
11
12
  "types": {
12
13
  "import": "./dist/esm/index.d.mts",
13
14
  "require": "./dist/cjs/index.d.ts"
@@ -17,7 +18,8 @@
17
18
  }
18
19
  },
19
20
  "files": [
20
- "dist"
21
+ "dist",
22
+ "src"
21
23
  ],
22
24
  "repository": {
23
25
  "type": "git",
@@ -39,7 +41,7 @@
39
41
  "homepage": "https://github.com/greydragon888/real-router",
40
42
  "sideEffects": false,
41
43
  "dependencies": {
42
- "@real-router/core": "^0.22.0"
44
+ "@real-router/core": "^0.23.0"
43
45
  },
44
46
  "scripts": {
45
47
  "test": "vitest",
@@ -0,0 +1,18 @@
1
+ // packages/helpers/modules/constants.ts
2
+
3
+ /**
4
+ * Maximum allowed segment length (10,000 characters)
5
+ */
6
+ export const MAX_SEGMENT_LENGTH = 10_000;
7
+
8
+ /**
9
+ * Pattern for valid segment characters: alphanumeric + dot + dash + underscore
10
+ * Uses explicit character ranges for clarity and portability.
11
+ * Dash is placed at the end to avoid escaping (no range operator confusion).
12
+ */
13
+ export const SAFE_SEGMENT_PATTERN = /^[\w.-]+$/;
14
+
15
+ /**
16
+ * Route segment separator character
17
+ */
18
+ export const ROUTE_SEGMENT_SEPARATOR = ".";
package/src/helpers.ts ADDED
@@ -0,0 +1,288 @@
1
+ // packages/helpers/modules/index.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/core";
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
+ /**
34
+ * Builds a RegExp for testing segment matches.
35
+ * Validates length and character pattern. Type and empty checks are done by caller.
36
+ *
37
+ * This optimizes performance by avoiding redundant checks - callers verify
38
+ * type and empty before calling this function.
39
+ *
40
+ * @param segment - The segment to build a regex for (non-empty string, pre-validated)
41
+ * @returns RegExp for testing
42
+ * @throws {RangeError} If segment exceeds maximum length
43
+ * @throws {TypeError} If segment contains invalid characters
44
+ * @internal
45
+ */
46
+ const buildRegex = (segment: string): RegExp => {
47
+ // Type and empty checks are SKIPPED - caller already verified these
48
+
49
+ // Length check
50
+ if (segment.length > MAX_SEGMENT_LENGTH) {
51
+ throw new RangeError(
52
+ `Segment exceeds maximum length of ${MAX_SEGMENT_LENGTH} characters`,
53
+ );
54
+ }
55
+
56
+ // Character pattern check
57
+ if (!SAFE_SEGMENT_PATTERN.test(segment)) {
58
+ throw new TypeError(
59
+ `Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)`,
60
+ );
61
+ }
62
+
63
+ return new RegExp(start + escapeRegExp(segment) + end);
64
+ };
65
+
66
+ // TypeScript cannot infer conditional return type for curried function with union return.
67
+ // The function returns either boolean or a tester function based on whether segment is provided.
68
+ // This is an intentional design pattern for API flexibility.
69
+ // eslint-disable-next-line sonarjs/function-return-type
70
+ return (route: State | string, segment?: string | null) => {
71
+ // Extract route name, handling both string and State object inputs
72
+ // State.name is always string by real-router type definition
73
+ const name = typeof route === "string" ? route : route.name;
74
+
75
+ if (typeof name !== "string") {
76
+ return false;
77
+ }
78
+
79
+ // Empty route name always returns false
80
+ if (name.length === 0) {
81
+ return false;
82
+ }
83
+
84
+ // null always returns false (consistent behavior)
85
+ if (segment === null) {
86
+ return false;
87
+ }
88
+
89
+ // Currying: if no segment provided, return a tester function
90
+ if (segment === undefined) {
91
+ return (localSegment: string) => {
92
+ // Type check for runtime safety (consistent with direct call)
93
+ if (typeof localSegment !== "string") {
94
+ throw new TypeError(
95
+ `Segment must be a string, got ${typeof localSegment}`,
96
+ );
97
+ }
98
+
99
+ // Empty string returns false (consistent with direct call)
100
+ if (localSegment.length === 0) {
101
+ return false;
102
+ }
103
+
104
+ // Use buildRegex (type and empty checks already done above)
105
+ return buildRegex(localSegment).test(name);
106
+ };
107
+ }
108
+
109
+ if (typeof segment !== "string") {
110
+ // Runtime protection: TypeScript already narrows to 'string' here,
111
+ // but we keep this check for defense against unexpected runtime values
112
+ throw new TypeError(`Segment must be a string, got ${typeof segment}`);
113
+ }
114
+
115
+ // Empty string returns false (consistent behavior)
116
+ if (segment.length === 0) {
117
+ return false;
118
+ }
119
+
120
+ // Perform the actual regex test
121
+ // buildRegex skips type and empty checks (already validated above)
122
+ return buildRegex(segment).test(name);
123
+ };
124
+ };
125
+
126
+ /**
127
+ * Pattern that matches either a dot separator or end of string.
128
+ * Used for prefix/suffix matching that respects segment boundaries.
129
+ */
130
+ const dotOrEnd = `(?:${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)}|$)`;
131
+
132
+ /**
133
+ * Tests if a route name starts with the given segment.
134
+ *
135
+ * Supports both direct calls and curried form for flexible usage patterns.
136
+ * All segments are validated for safety (length and character constraints).
137
+ *
138
+ * @param route - Route state object or route name string
139
+ * @param segment - Segment to test. If omitted, returns a tester function.
140
+ *
141
+ * @returns
142
+ * - `boolean` if segment is provided (true if route starts with segment)
143
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
144
+ * - `false` if segment is null or empty string
145
+ *
146
+ * @example
147
+ * // Direct call
148
+ * startsWithSegment('users.list', 'users'); // true
149
+ * startsWithSegment('users.list', 'admin'); // false
150
+ * startsWithSegment('admin.panel', 'users'); // false
151
+ *
152
+ * @example
153
+ * // Curried form
154
+ * const tester = startsWithSegment('users.list');
155
+ * tester('users'); // true
156
+ * tester('users.list'); // true
157
+ * tester('admin'); // false
158
+ *
159
+ * @example
160
+ * // With State object
161
+ * const state: State = { name: 'users.list', params: {}, path: '/users/list' };
162
+ * startsWithSegment(state, 'users'); // true
163
+ *
164
+ * @example
165
+ * // Edge cases
166
+ * startsWithSegment('users', ''); // false
167
+ * startsWithSegment('users', null); // false
168
+ * startsWithSegment('', 'users'); // false
169
+ *
170
+ * @throws {TypeError} If segment contains invalid characters or is not a string
171
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
172
+ *
173
+ * @remarks
174
+ * **Validation rules:**
175
+ * - Allowed characters: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)
176
+ * - Maximum segment length: 10,000 characters
177
+ * - Empty segments are rejected
178
+ *
179
+ * **Performance:**
180
+ * - No caching (RegExp compiled on each call)
181
+ * - For high-frequency checks, consider caching results at application level
182
+ *
183
+ * **Segment boundaries:**
184
+ * - Respects dot-separated segments
185
+ * - 'users' matches 'users' and 'users.list'
186
+ * - 'users' does NOT match 'users2' or 'admin.users'
187
+ *
188
+ * @see endsWithSegment for suffix matching
189
+ * @see includesSegment for anywhere matching
190
+ */
191
+ export const startsWithSegment = makeSegmentTester(
192
+ "^",
193
+ dotOrEnd,
194
+ ) as SegmentTestFunction;
195
+
196
+ /**
197
+ * Tests if a route name ends with the given segment.
198
+ *
199
+ * Supports both direct calls and curried form for flexible usage patterns.
200
+ * All segments are validated for safety (length and character constraints).
201
+ *
202
+ * @param route - Route state object or route name string
203
+ * @param segment - Segment to test. If omitted, returns a tester function.
204
+ *
205
+ * @returns
206
+ * - `boolean` if segment is provided (true if route ends with segment)
207
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
208
+ * - `false` if segment is null or empty string
209
+ *
210
+ * @example
211
+ * // Direct call
212
+ * endsWithSegment('users.list', 'list'); // true
213
+ * endsWithSegment('users.profile.edit', 'edit'); // true
214
+ * endsWithSegment('users.list', 'users'); // false
215
+ *
216
+ * @example
217
+ * // Multi-segment suffix
218
+ * endsWithSegment('a.b.c.d', 'c.d'); // true
219
+ * endsWithSegment('a.b.c.d', 'b.c'); // false
220
+ *
221
+ * @example
222
+ * // Curried form
223
+ * const tester = endsWithSegment('users.list');
224
+ * tester('list'); // true
225
+ * tester('users'); // false
226
+ *
227
+ * @throws {TypeError} If segment contains invalid characters or is not a string
228
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
229
+ *
230
+ * @remarks
231
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
232
+ *
233
+ * @see startsWithSegment for prefix matching
234
+ * @see includesSegment for anywhere matching
235
+ */
236
+ export const endsWithSegment = makeSegmentTester(
237
+ `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,
238
+ "$",
239
+ ) as SegmentTestFunction;
240
+
241
+ /**
242
+ * Tests if a route name includes the given segment anywhere in its path.
243
+ *
244
+ * Supports both direct calls and curried form for flexible usage patterns.
245
+ * All segments are validated for safety (length and character constraints).
246
+ *
247
+ * @param route - Route state object or route name string
248
+ * @param segment - Segment to test. If omitted, returns a tester function.
249
+ *
250
+ * @returns
251
+ * - `boolean` if segment is provided (true if route includes segment)
252
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
253
+ * - `false` if segment is null or empty string
254
+ *
255
+ * @example
256
+ * // Direct call
257
+ * includesSegment('users.profile.edit', 'profile'); // true
258
+ * includesSegment('users.profile.edit', 'users'); // true
259
+ * includesSegment('users.profile.edit', 'edit'); // true
260
+ * includesSegment('users.profile.edit', 'admin'); // false
261
+ *
262
+ * @example
263
+ * // Multi-segment inclusion
264
+ * includesSegment('a.b.c.d', 'b.c'); // true
265
+ * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)
266
+ *
267
+ * @example
268
+ * // Curried form
269
+ * const tester = includesSegment('users.profile.edit');
270
+ * tester('profile'); // true
271
+ * tester('admin'); // false
272
+ *
273
+ * @throws {TypeError} If segment contains invalid characters or is not a string
274
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
275
+ *
276
+ * @remarks
277
+ * **Important:** Segment must be contiguous in the route path.
278
+ * - 'a.b.c' includes 'a.b' and 'b.c' but NOT 'a.c'
279
+ *
280
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
281
+ *
282
+ * @see startsWithSegment for prefix matching
283
+ * @see endsWithSegment for suffix matching
284
+ */
285
+ export const includesSegment = makeSegmentTester(
286
+ `(?:^|${escapeRegExp(ROUTE_SEGMENT_SEPARATOR)})`,
287
+ dotOrEnd,
288
+ ) as SegmentTestFunction;
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // packages/helpers/modules/index.ts
2
+
3
+ export { startsWithSegment, endsWithSegment, includesSegment } from "./helpers";
4
+
5
+ export { areRoutesRelated } from "./routeRelation";
6
+
7
+ export type { SegmentTestFunction } from "./types";
@@ -0,0 +1,28 @@
1
+ // packages/helpers/modules/routeRelation.ts
2
+
3
+ /**
4
+ * Checks if two routes are related in the hierarchy.
5
+ *
6
+ * Routes are related if:
7
+ * - They are exactly the same
8
+ * - One is a parent of the other (e.g., "users" and "users.list")
9
+ * - One is a child of the other (e.g., "users.list" and "users")
10
+ *
11
+ * @param route1 - First route name
12
+ * @param route2 - Second route name
13
+ * @returns True if routes are related, false otherwise
14
+ *
15
+ * @example
16
+ * areRoutesRelated("users", "users.list"); // true (parent-child)
17
+ * areRoutesRelated("users.list", "users"); // true (child-parent)
18
+ * areRoutesRelated("users", "users"); // true (same)
19
+ * areRoutesRelated("users", "admin"); // false (different branches)
20
+ * areRoutesRelated("users.list", "users.view"); // false (siblings)
21
+ */
22
+ export function areRoutesRelated(route1: string, route2: string): boolean {
23
+ return (
24
+ route1 === route2 ||
25
+ route1.startsWith(`${route2}.`) ||
26
+ route2.startsWith(`${route1}.`)
27
+ );
28
+ }
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ // packages/helpers/modules/types.ts
2
+
3
+ import type { State } from "@real-router/core";
4
+
5
+ /**
6
+ * Type definition for segment test functions.
7
+ * These functions can be called directly with a segment, or curried for later use.
8
+ */
9
+ export interface SegmentTestFunction {
10
+ (route: State | string): (segment: string) => boolean;
11
+ (route: State | string, segment: string): boolean;
12
+ (route: State | string, segment: null): false;
13
+ (
14
+ route: State | string,
15
+ segment?: string | null,
16
+ ): boolean | ((segment: string) => boolean);
17
+ }