@avstantso/react-router-utils 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1] - 2026-02-07
4
+
5
+ ### Changed
6
+
7
+ - Updated README with comprehensive documentation for `AT` methods
8
+ - Updated README with documentation for RouteCreator callback syntax
9
+ - **RouteCreator callback syntax**: Nested routes now automatically use relative paths (`type='name'`), ensuring valid react-router nested route structures
10
+ - RouteCreator callback syntax is now stable (no longer experimental)
11
+
12
+ ### Performance
13
+
14
+ - RouteCreator: Added WeakMap caching for nested trees, improving performance when callbacks are invoked multiple times on the same route
15
+
16
+ ## [0.2.0] - 2026-02-05
17
+
18
+ ### Added
19
+
20
+ - `AT` methods for `isUrl`, `isSubUrl`, `Links`, `NavLinks`, `Navigates` and `Navigates.useNavigate()`
21
+ - New types internal structure based on `TypeMap`
22
+ - RouteCreator: +`makeRouteObjectCallback` experimental overload
23
+
3
24
  ## [0.1.2] - 2026-02-05
4
25
 
5
26
  ### Fixed
package/README.md CHANGED
@@ -174,6 +174,35 @@ Each link component has an `id` property derived from the route path, useful for
174
174
  console.log(Links.Profile.Settings.id); // HTML-safe id derived from "profile/settings"
175
175
  ```
176
176
 
177
+ ### AT Method (Direct Node Access)
178
+
179
+ The `AT` method provides direct access to any node in the tree by passing a URL tree node reference. This is useful when you need to dynamically select routes or work with node references:
180
+
181
+ ```tsx
182
+ const { Links, NavLinks } = ReactRouterTree(urlsTree);
183
+
184
+ // Access a specific node directly
185
+ const AboutLink = Links.AT(urlsTree.about);
186
+ <AboutLink>About</AboutLink>
187
+
188
+ // Access nested nodes
189
+ const SettingsLink = Links.AT(urlsTree.profile.settings);
190
+ <SettingsLink>Settings</SettingsLink>
191
+
192
+ // Access parameterized nodes
193
+ const UserLink = Links.AT(urlsTree.profile[':userId']);
194
+ <UserLink params={{ userId: '42' }}>User</UserLink>
195
+
196
+ // The returned node preserves its children
197
+ const ProfileNode = Links.AT(urlsTree.profile);
198
+ <ProfileNode.Settings>Settings</ProfileNode.Settings>
199
+ <ProfileNode.$UserId params={{ userId: '99' }}>User</ProfileNode.$UserId>
200
+
201
+ // Works the same for NavLinks
202
+ const AboutNavLink = NavLinks.AT(urlsTree.about);
203
+ <AboutNavLink>About</AboutNavLink>
204
+ ```
205
+
177
206
  ---
178
207
 
179
208
  ## Navigates
@@ -212,6 +241,27 @@ The `preserve` prop saves the current `search` and `hash` when redirecting:
212
241
  <Navigates.About preserve />
213
242
  ```
214
243
 
244
+ #### AT Method (Direct Node Access)
245
+
246
+ The `AT` method provides direct access to navigate components by node reference:
247
+
248
+ ```tsx
249
+ const { Navigates } = ReactRouterTree(urlsTree);
250
+
251
+ // Access a specific node directly
252
+ const AboutNav = Navigates.AT(urlsTree.about);
253
+ <AboutNav />
254
+
255
+ // Access nested nodes
256
+ const SettingsNav = Navigates.AT(urlsTree.profile.settings);
257
+ <SettingsNav />
258
+
259
+ // The returned node preserves its children
260
+ const ProfileNav = Navigates.AT(urlsTree.profile);
261
+ <ProfileNav.Settings />
262
+ <ProfileNav.$UserId params={{ userId: '42' }} />
263
+ ```
264
+
215
265
  ### useNavigate Hook
216
266
 
217
267
  A proxy-enhanced `useNavigate` hook that provides the same tree-based navigation as the components, but as callable functions.
@@ -257,6 +307,35 @@ function MyComponent() {
257
307
  }
