react-routes-forge 1.1.3 → 1.3.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 +480 -77
- package/dist/chunk-EFHUAXKE.js +2 -0
- package/dist/chunk-EFHUAXKE.js.map +1 -0
- package/dist/hooks/index.cjs +2 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +89 -0
- package/dist/hooks/index.d.ts +89 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index-Beg7xp8k.d.cts +148 -0
- package/dist/index-Beg7xp8k.d.ts +148 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +95 -110
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +59 -23
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# react-routes-forge
|
|
2
2
|
|
|
3
|
-
**Type-safe route definitions
|
|
3
|
+
**Type-safe route definitions, automatic path builders, query parameter handling, and active route matching for React applications with zero duplication.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
📖 **[Documentation](https://mhsmustafa84.github.io/react-routes-forge)** | 🚀 **[Live Demo (POC)](https://mhsmustafa84.github.io/react-routes-forge-poc)**
|
|
6
6
|
|
|
7
|
+
[](https://mhsmustafa84.github.io/react-routes-forge)
|
|
8
|
+
[](https://mhsmustafa84.github.io/react-routes-forge-poc)
|
|
7
9
|
[](#license)
|
|
8
10
|
[](#)
|
|
9
|
-
[](#requirements)
|
|
10
12
|
[](https://github.com/mhsmustafa84/react-routes-forge/actions/workflows/ci-security.yml)
|
|
11
13
|
|
|
12
14
|
---
|
|
@@ -17,20 +19,30 @@ One source of truth for your routes — templates for `<Route path={...} />` and
|
|
|
17
19
|
- [Installation](#installation)
|
|
18
20
|
- [Quick start](#quick-start)
|
|
19
21
|
- [Cheat sheet](#cheat-sheet)
|
|
20
|
-
- [Core concepts](#core-concepts)
|
|
21
22
|
- [API reference](#api-reference)
|
|
22
23
|
- [`defineRoutes(routeMap)`](#defineroutesroutemap)
|
|
23
24
|
- [`build(template, params, query?, options?)`](#buildtemplate-params-query-options)
|
|
24
25
|
- [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options)
|
|
25
26
|
- [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath)
|
|
27
|
+
- [`matchPath(template, options?)`](#matchpathtemplate-options)
|
|
26
28
|
- [`joinPaths(...segments)`](#joinpathssegments)
|
|
27
29
|
- [`getParamNames(template)`](#getparamnamestemplate)
|
|
28
30
|
- [`flattenRoutes(routes)`](#flattenroutesroutes)
|
|
31
|
+
- [`getBreadcrumbs(routes, currentPath, options?)`](#getbreadcrumbsroutes-currentpath-options)
|
|
32
|
+
- [`appendQuery(path, query?, hash?)`](#appendquerypath-query-hash)
|
|
33
|
+
- [`extractQueryFromPath(path, options?)`](#extractqueryfrompathpath-options)
|
|
34
|
+
- [`devWarn(message)`](#devwarnmessage)
|
|
35
|
+
- [`clearPathCache()`](#clearpathcache)
|
|
29
36
|
- [React hooks](#react-hooks)
|
|
30
37
|
- [`useRouteParams<T>()`](#useroutparamst)
|
|
31
38
|
- [`useNavigateTo()`](#usenavigateto)
|
|
32
39
|
- [`useResolvedPath(template, params, query?, options?)`](#useresolvedpathtemplate-params-query-options)
|
|
40
|
+
- [`useActivePath(template, options?)`](#useactivepathtemplate-options)
|
|
41
|
+
- [`useTypedSearchParams(options?)`](#usetypedsearchparamsoptions)
|
|
42
|
+
- [Splat (`/*`) segments](#splat--segments)
|
|
43
|
+
- [Route validation](#route-validation)
|
|
33
44
|
- [Query string support](#query-string-support)
|
|
45
|
+
- [Hash fragment support](#hash-fragment-support)
|
|
34
46
|
- [Strict mode](#strict-mode)
|
|
35
47
|
- [Migrating from the old pattern](#migrating-from-the-old-pattern)
|
|
36
48
|
- [Known behaviours & gotchas](#known-behaviours--gotchas)
|
|
@@ -79,7 +91,7 @@ You get:
|
|
|
79
91
|
- **Single source of truth** — no duplicate template/builder pairs to keep in sync
|
|
80
92
|
- **Compile-time param safety** — `.build()` is typed from the path string itself; missing or misspelled params are TypeScript errors
|
|
81
93
|
- **Query string support** — built into `.build()`, no manual `URLSearchParams` wrangling
|
|
82
|
-
- **Zero runtime dependencies** for the core API
|
|
94
|
+
- **Zero runtime dependencies** for the core API
|
|
83
95
|
- **Deep nesting supported out of the box** — organize routes into as many nested groups as your app needs
|
|
84
96
|
|
|
85
97
|
---
|
|
@@ -96,19 +108,7 @@ yarn add react-routes-forge
|
|
|
96
108
|
bun add react-routes-forge
|
|
97
109
|
```
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
npm install react-router-dom # only if you're using the hooks
|
|
103
|
-
# or
|
|
104
|
-
pnpm add react-router-dom
|
|
105
|
-
# or
|
|
106
|
-
yarn add react-router-dom
|
|
107
|
-
# or
|
|
108
|
-
bun add react-router-dom
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
> **Note:** this package ships ESM-only. See [Known behaviours & gotchas](#known-behaviours--gotchas) for details.
|
|
111
|
+
> **Note:** this package ships dual **ESM + CommonJS** builds. See [Known behaviours & gotchas](#known-behaviours--gotchas) for details.
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
@@ -133,6 +133,8 @@ export const PATHS = defineRoutes({
|
|
|
133
133
|
} as const);
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
> **Always pass `as const`** — it preserves the literal string types that power `.build()`'s compile-time param checking. Without it, TypeScript widens your path strings to generic `string` and you lose type safety.
|
|
137
|
+
|
|
136
138
|
```tsx
|
|
137
139
|
// App.tsx — static paths and dynamic templates both work directly as strings
|
|
138
140
|
import { Routes, Route } from "react-router-dom";
|
|
@@ -146,46 +148,76 @@ import { PATHS } from "./paths";
|
|
|
146
148
|
</Routes>;
|
|
147
149
|
```
|
|
148
150
|
|
|
149
|
-
```
|
|
151
|
+
```tsx
|
|
150
152
|
// Navigating — call .build() to resolve a dynamic path into a real URL
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
import { useNavigate } from "react-router-dom";
|
|
154
|
+
|
|
155
|
+
function MyComponent() {
|
|
156
|
+
const navigate = useNavigate();
|
|
157
|
+
// ↓ Param type-checked from the template ":id"
|
|
158
|
+
navigate(PATHS.USERS.EDIT.build({ id: 42 })); // → '/users/edit/42'
|
|
159
|
+
navigate(PATHS.ROLES.PERMISSIONS.build({ name: "admin" })); // → '/roles/permissions/admin'
|
|
160
|
+
navigate(PATHS.HOME); // → '/' (static paths work directly)
|
|
161
|
+
}
|
|
154
162
|
```
|
|
155
163
|
|
|
156
164
|
That's the entire API surface you need for most apps. Everything below covers the rest of the toolkit.
|
|
157
165
|
|
|
158
|
-
|
|
166
|
+
### Route types
|
|
159
167
|
|
|
160
|
-
|
|
168
|
+
| Route type | Example | Behaves as | Gains |
|
|
169
|
+
| ----------- | ----------------------- | --------------------------------------- | --------------------------------------------------------------------- |
|
|
170
|
+
| **Static** | `HOME: '/'` | String-like (coercible to its template) | `.build(query?, options?)` — attach query/hash, no params to fill |
|
|
171
|
+
| **Dynamic** | `DETAILS: '/users/:id'` | String-like (coercible to its template) | `.build(params, query?, options?)` and `.paramNames` |
|
|
172
|
+
| **Splat** | `FILES: '/files/*'` | String-like (coercible to its template) | `.build(params, query?, options?)` and `.paramNames` |
|
|
161
173
|
|
|
162
|
-
|
|
174
|
+
`defineRoutes()` walks your route object recursively, wrapping every path in a string-coercible object and attaching a `.build()` helper so both static and dynamic routes can carry a query string or hash. Dynamic paths (containing a `:param` segment or a trailing `/*` splat) additionally gain `.paramNames`.
|
|
163
175
|
|
|
164
|
-
|
|
165
|
-
| ---------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- |
|
|
166
|
-
| [`defineRoutes(routeMap)`](#defineroutesroutemap) | function | Builds the typed `PATHS` object from a nested route map |
|
|
167
|
-
| [`build(template, params, query?, options?)`](#buildtemplate-params-query-options) | function | Resolve a template into a URL without `defineRoutes` |
|
|
168
|
-
| [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options) | function | Check if a path matches a template (nav-highlighting) |
|
|
169
|
-
| [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath) | function | Pull param values back out of a resolved URL |
|
|
170
|
-
| [`joinPaths(...segments)`](#joinpathssegments) | function | Join and normalize path segments |
|
|
171
|
-
| [`getParamNames(template)`](#getparamnamestemplate) | function | List the `:param` names in a template |
|
|
172
|
-
| [`flattenRoutes(routes)`](#flattenroutesroutes) | function | Flatten a `PATHS` tree for sitemaps / duplicate detection |
|
|
173
|
-
| [`useRouteParams<T>()`](#useroutparamst) | hook | Typed wrapper around React Router's `useParams` |
|
|
174
|
-
| [`useNavigateTo()`](#usenavigateto) | hook | Typed wrapper around React Router's `useNavigate` |
|
|
175
|
-
| [`useResolvedPath(...)`](#useresolvedpathtemplate-params-query-options) | hook | Resolve a template to a string without navigating |
|
|
176
|
-
| `.build(params, query?, options?)` | method | On every dynamic route — resolves to a concrete URL |
|
|
177
|
-
| `.paramNames` | property | On every dynamic route — the param names it expects |
|
|
176
|
+
Param names are `[A-Za-z0-9_]` only (matching React Router), so a static suffix after a param stays literal — `/files/:name.json` builds `{ name: "report" }` → `/files/report.json`, and `:name.json` is **not** treated as a single param name.
|
|
178
177
|
|
|
179
|
-
|
|
178
|
+
> `defineRoutes()` also validates your templates in development — missing leading `/`, non-trailing `*`, and duplicate path templates all produce a `console.warn`. See [Route validation](#route-validation).
|
|
180
179
|
|
|
181
|
-
|
|
180
|
+
---
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
| ----------- | ----------------------- | --------------------------------------- | ---------------------------------------------------- |
|
|
185
|
-
| **Static** | `HOME: '/'` | Plain string primitive | Nothing extra — use it directly |
|
|
186
|
-
| **Dynamic** | `DETAILS: '/users/:id'` | String-like (coercible to its template) | `.build(params, query?, options?)` and `.paramNames` |
|
|
182
|
+
## Cheat sheet
|
|
187
183
|
|
|
188
|
-
|
|
184
|
+
Quick reference for everything the package exports — grouped by kind. Click through to the full section for details and examples.
|
|
185
|
+
|
|
186
|
+
### Route definition
|
|
187
|
+
|
|
188
|
+
| Export | Purpose |
|
|
189
|
+
| ------------------------------------------------- | ------------------------------------------------------- |
|
|
190
|
+
| [`defineRoutes(routeMap)`](#defineroutesroutemap) | Builds the typed `PATHS` object from a nested route map |
|
|
191
|
+
| `.build(query?, options?)` | On every **static** route — attach query string / hash |
|
|
192
|
+
| `.build(params, query?, options?)` | On every **dynamic** route — resolves to a concrete URL |
|
|
193
|
+
| `.paramNames` | On every dynamic route — the param names it expects |
|
|
194
|
+
|
|
195
|
+
### Utilities
|
|
196
|
+
|
|
197
|
+
| Export | Purpose |
|
|
198
|
+
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
199
|
+
| [`build(template, params, query?, options?)`](#buildtemplate-params-query-options) | Resolve a template into a URL without `defineRoutes` |
|
|
200
|
+
| [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options) | Check if a path matches a template (nav-highlighting) |
|
|
201
|
+
| [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath) | Pull param values back out of a resolved URL |
|
|
202
|
+
| [`matchPath(template, options?)`](#matchpathtemplate-options) | Convert a route template into an anchored `RegExp` |
|
|
203
|
+
| [`joinPaths(...segments)`](#joinpathssegments) | Join and normalize path segments |
|
|
204
|
+
| [`getParamNames(template)`](#getparamnamestemplate) | List the `:param` names in a template |
|
|
205
|
+
| [`flattenRoutes(routes)`](#flattenroutesroutes) | Flatten a `PATHS` tree for sitemaps / duplicate detection |
|
|
206
|
+
| [`getBreadcrumbs(routes, currentPath, options?)`](#getbreadcrumbsroutes-currentpath-options) | Build a breadcrumb trail from a route tree and current URL |
|
|
207
|
+
| [`appendQuery(path, query?, hash?)`](#appendquerypath-query-hash) | Append query params / hash to an existing path |
|
|
208
|
+
| [`extractQueryFromPath(path, options?)`](#extractqueryfrompathpath-options) | Parse a query string back into an object |
|
|
209
|
+
| [`devWarn(message)`](#devwarnmessage) | Emit a `console.warn` in non-production builds |
|
|
210
|
+
| [`clearPathCache()`](#clearpathcache) | Reset internal regex caches (mainly for tests) |
|
|
211
|
+
|
|
212
|
+
### React hooks
|
|
213
|
+
|
|
214
|
+
| Export | Purpose |
|
|
215
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
216
|
+
| [`useRouteParams<T>()`](#useroutparamst) | Typed wrapper around React Router's `useParams` |
|
|
217
|
+
| [`useNavigateTo()`](#usenavigateto) | Typed wrapper around React Router's `useNavigate` |
|
|
218
|
+
| [`useResolvedPath(...)`](#useresolvedpathtemplate-params-query-options) | Resolve a template to a string without navigating |
|
|
219
|
+
| [`useActivePath(template, options?)`](#useactivepathtemplate-options) | Check if the current location matches a route template |
|
|
220
|
+
| [`useTypedSearchParams(options?)`](#usetypedsearchparamsoptions) | Typed `useSearchParams` with boolean/number coercion |
|
|
189
221
|
|
|
190
222
|
---
|
|
191
223
|
|
|
@@ -195,10 +227,11 @@ Quick reference for everything the package exports. Click through to the full se
|
|
|
195
227
|
|
|
196
228
|
Creates a fully typed route object from a nested plain object.
|
|
197
229
|
|
|
198
|
-
-
|
|
199
|
-
-
|
|
230
|
+
- Every path is string-coercible — use it directly anywhere a string is expected (e.g. `<Route path={...} />`).
|
|
231
|
+
- Static paths gain **`.build(query?, options?)`** — attach a query string and/or hash fragment without params.
|
|
232
|
+
- Dynamic paths (containing `:param`) and splat paths (trailing `/*`) gain:
|
|
200
233
|
- **`.build(params, query?, options?)`** — resolves the template into a concrete URL
|
|
201
|
-
- **`.paramNames`** — array of the param names extracted from the template, e.g. `['id']`
|
|
234
|
+
- **`.paramNames`** — array of the param names extracted from the template, e.g. `['id']` (a splat is reported as `['*']`)
|
|
202
235
|
|
|
203
236
|
Nesting is unlimited — organize routes into as many groups and sub-groups as your app needs.
|
|
204
237
|
|
|
@@ -206,16 +239,21 @@ Nesting is unlimited — organize routes into as many groups and sub-groups as y
|
|
|
206
239
|
const PATHS = defineRoutes({
|
|
207
240
|
SERVICES: {
|
|
208
241
|
ROOT: "/services",
|
|
209
|
-
|
|
210
|
-
DETAILS: "/services/
|
|
211
|
-
EDIT: "/services/
|
|
242
|
+
SUPPORT_CENTER: {
|
|
243
|
+
DETAILS: "/services/support-center/:id",
|
|
244
|
+
EDIT: "/services/support-center/edit/:id",
|
|
212
245
|
},
|
|
213
246
|
},
|
|
214
247
|
} as const);
|
|
215
248
|
|
|
216
249
|
PATHS.SERVICES.ROOT; // '/services'
|
|
217
|
-
PATHS.SERVICES.
|
|
218
|
-
PATHS.SERVICES.
|
|
250
|
+
PATHS.SERVICES.SUPPORT_CENTER.EDIT.build({ id: 7 }); // '/services/support-center/edit/7'
|
|
251
|
+
PATHS.SERVICES.SUPPORT_CENTER.EDIT.build(
|
|
252
|
+
{ id: 7 },
|
|
253
|
+
{ tab: "info" },
|
|
254
|
+
{ hash: "details" },
|
|
255
|
+
); // → '/services/support-center/edit/7?tab=info#details'
|
|
256
|
+
PATHS.SERVICES.SUPPORT_CENTER.EDIT.paramNames; // ['id']
|
|
219
257
|
```
|
|
220
258
|
|
|
221
259
|
> Always pass `as const` to `defineRoutes()` — it preserves the literal string types that power `.build()`'s compile-time param checking.
|
|
@@ -224,7 +262,7 @@ PATHS.SERVICES.BENEFICIARY_CARE_CENTER.EDIT.paramNames; // ['id']
|
|
|
224
262
|
|
|
225
263
|
### `build(template, params, query?, options?)`
|
|
226
264
|
|
|
227
|
-
Standalone path resolver — for building a URL without going through `defineRoutes`, or for
|
|
265
|
+
Standalone path resolver — for building a URL without going through `defineRoutes`, or for resolving a raw template string (rather than a route from a `PATHS` tree).
|
|
228
266
|
|
|
229
267
|
```ts
|
|
230
268
|
import { build } from "react-routes-forge";
|
|
@@ -239,13 +277,40 @@ build("/users", {}, { sort: "asc" });
|
|
|
239
277
|
// Strict mode — throw instead of warn when a param is missing
|
|
240
278
|
build("/users/:id", {}, undefined, { strict: true });
|
|
241
279
|
// ✗ throws RangeError: [route-forge] Missing required param(s) ":id" in template "/users/:id".
|
|
280
|
+
|
|
281
|
+
// Hash fragment — appended after the query string
|
|
282
|
+
build("/users/:id", { id: 42 }, { tab: "info" }, { hash: "details" });
|
|
283
|
+
// → '/users/42?tab=info#details'
|
|
284
|
+
build("/page", {}, undefined, { hash: "section" });
|
|
285
|
+
// → '/page#section'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Param values are URL-encoded by default** (`encodeURIComponent`), so characters like `/`, `?`, `#`, or `%` in a value can't break the URL structure:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
build("/search/:query", { query: "a/b" });
|
|
292
|
+
// → '/search/a%2Fb'
|
|
293
|
+
|
|
294
|
+
// Pass { encode: false } if a value is already encoded
|
|
295
|
+
build("/search/:query", { query: "a%2Fb" }, undefined, { encode: false });
|
|
296
|
+
// → '/search/a%2Fb'
|
|
297
|
+
|
|
298
|
+
// Splat segments capture a path-like remainder, preserving `/` separators
|
|
299
|
+
build("/files/*", { "*": "reports/2026/q1" });
|
|
300
|
+
// → '/files/reports/2026/q1'
|
|
242
301
|
```
|
|
243
302
|
|
|
303
|
+
See [Splat (`/*`) segments](#splat--segments) for details.
|
|
304
|
+
|
|
244
305
|
---
|
|
245
306
|
|
|
246
307
|
### `isActivePath(currentPath, template, options?)`
|
|
247
308
|
|
|
248
|
-
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.
|
|
309
|
+
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. It mirrors React Router's `NavLink` matching semantics:
|
|
310
|
+
|
|
311
|
+
- **Case-insensitive by default** — pass `{ caseSensitive: true }` to opt out.
|
|
312
|
+
- **Trailing slashes are tolerated** — `/users/` matches `/users`.
|
|
313
|
+
- **`exact: true` (default)** requires a full match; **`exact: false`** matches any path that starts with the template (so `/` matches every path as a prefix).
|
|
249
314
|
|
|
250
315
|
```ts
|
|
251
316
|
import { isActivePath } from "react-routes-forge";
|
|
@@ -254,6 +319,9 @@ isActivePath("/users/42", "/users/:id"); // true
|
|
|
254
319
|
isActivePath("/users/42/posts", "/users/:id"); // false (exact match by default)
|
|
255
320
|
isActivePath("/users/42/posts", "/users/:id", { exact: false }); // true (prefix match)
|
|
256
321
|
isActivePath("/users/42?tab=profile", "/users/:id"); // true (query string ignored)
|
|
322
|
+
isActivePath("/Users/42", "/users/:id"); // true (case-insensitive by default)
|
|
323
|
+
isActivePath("/Users/42", "/users/:id", { caseSensitive: true }); // false
|
|
324
|
+
isActivePath("/users/42/", "/users/:id"); // true (trailing slash tolerated)
|
|
257
325
|
```
|
|
258
326
|
|
|
259
327
|
A common real-world use — highlighting the active nav link:
|
|
@@ -289,6 +357,33 @@ extractParamsFromPath("/a/:x/b/:y", "/a/foo/b/bar");
|
|
|
289
357
|
|
|
290
358
|
---
|
|
291
359
|
|
|
360
|
+
### `matchPath(template, options?)`
|
|
361
|
+
|
|
362
|
+
Converts a route template string into an anchored `RegExp` — useful when you need custom matching logic beyond [`isActivePath`](#isactivepathcurrentpath-template-options) or [`extractParamsFromPath`](#extractparamsfrompathtemplate-resolvedpath). Query strings are **not** stripped; split on `"?"` first if needed.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
import { matchPath } from "react-routes-forge";
|
|
366
|
+
|
|
367
|
+
const re = matchPath("/users/:id");
|
|
368
|
+
re.test("/users/42"); // true
|
|
369
|
+
re.exec("/users/42"); // ['/users/42', '42']
|
|
370
|
+
re.test("/users/42/posts"); // false (exact match only)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Options:**
|
|
374
|
+
|
|
375
|
+
- **`end?: boolean`** (default `true`) — anchor the pattern to the end of the path. Pass `false` to match a prefix at a segment boundary (`/users` matches `/users/42` but not `/usersettings`).
|
|
376
|
+
- **`caseSensitive?: boolean`** (default `false`) — match case-insensitively by default; pass `true` to opt out.
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
matchPath("/users/:id", { end: false }).test("/users/42/posts"); // true
|
|
380
|
+
matchPath("/Users/42", { caseSensitive: true }).test("/users/42"); // false
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
This is the building block used internally by `isActivePath` and `extractParamsFromPath`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
292
387
|
### `joinPaths(...segments)`
|
|
293
388
|
|
|
294
389
|
Safely joins path segments, normalizing duplicate/missing slashes.
|
|
@@ -310,6 +405,7 @@ Returns the list of param names present in a template string.
|
|
|
310
405
|
import { getParamNames } from "react-routes-forge";
|
|
311
406
|
|
|
312
407
|
getParamNames("/users/:id/posts/:postId"); // → ['id', 'postId']
|
|
408
|
+
getParamNames("/files/*"); // → ['*'] (the splat param)
|
|
313
409
|
getParamNames("/users"); // → []
|
|
314
410
|
```
|
|
315
411
|
|
|
@@ -362,16 +458,148 @@ it("has no duplicate route paths", () => {
|
|
|
362
458
|
|
|
363
459
|
---
|
|
364
460
|
|
|
461
|
+
### `getBreadcrumbs(routes, currentPath, options?)`
|
|
462
|
+
|
|
463
|
+
Walks a route tree (or a pre-flattened array from `flattenRoutes()`) and returns every route that is an ancestor of (or an exact match to) the current URL. Dynamic params in ancestor paths are automatically resolved from the matched portion of the URL. Query strings on `currentPath` are ignored.
|
|
464
|
+
|
|
465
|
+
Each breadcrumb entry contains:
|
|
466
|
+
|
|
467
|
+
- **`key`** — the dot-joined key from the route tree (e.g. `"USERS.EDIT"`)
|
|
468
|
+
- **`label`** — a human-readable label derived from the key (e.g. `"USERS.ROOT"` → `"Users"`, `"USERS.EDIT"` → `"Edit"`)
|
|
469
|
+
- **`path`** — the resolved breadcrumb path with params filled in (e.g. `"/users/edit/42"`)
|
|
470
|
+
- **`isCurrent`** — `true` only for the deepest (exact) match
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { defineRoutes, getBreadcrumbs } from "react-routes-forge";
|
|
474
|
+
|
|
475
|
+
const PATHS = defineRoutes({
|
|
476
|
+
HOME: "/",
|
|
477
|
+
USERS: {
|
|
478
|
+
ROOT: "/users",
|
|
479
|
+
EDIT: "/users/edit/:id",
|
|
480
|
+
},
|
|
481
|
+
SERVICES: {
|
|
482
|
+
BCC: {
|
|
483
|
+
EDIT: "/services/bcc/edit/:id",
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
} as const);
|
|
487
|
+
|
|
488
|
+
getBreadcrumbs(PATHS, "/users/edit/42");
|
|
489
|
+
// →
|
|
490
|
+
// [
|
|
491
|
+
// { key: "HOME", label: "Home", path: "/", isCurrent: false },
|
|
492
|
+
// { key: "USERS.ROOT", label: "Users", path: "/users", isCurrent: false },
|
|
493
|
+
// { key: "USERS.EDIT", label: "Edit", path: "/users/edit/42", isCurrent: true },
|
|
494
|
+
// ]
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Custom label resolver** — override the default key-to-label conversion:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
getBreadcrumbs(PATHS, "/users/edit/42", {
|
|
501
|
+
labelResolver: (key) =>
|
|
502
|
+
key.split(".").pop()!.replace(/_/g, " ").toUpperCase(),
|
|
503
|
+
});
|
|
504
|
+
// → [{ label: "HOME" }, { label: "ROOT" }, { label: "EDIT" }]
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Label map** — the ergonomic alternative for a handful of overrides. Keys are dot-joined route keys; matching keys take precedence over `labelResolver`:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
getBreadcrumbs(PATHS, "/users/edit/42", {
|
|
511
|
+
labels: { "USERS.ROOT": "Members", "USERS.EDIT": "Edit member" },
|
|
512
|
+
});
|
|
513
|
+
// → [{ label: "Home" }, { label: "Members" }, { label: "Edit member" }]
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Pre-flattened input** — pass a cached `flattenRoutes()` result instead of the tree:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const flat = flattenRoutes(PATHS);
|
|
520
|
+
getBreadcrumbs(flat, "/users/edit/42"); // same result as passing the tree
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
### `appendQuery(path, query?, hash?)`
|
|
526
|
+
|
|
527
|
+
Appends a query string and/or hash fragment to a path that may already contain a query or hash. Existing query pairs are preserved, the query is inserted before any hash, and an existing hash is kept unless a new one is given.
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { appendQuery } from "react-routes-forge";
|
|
531
|
+
|
|
532
|
+
appendQuery("/users?tab=list", { page: 2 }); // → '/users?tab=list&page=2'
|
|
533
|
+
appendQuery("/users#top", { tab: "list" }); // → '/users?tab=list#top'
|
|
534
|
+
appendQuery("/users", { active: true }); // → '/users?active=true'
|
|
535
|
+
appendQuery("/users", { tag: ["a", "b"] }); // → '/users?tag=a&tag=b'
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
This is the same helper every path-resolving function uses internally.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### `extractQueryFromPath(path, options?)`
|
|
543
|
+
|
|
544
|
+
Parses the query string out of a path (or bare query string) back into a plain object. Repeated keys become arrays; single keys are scalar strings.
|
|
545
|
+
|
|
546
|
+
**Options:**
|
|
547
|
+
|
|
548
|
+
- **`coerceBooleans?: boolean`** — convert the strings `"true"`/`"false"` to real booleans.
|
|
549
|
+
- **`coerceNumbers?: boolean`** — convert numeric strings (`"42"`, `"3.14"`) to real numbers.
|
|
550
|
+
|
|
551
|
+
```ts
|
|
552
|
+
import { extractQueryFromPath } from "react-routes-forge";
|
|
553
|
+
|
|
554
|
+
extractQueryFromPath("/users/42?tab=profile&tag=a&tag=b");
|
|
555
|
+
// → { tab: "profile", tag: ["a", "b"] }
|
|
556
|
+
|
|
557
|
+
extractQueryFromPath("/search?active=true", { coerceBooleans: true });
|
|
558
|
+
// → { active: true }
|
|
559
|
+
|
|
560
|
+
extractQueryFromPath("/search?page=2&limit=10", { coerceNumbers: true });
|
|
561
|
+
// → { page: 2, limit: 10 }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
### `devWarn(message)`
|
|
567
|
+
|
|
568
|
+
Emits a `console.warn` in non-production environments. Shared by the core utilities and `defineRoutes()` so the production check lives in one place.
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
import { devWarn } from "react-routes-forge";
|
|
572
|
+
|
|
573
|
+
devWarn("[route-forge] Something looks wrong.");
|
|
574
|
+
// → console.warn in dev/test, silent in production bundles
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
### `clearPathCache()`
|
|
580
|
+
|
|
581
|
+
Clears the internal regex caches used by `matchPath()` / prefix matching. Primarily useful in test suites to prevent cached patterns from leaking across test cases.
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import { clearPathCache } from "react-routes-forge";
|
|
585
|
+
|
|
586
|
+
beforeEach(() => {
|
|
587
|
+
clearPathCache();
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
365
593
|
## React hooks
|
|
366
594
|
|
|
367
|
-
Import these only if you're using React Router — they
|
|
595
|
+
Import these only if you're using React Router — they live in a separate `react-routes-forge/hooks` entry, so the core package never pulls in `react-router-dom`.
|
|
368
596
|
|
|
369
597
|
### `useRouteParams<T>()`
|
|
370
598
|
|
|
371
|
-
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.
|
|
599
|
+
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. Alternatively, pass a **dynamic route value from your `PATHS` tree** and the params are inferred from it automatically:
|
|
372
600
|
|
|
373
601
|
```tsx
|
|
374
|
-
import { useRouteParams } from "react-routes-forge";
|
|
602
|
+
import { useRouteParams } from "react-routes-forge/hooks";
|
|
375
603
|
|
|
376
604
|
// Route: '/users/edit/:id'
|
|
377
605
|
function EditUser() {
|
|
@@ -386,6 +614,13 @@ function Comment() {
|
|
|
386
614
|
useRouteParams<"/posts/:postId/comments/:commentId">();
|
|
387
615
|
// ...
|
|
388
616
|
}
|
|
617
|
+
|
|
618
|
+
// Or pass a route from your PATHS tree — types are inferred:
|
|
619
|
+
const PATHS = defineRoutes({ USERS: { EDIT: "/users/edit/:id" } } as const);
|
|
620
|
+
function EditUserInferred() {
|
|
621
|
+
const { id } = useRouteParams(PATHS.USERS.EDIT);
|
|
622
|
+
// ...
|
|
623
|
+
}
|
|
389
624
|
```
|
|
390
625
|
|
|
391
626
|
---
|
|
@@ -395,7 +630,7 @@ function Comment() {
|
|
|
395
630
|
Thin, typed wrapper around `useNavigate()` that accepts a resolved path (the output of `.build()`) along with the usual navigation options.
|
|
396
631
|
|
|
397
632
|
```tsx
|
|
398
|
-
import { useNavigateTo } from "react-routes-forge";
|
|
633
|
+
import { useNavigateTo } from "react-routes-forge/hooks";
|
|
399
634
|
import { PATHS } from "./paths";
|
|
400
635
|
|
|
401
636
|
function Component() {
|
|
@@ -416,10 +651,10 @@ navigateTo(PATHS.USERS.ROOT, { state: { from: "settings" } });
|
|
|
416
651
|
|
|
417
652
|
### `useResolvedPath(template, params, query?, options?)`
|
|
418
653
|
|
|
419
|
-
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()`.
|
|
654
|
+
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()`. It mirrors the library's own [`build()`](#buildtemplate-params-query-options), so splat (`*`) and optional (`:param?`) segments work identically to the core API — and the encoding/`strict` behaviour is consistent across React Router v6 and v7. Accepts the same `query` and `options` as [`build()`](#buildtemplate-params-query-options).
|
|
420
655
|
|
|
421
656
|
```tsx
|
|
422
|
-
import { useResolvedPath } from "react-routes-forge";
|
|
657
|
+
import { useResolvedPath } from "react-routes-forge/hooks";
|
|
423
658
|
|
|
424
659
|
const path = useResolvedPath("/users/:id", { id: 42 });
|
|
425
660
|
// → '/users/42'
|
|
@@ -427,8 +662,117 @@ const path = useResolvedPath("/users/:id", { id: 42 });
|
|
|
427
662
|
const path = useResolvedPath("/users/:id", { id: 42 }, { tab: "info" });
|
|
428
663
|
// → '/users/42?tab=info'
|
|
429
664
|
|
|
665
|
+
// Splat segments are preserved
|
|
666
|
+
const path = useResolvedPath("/files/*", { "*": "a/b/c" });
|
|
667
|
+
// → '/files/a/b/c'
|
|
668
|
+
|
|
430
669
|
// Strict mode — throws RangeError instead of warning on missing params
|
|
431
670
|
const path = useResolvedPath("/users/:id", {}, undefined, { strict: true });
|
|
671
|
+
|
|
672
|
+
// With hash fragment
|
|
673
|
+
const path = useResolvedPath("/page", {}, undefined, { hash: "section" });
|
|
674
|
+
// → '/page#section'
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
### `useActivePath(template, options?)`
|
|
680
|
+
|
|
681
|
+
A hook that checks whether the current location matches a route template or path — a thin wrapper around [`isActivePath()`](#isactivepathcurrentpath-template-options) that reads the location from the router. Same matching semantics: case-insensitive by default, trailing slashes tolerated, `exact: true` by default.
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
import { useActivePath } from "react-routes-forge/hooks";
|
|
685
|
+
|
|
686
|
+
function Nav() {
|
|
687
|
+
const isUsersActive = useActivePath(PATHS.USERS.ROOT, { exact: false });
|
|
688
|
+
const isProfileActive = useActivePath("/users/:id", { caseSensitive: true });
|
|
689
|
+
|
|
690
|
+
return (
|
|
691
|
+
<Link className={isUsersActive ? "active" : ""} to={PATHS.USERS.ROOT}>
|
|
692
|
+
Users
|
|
693
|
+
</Link>
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
### `useTypedSearchParams(options?)`
|
|
701
|
+
|
|
702
|
+
A typed wrapper around React Router's `useSearchParams`. Returns a parsed query params object (using [`extractQueryFromPath()`](#extractqueryfrompathpath-options)) and a setter that updates the query string. The same coercion options are supported: `{ coerceBooleans: true }` and `{ coerceNumbers: true }`.
|
|
703
|
+
|
|
704
|
+
```tsx
|
|
705
|
+
import { useTypedSearchParams } from "react-routes-forge/hooks";
|
|
706
|
+
|
|
707
|
+
function Filters() {
|
|
708
|
+
const [query, setQuery] = useTypedSearchParams({
|
|
709
|
+
coerceBooleans: true,
|
|
710
|
+
coerceNumbers: true,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// query.page is a number when the URL is '/search?page=2'
|
|
714
|
+
const nextPage = (query.page ?? 0) + 1;
|
|
715
|
+
setQuery({ ...query, page: nextPage });
|
|
716
|
+
|
|
717
|
+
// Clear a filter by omitting it (or pass null/undefined)
|
|
718
|
+
setQuery({ page: 1, sort: "asc" });
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Splat (`/*`) segments
|
|
725
|
+
|
|
726
|
+
Splat routes (`/files/*`) capture the rest of the path — including slashes — into a single `*` param, matching React Router semantics. Supported across the entire core API, not just the hooks.
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
import { defineRoutes, build, isActivePath, extractParamsFromPath } from "react-routes-forge";
|
|
730
|
+
|
|
731
|
+
const PATHS = defineRoutes({
|
|
732
|
+
FILES: "/files/*",
|
|
733
|
+
} as const);
|
|
734
|
+
|
|
735
|
+
PATHS.FILES.build({ "*": "reports/2026/q1" }); // → '/files/reports/2026/q1'
|
|
736
|
+
String(PATHS.FILES); // → '/files/*'
|
|
737
|
+
PATHS.FILES.paramNames; // → ['*']
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Behaviour notes:
|
|
741
|
+
|
|
742
|
+
- **Slashes in the value are preserved** (they're path separators); other special characters are still URL-encoded — `"/files/*"` with `"a b/c?d"` → `"/files/a%20b/c%3Fd"`.
|
|
743
|
+
- **A missing splat value drops the `/*` suffix** — `/files/*` resolves to `/files` (matching React Router, where the splat route also matches the base path).
|
|
744
|
+
- `isActivePath("/files/a/b", "/files/*")` → `true`; `extractParamsFromPath("/files/*", "/files/a/b")` → `{ "*": "a/b" }`.
|
|
745
|
+
- A splat must be **trailing** (`/files/*`). A `*` in the middle of a path is invalid and produces a dev warning (see below).
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## Route validation
|
|
750
|
+
|
|
751
|
+
`defineRoutes()` validates every template in development (no-op in production) and warns via `console.warn` about likely mistakes:
|
|
752
|
+
|
|
753
|
+
| Problem | Example | Warning |
|
|
754
|
+
| ------- | ------- | ------- |
|
|
755
|
+
| Missing leading `/` | `"users/:id"` | `does not start with "/"` |
|
|
756
|
+
| Non-trailing splat | `"/files/*/extra"` | `*` outside a trailing `"/*"` |
|
|
757
|
+
| Duplicate path template | `FOO: "/foo"` and `BAR: "/foo"` | `Duplicate route path "/foo"` (names both keys) |
|
|
758
|
+
| Static route shadowed by a dynamic route above it | `DETAILS: "/users/:id"` defined before `ME: "/users/me"` | `"ME" is shadowed by dynamic route "DETAILS"` |
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
// duplicate route paths are caught at startup instead of as a routing bug later
|
|
762
|
+
defineRoutes({
|
|
763
|
+
A: { FOO: "/foo" },
|
|
764
|
+
B: { FOO: "/foo" },
|
|
765
|
+
} as const);
|
|
766
|
+
// ⚠ console.warn: [route-forge] Duplicate route path "/foo" for "A.FOO" and "B.FOO". Only one of them will be reachable.
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
These are warnings, not errors — invalid routes still build, so a broken definition can't crash your app at import time. Run a stricter check once in tests if you want duplicates to fail the build:
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
it("has no duplicate route paths", () => {
|
|
773
|
+
const paths = flattenRoutes(PATHS).map((r) => r.path);
|
|
774
|
+
expect(paths).toEqual([...new Set(paths)]);
|
|
775
|
+
});
|
|
432
776
|
```
|
|
433
777
|
|
|
434
778
|
---
|
|
@@ -451,6 +795,13 @@ build("/search", {}, { tags: ["admin", "moderator"] });
|
|
|
451
795
|
// → '/search?tags=admin&tags=moderator'
|
|
452
796
|
```
|
|
453
797
|
|
|
798
|
+
**Boolean values** serialize to `"true"`/`"false"`:
|
|
799
|
+
|
|
800
|
+
```ts
|
|
801
|
+
build("/search", {}, { active: true, draft: false });
|
|
802
|
+
// → '/search?active=true&draft=false'
|
|
803
|
+
```
|
|
804
|
+
|
|
454
805
|
**`null` and `undefined` values are dropped**, so you can pass optional filters without conditionally building the object:
|
|
455
806
|
|
|
456
807
|
```ts
|
|
@@ -458,15 +809,51 @@ build("/users", {}, { sort: "asc", filter: undefined });
|
|
|
458
809
|
// → '/users?sort=asc'
|
|
459
810
|
```
|
|
460
811
|
|
|
461
|
-
Static routes
|
|
812
|
+
Static routes have a `.build()` too — there are no params to interpolate, but you can still attach a query string or hash:
|
|
462
813
|
|
|
463
814
|
```ts
|
|
464
|
-
|
|
815
|
+
PATHS.USERS.ROOT.build({ sort: "asc", page: 2 });
|
|
816
|
+
// → '/users?sort=asc&page=2'
|
|
465
817
|
|
|
818
|
+
// The standalone build() util works the same way for raw templates:
|
|
819
|
+
import { build } from "react-routes-forge";
|
|
466
820
|
build(PATHS.USERS.ROOT, {}, { sort: "asc", page: 2 });
|
|
467
821
|
// → '/users?sort=asc&page=2'
|
|
468
822
|
```
|
|
469
823
|
|
|
824
|
+
Reading query params back out is handled by [`extractQueryFromPath(path, options?)`](#extractqueryfrompathpath-options), and appending to an existing URL (e.g. a link with pre-set filters) by [`appendQuery(path, query?, hash?)`](#appendquerypath-query-hash).
|
|
825
|
+
|
|
826
|
+
---
|
|
827
|
+
|
|
828
|
+
## Hash fragment support
|
|
829
|
+
|
|
830
|
+
URL hash fragments (`#section`) are supported in every path-resolving function — `.build()`, `build()`, and `useResolvedPath()` — via the `hash` option. The hash is appended after the query string, if any.
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
// Via fluent .build() on a dynamic route
|
|
834
|
+
PATHS.USERS.DETAILS.build({ id: 42 }, undefined, { hash: "profile" });
|
|
835
|
+
// → '/users/42#profile'
|
|
836
|
+
|
|
837
|
+
// With query + hash
|
|
838
|
+
PATHS.USERS.DETAILS.build({ id: 42 }, { tab: "info" }, { hash: "details" });
|
|
839
|
+
// → '/users/42?tab=info#details'
|
|
840
|
+
|
|
841
|
+
// Via standalone build()
|
|
842
|
+
build("/page", {}, undefined, { hash: "section" });
|
|
843
|
+
// → '/page#section'
|
|
844
|
+
|
|
845
|
+
// Via useResolvedPath
|
|
846
|
+
useResolvedPath(
|
|
847
|
+
"/users/:id",
|
|
848
|
+
{ id: 5 },
|
|
849
|
+
{ tab: "billing" },
|
|
850
|
+
{ hash: "invoice" },
|
|
851
|
+
);
|
|
852
|
+
// → '/users/5?tab=billing#invoice'
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
The leading `#` is added automatically — pass just the fragment name (e.g. `"details"`, not `"#details"`).
|
|
856
|
+
|
|
470
857
|
---
|
|
471
858
|
|
|
472
859
|
## Strict mode
|
|
@@ -543,30 +930,30 @@ Everywhere the template string itself was used (e.g. `<Route path={PATHS.SERVICE
|
|
|
543
930
|
|
|
544
931
|
## Known behaviours & gotchas
|
|
545
932
|
|
|
546
|
-
###
|
|
933
|
+
### Routes are `String` objects, not primitives
|
|
547
934
|
|
|
548
|
-
`defineRoutes` wraps dynamic
|
|
935
|
+
`defineRoutes` wraps **every** path — static, dynamic, and splat — in [`String` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String) so that `.build()` (and `.paramNames` on dynamic routes) can be attached as properties. This means:
|
|
549
936
|
|
|
550
937
|
```ts
|
|
551
938
|
// ✓ These all work as expected
|
|
552
|
-
String(PATHS.
|
|
939
|
+
String(PATHS.HOME); // '/'
|
|
553
940
|
`${PATHS.USERS.EDIT}`; // '/users/edit/:id'
|
|
554
941
|
PATHS.USERS.EDIT == "/users/edit/:id"; // true (loose equality)
|
|
555
942
|
|
|
556
943
|
// ✗ Watch out for these
|
|
557
|
-
typeof PATHS.
|
|
558
|
-
PATHS.
|
|
944
|
+
typeof PATHS.HOME; // 'object' ← not 'string'
|
|
945
|
+
PATHS.HOME === "/"; // false ← strict equality fails
|
|
559
946
|
```
|
|
560
947
|
|
|
561
|
-
Prefer template literals or explicit `String()` coercion when comparing
|
|
948
|
+
Prefer template literals or explicit `String()` coercion when comparing route values, and avoid using them as plain object/`Map` keys.
|
|
562
949
|
|
|
563
950
|
### `useResolvedPath` vs. the library's own `buildPath`
|
|
564
951
|
|
|
565
|
-
`useResolvedPath`
|
|
952
|
+
`useResolvedPath` is a thin wrapper around the library's own `buildPath`, so splat (`*`), optional (`:param?`) and encoding behaviour are identical across every entry point — and consistent across React Router v6 and v7 (v7's `generatePath` URL-encodes values itself, which would otherwise double-encode).
|
|
566
953
|
|
|
567
|
-
### ESM
|
|
954
|
+
### ESM + CommonJS builds
|
|
568
955
|
|
|
569
|
-
This package ships **ESM
|
|
956
|
+
This package ships both **ESM and CommonJS** bundles (`dist/index.js` for ESM, `dist/index.cjs` for CJS), with `exports` conditions routing each environment to the right format. Modern bundlers use the ESM build; Node.js `require()` gets the CommonJS build automatically. Consumers on plain CommonJS are fully supported.
|
|
570
957
|
|
|
571
958
|
---
|
|
572
959
|
|
|
@@ -582,15 +969,29 @@ PATHS.USERS.EDIT.build({ userId: 42 }); // ✗ compile error — 'id' expected,
|
|
|
582
969
|
|
|
583
970
|
`.paramNames` is similarly typed as a literal array of the exact param names in the template, not a generic `string[]`.
|
|
584
971
|
|
|
972
|
+
To annotate a plain route object (e.g. a shared constant used by `defineRoutes()`), import the `RouteTree` type:
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
import type { RouteTree } from "react-routes-forge";
|
|
976
|
+
|
|
977
|
+
const routes: RouteTree = {
|
|
978
|
+
HOME: "/",
|
|
979
|
+
USERS: { ROOT: "/users", EDIT: "/users/edit/:id" },
|
|
980
|
+
};
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
The result of `defineRoutes()` is typed as `ResolvedRoutes`, and individual leaves are `StaticRoute<T>` (static paths, with `.build(query?, options?)`) or `DynamicRoute<T>` (dynamic/splat paths, with `.build(params, ...)` and `.paramNames`) — all exported as types if you need to reference them. `MatchPathOptions` types the [`matchPath()`](#matchpathtemplate-options) options bag.
|
|
984
|
+
|
|
585
985
|
---
|
|
586
986
|
|
|
587
987
|
## Testing
|
|
588
988
|
|
|
589
|
-
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
|
|
989
|
+
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, booleans, `null`/`undefined` filtering), splat segments, route validation, nested route groups, and duplicate-path detection.
|
|
590
990
|
|
|
591
991
|
```bash
|
|
592
|
-
npm test
|
|
593
|
-
npm run test:watch
|
|
992
|
+
npm test # run the full suite once (bun test)
|
|
993
|
+
npm run test:watch # watch mode
|
|
994
|
+
npm run test:coverage # run with coverage (vitest + v8, outputs lcov.info)
|
|
594
995
|
|
|
595
996
|
# equivalent with other package managers
|
|
596
997
|
pnpm test / pnpm test:watch
|
|
@@ -598,6 +999,8 @@ yarn test / yarn test:watch
|
|
|
598
999
|
bun test / bun test:watch
|
|
599
1000
|
```
|
|
600
1001
|
|
|
1002
|
+
CI runs the suite across a **matrix of Node.js versions (18 / 20 / 22 / 24)** and **React Router v6 and v7**, plus lint, a production build, and a coverage job that uploads `lcov.info` as a build artifact (see `.github/workflows/ci-security.yml`).
|
|
1003
|
+
|
|
601
1004
|
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.
|
|
602
1005
|
|
|
603
1006
|
---
|
|
@@ -605,7 +1008,7 @@ If you're contributing, new behaviour should come with a matching test — the e
|
|
|
605
1008
|
## Requirements
|
|
606
1009
|
|
|
607
1010
|
- **React** ≥ 17 (peer dependency)
|
|
608
|
-
- **react-router-dom** ≥ 6 (optional peer dependency — required only for the
|
|
1011
|
+
- **react-router-dom** ≥ 6 (optional peer dependency — required only for the `react-routes-forge/hooks` entry)
|
|
609
1012
|
- **Node.js** ≥ 18
|
|
610
1013
|
- **TypeScript** ≥ 5 recommended for full type inference (the package works with plain JavaScript too, just without compile-time param checking)
|
|
611
1014
|
|