react-routes-forge 1.0.3 → 1.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/README.md CHANGED
@@ -1,170 +1,625 @@
1
- # react-routes-forge
2
-
3
- Type-safe route definitions with automatic path builders for React apps.
4
-
5
- ## Why react-routes-forge?
6
-
7
- `react-routes-forge` eliminates the duplicate template/builder pattern used in route definitions. One source of truth defines:
8
-
9
- - static path templates for routing
10
- - dynamic path builders for navigation
11
- - typed params for safer runtime usage
12
-
13
- ## Installation
14
-
15
- ```bash
16
- npm install react-routes-forge
17
- ```
18
-
19
- ## Quick start
20
-
21
- ```ts
22
- import { defineRoutes } from "react-routes-forge";
23
-
24
- export const PATHS = defineRoutes({
25
- HOME: "/",
26
- LOGIN: "/login",
27
- USERS: {
28
- ROOT: "/users",
29
- ADD: "/users/add",
30
- EDIT: "/users/edit/:id",
31
- DETAILS: "/users/:id",
32
- },
33
- ROLES: {
34
- PERMISSIONS: "/roles/permissions/:name",
35
- },
36
- } as const);
37
- ```
38
-
39
- ## Usage
40
-
41
- ### Router definitions
42
-
43
- Use route templates directly in route declarations.
44
-
45
- ```tsx
46
- import { PATHS } from './paths';
47
-
48
- <Route path={PATHS.HOME} />
49
- <Route path={PATHS.USERS.EDIT} />
50
- <Route path={PATHS.ROLES.PERMISSIONS} />
51
- ```
52
-
53
- ### Navigation
54
-
55
- Build resolved paths from dynamic templates.
56
-
57
- ```ts
58
- navigate(PATHS.USERS.EDIT.build({ id: 42 }));
59
- navigate(PATHS.ROLES.PERMISSIONS.build({ name: "admin" }));
60
- navigate(PATHS.HOME);
61
- ```
62
-
63
- ### Dynamic routes keep plain-string behavior
64
-
65
- Dynamic routes remain usable as strings while gaining helper methods.
66
-
67
- ```ts
68
- PATHS.USERS.EDIT; // '/users/edit/:id'
69
- PATHS.USERS.EDIT.build({ id: 42 }); // '/users/edit/42'
70
- PATHS.USERS.EDIT.paramNames; // ['id']
71
- ```
72
-
73
- ## API
74
-
75
- ### `defineRoutes(routeMap)`
76
-
77
- Create a typed route object from a nested route definition.
78
-
79
- - static routes remain plain strings
80
- - dynamic routes gain `.build(params)` and `.paramNames`
81
-
82
- ### `build(template, params)`
83
-
84
- Resolve a route template without using `defineRoutes`.
85
-
86
- ```ts
87
- import { build } from "react-routes-forge";
88
-
89
- build("/users/:id/posts/:postId", { id: 1, postId: 42 });
90
- // '/users/1/posts/42'
91
- ```
92
-
93
- ### `isActivePath(currentPath, template, options?)`
94
-
95
- Check whether a path matches a template.
96
-
97
- ```ts
98
- import { isActivePath } from "react-routes-forge";
99
-
100
- isActivePath("/users/42", "/users/:id");
101
- isActivePath("/users/42/posts", "/users/:id");
102
- isActivePath("/users/42/posts", "/users/:id", { exact: false });
103
- ```
104
-
105
- ### `extractParamsFromPath(template, resolvedPath)`
106
-
107
- Extract path params from a resolved route.
108
-
109
- ```ts
110
- import { extractParamsFromPath } from "react-routes-forge";
111
-
112
- extractParamsFromPath("/users/:id", "/users/42");
113
- // { id: '42' }
114
- ```
115
-
116
- ### `joinPaths(...segments)`
117
-
118
- Join path fragments and normalise slashes.
119
-
120
- ```ts
121
- import { joinPaths } from "react-routes-forge";
122
-
123
- joinPaths("/api/", "/v1/", "/users");
124
- // '/api/v1/users'
125
- ```
126
-
127
- ### `getParamNames(template)`
128
-
129
- Return all parameter names from a template.
130
-
131
- ```ts
132
- import { getParamNames } from "react-routes-forge";
133
-
134
- getParamNames("/users/:id/posts/:postId");
135
- // ['id', 'postId']
136
- ```
137
-
138
- ## React hooks
139
-
140
- Import only when using React Router.
141
-
142
- ### `useRouteParams<T>()`
143
-
144
- Typed wrapper around React Router's `useParams`.
145
-
146
- ```tsx
147
- import { useRouteParams } from "react-routes-forge";
148
-
149
- function EditUser() {
150
- const { id } = useRouteParams<"/users/edit/:id">();
151
- return <div>{id}</div>;
152
- }
153
- ```
154
-
155
- ### `useNavigateTo()`
156
-
157
- Thin, typed wrapper around React Router's `useNavigate()`.
158
-
159
- ```tsx
160
- import { useNavigateTo } from "react-routes-forge";
161
-
162
- function Component() {
163
- const navigateTo = useNavigateTo();
164
- return (
165
- <button onClick={() => navigateTo(PATHS.USERS.EDIT.build({ id: 42 }))}>
166
- Edit
167
- </button>
168
- );
169
- }
170
- ```
1
+ # react-routes-forge
2
+
3
+ **Type-safe route definitions with automatic path builders for React apps.**
4
+
5
+ One source of truth for your routes — templates for `<Route path={...} />` and typed builders for navigation — with no duplication and no manual string concatenation.
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](#)
9
+
10
+ ---
11
+
12
+ ## Table of contents
13
+
14
+ - [Why react-routes-forge?](#why-react-routes-forge)
15
+ - [Installation](#installation)
16
+ - [Quick start](#quick-start)
17
+ - [Cheat sheet](#cheat-sheet)
18
+ - [Core concepts](#core-concepts)
19
+ - [API reference](#api-reference)
20
+ - [`defineRoutes(routeMap)`](#defineroutesroutemap)
21
+ - [`build(template, params, query?, options?)`](#buildtemplate-params-query-options)
22
+ - [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options)
23
+ - [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath)
24
+ - [`joinPaths(...segments)`](#joinpathssegments)
25
+ - [`getParamNames(template)`](#getparamnamestemplate)
26
+ - [`flattenRoutes(routes)`](#flattenroutesroutes)
27
+ - [React hooks](#react-hooks)
28
+ - [`useRouteParams<T>()`](#useroutparamst)
29
+ - [`useNavigateTo()`](#usenavigateto)
30
+ - [`useResolvedPath(template, params, query?, options?)`](#useresolvedpathtemplate-params-query-options)
31
+ - [Query string support](#query-string-support)
32
+ - [Strict mode](#strict-mode)
33
+ - [Migrating from the old pattern](#migrating-from-the-old-pattern)
34
+ - [Known behaviours & gotchas](#known-behaviours--gotchas)
35
+ - [TypeScript support](#typescript-support)
36
+ - [Testing](#testing)
37
+ - [Requirements](#requirements)
38
+ - [Contributing](#contributing)
39
+ - [License](#license)
40
+
41
+ ---
42
+
43
+ ## Why react-routes-forge?
44
+
45
+ Most React apps end up with route definitions like this:
46
+
47
+ ```ts
48
+ // The common pattern
49
+ export const PATHS = {
50
+ USERS: {
51
+ ROOT: "/users",
52
+ DETAILS: "/users/:id",
53
+ },
54
+ };
55
+ ```
56
+
57
+ Dynamic routes typically need a **second** entry alongside the template — a hand-written function to build the real URL. As the app grows, the two drift apart, and nothing stops the template and the builder from disagreeing.
58
+
59
+ `react-routes-forge` collapses both into a single key:
60
+
61
+ ```ts
62
+ // ✅ One key, two uses
63
+ export const PATHS = defineRoutes({
64
+ USERS: {
65
+ ROOT: "/users",
66
+ DETAILS: "/users/:id",
67
+ },
68
+ } as const);
69
+
70
+ PATHS.USERS.ROOT; // '/users' → static, used directly
71
+ PATHS.USERS.DETAILS; // '/users/:id' → use in <Route path={...} />
72
+ PATHS.USERS.DETAILS.build({ id: 42 }); // '/users/42' → use when navigating
73
+ ```
74
+
75
+ You get:
76
+
77
+ - **Single source of truth** no duplicate template/builder pairs to keep in sync
78
+ - **Compile-time param safety** — `.build()` is typed from the path string itself; missing or misspelled params are TypeScript errors
79
+ - **Query string support** built into `.build()`, no manual `URLSearchParams` wrangling
80
+ - **Zero runtime dependencies** for the core API — React Router is an optional peer dependency, only required if you use the hooks
81
+ - **Deep nesting supported out of the box** — organize routes into as many nested groups as your app needs
82
+
83
+ ---
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ npm install react-routes-forge
89
+ # or
90
+ pnpm add react-routes-forge
91
+ # or
92
+ yarn add react-routes-forge
93
+ # or
94
+ bun add react-routes-forge
95
+ ```
96
+
97
+ React Router is an **optional** peer dependency — only needed if you use the bundled hooks (`useRouteParams`, `useNavigateTo`, `useResolvedPath`).
98
+
99
+ ```bash
100
+ npm install react-router-dom # only if you're using the hooks
101
+ # or
102
+ pnpm add react-router-dom
103
+ # or
104
+ yarn add react-router-dom
105
+ # or
106
+ bun add react-router-dom
107
+ ```
108
+
109
+ > **Note:** this package ships ESM-only. See [Known behaviours & gotchas](#known-behaviours--gotchas) for details.
110
+
111
+ ---
112
+
113
+ ## Quick start
114
+
115
+ ```ts
116
+ // paths.ts
117
+ import { defineRoutes } from "react-routes-forge";
118
+
119
+ export const PATHS = defineRoutes({
120
+ HOME: "/",
121
+ LOGIN: "/login",
122
+ USERS: {
123
+ ROOT: "/users",
124
+ ADD: "/users/add",
125
+ EDIT: "/users/edit/:id",
126
+ DETAILS: "/users/:id",
127
+ },
128
+ ROLES: {
129
+ PERMISSIONS: "/roles/permissions/:name",
130
+ },
131
+ } as const);
132
+ ```
133
+
134
+ ```tsx
135
+ // App.tsx — static paths and dynamic templates both work directly as strings
136
+ import { Routes, Route } from "react-router-dom";
137
+ import { PATHS } from "./paths";
138
+
139
+ <Routes>
140
+ <Route path={PATHS.HOME} element={<Home />} />
141
+ <Route path={PATHS.USERS.ROOT} element={<UserList />} />
142
+ <Route path={PATHS.USERS.EDIT} element={<EditUser />} />
143
+ <Route path={PATHS.ROLES.PERMISSIONS} element={<RolePermissions />} />
144
+ </Routes>;
145
+ ```
146
+
147
+ ```ts
148
+ // Navigating — call .build() to resolve a dynamic path into a real URL
149
+ navigate(PATHS.USERS.EDIT.build({ id: 42 })); // '/users/edit/42'
150
+ navigate(PATHS.ROLES.PERMISSIONS.build({ name: "admin" })); // '/roles/permissions/admin'
151
+ navigate(PATHS.HOME); // '/'
152
+ ```
153
+
154
+ That's the entire API surface you need for most apps. Everything below covers the rest of the toolkit.
155
+
156
+ ---
157
+
158
+ ## Cheat sheet
159
+
160
+ Quick reference for everything the package exports. Click through to the full section for details and examples.
161
+
162
+ | Export | Kind | Purpose |
163
+ | ---------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- |
164
+ | [`defineRoutes(routeMap)`](#defineroutesroutemap) | function | Builds the typed `PATHS` object from a nested route map |
165
+ | [`build(template, params, query?, options?)`](#buildtemplate-params-query-options) | function | Resolve a template into a URL without `defineRoutes` |
166
+ | [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options) | function | Check if a path matches a template (nav-highlighting) |
167
+ | [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath) | function | Pull param values back out of a resolved URL |
168
+ | [`joinPaths(...segments)`](#joinpathssegments) | function | Join and normalize path segments |
169
+ | [`getParamNames(template)`](#getparamnamestemplate) | function | List the `:param` names in a template |
170
+ | [`flattenRoutes(routes)`](#flattenroutesroutes) | function | Flatten a `PATHS` tree for sitemaps / duplicate detection |
171
+ | [`useRouteParams<T>()`](#useroutparamst) | hook | Typed wrapper around React Router's `useParams` |
172
+ | [`useNavigateTo()`](#usenavigateto) | hook | Typed wrapper around React Router's `useNavigate` |
173
+ | [`useResolvedPath(...)`](#useresolvedpathtemplate-params-query-options) | hook | Resolve a template to a string without navigating |
174
+ | `.build(params, query?, options?)` | method | On every dynamic route — resolves to a concrete URL |
175
+ | `.paramNames` | property | On every dynamic route — the param names it expects |
176
+
177
+ ---
178
+
179
+ ## Core concepts
180
+
181
+ | Route type | Example | Behaves as | Gains |
182
+ | ----------- | ----------------------- | --------------------------------------- | ---------------------------------------------------- |
183
+ | **Static** | `HOME: '/'` | Plain string primitive | Nothing extra — use it directly |
184
+ | **Dynamic** | `DETAILS: '/users/:id'` | String-like (coercible to its template) | `.build(params, query?, options?)` and `.paramNames` |
185
+
186
+ `defineRoutes()` walks your route object recursively, leaving static paths untouched and wrapping any path containing a `:param` segment so it can carry a builder alongside its template string.
187
+
188
+ ---
189
+
190
+ ## API reference
191
+
192
+ ### `defineRoutes(routeMap)`
193
+
194
+ Creates a fully typed route object from a nested plain object.
195
+
196
+ - Static paths are returned as-is — use them directly anywhere a string is expected (e.g. `<Route path={...} />`).
197
+ - Dynamic paths (containing `:param`) gain:
198
+ - **`.build(params, query?, options?)`** — resolves the template into a concrete URL
199
+ - **`.paramNames`** — array of the param names extracted from the template, e.g. `['id']`
200
+
201
+ Nesting is unlimited — organize routes into as many groups and sub-groups as your app needs.
202
+
203
+ ```ts
204
+ const PATHS = defineRoutes({
205
+ SERVICES: {
206
+ ROOT: "/services",
207
+ BENEFICIARY_CARE_CENTER: {
208
+ DETAILS: "/services/beneficiary-care-center/:id",
209
+ EDIT: "/services/beneficiary-care-center/edit/:id",
210
+ },
211
+ },
212
+ } as const);
213
+
214
+ PATHS.SERVICES.ROOT; // '/services'
215
+ PATHS.SERVICES.BENEFICIARY_CARE_CENTER.EDIT.build({ id: 7 }); // '/services/beneficiary-care-center/edit/7'
216
+ PATHS.SERVICES.BENEFICIARY_CARE_CENTER.EDIT.paramNames; // ['id']
217
+ ```
218
+
219
+ > Always pass `as const` to `defineRoutes()` — it preserves the literal string types that power `.build()`'s compile-time param checking.
220
+
221
+ ---
222
+
223
+ ### `build(template, params, query?, options?)`
224
+
225
+ Standalone path resolver — for building a URL without going through `defineRoutes`, or for adding a query string to a **static** path (which has no `.build()` of its own).
226
+
227
+ ```ts
228
+ import { build } from "react-routes-forge";
229
+
230
+ build("/users/:id/posts/:postId", { id: 1, postId: 42 });
231
+ // → '/users/1/posts/42'
232
+
233
+ // Append a query string to any path — including static ones
234
+ build("/users", {}, { sort: "asc" });
235
+ // → '/users?sort=asc'
236
+
237
+ // Strict mode — throw instead of warn when a param is missing
238
+ build("/users/:id", {}, undefined, { strict: true });
239
+ // ✗ throws RangeError: [route-forge] Missing required param(s) ":id" in template "/users/:id".
240
+ ```
241
+
242
+ ---
243
+
244
+ ### `isActivePath(currentPath, template, options?)`
245
+
246
+ Checks whether a resolved path matches a route template — the building block for nav-highlighting ("is this link active?"). Query strings on `currentPath` are ignored automatically.
247
+
248
+ ```ts
249
+ import { isActivePath } from "react-routes-forge";
250
+
251
+ isActivePath("/users/42", "/users/:id"); // true
252
+ isActivePath("/users/42/posts", "/users/:id"); // false (exact match by default)
253
+ isActivePath("/users/42/posts", "/users/:id", { exact: false }); // true (prefix match)
254
+ isActivePath("/users/42?tab=profile", "/users/:id"); // true (query string ignored)
255
+ ```
256
+
257
+ A common real-world use — highlighting the active nav link:
258
+
259
+ ```tsx
260
+ function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
261
+ const location = useLocation();
262
+ const active = isActivePath(location.pathname, to, { exact: false });
263
+
264
+ return (
265
+ <Link to={to} className={active ? "nav-link active" : "nav-link"}>
266
+ {children}
267
+ </Link>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ### `extractParamsFromPath(template, resolvedPath)`
275
+
276
+ Extracts param values back out of a resolved URL, given its template. Also strips query strings before matching.
277
+
278
+ ```ts
279
+ import { extractParamsFromPath } from "react-routes-forge";
280
+
281
+ extractParamsFromPath("/users/:id", "/users/42");
282
+ // → { id: '42' }
283
+
284
+ extractParamsFromPath("/a/:x/b/:y", "/a/foo/b/bar");
285
+ // → { x: 'foo', y: 'bar' }
286
+ ```
287
+
288
+ ---
289
+
290
+ ### `joinPaths(...segments)`
291
+
292
+ Safely joins path segments, normalizing duplicate/missing slashes.
293
+
294
+ ```ts
295
+ import { joinPaths } from "react-routes-forge";
296
+
297
+ joinPaths("/users", "edit", ":id"); // → '/users/edit/:id'
298
+ joinPaths("/api/", "/v1/", "/users"); // → '/api/v1/users'
299
+ ```
300
+
301
+ ---
302
+
303
+ ### `getParamNames(template)`
304
+
305
+ Returns the list of param names present in a template string.
306
+
307
+ ```ts
308
+ import { getParamNames } from "react-routes-forge";
309
+
310
+ getParamNames("/users/:id/posts/:postId"); // → ['id', 'postId']
311
+ getParamNames("/users"); // → []
312
+ ```
313
+
314
+ ---
315
+
316
+ ### `flattenRoutes(routes)`
317
+
318
+ Walks a `defineRoutes()` tree and returns a flat array of `{ key, path }` entries, where `key` is the dot-joined path from the root (e.g. `"SERVICES.BENEFICIARY_CARE_CENTER.EDIT"`) and `path` is the raw template string.
319
+
320
+ Primary uses:
321
+
322
+ - **Sitemap generation** — one call gives you every route in the app.
323
+ - **Duplicate detection** — catch the same path string defined under two different keys before it ships.
324
+
325
+ ```ts
326
+ import { defineRoutes, flattenRoutes } from "react-routes-forge";
327
+
328
+ const PATHS = defineRoutes({
329
+ HOME: "/",
330
+ USERS: {
331
+ ROOT: "/users",
332
+ EDIT: "/users/edit/:id",
333
+ },
334
+ } as const);
335
+
336
+ flattenRoutes(PATHS);
337
+ // [
338
+ // { key: 'HOME', path: '/' },
339
+ // { key: 'USERS.ROOT', path: '/users' },
340
+ // { key: 'USERS.EDIT', path: '/users/edit/:id' },
341
+ // ]
342
+
343
+ // Detect duplicate paths across the tree
344
+ const flat = flattenRoutes(PATHS);
345
+ const paths = flat.map((r) => r.path);
346
+ const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);
347
+ if (dupes.length) console.warn("Duplicate route paths:", dupes);
348
+ ```
349
+
350
+ A useful pattern is running the duplicate check once at app startup (or in a test) so a copy-paste route collision fails fast instead of surfacing as a confusing routing bug later:
351
+
352
+ ```ts
353
+ // routes.test.ts
354
+ it("has no duplicate route paths", () => {
355
+ const paths = flattenRoutes(PATHS).map((r) => r.path);
356
+ const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);
357
+ expect(dupes).toEqual([]);
358
+ });
359
+ ```
360
+
361
+ ---
362
+
363
+ ## React hooks
364
+
365
+ Import these only if you're using React Router — they're tree-shakeable and won't be bundled unless imported.
366
+
367
+ ### `useRouteParams<T>()`
368
+
369
+ Typed wrapper around React Router's `useParams`. Pass the route's template string as a generic to get a correctly typed params object back — no casting, and it works for any number of `:param` segments.
370
+
371
+ ```tsx
372
+ import { useRouteParams } from "react-routes-forge";
373
+
374
+ // Route: '/users/edit/:id'
375
+ function EditUser() {
376
+ const { id } = useRouteParams<"/users/edit/:id">();
377
+ return <div>Editing user {id}</div>;
378
+ }
379
+
380
+ // Multiple params also work correctly
381
+ // Route: '/posts/:postId/comments/:commentId'
382
+ function Comment() {
383
+ const { postId, commentId } =
384
+ useRouteParams<"/posts/:postId/comments/:commentId">();
385
+ // ...
386
+ }
387
+ ```
388
+
389
+ ---
390
+
391
+ ### `useNavigateTo()`
392
+
393
+ Thin, typed wrapper around `useNavigate()` that accepts a resolved path (the output of `.build()`) along with the usual navigation options.
394
+
395
+ ```tsx
396
+ import { useNavigateTo } from "react-routes-forge";
397
+ import { PATHS } from "./paths";
398
+
399
+ function Component() {
400
+ const navigateTo = useNavigateTo();
401
+
402
+ return (
403
+ <button onClick={() => navigateTo(PATHS.USERS.EDIT.build({ id: 42 }))}>
404
+ Edit
405
+ </button>
406
+ );
407
+ }
408
+
409
+ navigateTo(PATHS.HOME, { replace: true });
410
+ navigateTo(PATHS.USERS.ROOT, { state: { from: "settings" } });
411
+ ```
412
+
413
+ ---
414
+
415
+ ### `useResolvedPath(template, params, query?, options?)`
416
+
417
+ Resolves a path template to a concrete URL string without navigating — useful for `<Link to={...} />`, preloading, or building a URL for something other than `navigate()`. Backed by React Router's `generatePath`, so it correctly supports splat (`*`) and optional (`:param?`) segments. Accepts the same `query` and `options` as [`build()`](#buildtemplate-params-query-options).
418
+
419
+ ```tsx
420
+ import { useResolvedPath } from "react-routes-forge";
421
+
422
+ const path = useResolvedPath("/users/:id", { id: 42 });
423
+ // → '/users/42'
424
+
425
+ const path = useResolvedPath("/users/:id", { id: 42 }, { tab: "info" });
426
+ // → '/users/42?tab=info'
427
+
428
+ // Strict mode — throws RangeError instead of warning on missing params
429
+ const path = useResolvedPath("/users/:id", {}, undefined, { strict: true });
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Query string support
435
+
436
+ Every path-resolving function — `.build()`, `build()`, and `useResolvedPath()` — accepts an optional query object as its second-to-last argument.
437
+
438
+ ```ts
439
+ navigate(
440
+ PATHS.USERS.DETAILS.build({ id: 42 }, { tab: "billing", sort: "asc" }),
441
+ );
442
+ // → '/users/42?tab=billing&sort=asc'
443
+ ```
444
+
445
+ **Array values** are serialized as repeated keys:
446
+
447
+ ```ts
448
+ build("/search", {}, { tags: ["admin", "moderator"] });
449
+ // → '/search?tags=admin&tags=moderator'
450
+ ```
451
+
452
+ **`null` and `undefined` values are dropped**, so you can pass optional filters without conditionally building the object:
453
+
454
+ ```ts
455
+ build("/users", {}, { sort: "asc", filter: undefined });
456
+ // → '/users?sort=asc'
457
+ ```
458
+
459
+ Static routes don't have a fluent `.build()` (there's nothing to interpolate), so use the standalone `build()` util to attach a query string to them:
460
+
461
+ ```ts
462
+ import { build } from "react-routes-forge";
463
+
464
+ build(PATHS.USERS.ROOT, {}, { sort: "asc", page: 2 });
465
+ // → '/users?sort=asc&page=2'
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Strict mode
471
+
472
+ By default, a missing required param leaves the `:param` placeholder in the resolved string and logs a `console.warn` — useful for catching bugs during development without crashing the app.
473
+
474
+ Pass `{ strict: true }` as the last argument to any builder to throw a `RangeError` instead:
475
+
476
+ ```ts
477
+ build("/users/:id", {}, undefined, { strict: true });
478
+ // ✗ throws RangeError: [route-forge] Missing required param(s) ":id" in template "/users/:id".
479
+ ```
480
+
481
+ This is consistent across the whole API surface:
482
+
483
+ | API | Default (no `strict`) | `{ strict: true }` |
484
+ | -------------------------------------- | --------------------------------------------- | ------------------- |
485
+ | `.build()` (fluent, on dynamic routes) | `console.warn`, leaves `:param` in the string | throws `RangeError` |
486
+ | `build()` / `buildPath()` (standalone) | `console.warn`, leaves `:param` in the string | throws `RangeError` |
487
+ | `useResolvedPath()` | `console.warn`, leaves `:param` in the string | throws `RangeError` |
488
+
489
+ A common pattern is enabling strict mode only in tests or development builds:
490
+
491
+ ```ts
492
+ const opts = { strict: process.env.NODE_ENV === "test" };
493
+ navigate(PATHS.USERS.EDIT.build({ id: userId }, undefined, opts));
494
+ ```
495
+
496
+ ---
497
+
498
+ ## Migrating from the old pattern
499
+
500
+ If your routes currently look like this:
501
+
502
+ ```ts
503
+ // ❌ Before
504
+ export const PATHS = {
505
+ SERVICES: {
506
+ ROOT: "/services",
507
+ DETAILS: "/services/:id",
508
+ },
509
+ };
510
+
511
+ navigate(`/services/${id}`);
512
+ navigate(`${PATHS.SERVICES.ROOT}/${id}`);
513
+ ```
514
+
515
+ Drop the manual string concatenation and wrap the object in `defineRoutes()`:
516
+
517
+ ```ts
518
+ // ✅ After
519
+ export const PATHS = defineRoutes({
520
+ SERVICES: {
521
+ ROOT: "/services",
522
+ DETAILS: "/services/:id",
523
+ },
524
+ } as const);
525
+ ```
526
+
527
+ Update call sites to use `.build()` instead of template literals or hand-written helper functions:
528
+
529
+ ```ts
530
+ // Before
531
+ navigate(`/services/${id}`);
532
+ navigate(`${PATHS.SERVICES.ROOT}/${id}`);
533
+
534
+ // After
535
+ navigate(PATHS.SERVICES.DETAILS.build({ id }));
536
+ ```
537
+
538
+ Everywhere the template string itself was used (e.g. `<Route path={PATHS.SERVICES.DETAILS} />`) needs no changes at all.
539
+
540
+ ---
541
+
542
+ ## Known behaviours & gotchas
543
+
544
+ ### Dynamic routes are `String` objects, not primitives
545
+
546
+ `defineRoutes` wraps dynamic paths in [`String` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) so that `.build()` and `.paramNames` can be attached as properties. This means:
547
+
548
+ ```ts
549
+ // ✓ These all work as expected
550
+ String(PATHS.USERS.EDIT); // '/users/edit/:id'
551
+ `${PATHS.USERS.EDIT}`; // '/users/edit/:id'
552
+ PATHS.USERS.EDIT == "/users/edit/:id"; // true (loose equality)
553
+
554
+ // ✗ Watch out for these
555
+ typeof PATHS.USERS.EDIT; // 'object' ← not 'string'
556
+ PATHS.USERS.EDIT === "/users/edit/:id"; // false ← strict equality fails
557
+ ```
558
+
559
+ Prefer template literals or explicit `String()` coercion when comparing dynamic route values, and avoid using them as plain object/`Map` keys. Static routes (`HOME`, `USERS.ROOT`, …) remain genuine string primitives and are unaffected.
560
+
561
+ ### `useResolvedPath` vs. the library's own `buildPath`
562
+
563
+ `useResolvedPath` delegates to React Router's `generatePath` when all params are present, which correctly handles splat (`*`) and optional (`:param?`) segments that the library's own regex-based substitution does not. If params are missing, it falls back to the same `buildPath`/strict-mode behaviour described above — so failure modes stay consistent, but full splat/optional support is only guaranteed via the hook, not via `.build()`.
564
+
565
+ ### ESM-only package
566
+
567
+ This package ships **ESM only** (`"type": "module"`, no `require` export condition). Consumers on a plain CommonJS setup (`require('react-routes-forge')`) are not supported. All modern bundlers (Vite, Webpack ≥ 5, esbuild, Rollup) handle ESM packages transparently. If you're in a CJS-only environment, you'll need a bundler transform or a compatibility shim.
568
+
569
+ ---
570
+
571
+ ## TypeScript support
572
+
573
+ Written in strict TypeScript with no `any` in the public API surface. Param types for `.build()` are inferred directly from each path template via a recursive template-literal type, so:
574
+
575
+ ```ts
576
+ PATHS.USERS.EDIT.build({ id: 42 }); // ✓ compiles
577
+ PATHS.USERS.EDIT.build({}); // ✗ compile error — 'id' is required
578
+ PATHS.USERS.EDIT.build({ userId: 42 }); // ✗ compile error — 'id' expected, not 'userId'
579
+ ```
580
+
581
+ `.paramNames` is similarly typed as a literal array of the exact param names in the template, not a generic `string[]`.
582
+
583
+ ---
584
+
585
+ ## Testing
586
+
587
+ The package ships with a full test suite covering the core builder/utility functions and the React hooks, including strict-mode behaviour, query string edge cases (arrays, `null`/`undefined` filtering), nested route groups, and duplicate-path detection via `flattenRoutes`.
588
+
589
+ ```bash
590
+ npm test # run the full suite once
591
+ npm run test:watch # watch mode
592
+
593
+ # equivalent with other package managers
594
+ pnpm test / pnpm test:watch
595
+ yarn test / yarn test:watch
596
+ bun test / bun test:watch
597
+ ```
598
+
599
+ If you're contributing, new behaviour should come with a matching test — the existing suite is organized by function/hook, so add cases alongside the relevant `describe` block rather than starting a new file.
600
+
601
+ ---
602
+
603
+ ## Requirements
604
+
605
+ - **React** ≥ 17 (peer dependency)
606
+ - **react-router-dom** ≥ 6 (optional peer dependency — required only for the bundled hooks)
607
+ - **Node.js** ≥ 18
608
+ - **TypeScript** ≥ 5 recommended for full type inference (the package works with plain JavaScript too, just without compile-time param checking)
609
+
610
+ ---
611
+
612
+ ## Contributing
613
+
614
+ Issues and pull requests are welcome.
615
+
616
+ 1. Fork the repo and create a branch for your change.
617
+ 2. Add or update tests for any behavioural change — see [Testing](#testing).
618
+ 3. Run `npm run lint` and `npm test` before opening a PR.
619
+ 4. Keep commit messages conventional (`feat:`, `fix:`, `docs:`, …) — this repo uses [standard-version](https://github.com/conventional-changelog/standard-version) for releases.
620
+
621
+ ---
622
+
623
+ ## License
624
+
625
+ MIT
package/dist/index.d.ts CHANGED
@@ -33,8 +33,37 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
33
33
  type PathParams<T extends string> = ExtractParams<T> extends never ? never : {
34
34
  [K in ExtractParams<T>]: RouteParam;
35
35
  };
36
+ /**
37
+ * Acceptable query param value types for route builders.
38
+ */
39
+ type QueryParams = Record<string, RouteParam | RouteParam[] | null | undefined>;
40
+ /**
41
+ * Options accepted by `buildPath` (4th positional argument).
42
+ *
43
+ * @example
44
+ * // Throws a RangeError when a :param segment is missing rather than
45
+ * // silently leaving the colon-placeholder in the output string.
46
+ * buildPath('/users/:id', {}, undefined, { strict: true });
47
+ */
48
+ type BuildPathOptions = {
49
+ /**
50
+ * When `true`, `buildPath` throws a `RangeError` if any `:param`
51
+ * placeholder is left unresolved instead of emitting a console.warn.
52
+ * Useful in dev/test environments to catch missing params early.
53
+ */
54
+ strict?: boolean;
55
+ };
56
+ /**
57
+ * A single entry produced by `flattenRoutes()`.
58
+ */
59
+ type FlatRoute = {
60
+ /** Dot-joined key path from the root, e.g. `"SERVICES.BCC.EDIT"`. */
61
+ key: string;
62
+ /** The raw path template string, e.g. `"/services/bcc/edit/:id"`. */
63
+ path: string;
64
+ };
36
65
 
37
- declare function buildPath(template: string, params: Record<string, RouteParam>): string;
66
+ declare function buildPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
38
67
  declare function extractParamNames(template: string): string[];
39
68
  declare function isDynamic(path: string): boolean;
40
69
  declare function isActivePath(currentPath: string, template: string, options?: {
@@ -42,14 +71,31 @@ declare function isActivePath(currentPath: string, template: string, options?: {
42
71
  }): boolean;
43
72
  declare function extractParamsFromPath(template: string, resolvedPath: string): Record<string, string>;
44
73
  declare function joinPaths(...segments: string[]): string;
45
- declare function build(template: string, params: Record<string, RouteParam>): string;
74
+ declare function build(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
46
75
  declare function getParamNames(template: string): string[];
76
+ /**
77
+ * Walk a `defineRoutes` output tree and return a flat array of
78
+ * `{ key, path }` entries where `key` is the dot-joined key path from
79
+ * the root (e.g. `"SERVICES.BCC.EDIT"`) and `path` is the raw template
80
+ * string (e.g. `"/services/bcc/edit/:id"`).
81
+ *
82
+ * Useful for:
83
+ * - Generating sitemaps from a single source of truth.
84
+ * - Detecting duplicate path strings across branches at startup:
85
+ *
86
+ * @example
87
+ * const flat = flattenRoutes(PATHS);
88
+ * const paths = flat.map((r) => r.path);
89
+ * const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);
90
+ * if (dupes.length) console.warn('Duplicate paths:', dupes);
91
+ */
92
+ declare function flattenRoutes(routes: Record<string, unknown>, prefix?: string): FlatRoute[];
47
93
 
48
94
  type RouteInput = {
49
95
  [key: string]: string | RouteInput;
50
96
  };
51
97
  type DynamicRoute<T extends string> = T extends `${string}:${string}` ? T & {
52
- build(params: PathParams<T>): RoutePath;
98
+ build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;
53
99
  paramNames: Array<ExtractParams<T>>;
54
100
  } : T;
55
101
  type ResolvedRoutes<T extends RouteInput> = {
@@ -65,16 +111,13 @@ declare function defineRoutes<T extends RouteInput>(routes: T): ResolvedRoutes<T
65
111
  /**
66
112
  * A typed wrapper around React Router's `useParams`.
67
113
  *
68
- * Pass the route's path template as a const generic to get a properly typed
69
- * params object back — no casting needed.
70
- *
71
114
  * @example
72
115
  * ```tsx
73
- * // Route is defined as '/users/edit/:id'
74
- * const { id } = useRouteParams<'/users/edit/:id'>();
116
+ * // Route: '/a/:x/b/:y/c/:z'
117
+ * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();
75
118
  * ```
76
119
  */
77
- declare function useRouteParams<T extends string, K extends string = T extends `${string}:${infer P}/${infer R}` ? P | (R extends `${string}:${infer Q}` ? Q : never) : T extends `${string}:${infer P}` ? P : never>(): Record<K, string>;
120
+ declare function useRouteParams<T extends string>(): Record<ExtractParams<T>, string>;
78
121
  type NavigateOptions = {
79
122
  replace?: boolean;
80
123
  state?: unknown;
@@ -95,13 +138,19 @@ declare function useNavigateTo(): (path: string, options?: NavigateOptions) => v
95
138
  * Resolves a dynamic path template against params using React Router's
96
139
  * `generatePath`, with proper typing.
97
140
  *
98
- * Useful when you need the resolved path string without navigating.
141
+ * Accepts the same `options` bag as `build()` / `buildPath()`:
142
+ * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.
143
+ * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.
144
+ *
145
+ * When all params are present, resolution is delegated to React Router's `generatePath`,
146
+ * which correctly handles splat (`*`) and optional (`:param?`) segments.
99
147
  *
100
148
  * @example
101
149
  * ```tsx
102
150
  * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'
151
+ * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError
103
152
  * ```
104
153
  */
105
- declare function useResolvedPath(template: string, params: Record<string, RouteParam>): string;
154
+ declare function useResolvedPath(template: string, params: RouteParams, query?: QueryParams, options?: BuildPathOptions): string;
106
155
 
107
- export { type ExtractParams, type PathParams, type RouteBuilder, type RouteLeaf, type RouteMap, type RouteParam, type RouteParams, type RoutePath, build, buildPath, defineRoutes, extractParamNames, extractParamsFromPath, getParamNames, isActivePath, isDynamic, joinPaths, useNavigateTo, useResolvedPath, useRouteParams };
156
+ export { type BuildPathOptions, type ExtractParams, type FlatRoute, type PathParams, type QueryParams, type RouteBuilder, type RouteLeaf, type RouteMap, type RouteParam, type RouteParams, type RoutePath, build, buildPath, defineRoutes, extractParamNames, extractParamsFromPath, flattenRoutes, getParamNames, isActivePath, isDynamic, joinPaths, useNavigateTo, useResolvedPath, useRouteParams };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var c=()=>/:([^/]+)/g,f=()=>/[.*+?^${}()|[\]\\]/g;function g(t){return t.replace(f(),"\\$&")}function R(t){return g(t).replace(c(),"([^/]+)")}function i(t,e){let r=a(t).reduce((u,s)=>{let p=e[s],x=p===void 0?`:${s}`:String(p);return u.replace(new RegExp(`:${g(s)}(?=/|$)`,"g"),x)},t);return globalThis.process?.env?.NODE_ENV!=="production"&&r.includes(":")&&console.warn(`[route-forge] Unresolved params in path "${r}". Check that all :param segments have matching keys.`),r}function a(t){return[...t.matchAll(c())].map(e=>e[1])}function m(t){return c().test(t)}function d(t,e,n={exact:!0}){let r=R(e);return(n.exact?new RegExp(`^${r}$`):new RegExp(`^${r}`)).test(t)}function l(t,e){let n=a(t),r=new RegExp(`^${R(t)}$`),o=e.match(r);return o?Object.fromEntries(n.map((u,s)=>[u,o[s+1]??""])):{}}function T(...t){return"/"+t.map(e=>e.replace(/^\/+|\/+$/g,"")).filter(Boolean).join("/")}function y(t,e){return i(t,e)}function h(t){return a(t)}var v=t=>typeof t=="object"&&t!==null;function $(t){let e=a(t),n=new String(t);return n.build=r=>i(t,r),n.paramNames=e,n}function P(t){let e={};for(let n in t){if(!Object.prototype.hasOwnProperty.call(t,n))continue;let r=t[n];typeof r=="string"?e[n]=m(r)?$(r):r:v(r)&&(e[n]=P(r))}return e}function b(t){return P(t)}import{useParams as N,useNavigate as E,generatePath as w}from"react-router-dom";function k(){return N()}function D(){let t=E();return(e,n)=>{t(e,n)}}function A(t,e){return w(t,Object.fromEntries(Object.entries(e).map(([n,r])=>[n,String(r)])))}export{y as build,i as buildPath,b as defineRoutes,a as extractParamNames,l as extractParamsFromPath,h as getParamNames,d as isActivePath,m as isDynamic,T as joinPaths,D as useNavigateTo,A as useResolvedPath,k as useRouteParams};
1
+ var l=()=>/:([^/]+)/g,x=()=>/[.*+?^${}()|[\]\\]/g;function f(t){return t.replace(x(),"\\$&")}function R(t){return f(t).replace(l(),"([^/]+)")}function P(t,e){if(!e)return t;let r=new URLSearchParams;for(let[s,a]of Object.entries(e))a!=null&&(Array.isArray(a)?a.forEach(i=>{i!=null&&r.append(s,String(i))}):r.append(s,String(a)));let n=r.toString();return n?t+(t.includes("?")?"&":"?")+n:t}function p(t,e,r,n){let s=u(t),a=s.filter(o=>e[o]===void 0||e[o]===null),i=s.reduce((o,c)=>{let m=e[c],h=m==null?`:${c}`:String(m);return o.replace(new RegExp(`:${f(c)}(?=/|$)`,"g"),h)},t);if(a.length>0){if(n?.strict)throw new RangeError(`[route-forge] Missing required param(s) ${a.map(c=>`":${c}"`).join(", ")} in template "${t}".`);globalThis.process?.env?.NODE_ENV!=="production"&&console.warn(`[route-forge] Unresolved params in path "${i}". Check that all :param segments have matching keys.`)}return P(i,r)}function u(t){return[...t.matchAll(l())].map(e=>e[1])}function g(t){return l().test(t)}function T(t,e,r={exact:!0}){let n=t.split("?")[0]??"",s=R(e);return(r.exact?new RegExp(`^${s}$`):new RegExp(`^${s}`)).test(n)}function v(t,e){let r=e.split("?")[0]??"",n=u(t),s=new RegExp(`^${R(t)}$`),a=r.match(s);return a?Object.fromEntries(n.map((i,o)=>[i,a[o+1]??""])):{}}function b(...t){return"/"+t.map(e=>e.replace(/^\/+|\/+$/g,"")).filter(Boolean).join("/")}function E(t,e,r,n){return p(t,e,r,n)}function N(t){return u(t)}function d(t,e=""){let r=[];for(let n of Object.keys(t)){let s=e?`${e}.${n}`:n,a=t[n];typeof a=="string"?r.push({key:s,path:a}):a instanceof String?r.push({key:s,path:a.valueOf()}):typeof a=="object"&&a!==null&&r.push(...d(a,s))}return r}var O=t=>typeof t=="object"&&t!==null;function k(t){let e=u(t),r=new String(t);return r.build=(n,s,a)=>p(t,n,s,a),r.paramNames=e,r}function y(t){let e={};for(let r in t){if(!Object.prototype.hasOwnProperty.call(t,r))continue;let n=t[r];typeof n=="string"?e[r]=g(n)?k(n):n:O(n)&&(e[r]=y(n))}return e}function w(t){return y(t)}import{useParams as $,useNavigate as A,generatePath as Q}from"react-router-dom";function j(){return $()}function B(){let t=A();return(e,r)=>{t(e,r)}}function S(t,e,r,n){if(!u(t).every(o=>e[o]!==void 0&&e[o]!==null))return p(t,e,r,n);let i=Q(t,Object.fromEntries(Object.entries(e).map(([o,c])=>[o,String(c)])));return P(i,r)}export{E as build,p as buildPath,w as defineRoutes,u as extractParamNames,v as extractParamsFromPath,d as flattenRoutes,N as getParamNames,T as isActivePath,g as isDynamic,b as joinPaths,B as useNavigateTo,S as useResolvedPath,j as useRouteParams};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/utils.ts","../src/core/defineRoutes.ts","../src/hooks/index.ts"],"sourcesContent":["import type { RouteParam } from '../types';\n\n/** Returns a fresh RegExp each call — avoids shared `lastIndex` state on /g patterns. */\nconst PATH_PARAM_RE = () => /:([^/]+)/g;\nconst ESCAPE_RE = () => /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction escapeRegex(value: string): string {\n return value.replace(ESCAPE_RE(), '\\\\$&');\n}\n\nfunction createTemplatePattern(template: string): string {\n return escapeRegex(template).replace(PATH_PARAM_RE(), '([^/]+)');\n}\n\nexport function buildPath(\n template: string,\n params: Record<string, RouteParam>\n): string {\n const paramNames = extractParamNames(template);\n\n const resolved = paramNames.reduce((path, name) => {\n const value = params[name];\n const replacement = value === undefined ? `:${name}` : String(value);\n return path.replace(new RegExp(`:${escapeRegex(name)}(?=/|$)`, 'g'), replacement);\n }, template);\n\n const runtimeProcess = (globalThis as typeof globalThis & {\n process?: {\n env?: Record<string, string | undefined>;\n };\n }).process;\n\n if (runtimeProcess?.env?.NODE_ENV !== 'production' && resolved.includes(':')) {\n console.warn(\n `[route-forge] Unresolved params in path \"${resolved}\". ` +\n `Check that all :param segments have matching keys.`\n );\n }\n\n return resolved;\n}\n\nexport function extractParamNames(template: string): string[] {\n return [...template.matchAll(PATH_PARAM_RE())].map((match) => match[1] as string);\n}\n\nexport function isDynamic(path: string): boolean {\n return PATH_PARAM_RE().test(path);\n}\n\nexport function isActivePath(\n currentPath: string,\n template: string,\n options: { exact?: boolean } = { exact: true }\n): boolean {\n const pattern = createTemplatePattern(template);\n const regex = options.exact\n ? new RegExp(`^${pattern}$`)\n : new RegExp(`^${pattern}`);\n\n return regex.test(currentPath);\n}\n\nexport function extractParamsFromPath(\n template: string,\n resolvedPath: string\n): Record<string, string> {\n const paramNames = extractParamNames(template);\n const regex = new RegExp(`^${createTemplatePattern(template)}$`);\n const match = resolvedPath.match(regex);\n\n if (!match) return {};\n\n return Object.fromEntries(\n paramNames.map((name, index) => [name, match[index + 1] ?? ''])\n );\n}\n\nexport function joinPaths(...segments: string[]): string {\n return (\n '/' +\n segments\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, ''))\n .filter(Boolean)\n .join('/')\n );\n}\n\nexport function build(\n template: string,\n params: Record<string, RouteParam>\n): string {\n return buildPath(template, params);\n}\n\nexport function getParamNames(template: string): string[] {\n return extractParamNames(template);\n}\n","import { buildPath, extractParamNames, isDynamic } from './utils';\nimport type { ExtractParams, PathParams, RoutePath } from '../types';\n\n// Re-export so consumers that import from 'core/defineRoutes' get the full surface\nexport { buildPath, extractParamNames, isDynamic } from './utils';\n\ntype RouteInput = {\n [key: string]: string | RouteInput;\n};\n\ntype DynamicRoute<T extends string> = T extends `${string}:${string}`\n ? T & {\n build(params: PathParams<T>): RoutePath;\n paramNames: Array<ExtractParams<T>>;\n }\n : T;\n\ntype ResolvedRoutes<T extends RouteInput> = {\n [K in keyof T]: T[K] extends RouteInput\n ? ResolvedRoutes<T[K]>\n : T[K] extends string\n ? DynamicRoute<T[K]>\n : never;\n};\n\nconst isRouteGroup = (value: unknown): value is RouteInput =>\n typeof value === 'object' && value !== null;\n\nfunction wrapDynamicPath<T extends string>(template: T): DynamicRoute<T> {\n const paramNames = extractParamNames(template);\n const wrapped = new String(template) as unknown as DynamicRoute<T> & {\n build: (params: PathParams<T>) => RoutePath;\n paramNames: Array<ExtractParams<T>>;\n };\n\n wrapped.build = (params: PathParams<T>) => buildPath(template, params) as RoutePath;\n wrapped.paramNames = paramNames as Array<ExtractParams<T>>;\n\n return wrapped as unknown as DynamicRoute<T>;\n}\n\nfunction processRouteMap<T extends RouteInput>(routes: T): ResolvedRoutes<T> {\n const result = {} as ResolvedRoutes<T>;\n\n for (const key in routes) {\n if (!Object.prototype.hasOwnProperty.call(routes, key)) continue;\n\n const value = routes[key];\n\n if (typeof value === 'string') {\n result[key] = (isDynamic(value)\n ? wrapDynamicPath(value)\n : value) as ResolvedRoutes<T>[typeof key];\n } else if (isRouteGroup(value)) {\n result[key] = processRouteMap(value as RouteInput) as unknown as ResolvedRoutes<T>[typeof key];\n }\n }\n\n return result;\n}\n\nexport function defineRoutes<T extends RouteInput>(routes: T): ResolvedRoutes<T> {\n return processRouteMap(routes);\n}\n","/**\n * React integration hooks for route-forge.\n * These are thin wrappers — import only if you're using React Router.\n */\n\nimport { useParams, useNavigate, generatePath } from 'react-router-dom';\nimport type { RouteParam } from '../types';\n\n// ─── useRouteParams ──────────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useParams`.\n *\n * Pass the route's path template as a const generic to get a properly typed\n * params object back — no casting needed.\n *\n * @example\n * ```tsx\n * // Route is defined as '/users/edit/:id'\n * const { id } = useRouteParams<'/users/edit/:id'>();\n * ```\n */\nexport function useRouteParams<\n T extends string,\n // Extracts ':id' → 'id', ':postId' → 'postId', etc.\n K extends string = T extends `${string}:${infer P}/${infer R}`\n ? P | (R extends `${string}:${infer Q}` ? Q : never)\n : T extends `${string}:${infer P}`\n ? P\n : never,\n>(): Record<K, string> {\n return useParams() as Record<K, string>;\n}\n\n// ─── useNavigateTo ──────────────────────────────────────────────────────────\n\ntype NavigateOptions = {\n replace?: boolean;\n state?: unknown;\n};\n\n/**\n * A typed `navigate` helper that accepts a resolved path (output of `.build()`)\n * or a plain static path, with optional navigation options.\n *\n * @example\n * ```tsx\n * const navigateTo = useNavigateTo();\n * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));\n * navigateTo(PATHS.HOME, { replace: true });\n * ```\n */\nexport function useNavigateTo() {\n const navigate = useNavigate();\n\n return (path: string, options?: NavigateOptions) => {\n navigate(path, options);\n };\n}\n\n// ─── useResolvedPath ─────────────────────────────────────────────────────────\n\n/**\n * Resolves a dynamic path template against params using React Router's\n * `generatePath`, with proper typing.\n *\n * Useful when you need the resolved path string without navigating.\n *\n * @example\n * ```tsx\n * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'\n * ```\n */\nexport function useResolvedPath(\n template: string,\n params: Record<string, RouteParam>\n): string {\n return generatePath(\n template,\n Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))\n );\n}\n"],"mappings":"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","names":["PATH_PARAM_RE","ESCAPE_RE","escapeRegex","value","createTemplatePattern","template","buildPath","params","resolved","extractParamNames","path","name","replacement","match","isDynamic","isActivePath","currentPath","options","pattern","extractParamsFromPath","resolvedPath","paramNames","regex","index","joinPaths","segments","segment","build","getParamNames","isRouteGroup","value","wrapDynamicPath","template","paramNames","extractParamNames","wrapped","params","buildPath","processRouteMap","routes","result","key","isDynamic","defineRoutes","useParams","useNavigate","generatePath","useRouteParams","useNavigateTo","navigate","path","options","useResolvedPath","template","params","k","v"]}
1
+ {"version":3,"sources":["../src/core/utils.ts","../src/core/defineRoutes.ts","../src/hooks/index.ts"],"sourcesContent":["import type { QueryParams, RouteParam, RouteParams, BuildPathOptions, FlatRoute } from \"../types\";\n\n/** Returns a fresh RegExp each call — avoids shared `lastIndex` state on /g patterns. */\nconst PATH_PARAM_RE = () => /:([^/]+)/g;\nconst ESCAPE_RE = () => /[.*+?^${}()|[\\]\\\\]/g;\n\nfunction escapeRegex(value: string): string {\n return value.replace(ESCAPE_RE(), \"\\\\$&\");\n}\n\nfunction createTemplatePattern(template: string): string {\n return escapeRegex(template).replace(PATH_PARAM_RE(), \"([^/]+)\");\n}\n\nexport function appendQuery(path: string, query?: QueryParams): string {\n if (!query) return path;\n\n const searchParams = new URLSearchParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined || value === null) continue;\n if (Array.isArray(value)) {\n value.forEach((v) => {\n if (v !== undefined && v !== null) searchParams.append(key, String(v));\n });\n } else {\n searchParams.append(key, String(value));\n }\n }\n\n const queryString = searchParams.toString();\n if (!queryString) return path;\n\n return path + (path.includes(\"?\") ? \"&\" : \"?\") + queryString;\n}\n\nexport function buildPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n const paramNames = extractParamNames(template);\n const unresolved = paramNames.filter(\n (name) => params[name] === undefined || params[name] === null,\n );\n\n const resolved = paramNames.reduce((path, name) => {\n const value = params[name];\n const replacement = value === undefined || value === null ? `:${name}` : String(value);\n return path.replace(\n new RegExp(`:${escapeRegex(name)}(?=/|$)`, \"g\"),\n replacement,\n );\n }, template);\n\n if (unresolved.length > 0) {\n if (options?.strict) {\n throw new RangeError(\n `[route-forge] Missing required param(s) ${unresolved.map((p) => `\":${p}\"`).join(\", \")} in template \"${template}\".`,\n );\n }\n\n const runtimeProcess = (\n globalThis as typeof globalThis & {\n process?: {\n env?: Record<string, string | undefined>;\n };\n }\n ).process;\n\n if (runtimeProcess?.env?.NODE_ENV !== \"production\") {\n console.warn(\n `[route-forge] Unresolved params in path \"${resolved}\". ` +\n `Check that all :param segments have matching keys.`,\n );\n }\n }\n\n return appendQuery(resolved, query);\n}\n\nexport function extractParamNames(template: string): string[] {\n return [...template.matchAll(PATH_PARAM_RE())].map(\n (match) => match[1] as string,\n );\n}\n\nexport function isDynamic(path: string): boolean {\n return PATH_PARAM_RE().test(path);\n}\n\nexport function isActivePath(\n currentPath: string,\n template: string,\n options: { exact?: boolean } = { exact: true },\n): boolean {\n const pathWithoutSearch = currentPath.split(\"?\")[0] ?? \"\";\n const pattern = createTemplatePattern(template);\n const regex = options.exact\n ? new RegExp(`^${pattern}$`)\n : new RegExp(`^${pattern}`);\n\n return regex.test(pathWithoutSearch);\n}\n\nexport function extractParamsFromPath(\n template: string,\n resolvedPath: string,\n): Record<string, string> {\n const pathWithoutSearch = resolvedPath.split(\"?\")[0] ?? \"\";\n const paramNames = extractParamNames(template);\n const regex = new RegExp(`^${createTemplatePattern(template)}$`);\n const match = pathWithoutSearch.match(regex);\n\n if (!match) return {};\n\n return Object.fromEntries(\n paramNames.map((name, index) => [name, match[index + 1] ?? \"\"]),\n );\n}\n\nexport function joinPaths(...segments: string[]): string {\n return (\n \"/\" +\n segments\n .map((segment) => segment.replace(/^\\/+|\\/+$/g, \"\"))\n .filter(Boolean)\n .join(\"/\")\n );\n}\n\nexport function build(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n return buildPath(template, params, query, options);\n}\n\nexport function getParamNames(template: string): string[] {\n return extractParamNames(template);\n}\n\n/**\n * Walk a `defineRoutes` output tree and return a flat array of\n * `{ key, path }` entries where `key` is the dot-joined key path from\n * the root (e.g. `\"SERVICES.BCC.EDIT\"`) and `path` is the raw template\n * string (e.g. `\"/services/bcc/edit/:id\"`).\n *\n * Useful for:\n * - Generating sitemaps from a single source of truth.\n * - Detecting duplicate path strings across branches at startup:\n *\n * @example\n * const flat = flattenRoutes(PATHS);\n * const paths = flat.map((r) => r.path);\n * const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);\n * if (dupes.length) console.warn('Duplicate paths:', dupes);\n */\nexport function flattenRoutes(\n routes: Record<string, unknown>,\n prefix = \"\",\n): FlatRoute[] {\n const entries: FlatRoute[] = [];\n\n for (const key of Object.keys(routes)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n const value = routes[key];\n\n if (typeof value === \"string\") {\n // Plain static string leaf.\n entries.push({ key: fullKey, path: value });\n } else if (value instanceof String) {\n // String-object leaf (wrapped dynamic path from defineRoutes).\n entries.push({ key: fullKey, path: value.valueOf() });\n } else if (typeof value === \"object\" && value !== null) {\n // Nested route group — recurse.\n entries.push(\n ...flattenRoutes(value as Record<string, unknown>, fullKey),\n );\n }\n // Anything else (functions, numbers, …) is silently skipped.\n }\n\n return entries;\n}\n\n","import { buildPath, extractParamNames, isDynamic } from \"./utils\";\nimport type {\n BuildPathOptions,\n ExtractParams,\n PathParams,\n QueryParams,\n RoutePath,\n} from \"../types\";\n\n// Re-export so consumers that import from 'core/defineRoutes' get the full surface\nexport { buildPath, extractParamNames, isDynamic } from \"./utils\";\n\ntype RouteInput = {\n [key: string]: string | RouteInput;\n};\n\ntype DynamicRoute<T extends string> = T extends `${string}:${string}`\n ? T & {\n build(params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions): RoutePath;\n paramNames: Array<ExtractParams<T>>;\n }\n : T;\n\ntype ResolvedRoutes<T extends RouteInput> = {\n [K in keyof T]: T[K] extends RouteInput\n ? ResolvedRoutes<T[K]>\n : T[K] extends string\n ? DynamicRoute<T[K]>\n : never;\n};\n\nconst isRouteGroup = (value: unknown): value is RouteInput =>\n typeof value === \"object\" && value !== null;\n\nfunction wrapDynamicPath<T extends string>(template: T): DynamicRoute<T> {\n const paramNames = extractParamNames(template);\n const wrapped = new String(template) as unknown as DynamicRoute<T> & {\n build: (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) => RoutePath;\n paramNames: Array<ExtractParams<T>>;\n };\n\n wrapped.build = (params: PathParams<T>, query?: QueryParams, options?: BuildPathOptions) =>\n buildPath(template, params, query, options) as RoutePath;\n wrapped.paramNames = paramNames as Array<ExtractParams<T>>;\n\n return wrapped as unknown as DynamicRoute<T>;\n}\n\nfunction processRouteMap<T extends RouteInput>(routes: T): ResolvedRoutes<T> {\n const result = {} as ResolvedRoutes<T>;\n\n for (const key in routes) {\n if (!Object.prototype.hasOwnProperty.call(routes, key)) continue;\n\n const value = routes[key];\n\n if (typeof value === \"string\") {\n result[key] = (\n isDynamic(value) ? wrapDynamicPath(value) : value\n ) as ResolvedRoutes<T>[typeof key];\n } else if (isRouteGroup(value)) {\n result[key] = processRouteMap(\n value as RouteInput,\n ) as unknown as ResolvedRoutes<T>[typeof key];\n }\n }\n\n return result;\n}\n\nexport function defineRoutes<T extends RouteInput>(\n routes: T,\n): ResolvedRoutes<T> {\n return processRouteMap(routes);\n}\n","/**\n * React integration hooks for route-forge.\n * These are thin wrappers — import only if you're using React Router.\n */\n\nimport { useParams, useNavigate, generatePath } from \"react-router-dom\";\nimport type {\n ExtractParams,\n QueryParams,\n RouteParams,\n BuildPathOptions,\n} from \"../types\";\nimport { appendQuery, extractParamNames, buildPath } from \"../core/utils\";\n\n// ─── useRouteParams ──────────────────────────────────────────────────────────\n\n/**\n * A typed wrapper around React Router's `useParams`.\n *\n * @example\n * ```tsx\n * // Route: '/a/:x/b/:y/c/:z'\n * const { x, y, z } = useRouteParams<'/a/:x/b/:y/c/:z'>();\n * ```\n */\nexport function useRouteParams<T extends string>(): Record<\n ExtractParams<T>,\n string\n> {\n return useParams() as Record<ExtractParams<T>, string>;\n}\n\n// ─── useNavigateTo ──────────────────────────────────────────────────────────\n\ntype NavigateOptions = {\n replace?: boolean;\n state?: unknown;\n};\n\n/**\n * A typed `navigate` helper that accepts a resolved path (output of `.build()`)\n * or a plain static path, with optional navigation options.\n *\n * @example\n * ```tsx\n * const navigateTo = useNavigateTo();\n * navigateTo(PATHS.USERS.EDIT.build({ id: 42 }));\n * navigateTo(PATHS.HOME, { replace: true });\n * ```\n */\nexport function useNavigateTo() {\n const navigate = useNavigate();\n\n return (path: string, options?: NavigateOptions) => {\n navigate(path, options);\n };\n}\n\n// ─── useResolvedPath ─────────────────────────────────────────────────────────\n\n/**\n * Resolves a dynamic path template against params using React Router's\n * `generatePath`, with proper typing.\n *\n * Accepts the same `options` bag as `build()` / `buildPath()`:\n * - (default) soft-fail: `console.warn` and return the partial path with unresolved `:param` placeholders.\n * - `{ strict: true }`: throw a `RangeError` on missing params — matching `.build()`'s strict behaviour.\n *\n * When all params are present, resolution is delegated to React Router's `generatePath`,\n * which correctly handles splat (`*`) and optional (`:param?`) segments.\n *\n * @example\n * ```tsx\n * const path = useResolvedPath('/users/:id', { id: 42 }); // → '/users/42'\n * const path = useResolvedPath('/users/:id', {}, undefined, { strict: true }); // throws RangeError\n * ```\n */\nexport function useResolvedPath(\n template: string,\n params: RouteParams,\n query?: QueryParams,\n options?: BuildPathOptions,\n): string {\n const paramNames = extractParamNames(template);\n const hasAllParams = paramNames.every(\n (name) => params[name] !== undefined && params[name] !== null,\n );\n\n if (!hasAllParams) {\n // Let buildPath own all missing-param behaviour (warn/throw) —\n // avoids re-implementing (and duplicating) the same check here.\n return buildPath(template, params, query, options);\n }\n\n // All params present — use generatePath for correct splat / optional-segment handling.\n const path = generatePath(\n template,\n Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),\n );\n\n return appendQuery(path, query);\n}\n"],"mappings":"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","names":["PATH_PARAM_RE","ESCAPE_RE","escapeRegex","value","createTemplatePattern","template","appendQuery","path","query","searchParams","key","v","queryString","buildPath","params","options","paramNames","extractParamNames","unresolved","name","resolved","replacement","p","match","isDynamic","isActivePath","currentPath","pathWithoutSearch","pattern","extractParamsFromPath","resolvedPath","regex","index","joinPaths","segments","segment","build","getParamNames","flattenRoutes","routes","prefix","entries","fullKey","isRouteGroup","value","wrapDynamicPath","template","paramNames","extractParamNames","wrapped","params","query","options","buildPath","processRouteMap","routes","result","key","isDynamic","defineRoutes","useParams","useNavigate","generatePath","useRouteParams","useParams","useNavigateTo","navigate","useNavigate","path","options","useResolvedPath","template","params","query","extractParamNames","name","buildPath","generatePath","k","v","appendQuery"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-routes-forge",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Type-safe route definitions with automatic path builders for React apps",
5
5
  "type": "module",
6
6
  "author": {
@@ -62,14 +62,19 @@
62
62
  }
63
63
  },
64
64
  "devDependencies": {
65
- "react-router-dom": "^6.0.0",
66
65
  "@commitlint/cli": "^21.0.2",
67
66
  "@commitlint/config-conventional": "^21.0.2",
68
- "@types/node": "^26.1.1",
67
+ "@testing-library/react": "^16.3.2",
69
68
  "@types/bun": "^1.3.14",
69
+ "@types/jsdom": "^28.0.3",
70
+ "@types/node": "^26.1.1",
71
+ "@types/react": "^19.2.17",
70
72
  "husky": "^9.0.0",
73
+ "jsdom": "^29.1.1",
74
+ "react-router-dom": "^6.0.0",
71
75
  "standard-version": "^9.5.0",
72
76
  "tsup": "^8.0.0",
73
- "typescript": "^5.0.0"
77
+ "typescript": "^5.0.0",
78
+ "vitest": "^4.1.10"
74
79
  }
75
80
  }