258
308
  ```
259
309
 
310
+ #### AT Method (Direct Node Access)
311
+
312
+ The `AT` method works with the `useNavigate` hook as well, allowing navigation by node reference:
313
+
314
+ ```tsx
315
+ function MyComponent() {
316
+ const nav = Navigates.useNavigate();
317
+
318
+ return (
319
+ <div>
320
+ {/* Navigate using direct node access */}
321
+ <button onClick={() => nav.AT(urlsTree.about)()}>
322
+ Go to About
323
+ </button>
324
+
325
+ {/* With params */}
326
+ <button onClick={() => nav.AT(urlsTree.profile[':userId'])({ userId: '42' })}>
327
+ Go to User 42
328
+ </button>
329
+
330
+ {/* The returned node preserves its children */}
331
+ <button onClick={() => nav.AT(urlsTree.profile).Settings()}>
332
+ Go to Profile Settings
333
+ </button>
334
+ </div>
335
+ );
336
+ }
337
+ ```
338
+
260
339
  ---
261
340
 
262
341
  ## isUrl & isSubUrl
@@ -324,6 +403,29 @@ isSubUrl.Profile('/about'); // false
324
403
  isSubUrl.Home('/anything'); // true
325
404
  ```
326
405
 
406
+ ### AT Method (Direct Node Access)
407
+
408
+ Both `isUrl` and `isSubUrl` support the `AT` method for direct node access:
409
+
410
+ ```typescript
411
+ const { isUrl, isSubUrl } = ReactRouterTree(urlsTree);
412
+
413
+ // Access nodes directly
414
+ isUrl.AT(urlsTree.about)('/about'); // true
415
+ isUrl.AT(urlsTree.profile.settings)('/profile/settings'); // true
416
+ isUrl.AT(urlsTree.profile[':userId'])('/profile/123'); // true
417
+
418
+ // The returned function preserves children
419
+ const profile = isUrl.AT(urlsTree.profile);
420
+ profile.Settings('/profile/settings'); // true
421
+ profile.$UserId('/profile/123'); // true
422
+ profile.$UserId.Posts('/profile/42/posts'); // true
423
+
424
+ // Works the same for isSubUrl
425
+ isSubUrl.AT(urlsTree.profile)('/profile'); // true
426
+ isSubUrl.AT(urlsTree.profile)('/profile/settings'); // true
427
+ ```
428
+
327
429
  ---
328
430
 
329
431
  ## RouteCreator
@@ -386,6 +488,72 @@ route2.About({ element: <AboutPage /> });
386
488
  // { element: <AboutPage />, path: '/about' }
