@real-router/helpers 0.1.1 → 0.1.3

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,20 +1,9 @@
1
1
  # @real-router/helpers
2
2
 
3
- [![npm version](https://badge.fury.io/js/@real-router%2Fhelpers.svg)](https://www.npmjs.com/package/@real-router/helpers)
4
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
4
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
6
5
 
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
6
+ Route segment testing utilities for Real-Router. Useful for navigation menus, breadcrumbs, conditional rendering, and route guards.
18
7
 
19
8
  ## Installation
20
9
 
@@ -38,247 +27,113 @@ import {
38
27
  areRoutesRelated,
39
28
  } from "@real-router/helpers";
40
29
 
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
30
+ startsWithSegment("users.profile.edit", "users"); // true
31
+ endsWithSegment("users.profile.edit", "edit"); // true
32
+ includesSegment("users.profile.edit", "profile"); // true
33
+ areRoutesRelated("users", "users.profile"); // true
56
34
  ```
57
35
 
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)
36
+ ---
68
37
 
69
- **Returns:**
38
+ ## API
70
39
 
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
40
+ ### `startsWithSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
74
41
 
75
- **Examples:**
42
+ Tests if route name starts with segment.\
43
+ `route: State | string` — route state object or route name string\
44
+ `segment?: string | null` — segment to test (optional for curried form)\
45
+ Returns: `boolean` (true if starts with segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
76
46
 
77
47
  ```typescript
78
48
  // 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
49
+ startsWithSegment("users.list", "users"); // true
50
+ startsWithSegment("users.profile", "users.profile"); // true (multi-segment)
84
51
 
85
52
  // With State object
86
- const state = { name: "users.list", params: {}, path: "/users" };
87
- startsWithSegment(state, "users"); // true
53
+ startsWithSegment({ name: "users.list", params: {}, path: "/" }, "users"); // true
88
54
 
89
55
  // Curried form
90
56
  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
- }
57
+ tester("users"); // true
58
+ tester("admin"); // false
112
59
  ```
113
60
 
114
- ---
61
+ ### `endsWithSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
115
62
 
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:**
63
+ Tests if route name ends with segment.\
64
+ `route: State | string` — route state object or route name string\
65
+ `segment?: string | null` segment to test (optional for curried form)\
66
+ Returns: `boolean` (true if ends with segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
132
67
 
133
68
  ```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
69
+ endsWithSegment("users.profile.edit", "edit"); // true
70
+ endsWithSegment("a.b.c.d", "c.d"); // true (multi-segment)
140
71
 
141
72
  // Curried form
142
73
  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 />}
74
+ tester("list"); // true
160
75
  ```
161
76
 
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:**
77
+ ### `includesSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
174
78
 
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:**
79
+ Tests if route name includes segment anywhere.\
80
+ `route: State | string` route state object or route name string\
81
+ `segment?: string | null` segment to test (optional for curried form)\
82
+ Returns: `boolean` (true if includes segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
180
83
 
181
84
  ```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)
85
+ includesSegment("admin.users.profile", "users"); // true
86
+ includesSegment("a.b.c.d", "b.c"); // true (contiguous)
87
+ includesSegment("a.b.c.d", "a.c"); // false (not contiguous)
190
88
 
191
89
  // Curried form
192
90
  const tester = includesSegment("admin.users.profile");
193
- tester("users"); // true
194
- tester("settings"); // false
91
+ tester("users"); // true
92
+ tester("settings"); // false
195
93
  ```
196
94
 
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
- ---
95
+ ### `areRoutesRelated(route1: string, route2: string): boolean`
213
96
 
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:**
97
+ Tests if routes are in same hierarchy (parent-child, child-parent, or same).\
98
+ `route1: string` — first route name\
99
+ `route2: string` second route name\
100
+ Returns: `boolean` — true if routes are related (same, parent-child, or child-parent)
228
101
 
229
102
  ```typescript
230
- // Same route
231
- areRoutesRelated("users", "users"); // true
232
-
233
103
  // Parent-child relationship
234
- areRoutesRelated("users", "users.list"); // true
104
+ areRoutesRelated("users", "users.list"); // true
235
105
  areRoutesRelated("users", "users.profile.edit"); // true
236
106
 
237
107
  // Child-parent relationship
238
- areRoutesRelated("users.list", "users"); // true
108
+ areRoutesRelated("users.list", "users"); // true
239
109
  areRoutesRelated("users.profile.edit", "users"); // true
240
110
 
241
- // Different branches (not related)
242
- areRoutesRelated("users", "admin"); // false
243
- areRoutesRelated("users.list", "admin.dashboard"); // false
111
+ // Same route
112
+ areRoutesRelated("users", "users"); // true
244
113
 
245
114
  // 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");
115
+ areRoutesRelated("users.list", "users.view"); // false
258
116
 
259
- // Breadcrumb visibility
260
- const showBreadcrumb = areRoutesRelated(currentRoute, basePath);
117
+ // Different branches (not related)
118
+ areRoutesRelated("users", "admin"); // false
261
119
  ```
262
120
 
263
121
  ---
264
122
 
265
- ## Real-World Examples
266
-
267
- ### Active Navigation Menu
123
+ ## Usage Examples
268
124
 
269
- ```typescript
270
- import { startsWithSegment } from '@real-router/helpers';
125
+ ### Navigation Menu
271
126
 
127
+ ```tsx
272
128
  function NavigationMenu({ currentRoute }) {
273
- const menuItems = [
274
- { name: 'Dashboard', route: 'dashboard' },
275
- { name: 'Users', route: 'users' },
276
- { name: 'Settings', route: 'settings' },
129
+ const items = [
130
+ { name: "Dashboard", route: "dashboard" },
131
+ { name: "Users", route: "users" },
277
132
  ];
278
133
 
279
134
  return (
280
135
  <nav>
281
- {menuItems.map(item => (
136
+ {items.map((item) => (
282
137
  <MenuItem
283
138
  key={item.route}
284
139
  active={startsWithSegment(currentRoute, item.route)}
@@ -291,44 +146,11 @@ function NavigationMenu({ currentRoute }) {
291
146
  }
292
147
  ```
293
148
 
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
149
+ ### Route Guard
326
150
 
327
151
  ```typescript
328
- import { startsWithSegment } from "@real-router/helpers";
329
-
330
152
  const adminGuard = (router) => (toState, fromState, done) => {
331
- if (startsWithSegment(toState, "admin") && !userHasAdminRole()) {
153
+ if (startsWithSegment(toState, "admin") && !isAdmin()) {
332
154
  done({ redirect: { name: "unauthorized" } });
333
155
  } else {
334
156
  done();
@@ -340,236 +162,62 @@ router.useMiddleware(adminGuard);
340
162
 
341
163
  ### Conditional Rendering
342
164
 
343
- ```typescript
344
- import { startsWithSegment, includesSegment, endsWithSegment } from '@real-router/helpers';
345
-
346
- function PageLayout({ route, children }) {
165
+ ```tsx
166
+ function Layout({ route, children }) {
347
167
  return (
348
168
  <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
-
169
+ {startsWithSegment(route, "admin") && <AdminSidebar />}
170
+ {endsWithSegment(route, "edit") && <EditToolbar />}
358
171
  <main>{children}</main>
359
172
  </div>
360
173
  );
361
174
  }
362
175
  ```
363
176
 
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
177
  ---
387
178
 
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");
179
+ ## Validation
461
180
 
462
- // Curried form also typed
463
- const tester: (segment: string) => boolean = startsWithSegment("route");
181
+ Segments are validated for security:
464
182
 
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
183
+ - **Allowed:** `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`
184
+ - **Max length:** 10,000 characters
185
+ - **Empty/null:** Returns `false`
186
+ - **Invalid chars:** Throws `TypeError`
471
187
 
472
188
  ```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
- }
189
+ startsWithSegment("route", "valid-segment_v2"); // OK
190
+ startsWithSegment("route", "invalid!char"); // Throws TypeError
191
+ startsWithSegment("route", ""); // false
482
192
  ```
483
193
 
484
194
  ---
485
195
 
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
196
  ## Migration from router5-helpers
506
197
 
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
198
  ```diff
556
199
  - import { startsWithSegment, redirect } from 'router5-helpers';
557
200
  + import { startsWithSegment } from '@real-router/helpers';
558
201
 
559
- // Segment testing - unchanged
560
- if (startsWithSegment(route, 'admin')) {
561
- // ...
562
- }
202
+ // Segment testing unchanged
203
+ startsWithSegment(route, 'admin');
563
204
 
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' } });
205
+ // redirect removed use guards:
206
+ - router.canActivate('old', () => redirect('new'));
207
+ + router.canActivate('old', (to, from, done) => {
208
+ + done({ redirect: { name: 'new' } });
568
209
  + });
569
210
  ```
570
211
 
212
+ **New:** `areRoutesRelated()` for hierarchy checks.
213
+
571
214
  ---
572
215
 
216
+ ## Related Packages
217
+
218
+ - [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
219
+ - [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration
220
+
573
221
  ## License
574
222
 
575
223
  MIT © [Oleg Ivanov](https://github.com/greydragon888)
@@ -1,16 +1,238 @@
1
- import { State } from '@real-router/core';
1
+ // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export interface State<P extends Params = Params, MP extends Params = Params> {
4
+ name: string;
5
+ params: P;
6
+ path: string;
7
+ meta?: StateMeta<MP> | undefined;
8
+ }
9
+ export interface StateMeta<P extends Params = Params> {
10
+ id: number;
11
+ params: P;
12
+ options: NavigationOptions;
13
+ redirected: boolean;
14
+ source?: string | undefined;
15
+ }
16
+ /**
17
+ * Configuration options that control navigation transition behavior.
18
+ *
19
+ * @description
20
+ * NavigationOptions provides fine-grained control over how the router performs navigation
21
+ * transitions. These options affect history management, transition lifecycle execution,
22
+ * guard enforcement, and state comparison logic.
23
+ *
24
+ * All options are optional and have sensible defaults. Options can be combined to achieve
25
+ * complex navigation behaviors. The options object is stored in state.meta.options and is
26
+ * available to middleware, guards, and event listeners.
27
+ *
28
+ * @see {@link Router.navigate} for navigation method that accepts these options
29
+ * @see {@link State.meta} for where options are stored after navigation
30
+ */
31
+ export interface NavigationOptions {
32
+ [key: string]: string | number | boolean | Record<string, unknown> | undefined;
33
+ /**
34
+ * Replace the current history entry instead of pushing a new one.
35
+ *
36
+ * @description
37
+ * When `true`, the navigation will replace the current entry in browser history instead
38
+ * of adding a new entry. This is typically used by history plugins (browser plugin) to
39
+ * control how navigation affects the browser's back/forward buttons.
40
+ *
41
+ * @default false
42
+ *
43
+ * @example
44
+ * // Redirect after login - prevent back button to login page
45
+ * router.navigate('dashboard', {}, { replace: true });
46
+ *
47
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState}
48
+ */
49
+ replace?: boolean | undefined;
50
+ /**
51
+ * Force reload of the current route even if states are equal.
52
+ *
53
+ * @description
54
+ * When `true`, bypasses the "same state" check that normally prevents navigation when
55
+ * the target state equals the current state. This forces a full transition lifecycle
56
+ * execution, allowing route components to reload with the same parameters.
57
+ *
58
+ * Without `reload`:
59
+ * - Navigation to current route throws SAME_STATES error
60
+ * - No lifecycle hooks or middleware execute
61
+ * - No events are fired
62
+ *
63
+ * With `reload`:
64
+ * - Full transition executes (deactivate → activate → middleware)
65
+ * - All lifecycle hooks run again
66
+ * - TRANSITION_SUCCESS event fires with same state
67
+ * - State object is recreated (new reference)
68
+ *
69
+ * @default false
70
+ *
71
+ * @example
72
+ * // Refresh current page data
73
+ * router.navigate(currentRoute.name, currentRoute.params, { reload: true });
74
+ *
75
+ * @example
76
+ * // Force re-fetch on same route with different query params
77
+ * // Note: query params are in path, not checked for equality
78
+ * router.navigate('search', { term: 'react' }, { reload: true });
79
+ *
80
+ * @see {@link force} for alternative that forces transition
81
+ * @see {@link Router.areStatesEqual} for state comparison logic
82
+ */
83
+ reload?: boolean | undefined;
84
+ /**
85
+ * Preview navigation without any side effects (dry-run mode).
86
+ *
87
+ * @description
88
+ * When `true`, returns the would-be target state via callback WITHOUT:
89
+ * - Executing canDeactivate/canActivate guards
90
+ * - Executing middleware
91
+ * - Updating router state (`router.getState()` remains unchanged)
92
+ * - Emitting any transition events (TRANSITION_START, TRANSITION_SUCCESS, etc.)
93
+ *
94
+ * The callback receives `(undefined, toState)` where `toState` is the computed
95
+ * target state that WOULD result from this navigation.
96
+ *
97
+ * @default false
98
+ *
99
+ * @remarks
100
+ * This option is useful for:
101
+ * - Validating that a route exists and params are correct
102
+ * - SSR: previewing state for pre-rendering without side effects
103
+ * - Dry-run before actual navigation
104
+ *
105
+ * @deprecated Consider using `router.buildState()` + `router.makeState()` instead
106
+ * for clearer intent. This option may be removed in a future major version.
107
+ *
108
+ * @example
109
+ * // Preview navigation - router.getState() is NOT changed
110
+ * router.navigate('users.view', { id: 123 }, { skipTransition: true }, (err, previewState) => {
111
+ * console.log(previewState); // { name: 'users.view', params: { id: 123 }, path: '/users/view/123', ... }
112
+ * console.log(router.getState()); // Still the previous state!
113
+ * });
114
+ *
115
+ * @example
116
+ * // Recommended alternative (clearer intent)
117
+ * const route = router.buildState('users.view', { id: 123 });
118
+ * if (route) {
119
+ * const path = router.buildPath(route.name, route.params);
120
+ * const previewState = router.makeState(route.name, route.params, path, { params: route.meta });
121
+ * }
122
+ *
123
+ * @see {@link forceDeactivate} for skipping only canDeactivate guards
124
+ * @see {@link force} for forcing navigation while preserving lifecycle
125
+ */
126
+ skipTransition?: boolean | undefined;
127
+ /**
128
+ * Force navigation even if target state equals current state.
129
+ *
130
+ * @description
131
+ * When `true`, bypasses the "same state" equality check but still executes the full
132
+ * transition lifecycle (unlike `skipTransition`). Similar to `reload` but can be used
133
+ * for any forced navigation scenario.
134
+ *
135
+ * Difference from `reload`:
136
+ * - `reload`: semantic meaning is "refresh current route"
137
+ * - `force`: general-purpose bypass of equality check
138
+ * - Both have identical implementation effect
139
+ *
140
+ * The equality check compares:
141
+ * - state.name (route name)
142
+ * - state.params (route parameters, shallow comparison)
143
+ *
144
+ * @default false
145
+ *
146
+ * @example
147
+ * // Force transition for tracking even if params didn't change
148
+ * router.navigate('analytics', { event: 'pageview' }, { force: true });
149
+ *
150
+ * @see {@link reload} for semantic equivalent (preferred for refresh scenarios)
151
+ * @see {@link skipTransition} for bypassing entire lifecycle
152
+ */
153
+ force?: boolean | undefined;
154
+ /**
155
+ * Skip canDeactivate guards during transition.
156
+ *
157
+ * @description
158
+ * When `true`, bypasses only the canDeactivate lifecycle hooks for segments being
159
+ * deactivated. canActivate guards and middleware still execute normally. This allows
160
+ * forcing navigation away from routes with confirmation dialogs or unsaved changes.
161
+ *
162
+ * Skipped vs executed:
163
+ * ```
164
+ * // Normal transition
165
+ * deactivate(fromSegments) → activate(toSegments) → middleware → success
166
+ *
167
+ * // With forceDeactivate: true
168
+ * [skip deactivate] → activate(toSegments) → middleware → success
169
+ * ```
170
+ *
171
+ * ⚠️ Data loss risk: Bypassing canDeactivate means unsaved changes will be lost
172
+ *
173
+ * @default false
174
+ *
175
+ * @example
176
+ * // Force logout even with unsaved changes
177
+ * function forceLogout() {
178
+ * router.navigate('login', {}, {
179
+ * forceDeactivate: true,
180
+ * replace: true
181
+ * });
182
+ * }
183
+ *
184
+ * @see {@link skipTransition} for bypassing all guards and middleware
185
+ * @see {@link Router.clearCanDeactivate} for programmatically clearing guards
186
+ */
187
+ forceDeactivate?: boolean | undefined;
188
+ /**
189
+ * Internal flag indicating navigation is result of a redirect.
190
+ *
191
+ * @internal
192
+ *
193
+ * @description
194
+ * Automatically set by the router when a navigation is triggered by a redirect from
195
+ * middleware or lifecycle hooks. This flag is used internally to track redirect chains
196
+ * and is stored in state.meta.redirected.
197
+ *
198
+ * @default false (auto-set by router during redirects)
199
+ *
200
+ * @example
201
+ * // Middleware triggers automatic redirect
202
+ * router.useMiddleware((toState, fromState, opts) => {
203
+ * if (!isAuthenticated && toState.name !== 'login') {
204
+ * // Router will automatically set redirected: true
205
+ * return { name: 'login', params: { next: toState.path } };
206
+ * }
207
+ * });
208
+ *
209
+ * @example
210
+ * // Accessing redirect flag in lifecycle
211
+ * router.canActivate('dashboard', (toState, fromState) => {
212
+ * if (toState.meta?.redirected) {
213
+ * console.log('This navigation is from a redirect');
214
+ * }
215
+ * return true;
216
+ * });
217
+ *
218
+ * @see {@link Router.navigate} for redirect handling implementation
219
+ * @see {@link State.meta.redirected} for redirect flag in state
220
+ */
221
+ redirected?: boolean | undefined;
222
+ }
223
+ export interface Params {
224
+ [key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
225
+ }
3
226
  /**
4
227
  * Type definition for segment test functions.
5
228
  * These functions can be called directly with a segment, or curried for later use.
6
229
  */
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);
230
+ export interface SegmentTestFunction {
231
+ (route: State | string): (segment: string) => boolean;
232
+ (route: State | string, segment: string): boolean;
233
+ (route: State | string, segment: null): false;
234
+ (route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
12
235
  }
13
-
14
236
  /**
15
237
  * Tests if a route name starts with the given segment.
16
238
  *
@@ -70,7 +292,7 @@ interface SegmentTestFunction {
70
292
  * @see endsWithSegment for suffix matching
71
293
  * @see includesSegment for anywhere matching
72
294
  */
73
- declare const startsWithSegment: SegmentTestFunction;
295
+ export declare const startsWithSegment: SegmentTestFunction;
74
296
  /**
75
297
  * Tests if a route name ends with the given segment.
76
298
  *
@@ -111,7 +333,7 @@ declare const startsWithSegment: SegmentTestFunction;
111
333
  * @see startsWithSegment for prefix matching
112
334
  * @see includesSegment for anywhere matching
113
335
  */
114
- declare const endsWithSegment: SegmentTestFunction;
336
+ export declare const endsWithSegment: SegmentTestFunction;
115
337
  /**
116
338
  * Tests if a route name includes the given segment anywhere in its path.
117
339
  *
@@ -156,8 +378,7 @@ declare const endsWithSegment: SegmentTestFunction;
156
378
  * @see startsWithSegment for prefix matching
157
379
  * @see endsWithSegment for suffix matching
158
380
  */
159
- declare const includesSegment: SegmentTestFunction;
160
-
381
+ export declare const includesSegment: SegmentTestFunction;
161
382
  /**
162
383
  * Checks if two routes are related in the hierarchy.
163
384
  *
@@ -177,6 +398,6 @@ declare const includesSegment: SegmentTestFunction;
177
398
  * areRoutesRelated("users", "admin"); // false (different branches)
178
399
  * areRoutesRelated("users.list", "users.view"); // false (siblings)
179
400
  */
180
- declare function areRoutesRelated(route1: string, route2: string): boolean;
401
+ export declare function areRoutesRelated(route1: string, route2: string): boolean;
181
402
 
182
- export { type SegmentTestFunction, areRoutesRelated, endsWithSegment, includesSegment, startsWithSegment };
403
+ export {};
@@ -1,16 +1,238 @@
1
- import { State } from '@real-router/core';
1
+ // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export interface State<P extends Params = Params, MP extends Params = Params> {
4
+ name: string;
5
+ params: P;
6
+ path: string;
7
+ meta?: StateMeta<MP> | undefined;
8
+ }
9
+ export interface StateMeta<P extends Params = Params> {
10
+ id: number;
11
+ params: P;
12
+ options: NavigationOptions;
13
+ redirected: boolean;
14
+ source?: string | undefined;
15
+ }
16
+ /**
17
+ * Configuration options that control navigation transition behavior.
18
+ *
19
+ * @description
20
+ * NavigationOptions provides fine-grained control over how the router performs navigation
21
+ * transitions. These options affect history management, transition lifecycle execution,
22
+ * guard enforcement, and state comparison logic.
23
+ *
24
+ * All options are optional and have sensible defaults. Options can be combined to achieve
25
+ * complex navigation behaviors. The options object is stored in state.meta.options and is
26
+ * available to middleware, guards, and event listeners.
27
+ *
28
+ * @see {@link Router.navigate} for navigation method that accepts these options
29
+ * @see {@link State.meta} for where options are stored after navigation
30
+ */
31
+ export interface NavigationOptions {
32
+ [key: string]: string | number | boolean | Record<string, unknown> | undefined;
33
+ /**
34
+ * Replace the current history entry instead of pushing a new one.
35
+ *
36
+ * @description
37
+ * When `true`, the navigation will replace the current entry in browser history instead
38
+ * of adding a new entry. This is typically used by history plugins (browser plugin) to
39
+ * control how navigation affects the browser's back/forward buttons.
40
+ *
41
+ * @default false
42
+ *
43
+ * @example
44
+ * // Redirect after login - prevent back button to login page
45
+ * router.navigate('dashboard', {}, { replace: true });
46
+ *
47
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState}
48
+ */
49
+ replace?: boolean | undefined;
50
+ /**
51
+ * Force reload of the current route even if states are equal.
52
+ *
53
+ * @description
54
+ * When `true`, bypasses the "same state" check that normally prevents navigation when
55
+ * the target state equals the current state. This forces a full transition lifecycle
56
+ * execution, allowing route components to reload with the same parameters.
57
+ *
58
+ * Without `reload`:
59
+ * - Navigation to current route throws SAME_STATES error
60
+ * - No lifecycle hooks or middleware execute
61
+ * - No events are fired
62
+ *
63
+ * With `reload`:
64
+ * - Full transition executes (deactivate → activate → middleware)
65
+ * - All lifecycle hooks run again
66
+ * - TRANSITION_SUCCESS event fires with same state
67
+ * - State object is recreated (new reference)
68
+ *
69
+ * @default false
70
+ *
71
+ * @example
72
+ * // Refresh current page data
73
+ * router.navigate(currentRoute.name, currentRoute.params, { reload: true });
74
+ *
75
+ * @example
76
+ * // Force re-fetch on same route with different query params
77
+ * // Note: query params are in path, not checked for equality
78
+ * router.navigate('search', { term: 'react' }, { reload: true });
79
+ *
80
+ * @see {@link force} for alternative that forces transition
81
+ * @see {@link Router.areStatesEqual} for state comparison logic
82
+ */
83
+ reload?: boolean | undefined;
84
+ /**
85
+ * Preview navigation without any side effects (dry-run mode).
86
+ *
87
+ * @description
88
+ * When `true`, returns the would-be target state via callback WITHOUT:
89
+ * - Executing canDeactivate/canActivate guards
90
+ * - Executing middleware
91
+ * - Updating router state (`router.getState()` remains unchanged)
92
+ * - Emitting any transition events (TRANSITION_START, TRANSITION_SUCCESS, etc.)
93
+ *
94
+ * The callback receives `(undefined, toState)` where `toState` is the computed
95
+ * target state that WOULD result from this navigation.
96
+ *
97
+ * @default false
98
+ *
99
+ * @remarks
100
+ * This option is useful for:
101
+ * - Validating that a route exists and params are correct
102
+ * - SSR: previewing state for pre-rendering without side effects
103
+ * - Dry-run before actual navigation
104
+ *
105
+ * @deprecated Consider using `router.buildState()` + `router.makeState()` instead
106
+ * for clearer intent. This option may be removed in a future major version.
107
+ *
108
+ * @example
109
+ * // Preview navigation - router.getState() is NOT changed
110
+ * router.navigate('users.view', { id: 123 }, { skipTransition: true }, (err, previewState) => {
111
+ * console.log(previewState); // { name: 'users.view', params: { id: 123 }, path: '/users/view/123', ... }
112
+ * console.log(router.getState()); // Still the previous state!
113
+ * });
114
+ *
115
+ * @example
116
+ * // Recommended alternative (clearer intent)
117
+ * const route = router.buildState('users.view', { id: 123 });
118
+ * if (route) {
119
+ * const path = router.buildPath(route.name, route.params);
120
+ * const previewState = router.makeState(route.name, route.params, path, { params: route.meta });
121
+ * }
122
+ *
123
+ * @see {@link forceDeactivate} for skipping only canDeactivate guards
124
+ * @see {@link force} for forcing navigation while preserving lifecycle
125
+ */
126
+ skipTransition?: boolean | undefined;
127
+ /**
128
+ * Force navigation even if target state equals current state.
129
+ *
130
+ * @description
131
+ * When `true`, bypasses the "same state" equality check but still executes the full
132
+ * transition lifecycle (unlike `skipTransition`). Similar to `reload` but can be used
133
+ * for any forced navigation scenario.
134
+ *
135
+ * Difference from `reload`:
136
+ * - `reload`: semantic meaning is "refresh current route"
137
+ * - `force`: general-purpose bypass of equality check
138
+ * - Both have identical implementation effect
139
+ *
140
+ * The equality check compares:
141
+ * - state.name (route name)
142
+ * - state.params (route parameters, shallow comparison)
143
+ *
144
+ * @default false
145
+ *
146
+ * @example
147
+ * // Force transition for tracking even if params didn't change
148
+ * router.navigate('analytics', { event: 'pageview' }, { force: true });
149
+ *
150
+ * @see {@link reload} for semantic equivalent (preferred for refresh scenarios)
151
+ * @see {@link skipTransition} for bypassing entire lifecycle
152
+ */
153
+ force?: boolean | undefined;
154
+ /**
155
+ * Skip canDeactivate guards during transition.
156
+ *
157
+ * @description
158
+ * When `true`, bypasses only the canDeactivate lifecycle hooks for segments being
159
+ * deactivated. canActivate guards and middleware still execute normally. This allows
160
+ * forcing navigation away from routes with confirmation dialogs or unsaved changes.
161
+ *
162
+ * Skipped vs executed:
163
+ * ```
164
+ * // Normal transition
165
+ * deactivate(fromSegments) → activate(toSegments) → middleware → success
166
+ *
167
+ * // With forceDeactivate: true
168
+ * [skip deactivate] → activate(toSegments) → middleware → success
169
+ * ```
170
+ *
171
+ * ⚠️ Data loss risk: Bypassing canDeactivate means unsaved changes will be lost
172
+ *
173
+ * @default false
174
+ *
175
+ * @example
176
+ * // Force logout even with unsaved changes
177
+ * function forceLogout() {
178
+ * router.navigate('login', {}, {
179
+ * forceDeactivate: true,
180
+ * replace: true
181
+ * });
182
+ * }
183
+ *
184
+ * @see {@link skipTransition} for bypassing all guards and middleware
185
+ * @see {@link Router.clearCanDeactivate} for programmatically clearing guards
186
+ */
187
+ forceDeactivate?: boolean | undefined;
188
+ /**
189
+ * Internal flag indicating navigation is result of a redirect.
190
+ *
191
+ * @internal
192
+ *
193
+ * @description
194
+ * Automatically set by the router when a navigation is triggered by a redirect from
195
+ * middleware or lifecycle hooks. This flag is used internally to track redirect chains
196
+ * and is stored in state.meta.redirected.
197
+ *
198
+ * @default false (auto-set by router during redirects)
199
+ *
200
+ * @example
201
+ * // Middleware triggers automatic redirect
202
+ * router.useMiddleware((toState, fromState, opts) => {
203
+ * if (!isAuthenticated && toState.name !== 'login') {
204
+ * // Router will automatically set redirected: true
205
+ * return { name: 'login', params: { next: toState.path } };
206
+ * }
207
+ * });
208
+ *
209
+ * @example
210
+ * // Accessing redirect flag in lifecycle
211
+ * router.canActivate('dashboard', (toState, fromState) => {
212
+ * if (toState.meta?.redirected) {
213
+ * console.log('This navigation is from a redirect');
214
+ * }
215
+ * return true;
216
+ * });
217
+ *
218
+ * @see {@link Router.navigate} for redirect handling implementation
219
+ * @see {@link State.meta.redirected} for redirect flag in state
220
+ */
221
+ redirected?: boolean | undefined;
222
+ }
223
+ export interface Params {
224
+ [key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
225
+ }
3
226
  /**
4
227
  * Type definition for segment test functions.
5
228
  * These functions can be called directly with a segment, or curried for later use.
6
229
  */
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);
230
+ export interface SegmentTestFunction {
231
+ (route: State | string): (segment: string) => boolean;
232
+ (route: State | string, segment: string): boolean;
233
+ (route: State | string, segment: null): false;
234
+ (route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
12
235
  }
13
-
14
236
  /**
15
237
  * Tests if a route name starts with the given segment.
16
238
  *
@@ -70,7 +292,7 @@ interface SegmentTestFunction {
70
292
  * @see endsWithSegment for suffix matching
71
293
  * @see includesSegment for anywhere matching
72
294
  */
73
- declare const startsWithSegment: SegmentTestFunction;
295
+ export declare const startsWithSegment: SegmentTestFunction;
74
296
  /**
75
297
  * Tests if a route name ends with the given segment.
76
298
  *
@@ -111,7 +333,7 @@ declare const startsWithSegment: SegmentTestFunction;
111
333
  * @see startsWithSegment for prefix matching
112
334
  * @see includesSegment for anywhere matching
113
335
  */
114
- declare const endsWithSegment: SegmentTestFunction;
336
+ export declare const endsWithSegment: SegmentTestFunction;
115
337
  /**
116
338
  * Tests if a route name includes the given segment anywhere in its path.
117
339
  *
@@ -156,8 +378,7 @@ declare const endsWithSegment: SegmentTestFunction;
156
378
  * @see startsWithSegment for prefix matching
157
379
  * @see endsWithSegment for suffix matching
158
380
  */
159
- declare const includesSegment: SegmentTestFunction;
160
-
381
+ export declare const includesSegment: SegmentTestFunction;
161
382
  /**
162
383
  * Checks if two routes are related in the hierarchy.
163
384
  *
@@ -177,6 +398,6 @@ declare const includesSegment: SegmentTestFunction;
177
398
  * areRoutesRelated("users", "admin"); // false (different branches)
178
399
  * areRoutesRelated("users.list", "users.view"); // false (siblings)
179
400
  */
180
- declare function areRoutesRelated(route1: string, route2: string): boolean;
401
+ export declare function areRoutesRelated(route1: string, route2: string): boolean;
181
402
 
182
- export { type SegmentTestFunction, areRoutesRelated, endsWithSegment, includesSegment, startsWithSegment };
403
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/helpers",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "commonjs",
5
5
  "description": "Helper utilities for comparing and checking routes",
6
6
  "main": "./dist/cjs/index.js",