@real-router/helpers 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Oleg Ivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,575 @@
1
+ # @real-router/helpers
2
+
3
+ [![npm version](https://badge.fury.io/js/@real-router%2Fhelpers.svg)](https://www.npmjs.com/package/@real-router/helpers)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
6
+
7
+ > High-performance route segment testing utilities for Real-Router
8
+
9
+ ## Overview
10
+
11
+ `@real-router/helpers` provides efficient utilities for testing route name segments in Real-Router applications. These helpers are essential for:
12
+
13
+ - Conditional rendering based on route hierarchy
14
+ - Active navigation menu items
15
+ - Breadcrumb construction
16
+ - Route-based access guards
17
+ - UI component visibility logic
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @real-router/helpers
23
+ # or
24
+ pnpm add @real-router/helpers
25
+ # or
26
+ yarn add @real-router/helpers
27
+ # or
28
+ bun add @real-router/helpers
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import {
35
+ startsWithSegment,
36
+ endsWithSegment,
37
+ includesSegment,
38
+ areRoutesRelated,
39
+ } from "@real-router/helpers";
40
+
41
+ // Check if route starts with segment
42
+ startsWithSegment("users.profile.edit", "users"); // true
43
+ startsWithSegment("admin.dashboard", "users"); // false
44
+
45
+ // Check if route ends with segment
46
+ endsWithSegment("users.profile.edit", "edit"); // true
47
+ endsWithSegment("users.profile.edit", "view"); // false
48
+
49
+ // Check if route includes segment anywhere
50
+ includesSegment("users.profile.edit", "profile"); // true
51
+ includesSegment("users.profile.edit", "admin"); // false
52
+
53
+ // Check if routes are related (parent-child or same)
54
+ areRoutesRelated("users", "users.list"); // true
55
+ areRoutesRelated("users", "admin"); // false
56
+ ```
57
+
58
+ ## API Reference
59
+
60
+ ### `startsWithSegment(route, segment?)`
61
+
62
+ Tests if a route name starts with the given segment.
63
+
64
+ **Parameters:**
65
+
66
+ - `route: State | string` - Route state object or route name string
67
+ - `segment?: string | null` - Segment to test (optional for curried form)
68
+
69
+ **Returns:**
70
+
71
+ - `boolean` - True if route starts with segment
72
+ - `(segment: string) => boolean` - Tester function if segment omitted
73
+ - `false` - If segment is null or empty string
74
+
75
+ **Examples:**
76
+
77
+ ```typescript
78
+ // Direct usage
79
+ startsWithSegment("users.list", "users"); // true
80
+ startsWithSegment("users.list", "admin"); // false
81
+
82
+ // Multi-segment matching
83
+ startsWithSegment("users.profile.edit", "users.profile"); // true
84
+
85
+ // With State object
86
+ const state = { name: "users.list", params: {}, path: "/users" };
87
+ startsWithSegment(state, "users"); // true
88
+
89
+ // Curried form
90
+ const tester = startsWithSegment("users.profile.edit");
91
+ tester("users"); // true
92
+ tester("admin"); // false
93
+
94
+ // Edge cases
95
+ startsWithSegment("users", ""); // false
96
+ startsWithSegment("users", null); // false
97
+ ```
98
+
99
+ **Use Cases:**
100
+
101
+ ```typescript
102
+ // Active navigation menu
103
+ const isAdminSection = startsWithSegment(currentRoute, 'admin');
104
+
105
+ // Conditional rendering
106
+ {startsWithSegment(route, 'users') && <UsersToolbar />}
107
+
108
+ // Access guards
109
+ if (!startsWithSegment(route, 'admin')) {
110
+ throw new UnauthorizedError();
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ### `endsWithSegment(route, segment?)`
117
+
118
+ Tests if a route name ends with the given segment.
119
+
120
+ **Parameters:**
121
+
122
+ - `route: State | string` - Route state object or route name string
123
+ - `segment?: string | null` - Segment to test (optional for curried form)
124
+
125
+ **Returns:**
126
+
127
+ - `boolean` - True if route ends with segment
128
+ - `(segment: string) => boolean` - Tester function if segment omitted
129
+ - `false` - If segment is null or empty string
130
+
131
+ **Examples:**
132
+
133
+ ```typescript
134
+ // Direct usage
135
+ endsWithSegment("users.profile.edit", "edit"); // true
136
+ endsWithSegment("users.profile.edit", "view"); // false
137
+
138
+ // Multi-segment matching
139
+ endsWithSegment("a.b.c.d", "c.d"); // true
140
+
141
+ // Curried form
142
+ const tester = endsWithSegment("users.list");
143
+ tester("list"); // true
144
+ tester("edit"); // false
145
+ ```
146
+
147
+ **Use Cases:**
148
+
149
+ ```typescript
150
+ // Detect edit pages
151
+ const isEditPage = endsWithSegment(route, 'edit');
152
+
153
+ // Page type detection
154
+ const pageType = ['view', 'edit', 'create'].find(
155
+ type => endsWithSegment(route, type)
156
+ );
157
+
158
+ // Conditional toolbar
159
+ {endsWithSegment(route, 'edit') && <EditToolbar />}
160
+ ```
161
+
162
+ ---
163
+
164
+ ### `includesSegment(route, segment?)`
165
+
166
+ Tests if a route name includes the given segment anywhere in its path.
167
+
168
+ **Parameters:**
169
+
170
+ - `route: State | string` - Route state object or route name string
171
+ - `segment?: string | null` - Segment to test (optional for curried form)
172
+
173
+ **Returns:**
174
+
175
+ - `boolean` - True if route includes segment
176
+ - `(segment: string) => boolean` - Tester function if segment omitted
177
+ - `false` - If segment is null or empty string
178
+
179
+ **Examples:**
180
+
181
+ ```typescript
182
+ // Direct usage
183
+ includesSegment("admin.users.profile", "users"); // true
184
+ includesSegment("admin.users.profile", "profile"); // true
185
+ includesSegment("admin.users.profile", "settings"); // false
186
+
187
+ // Multi-segment matching (contiguous)
188
+ includesSegment("a.b.c.d", "b.c"); // true
189
+ includesSegment("a.b.c.d", "a.c"); // false (not contiguous)
190
+
191
+ // Curried form
192
+ const tester = includesSegment("admin.users.profile");
193
+ tester("users"); // true
194
+ tester("settings"); // false
195
+ ```
196
+
197
+ **Use Cases:**
198
+
199
+ ```typescript
200
+ // Check if anywhere in users section
201
+ const inUsersSection = includesSegment(route, "users");
202
+
203
+ // Feature detection
204
+ const hasProfileFeature = includesSegment(route, "profile");
205
+
206
+ // Analytics tracking
207
+ if (includesSegment(route, "checkout")) {
208
+ analytics.track("checkout_flow");
209
+ }
210
+ ```
211
+
212
+ ---
213
+
214
+ ### `areRoutesRelated(route1, route2)`
215
+
216
+ Tests if two routes are related in the hierarchy (same, parent-child, or child-parent).
217
+
218
+ **Parameters:**
219
+
220
+ - `route1: string` - First route name
221
+ - `route2: string` - Second route name
222
+
223
+ **Returns:**
224
+
225
+ - `boolean` - True if routes are related
226
+
227
+ **Examples:**
228
+
229
+ ```typescript
230
+ // Same route
231
+ areRoutesRelated("users", "users"); // true
232
+
233
+ // Parent-child relationship
234
+ areRoutesRelated("users", "users.list"); // true
235
+ areRoutesRelated("users", "users.profile.edit"); // true
236
+
237
+ // Child-parent relationship
238
+ areRoutesRelated("users.list", "users"); // true
239
+ areRoutesRelated("users.profile.edit", "users"); // true
240
+
241
+ // Different branches (not related)
242
+ areRoutesRelated("users", "admin"); // false
243
+ areRoutesRelated("users.list", "admin.dashboard"); // false
244
+
245
+ // Siblings (not related)
246
+ areRoutesRelated("users.list", "users.view"); // false
247
+ areRoutesRelated("users.profile", "users.settings"); // false
248
+ ```
249
+
250
+ **Use Cases:**
251
+
252
+ ```typescript
253
+ // Optimized re-rendering (only update when route is related)
254
+ const shouldUpdate = areRoutesRelated(newRoute, watchedRoute);
255
+
256
+ // Navigation menu highlighting
257
+ const isInSection = areRoutesRelated(currentRoute, "admin");
258
+
259
+ // Breadcrumb visibility
260
+ const showBreadcrumb = areRoutesRelated(currentRoute, basePath);
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Real-World Examples
266
+
267
+ ### Active Navigation Menu
268
+
269
+ ```typescript
270
+ import { startsWithSegment } from '@real-router/helpers';
271
+
272
+ function NavigationMenu({ currentRoute }) {
273
+ const menuItems = [
274
+ { name: 'Dashboard', route: 'dashboard' },
275
+ { name: 'Users', route: 'users' },
276
+ { name: 'Settings', route: 'settings' },
277
+ ];
278
+
279
+ return (
280
+ <nav>
281
+ {menuItems.map(item => (
282
+ <MenuItem
283
+ key={item.route}
284
+ active={startsWithSegment(currentRoute, item.route)}
285
+ >
286
+ {item.name}
287
+ </MenuItem>
288
+ ))}
289
+ </nav>
290
+ );
291
+ }
292
+ ```
293
+
294
+ ### Breadcrumbs
295
+
296
+ ```typescript
297
+ import { startsWithSegment } from '@real-router/helpers';
298
+
299
+ function Breadcrumbs({ currentRoute }) {
300
+ const segments = currentRoute.split('.');
301
+ const breadcrumbs = segments.reduce((acc, segment, index) => {
302
+ const path = segments.slice(0, index + 1).join('.');
303
+ return [...acc, {
304
+ path,
305
+ label: segment,
306
+ isActive: path === currentRoute,
307
+ }];
308
+ }, []);
309
+
310
+ return (
311
+ <nav>
312
+ {breadcrumbs.map(crumb => (
313
+ <Breadcrumb
314
+ key={crumb.path}
315
+ active={crumb.isActive}
316
+ >
317
+ {crumb.label}
318
+ </Breadcrumb>
319
+ ))}
320
+ </nav>
321
+ );
322
+ }
323
+ ```
324
+
325
+ ### Route Guards
326
+
327
+ ```typescript
328
+ import { startsWithSegment } from "@real-router/helpers";
329
+
330
+ const adminGuard = (router) => (toState, fromState, done) => {
331
+ if (startsWithSegment(toState, "admin") && !userHasAdminRole()) {
332
+ done({ redirect: { name: "unauthorized" } });
333
+ } else {
334
+ done();
335
+ }
336
+ };
337
+
338
+ router.useMiddleware(adminGuard);
339
+ ```
340
+
341
+ ### Conditional Rendering
342
+
343
+ ```typescript
344
+ import { startsWithSegment, includesSegment, endsWithSegment } from '@real-router/helpers';
345
+
346
+ function PageLayout({ route, children }) {
347
+ return (
348
+ <div>
349
+ {/* Show admin sidebar only in admin section */}
350
+ {startsWithSegment(route, 'admin') && <AdminSidebar />}
351
+
352
+ {/* Show edit toolbar on edit pages */}
353
+ {endsWithSegment(route, 'edit') && <EditToolbar />}
354
+
355
+ {/* Show user profile widget in user-related pages */}
356
+ {includesSegment(route, 'users') && <UserProfileWidget />}
357
+
358
+ <main>{children}</main>
359
+ </div>
360
+ );
361
+ }
362
+ ```
363
+
364
+ ### Analytics Tracking
365
+
366
+ ```typescript
367
+ import { startsWithSegment, includesSegment } from "@real-router/helpers";
368
+
369
+ router.subscribe((state) => {
370
+ const routeName = state.route.name;
371
+
372
+ // Track section
373
+ if (startsWithSegment(routeName, "admin")) {
374
+ analytics.track("admin_section_view");
375
+ }
376
+
377
+ // Track feature usage
378
+ if (includesSegment(routeName, "checkout")) {
379
+ analytics.track("checkout_flow_step", {
380
+ step: routeName.split(".").pop(),
381
+ });
382
+ }
383
+ });
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Validation & Security
389
+
390
+ ### Automatic Validation
391
+
392
+ All segment inputs are automatically validated for security and correctness:
393
+
394
+ **Character Whitelist:**
395
+
396
+ - Allowed: `a-z`, `A-Z`, `0-9`, `.` (dot), `-` (dash), `_` (underscore)
397
+ - Disallowed: Special characters, spaces, slashes, etc.
398
+
399
+ **Length Limits:**
400
+
401
+ - Maximum segment length: 10,000 characters
402
+ - Empty segments are rejected
403
+
404
+ **Examples:**
405
+
406
+ ```typescript
407
+ // Valid segments
408
+ startsWithSegment("route", "users"); // OK
409
+ startsWithSegment("route", "admin-panel"); // OK
410
+ startsWithSegment("route", "users_v2"); // OK
411
+ startsWithSegment("route", "app.settings"); // OK
412
+
413
+ // Invalid segments (throw TypeError)
414
+ startsWithSegment("route", "invalid!char"); // Throws
415
+ startsWithSegment("route", "has space"); // Throws
416
+ startsWithSegment("route", "path/segment"); // Throws
417
+ startsWithSegment("route", "ns:segment"); // Throws
418
+
419
+ // Too long (throw RangeError)
420
+ startsWithSegment("route", "a".repeat(10001)); // Throws
421
+
422
+ // Empty (returns false)
423
+ startsWithSegment("route", ""); // false
424
+ startsWithSegment("route", null); // false
425
+ ```
426
+
427
+ ### Error Handling
428
+
429
+ ```typescript
430
+ try {
431
+ startsWithSegment("route", "invalid!segment");
432
+ } catch (error) {
433
+ if (error instanceof TypeError) {
434
+ console.error("Invalid segment:", error.message);
435
+ // "Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)"
436
+ }
437
+ }
438
+
439
+ try {
440
+ startsWithSegment("route", "a".repeat(10001));
441
+ } catch (error) {
442
+ if (error instanceof RangeError) {
443
+ console.error("Segment too long:", error.message);
444
+ // "Segment exceeds maximum length of 10000 characters"
445
+ }
446
+ }
447
+ ```
448
+
449
+ ---
450
+
451
+ ## TypeScript Support
452
+
453
+ Full TypeScript support with type definitions included.
454
+
455
+ ```typescript
456
+ import type { State } from "@real-router/core";
457
+ import { startsWithSegment } from "@real-router/helpers";
458
+
459
+ // Type inference works correctly
460
+ const result: boolean = startsWithSegment("route", "segment");
461
+
462
+ // Curried form also typed
463
+ const tester: (segment: string) => boolean = startsWithSegment("route");
464
+
465
+ // Works with State objects
466
+ const state: State = { name: "route", params: {}, path: "/route" };
467
+ const matches: boolean = startsWithSegment(state, "segment");
468
+ ```
469
+
470
+ ### Type Definitions
471
+
472
+ ```typescript
473
+ export interface SegmentTestFunction {
474
+ (route: State | string): (segment: string) => boolean;
475
+ (route: State | string, segment: string): boolean;
476
+ (route: State | string, segment: null): false;
477
+ (
478
+ route: State | string,
479
+ segment?: string | null,
480
+ ): boolean | ((segment: string) => boolean);
481
+ }
482
+ ```
483
+
484
+ ---
485
+
486
+ ## Documentation
487
+
488
+ Full documentation available on the [Real-Router Wiki](https://github.com/greydragon888/real-router/wiki):
489
+
490
+ - [startsWithSegment](https://github.com/greydragon888/real-router/wiki/startsWithSegment)
491
+ - [endsWithSegment](https://github.com/greydragon888/real-router/wiki/endsWithSegment)
492
+ - [includesSegment](https://github.com/greydragon888/real-router/wiki/includesSegment)
493
+ - [areRoutesRelated](https://github.com/greydragon888/real-router/wiki/areRoutesRelated)
494
+
495
+ ---
496
+
497
+ ## Related Packages
498
+
499
+ - [@real-router/core](https://www.npmjs.com/package/@real-router/core) - Core router
500
+ - [@real-router/react](https://www.npmjs.com/package/@real-router/react) - React integration
501
+ - [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) - Browser integration
502
+
503
+ ---
504
+
505
+ ## Migration from router5-helpers
506
+
507
+ ### Import Changes
508
+
509
+ ```diff
510
+ - import { startsWithSegment, endsWithSegment, includesSegment } from 'router5-helpers';
511
+ + import { startsWithSegment, endsWithSegment, includesSegment } from '@real-router/helpers';
512
+ ```
513
+
514
+ ### Removed: `redirect`
515
+
516
+ The `redirect` helper has been removed. Use router guards instead:
517
+
518
+ ```diff
519
+ - import { redirect } from 'router5-helpers';
520
+ -
521
+ - router.canActivate('protected', () => redirect('login'));
522
+ + router.canActivate('protected', (toState, fromState, done) => {
523
+ + done({ redirect: { name: 'login' } });
524
+ + });
525
+ ```
526
+
527
+ ### New: `areRoutesRelated`
528
+
529
+ New helper function for checking route hierarchy:
530
+
531
+ ```typescript
532
+ import { areRoutesRelated } from "@real-router/helpers";
533
+
534
+ areRoutesRelated("users", "users.profile"); // true (parent-child)
535
+ areRoutesRelated("users.profile", "users"); // true (child-parent)
536
+ areRoutesRelated("users", "admin"); // false (different branches)
537
+ ```
538
+
539
+ ### New: Input Validation
540
+
541
+ @real-router/helpers validates segment inputs for security:
542
+
543
+ ```typescript
544
+ // Valid segments
545
+ startsWithSegment("route", "users");
546
+ startsWithSegment("route", "admin-panel");
547
+
548
+ // Throws TypeError (invalid characters)
549
+ startsWithSegment("route", "invalid!char");
550
+ startsWithSegment("route", "has space");
551
+ ```
552
+
553
+ ### Full Migration Example
554
+
555
+ ```diff
556
+ - import { startsWithSegment, redirect } from 'router5-helpers';
557
+ + import { startsWithSegment } from '@real-router/helpers';
558
+
559
+ // Segment testing - unchanged
560
+ if (startsWithSegment(route, 'admin')) {
561
+ // ...
562
+ }
563
+
564
+ // Redirects - use guards instead
565
+ - router.canActivate('old-page', () => redirect('new-page'));
566
+ + router.canActivate('old-page', (toState, fromState, done) => {
567
+ + done({ redirect: { name: 'new-page' } });
568
+ + });
569
+ ```
570
+
571
+ ---
572
+
573
+ ## License
574
+
575
+ MIT © [Oleg Ivanov](https://github.com/greydragon888)
@@ -0,0 +1,182 @@
1
+ import { State } from '@real-router/core';
2
+
3
+ /**
4
+ * Type definition for segment test functions.
5
+ * These functions can be called directly with a segment, or curried for later use.
6
+ */
7
+ interface SegmentTestFunction {
8
+ (route: State | string): (segment: string) => boolean;
9
+ (route: State | string, segment: string): boolean;
10
+ (route: State | string, segment: null): false;
11
+ (route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
12
+ }
13
+
14
+ /**
15
+ * Tests if a route name starts with the given segment.
16
+ *
17
+ * Supports both direct calls and curried form for flexible usage patterns.
18
+ * All segments are validated for safety (length and character constraints).
19
+ *
20
+ * @param route - Route state object or route name string
21
+ * @param segment - Segment to test. If omitted, returns a tester function.
22
+ *
23
+ * @returns
24
+ * - `boolean` if segment is provided (true if route starts with segment)
25
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
26
+ * - `false` if segment is null or empty string
27
+ *
28
+ * @example
29
+ * // Direct call
30
+ * startsWithSegment('users.list', 'users'); // true
31
+ * startsWithSegment('users.list', 'admin'); // false
32
+ * startsWithSegment('admin.panel', 'users'); // false
33
+ *
34
+ * @example
35
+ * // Curried form
36
+ * const tester = startsWithSegment('users.list');
37
+ * tester('users'); // true
38
+ * tester('users.list'); // true
39
+ * tester('admin'); // false
40
+ *
41
+ * @example
42
+ * // With State object
43
+ * const state: State = { name: 'users.list', params: {}, path: '/users/list' };
44
+ * startsWithSegment(state, 'users'); // true
45
+ *
46
+ * @example
47
+ * // Edge cases
48
+ * startsWithSegment('users', ''); // false
49
+ * startsWithSegment('users', null); // false
50
+ * startsWithSegment('', 'users'); // false
51
+ *
52
+ * @throws {TypeError} If segment contains invalid characters or is not a string
53
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
54
+ *
55
+ * @remarks
56
+ * **Validation rules:**
57
+ * - Allowed characters: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)
58
+ * - Maximum segment length: 10,000 characters
59
+ * - Empty segments are rejected
60
+ *
61
+ * **Performance:**
62
+ * - No caching (RegExp compiled on each call)
63
+ * - For high-frequency checks, consider caching results at application level
64
+ *
65
+ * **Segment boundaries:**
66
+ * - Respects dot-separated segments
67
+ * - 'users' matches 'users' and 'users.list'
68
+ * - 'users' does NOT match 'users2' or 'admin.users'
69
+ *
70
+ * @see endsWithSegment for suffix matching
71
+ * @see includesSegment for anywhere matching
72
+ */
73
+ declare const startsWithSegment: SegmentTestFunction;
74
+ /**
75
+ * Tests if a route name ends with the given segment.
76
+ *
77
+ * Supports both direct calls and curried form for flexible usage patterns.
78
+ * All segments are validated for safety (length and character constraints).
79
+ *
80
+ * @param route - Route state object or route name string
81
+ * @param segment - Segment to test. If omitted, returns a tester function.
82
+ *
83
+ * @returns
84
+ * - `boolean` if segment is provided (true if route ends with segment)
85
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
86
+ * - `false` if segment is null or empty string
87
+ *
88
+ * @example
89
+ * // Direct call
90
+ * endsWithSegment('users.list', 'list'); // true
91
+ * endsWithSegment('users.profile.edit', 'edit'); // true
92
+ * endsWithSegment('users.list', 'users'); // false
93
+ *
94
+ * @example
95
+ * // Multi-segment suffix
96
+ * endsWithSegment('a.b.c.d', 'c.d'); // true
97
+ * endsWithSegment('a.b.c.d', 'b.c'); // false
98
+ *
99
+ * @example
100
+ * // Curried form
101
+ * const tester = endsWithSegment('users.list');
102
+ * tester('list'); // true
103
+ * tester('users'); // false
104
+ *
105
+ * @throws {TypeError} If segment contains invalid characters or is not a string
106
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
107
+ *
108
+ * @remarks
109
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
110
+ *
111
+ * @see startsWithSegment for prefix matching
112
+ * @see includesSegment for anywhere matching
113
+ */
114
+ declare const endsWithSegment: SegmentTestFunction;
115
+ /**
116
+ * Tests if a route name includes the given segment anywhere in its path.
117
+ *
118
+ * Supports both direct calls and curried form for flexible usage patterns.
119
+ * All segments are validated for safety (length and character constraints).
120
+ *
121
+ * @param route - Route state object or route name string
122
+ * @param segment - Segment to test. If omitted, returns a tester function.
123
+ *
124
+ * @returns
125
+ * - `boolean` if segment is provided (true if route includes segment)
126
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
127
+ * - `false` if segment is null or empty string
128
+ *
129
+ * @example
130
+ * // Direct call
131
+ * includesSegment('users.profile.edit', 'profile'); // true
132
+ * includesSegment('users.profile.edit', 'users'); // true
133
+ * includesSegment('users.profile.edit', 'edit'); // true
134
+ * includesSegment('users.profile.edit', 'admin'); // false
135
+ *
136
+ * @example
137
+ * // Multi-segment inclusion
138
+ * includesSegment('a.b.c.d', 'b.c'); // true
139
+ * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)
140
+ *
141
+ * @example
142
+ * // Curried form
143
+ * const tester = includesSegment('users.profile.edit');
144
+ * tester('profile'); // true
145
+ * tester('admin'); // false
146
+ *
147
+ * @throws {TypeError} If segment contains invalid characters or is not a string
148
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
149
+ *
150
+ * @remarks
151
+ * **Important:** Segment must be contiguous in the route path.
152
+ * - 'a.b.c' includes 'a.b' and 'b.c' but NOT 'a.c'
153
+ *
154
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
155
+ *
156
+ * @see startsWithSegment for prefix matching
157
+ * @see endsWithSegment for suffix matching
158
+ */
159
+ declare const includesSegment: SegmentTestFunction;
160
+
161
+ /**
162
+ * Checks if two routes are related in the hierarchy.
163
+ *
164
+ * Routes are related if:
165
+ * - They are exactly the same
166
+ * - One is a parent of the other (e.g., "users" and "users.list")
167
+ * - One is a child of the other (e.g., "users.list" and "users")
168
+ *
169
+ * @param route1 - First route name
170
+ * @param route2 - Second route name
171
+ * @returns True if routes are related, false otherwise
172
+ *
173
+ * @example
174
+ * areRoutesRelated("users", "users.list"); // true (parent-child)
175
+ * areRoutesRelated("users.list", "users"); // true (child-parent)
176
+ * areRoutesRelated("users", "users"); // true (same)
177
+ * areRoutesRelated("users", "admin"); // false (different branches)
178
+ * areRoutesRelated("users.list", "users.view"); // false (siblings)
179
+ */
180
+ declare function areRoutesRelated(route1: string, route2: string): boolean;
181
+
182
+ export { type SegmentTestFunction, areRoutesRelated, endsWithSegment, includesSegment, startsWithSegment };
@@ -0,0 +1 @@
1
+ var e=/^[\w.-]+$/,t=e=>e.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`),r=(r,n)=>{const s=s=>{if(s.length>1e4)throw new RangeError("Segment exceeds maximum length of 10000 characters");if(!e.test(s))throw new TypeError("Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)");return new RegExp(r+t(s)+n)};return(e,t)=>{const r="string"==typeof e?e:e.name;if("string"!=typeof r)return!1;if(0===r.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&&s(e).test(r)};if("string"!=typeof t)throw new TypeError("Segment must be a string, got "+typeof t);return 0!==t.length&&s(t).test(r)}},n=`(?:${t(".")}|$)`,s=r("^",n),o=r(`(?:^|${t(".")})`,"$"),i=r(`(?:^|${t(".")})`,n);exports.areRoutesRelated=function(e,t){return e===t||e.startsWith(`${t}.`)||t.startsWith(`${e}.`)},exports.endsWithSegment=o,exports.includesSegment=i,exports.startsWithSegment=s;//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/helpers.ts","../../src/routeRelation.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// packages/helpers/modules/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/helpers/modules/index.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/core\";\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 /**\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 // 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 return new RegExp(start + escapeRegExp(segment) + end);\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 * startsWithSegment('admin.panel', 'users'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('users.list'); // 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 * @example\n * // Edge cases\n * startsWithSegment('users', ''); // false\n * startsWithSegment('users', null); // false\n * startsWithSegment('', '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 * @remarks\n * **Validation rules:**\n * - Allowed characters: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)\n * - Maximum segment length: 10,000 characters\n * - Empty segments are rejected\n *\n * **Performance:**\n * - No caching (RegExp compiled on each call)\n * - For high-frequency checks, consider caching results at application level\n *\n * **Segment boundaries:**\n * - Respects dot-separated segments\n * - 'users' matches 'users' and 'users.list'\n * - 'users' does NOT match 'users2' or 'admin.users'\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 * endsWithSegment('users.list', 'users'); // false\n *\n * @example\n * // Multi-segment suffix\n * endsWithSegment('a.b.c.d', 'c.d'); // true\n * endsWithSegment('a.b.c.d', 'b.c'); // false\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 * @remarks\n * See {@link startsWithSegment} for detailed validation rules and performance notes.\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 * includesSegment('users.profile.edit', 'users'); // true\n * includesSegment('users.profile.edit', 'edit'); // true\n * includesSegment('users.profile.edit', 'admin'); // false\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 * @example\n * // Curried form\n * const tester = includesSegment('users.profile.edit');\n * tester('profile'); // true\n * tester('admin'); // 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 * @remarks\n * **Important:** Segment must be contiguous in the route path.\n * - 'a.b.c' includes 'a.b' and 'b.c' but NOT 'a.c'\n *\n * See {@link startsWithSegment} for detailed validation rules and performance notes.\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","// packages/helpers/modules/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"]}
@@ -0,0 +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/constants.ts":{"bytes":515,"imports":[{"path":"/Users/olegivanov/WebstormProjects/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/helpers.ts":{"bytes":9955,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/Users/olegivanov/WebstormProjects/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/routeRelation.ts":{"bytes":994,"imports":[{"path":"/Users/olegivanov/WebstormProjects/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":225,"imports":[{"path":"src/helpers.ts","kind":"import-statement","original":"./helpers"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation"},{"path":"/Users/olegivanov/WebstormProjects/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":13383},"dist/cjs/index.js":{"imports":[],"exports":["areRoutesRelated","endsWithSegment","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/constants.ts":{"bytesInOutput":105},"src/helpers.ts":{"bytesInOutput":1800},"src/index.ts":{"bytesInOutput":0},"src/routeRelation.ts":{"bytesInOutput":144}},"bytes":2203}}}
@@ -0,0 +1,182 @@
1
+ import { State } from '@real-router/core';
2
+
3
+ /**
4
+ * Type definition for segment test functions.
5
+ * These functions can be called directly with a segment, or curried for later use.
6
+ */
7
+ interface SegmentTestFunction {
8
+ (route: State | string): (segment: string) => boolean;
9
+ (route: State | string, segment: string): boolean;
10
+ (route: State | string, segment: null): false;
11
+ (route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
12
+ }
13
+
14
+ /**
15
+ * Tests if a route name starts with the given segment.
16
+ *
17
+ * Supports both direct calls and curried form for flexible usage patterns.
18
+ * All segments are validated for safety (length and character constraints).
19
+ *
20
+ * @param route - Route state object or route name string
21
+ * @param segment - Segment to test. If omitted, returns a tester function.
22
+ *
23
+ * @returns
24
+ * - `boolean` if segment is provided (true if route starts with segment)
25
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
26
+ * - `false` if segment is null or empty string
27
+ *
28
+ * @example
29
+ * // Direct call
30
+ * startsWithSegment('users.list', 'users'); // true
31
+ * startsWithSegment('users.list', 'admin'); // false
32
+ * startsWithSegment('admin.panel', 'users'); // false
33
+ *
34
+ * @example
35
+ * // Curried form
36
+ * const tester = startsWithSegment('users.list');
37
+ * tester('users'); // true
38
+ * tester('users.list'); // true
39
+ * tester('admin'); // false
40
+ *
41
+ * @example
42
+ * // With State object
43
+ * const state: State = { name: 'users.list', params: {}, path: '/users/list' };
44
+ * startsWithSegment(state, 'users'); // true
45
+ *
46
+ * @example
47
+ * // Edge cases
48
+ * startsWithSegment('users', ''); // false
49
+ * startsWithSegment('users', null); // false
50
+ * startsWithSegment('', 'users'); // false
51
+ *
52
+ * @throws {TypeError} If segment contains invalid characters or is not a string
53
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
54
+ *
55
+ * @remarks
56
+ * **Validation rules:**
57
+ * - Allowed characters: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)
58
+ * - Maximum segment length: 10,000 characters
59
+ * - Empty segments are rejected
60
+ *
61
+ * **Performance:**
62
+ * - No caching (RegExp compiled on each call)
63
+ * - For high-frequency checks, consider caching results at application level
64
+ *
65
+ * **Segment boundaries:**
66
+ * - Respects dot-separated segments
67
+ * - 'users' matches 'users' and 'users.list'
68
+ * - 'users' does NOT match 'users2' or 'admin.users'
69
+ *
70
+ * @see endsWithSegment for suffix matching
71
+ * @see includesSegment for anywhere matching
72
+ */
73
+ declare const startsWithSegment: SegmentTestFunction;
74
+ /**
75
+ * Tests if a route name ends with the given segment.
76
+ *
77
+ * Supports both direct calls and curried form for flexible usage patterns.
78
+ * All segments are validated for safety (length and character constraints).
79
+ *
80
+ * @param route - Route state object or route name string
81
+ * @param segment - Segment to test. If omitted, returns a tester function.
82
+ *
83
+ * @returns
84
+ * - `boolean` if segment is provided (true if route ends with segment)
85
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
86
+ * - `false` if segment is null or empty string
87
+ *
88
+ * @example
89
+ * // Direct call
90
+ * endsWithSegment('users.list', 'list'); // true
91
+ * endsWithSegment('users.profile.edit', 'edit'); // true
92
+ * endsWithSegment('users.list', 'users'); // false
93
+ *
94
+ * @example
95
+ * // Multi-segment suffix
96
+ * endsWithSegment('a.b.c.d', 'c.d'); // true
97
+ * endsWithSegment('a.b.c.d', 'b.c'); // false
98
+ *
99
+ * @example
100
+ * // Curried form
101
+ * const tester = endsWithSegment('users.list');
102
+ * tester('list'); // true
103
+ * tester('users'); // false
104
+ *
105
+ * @throws {TypeError} If segment contains invalid characters or is not a string
106
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
107
+ *
108
+ * @remarks
109
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
110
+ *
111
+ * @see startsWithSegment for prefix matching
112
+ * @see includesSegment for anywhere matching
113
+ */
114
+ declare const endsWithSegment: SegmentTestFunction;
115
+ /**
116
+ * Tests if a route name includes the given segment anywhere in its path.
117
+ *
118
+ * Supports both direct calls and curried form for flexible usage patterns.
119
+ * All segments are validated for safety (length and character constraints).
120
+ *
121
+ * @param route - Route state object or route name string
122
+ * @param segment - Segment to test. If omitted, returns a tester function.
123
+ *
124
+ * @returns
125
+ * - `boolean` if segment is provided (true if route includes segment)
126
+ * - `(segment: string) => boolean` if segment is omitted (curried tester function)
127
+ * - `false` if segment is null or empty string
128
+ *
129
+ * @example
130
+ * // Direct call
131
+ * includesSegment('users.profile.edit', 'profile'); // true
132
+ * includesSegment('users.profile.edit', 'users'); // true
133
+ * includesSegment('users.profile.edit', 'edit'); // true
134
+ * includesSegment('users.profile.edit', 'admin'); // false
135
+ *
136
+ * @example
137
+ * // Multi-segment inclusion
138
+ * includesSegment('a.b.c.d', 'b.c'); // true
139
+ * includesSegment('a.b.c.d', 'a.c'); // false (must be contiguous)
140
+ *
141
+ * @example
142
+ * // Curried form
143
+ * const tester = includesSegment('users.profile.edit');
144
+ * tester('profile'); // true
145
+ * tester('admin'); // false
146
+ *
147
+ * @throws {TypeError} If segment contains invalid characters or is not a string
148
+ * @throws {RangeError} If segment exceeds maximum length (10,000 characters)
149
+ *
150
+ * @remarks
151
+ * **Important:** Segment must be contiguous in the route path.
152
+ * - 'a.b.c' includes 'a.b' and 'b.c' but NOT 'a.c'
153
+ *
154
+ * See {@link startsWithSegment} for detailed validation rules and performance notes.
155
+ *
156
+ * @see startsWithSegment for prefix matching
157
+ * @see endsWithSegment for suffix matching
158
+ */
159
+ declare const includesSegment: SegmentTestFunction;
160
+
161
+ /**
162
+ * Checks if two routes are related in the hierarchy.
163
+ *
164
+ * Routes are related if:
165
+ * - They are exactly the same
166
+ * - One is a parent of the other (e.g., "users" and "users.list")
167
+ * - One is a child of the other (e.g., "users.list" and "users")
168
+ *
169
+ * @param route1 - First route name
170
+ * @param route2 - Second route name
171
+ * @returns True if routes are related, false otherwise
172
+ *
173
+ * @example
174
+ * areRoutesRelated("users", "users.list"); // true (parent-child)
175
+ * areRoutesRelated("users.list", "users"); // true (child-parent)
176
+ * areRoutesRelated("users", "users"); // true (same)
177
+ * areRoutesRelated("users", "admin"); // false (different branches)
178
+ * areRoutesRelated("users.list", "users.view"); // false (siblings)
179
+ */
180
+ declare function areRoutesRelated(route1: string, route2: string): boolean;
181
+
182
+ export { type SegmentTestFunction, areRoutesRelated, endsWithSegment, includesSegment, startsWithSegment };
@@ -0,0 +1 @@
1
+ var t=/^[\w.-]+$/,e=t=>t.replaceAll(/[$()*+.?[\\\]^{|}-]/g,String.raw`\$&`),r=(r,n)=>{const o=o=>{if(o.length>1e4)throw new RangeError("Segment exceeds maximum length of 10000 characters");if(!t.test(o))throw new TypeError("Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)");return new RegExp(r+e(o)+n)};return(t,e)=>{const r="string"==typeof t?t:t.name;if("string"!=typeof r)return!1;if(0===r.length)return!1;if(null===e)return!1;if(void 0===e)return t=>{if("string"!=typeof t)throw new TypeError("Segment must be a string, got "+typeof t);return 0!==t.length&&o(t).test(r)};if("string"!=typeof e)throw new TypeError("Segment must be a string, got "+typeof e);return 0!==e.length&&o(e).test(r)}},n=`(?:${e(".")}|$)`,o=r("^",n),i=r(`(?:^|${e(".")})`,"$"),s=r(`(?:^|${e(".")})`,n);function g(t,e){return t===e||t.startsWith(`${e}.`)||e.startsWith(`${t}.`)}export{g as areRoutesRelated,i as endsWithSegment,s as includesSegment,o as startsWithSegment};//# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/helpers.ts","../../src/routeRelation.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// packages/helpers/modules/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/helpers/modules/index.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/core\";\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 /**\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 // 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 return new RegExp(start + escapeRegExp(segment) + end);\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 * startsWithSegment('admin.panel', 'users'); // false\n *\n * @example\n * // Curried form\n * const tester = startsWithSegment('users.list');\n * tester('users'); // true\n * tester('users.list'); // 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 * @example\n * // Edge cases\n * startsWithSegment('users', ''); // false\n * startsWithSegment('users', null); // false\n * startsWithSegment('', '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 * @remarks\n * **Validation rules:**\n * - Allowed characters: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)\n * - Maximum segment length: 10,000 characters\n * - Empty segments are rejected\n *\n * **Performance:**\n * - No caching (RegExp compiled on each call)\n * - For high-frequency checks, consider caching results at application level\n *\n * **Segment boundaries:**\n * - Respects dot-separated segments\n * - 'users' matches 'users' and 'users.list'\n * - 'users' does NOT match 'users2' or 'admin.users'\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 * endsWithSegment('users.list', 'users'); // false\n *\n * @example\n * // Multi-segment suffix\n * endsWithSegment('a.b.c.d', 'c.d'); // true\n * endsWithSegment('a.b.c.d', 'b.c'); // false\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 * @remarks\n * See {@link startsWithSegment} for detailed validation rules and performance notes.\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 * includesSegment('users.profile.edit', 'users'); // true\n * includesSegment('users.profile.edit', 'edit'); // true\n * includesSegment('users.profile.edit', 'admin'); // false\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 * @example\n * // Curried form\n * const tester = includesSegment('users.profile.edit');\n * tester('profile'); // true\n * tester('admin'); // 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 * @remarks\n * **Important:** Segment must be contiguous in the route path.\n * - 'a.b.c' includes 'a.b' and 'b.c' but NOT 'a.c'\n *\n * See {@link startsWithSegment} for detailed validation rules and performance notes.\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","// packages/helpers/modules/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"]}
@@ -0,0 +1 @@
1
+ {"inputs":{"src/constants.ts":{"bytes":515,"imports":[],"format":"esm"},"src/helpers.ts":{"bytes":9955,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/routeRelation.ts":{"bytes":994,"imports":[],"format":"esm"},"src/index.ts":{"bytes":225,"imports":[{"path":"src/helpers.ts","kind":"import-statement","original":"./helpers"},{"path":"src/routeRelation.ts","kind":"import-statement","original":"./routeRelation"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":13383},"dist/esm/index.mjs":{"imports":[],"exports":["areRoutesRelated","endsWithSegment","includesSegment","startsWithSegment"],"entryPoint":"src/index.ts","inputs":{"src/constants.ts":{"bytesInOutput":105},"src/helpers.ts":{"bytesInOutput":1800},"src/index.ts":{"bytesInOutput":0},"src/routeRelation.ts":{"bytesInOutput":144}},"bytes":2203}}}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@real-router/helpers",
3
+ "version": "0.1.0",
4
+ "type": "commonjs",
5
+ "description": "Helper utilities for comparing and checking routes",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.mjs",
8
+ "types": "./dist/esm/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": {
12
+ "import": "./dist/esm/index.d.mts",
13
+ "require": "./dist/cjs/index.d.ts"
14
+ },
15
+ "import": "./dist/esm/index.mjs",
16
+ "require": "./dist/cjs/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/greydragon888/real-router.git"
25
+ },
26
+ "keywords": [
27
+ "real-router",
28
+ "helpers"
29
+ ],
30
+ "author": {
31
+ "name": "Oleg Ivanov",
32
+ "email": "greydragon888@gmail.com",
33
+ "url": "https://github.com/greydragon888"
34
+ },
35
+ "license": "MIT",
36
+ "bugs": {
37
+ "url": "https://github.com/greydragon888/real-router/issues"
38
+ },
39
+ "homepage": "https://github.com/greydragon888/real-router",
40
+ "scripts": {
41
+ "test": "vitest",
42
+ "build": "tsup",
43
+ "type-check": "tsc --noEmit",
44
+ "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
45
+ "lint:package": "publint",
46
+ "lint:types": "attw --pack ."
47
+ },
48
+ "sideEffects": false,
49
+ "dependencies": {
50
+ "@real-router/core": "workspace:^"
51
+ }
52
+ }