387
489
  ```
388
490
 
491
+ ### Callback Syntax
492
+
493
+ RouteCreator supports a callback syntax for defining nested children routes. When using callbacks, **nested routes automatically use relative paths** (equivalent to `type='name'`), regardless of the parent's type setting.
494
+
495
+ ```typescript
496
+ const route = RouteCreator('name');
497
+
498
+ const profileRoute = route.Profile((RouteCreator) => ({
499
+ element: <ProfilePage />,
500
+ children: [
501
+ RouteCreator[':userId']((RouteCreator) => ({
502
+ element: <UserPage />,
503
+ children: [
504
+ RouteCreator.Posts({
505
+ element: <PostsPage />
506
+ })
507
+ ]
508
+ }))
509
+ ]
510
+ }));
511
+
512
+ // Produces:
513
+ // {
514
+ // path: 'profile',
515
+ // element: <ProfilePage />,
516
+ // children: [
517
+ // {
518
+ // path: ':userId',
519
+ // element: <UserPage />,
520
+ // children: [
521
+ // { path: 'posts', element: <PostsPage /> }
522
+ // ]
523
+ // }
524
+ // ]
525
+ // }
526
+ ```
527
+
528
+ #### Mixing Types
529
+
530
+ The parent route respects the `type` parameter, but children always use relative paths:
531
+
532
+ ```typescript
533
+ const route = RouteCreator('url');
534
+
535
+ const profileRoute = route.Profile((RouteCreator) => ({
536
+ element: <ProfilePage />,
537
+ children: [
538
+ RouteCreator.Settings({ element: <SettingsPage /> })
539
+ ]
540
+ }));
541
+
542
+ // Produces:
543
+ // {
544
+ // path: '/profile', // Parent uses 'url' type
545
+ // element: <ProfilePage />,
546
+ // children: [
547
+ // {
548
+ // path: 'settings', // Child forced to relative path
549
+ // element: <SettingsPage />
550
+ // }
551
+ // ]
552
+ // }
553
+ ```
554
+
555
+ This ensures nested routes are always valid for react-router's nested route structure.
556
+
389
557
  ---
390
558
 
391
559
  ## Type Definitions
@@ -5,6 +5,7 @@ export type BindProps<T extends {
5
5
  to?: To;
6
6
  }> = TS.ReplaceKeyOpt<'to', T, Partial<BindedPath>>;
7
7
  export declare function resolveParams(url: string, params?: object): string;
8
+ export declare function Parts(url: string): string[];
8
9
  export declare function BindProps<P extends string, T extends {
9
10
  to?: To;
10
11
  }>(pathname: P, props: {
@@ -1,2 +1,2 @@
1
- import type * as Types from '../types';
2
- export declare function IsUrlTreeFactory<TTree extends Types.UrlsTree>(urlsTree: TTree): Types.IsUrlTree.Methods<TTree>;
1
+ import type { UrlsTree, IsUrlTree } from '../types';
2
+ export declare function IsUrlTreeFactory<TTree extends UrlsTree>(urlsTree: TTree): IsUrlTree.Methods<TTree>;
@@ -1,2 +1,2 @@
1
- import type * as Types from '../types';
2
- export declare function LinksFactory<TTree extends Types.UrlsTree>(urlsTree: TTree): Types.LinksTree.Set<TTree>;
1
+ import type { UrlsTree, LinksTree } from '../types';
2
+ export declare function LinksFactory<TTree extends UrlsTree>(urlsTree: TTree): LinksTree.Set<TTree>;
@@ -1,2 +1,2 @@
1
- import type * as Types from '../types';
2
- export declare function NavigatesTreeFactory<TTree extends Types.UrlsTree>(urlsTree: TTree): Types.NavigatesTree<TTree>;
1
+ import type { UrlsTree, NavigatesTree } from '../types';
2
+ export declare function NavigatesTreeFactory<TTree extends UrlsTree>(urlsTree: TTree): NavigatesTree<TTree>;
@@ -11,3 +11,4 @@ export declare namespace TreeFactory {
11
11
  };
12
12
  }
13
13
  export declare function TreeFactory<TTree extends UrlsTree, O extends TreeFactory.Options>(urlsTree: TTree, options: O): object | Function;
14
+ export declare function TreeAT(tree: any, node: UrlsTree.Node): any;
package/dist/index.js CHANGED
@@ -5,6 +5,30 @@ var utilsMisc = require('@avstantso/utils-misc');
5
5
  var require$$0 = require('react');
6
6
  var reactRouterDom = require('react-router-dom');
7
7
 
8
+ var JS$4 = AVStantso.JS;
9
+ const PARAMS_REPLACE_REGEXP = /:([^/]+)($|\/)/g;
10
+ function resolveParams(url, params) {
11
+ return !params
12
+ ? url
13
+ : url.replace(PARAMS_REPLACE_REGEXP, (m, g, trail) => {
14
+ return `${g in params ? `${params[g]}` : `:${g}`}${trail}`;
15
+ });
16
+ }
17
+ function Parts(url) {
18
+ return url.split('/').pack();
19
+ }
20
+ function BindProps(pathname, props, params) {
21
+ const { to: outerTo, ...rest } = props;
22
+ const pn = resolveParams(pathname, params);
23
+ const to = !outerTo || JS$4.is.string(outerTo)
24
+ ? pn
25
+ : {
26
+ ...outerTo,
27
+ pathname: pn
28
+ };
29
+ return Object.assign(rest, { to });
30
+ }
31
+
8
32
  var JS$3 = AVStantso.JS;
9
33
  var Func = AVStantso.Func;
10
34
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */
@@ -38,10 +62,19 @@ function recursiveCreateCmpByRoute(node, options, level = 0) {
38
62
  function TreeFactory(urlsTree, options) {
39
63
  return recursiveCreateCmpByRoute(urlsTree, options);
40
64
  }
65
+ function TreeAT(tree, node) {
66
+ let current = tree;
67
+ for (const part of Parts(node._url))
68
+ current = current[part.startsWith(':')
69
+ ? PP$Camel.paramProcessor(part)
70
+ : utilsMisc.Cases.pascal(part)];
71
+ return current;
72
+ }
41
73
 
42
74
  var JS$2 = AVStantso.JS;
43
75
  function CheckUrlFactory(urlsTree, callback) {
44
- return TreeFactory(urlsTree, {
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ const tree = TreeFactory(urlsTree, {
45
78
  nodeFactory: (node) => (pathnameOrLocation, params) => {
46
79
  const pathname = JS$2.is.string(pathnameOrLocation)
47
80
  ? pathnameOrLocation
@@ -50,9 +83,8 @@ function CheckUrlFactory(urlsTree, callback) {
50
83
  },
51
84
  ...PP$Camel
52
85
  });
53
- }
54
- function Parts(url) {
55
- return url.split('/').pack();
86
+ tree.AT = (node) => TreeAT(tree, node);
87
+ return tree;
56
88
  }
57
89
  function DoIsUrl(strictMatch) {
58
90
  return (node, pathname, params) => {
@@ -1449,29 +1481,9 @@ function requireJsxRuntime () {
1449
1481
 
1450
1482
  var jsxRuntimeExports = requireJsxRuntime();
1451
1483
 
1452
- var JS$1 = AVStantso.JS;
1453
- const PARAMS_REPLACE_REGEXP = /:([^/]+)($|\/)/g;
1454
- function resolveParams(url, params) {
1455
- return !params
1456
- ? url
1457
- : url.replace(PARAMS_REPLACE_REGEXP, (m, g, trail) => {
1458
- return `${g in params ? `${params[g]}` : `:${g}`}${trail}`;
1459
- });
1460
- }
1461
- function BindProps(pathname, props, params) {
1462
- const { to: outerTo, ...rest } = props;
1463
- const pn = resolveParams(pathname, params);
1464
- const to = !outerTo || JS$1.is.string(outerTo)
1465
- ? pn
1466
- : {
1467
- ...outerTo,
1468
- pathname: pn
1469
- };
1470
- return Object.assign(rest, { to });
1471
- }
1472
-
1473
1484
  function LinksKindFactory(urlsTree, Component) {
1474
- return TreeFactory(urlsTree, {
1485
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1486
+ const tree = TreeFactory(urlsTree, {
1475
1487
  nodeFactory(node) {
1476
1488
  const id = utilsMisc.HTMLUtils.str2ValidId(node._path);
1477
1489
  return Object.assign(function BindedLink({ id: outerId, params, ...rest }) {
@@ -1480,6 +1492,8 @@ function LinksKindFactory(urlsTree, Component) {
1480
1492
  },
1481
1493
  ...PP$Camel
1482
1494
  });
1495
+ tree.AT = (node) => TreeAT(tree, node);
1496
+ return tree;
1483
1497
  }
1484
1498
  function LinksFactory(urlsTree) {
1485
1499
  return {
@@ -1488,9 +1502,10 @@ function LinksFactory(urlsTree) {
1488
1502
  };
1489
1503
  }
1490
1504
 
1491
- var JS = AVStantso.JS;
1505
+ var JS$1 = AVStantso.JS;
1492
1506
  function NavigatesTreeFactory(urlsTree) {
1493
- return Object.assign(TreeFactory(urlsTree, {
1507
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1508
+ const componentTree = TreeFactory(urlsTree, {
1494
1509
  nodeFactory(node) {
1495
1510
  /**
1496
1511
  * @avstantso: Reimplementation `Navigate` with `preserve`
@@ -1511,18 +1526,25 @@ function NavigatesTreeFactory(urlsTree) {
1511
1526
  return BindedNavigates;
1512
1527
  },
1513
1528
  ...PP$Camel
1514
- }), {
1529
+ });
1530
+ componentTree.AT = (node) => TreeAT(componentTree, node);
1531
+ return Object.assign(componentTree, {
1515
1532
  useNavigate() {
1516
1533
  const navigate = reactRouterDom.useNavigate();
1517
1534
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1518
1535
  function createNavProxy(node, hasParams) {
1519
1536
  return new Proxy(navigate, {
1520
1537
  get(target, p) {
1538
+ if ('AT' === p)
1539
+ return (atNode) => {
1540
+ const hp = atNode._url.split('/').some((s) => s.startsWith(':'));
1541
+ return createNavProxy(atNode, hp);
1542
+ };
1521
1543
  const r = Reflect.get(target, p);
1522
1544
  if (undefined !== r)
1523
1545
  return r;
1524
1546
  for (const [k, v] of Object.entries(node))
1525
- if (JS.is.structure(v)) {
1547
+ if (JS$1.is.structure(v)) {
1526
1548
  const hp = k.startsWith(':');
1527
1549
  if (hp
1528
1550
  ? PP$Camel.paramProcessor(k) === p
@@ -1537,13 +1559,13 @@ function NavigatesTreeFactory(urlsTree) {
1537
1559
  const i = hasParams ? 1 : 0;
1538
1560
  const params = argArray[i - 1];
1539
1561
  const pathname = resolveParams(node._url, params);
1540
- const hasTo = JS.switch(argArray[i], {
1562
+ const hasTo = JS$1.switch(argArray[i], {
1541
1563
  string: true,
1542
1564
  object: (o) => 'search' in o || 'hash' in o
1543
1565
  });
1544
1566
  return Reflect.apply(target, thisArg, hasTo
1545
1567
  ? [
1546
- JS.is.object(argArray[i]) ? { ...argArray[i], pathname } : pathname,
1568
+ JS$1.is.object(argArray[i]) ? { ...argArray[i], pathname } : pathname,
1547
1569
  argArray[i + 1]
1548
1570
  ]
1549
1571
  : [
@@ -1558,12 +1580,34 @@ function NavigatesTreeFactory(urlsTree) {
1558
1580
  });
1559
1581
  }
1560
1582
 
1561
- const RouteCreatorFactory = (urlsTree) => (type, commonProps) => TreeFactory(urlsTree, {
1562
- nodeFactory: (node) => function CMRouteCreate(props) {
1563
- return { ...commonProps, ...props, path: node[`_${type}`] };
1564
- }
1565
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1566
- });
1583
+ var JS = AVStantso.JS;
1584
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1585
+ const RouteCreatorFactory = (urlsTree) => (type, commonProps) => {
1586
+ const nestedTreeCache = new WeakMap();
1587
+ const MetaFactory = (useCache, nodeField = type) => (rootNode) => {
1588
+ if (useCache) {
1589
+ const cached = nestedTreeCache.get(rootNode);
1590
+ if (cached)
1591
+ return cached;
1592
+ }
1593
+ const tree = TreeFactory(rootNode, {
1594
+ nodeFactory: (node) => {
1595
+ const RouteCreate = (param) => ({
1596
+ ...commonProps,
1597
+ ...(JS.is.function(param) ? param(createNestedTree(node)) : param),
1598
+ path: node[`_${nodeField}`]
1599
+ });
1600
+ return RouteCreate;
1601
+ }
1602
+ });
1603
+ if (useCache)
1604
+ nestedTreeCache.set(rootNode, tree);
1605
+ return tree;
1606
+ };
1607
+ const createNestedTree = MetaFactory(true, 'name');
1608
+ const treeFactory = MetaFactory();
1609
+ return treeFactory(urlsTree);
1610
+ };
1567
1611
 
1568
1612
  function ReactRouterTree(urlsTree) {
1569
1613
  return {