react-routes-forge 1.2.0 → 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 +355 -81
- 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 +64 -172
- 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
|
---
|
|
@@ -22,15 +24,23 @@ One source of truth for your routes — templates for `<Route path={...} />` and
|
|
|
22
24
|
- [`build(template, params, query?, options?)`](#buildtemplate-params-query-options)
|
|
23
25
|
- [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options)
|
|
24
26
|
- [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath)
|
|
25
|
-
- [`matchPath(template)`](#matchpathtemplate)
|
|
27
|
+
- [`matchPath(template, options?)`](#matchpathtemplate-options)
|
|
26
28
|
- [`joinPaths(...segments)`](#joinpathssegments)
|
|
27
29
|
- [`getParamNames(template)`](#getparamnamestemplate)
|
|
28
30
|
- [`flattenRoutes(routes)`](#flattenroutesroutes)
|
|
29
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)
|
|
30
36
|
- [React hooks](#react-hooks)
|
|
31
37
|
- [`useRouteParams<T>()`](#useroutparamst)
|
|
32
38
|
- [`useNavigateTo()`](#usenavigateto)
|
|
33
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)
|
|
34
44
|
- [Query string support](#query-string-support)
|
|
35
45
|
- [Hash fragment support](#hash-fragment-support)
|
|
36
46
|
- [Strict mode](#strict-mode)
|
|
@@ -81,7 +91,7 @@ You get:
|
|
|
81
91
|
- **Single source of truth** — no duplicate template/builder pairs to keep in sync
|
|
82
92
|
- **Compile-time param safety** — `.build()` is typed from the path string itself; missing or misspelled params are TypeScript errors
|
|
83
93
|
- **Query string support** — built into `.build()`, no manual `URLSearchParams` wrangling
|
|
84
|
-
- **Zero runtime dependencies** for the core API
|
|
94
|
+
- **Zero runtime dependencies** for the core API
|
|
85
95
|
- **Deep nesting supported out of the box** — organize routes into as many nested groups as your app needs
|
|
86
96
|
|
|
87
97
|
---
|
|
@@ -98,19 +108,7 @@ yarn add react-routes-forge
|
|
|
98
108
|
bun add react-routes-forge
|
|
99
109
|
```
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
npm install react-router-dom # only if you're using the hooks
|
|
105
|
-
# or
|
|
106
|
-
pnpm add react-router-dom
|
|
107
|
-
# or
|
|
108
|
-
yarn add react-router-dom
|
|
109
|
-
# or
|
|
110
|
-
bun add react-router-dom
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
> **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.
|
|
114
112
|
|
|
115
113
|
---
|
|
116
114
|
|
|
@@ -167,12 +165,17 @@ That's the entire API surface you need for most apps. Everything below covers th
|
|
|
167
165
|
|
|
168
166
|
### Route types
|
|
169
167
|
|
|
170
|
-
| Route type | Example | Behaves as | Gains
|
|
171
|
-
| ----------- | ----------------------- | --------------------------------------- |
|
|
172
|
-
| **Static** | `HOME: '/'` |
|
|
173
|
-
| **Dynamic** | `DETAILS: '/users/:id'` | String-like (coercible to its template) | `.build(params, query?, options?)` and `.paramNames`
|
|
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` |
|
|
173
|
+
|
|
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`.
|
|
175
|
+
|
|
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.
|
|
174
177
|
|
|
175
|
-
`defineRoutes()`
|
|
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).
|
|
176
179
|
|
|
177
180
|
---
|
|
178
181
|
|
|
@@ -182,32 +185,39 @@ Quick reference for everything the package exports — grouped by kind. Click th
|
|
|
182
185
|
|
|
183
186
|
### Route definition
|
|
184
187
|
|
|
185
|
-
| Export
|
|
186
|
-
|
|
|
187
|
-
| [`defineRoutes(routeMap)`](#defineroutesroutemap)
|
|
188
|
-
| `.build(
|
|
189
|
-
| `.
|
|
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 |
|
|
190
194
|
|
|
191
195
|
### Utilities
|
|
192
196
|
|
|
193
|
-
| Export
|
|
194
|
-
|
|
|
195
|
-
| [`build(template, params, query?, options?)`](#buildtemplate-params-query-options)
|
|
196
|
-
| [`isActivePath(currentPath, template, options?)`](#isactivepathcurrentpath-template-options)| Check if a path matches a template (nav-highlighting)
|
|
197
|
-
| [`extractParamsFromPath(template, resolvedPath)`](#extractparamsfrompathtemplate-resolvedpath)| Pull param values back out of a resolved URL
|
|
198
|
-
| [`matchPath(template)`](#matchpathtemplate)
|
|
199
|
-
| [`joinPaths(...segments)`](#joinpathssegments)
|
|
200
|
-
| [`getParamNames(template)`](#getparamnamestemplate)
|
|
201
|
-
| [`flattenRoutes(routes)`](#flattenroutesroutes)
|
|
202
|
-
| [`getBreadcrumbs(routes, currentPath, options?)`](#getbreadcrumbsroutes-currentpath-options)| Build a breadcrumb trail from a route tree and current URL |
|
|
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) |
|
|
203
211
|
|
|
204
212
|
### React hooks
|
|
205
213
|
|
|
206
|
-
| Export
|
|
207
|
-
|
|
|
208
|
-
| [`useRouteParams<T>()`](#useroutparamst)
|
|
209
|
-
| [`useNavigateTo()`](#usenavigateto)
|
|
210
|
-
| [`useResolvedPath(...)`](#useresolvedpathtemplate-params-query-options)
|
|
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 |
|
|
211
221
|
|
|
212
222
|
---
|
|
213
223
|
|
|
@@ -217,10 +227,11 @@ Quick reference for everything the package exports — grouped by kind. Click th
|
|
|
217
227
|
|
|
218
228
|
Creates a fully typed route object from a nested plain object.
|
|
219
229
|
|
|
220
|
-
-
|
|
221
|
-
-
|
|
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:
|
|
222
233
|
- **`.build(params, query?, options?)`** — resolves the template into a concrete URL
|
|
223
|
-
- **`.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 `['*']`)
|
|
224
235
|
|
|
225
236
|
Nesting is unlimited — organize routes into as many groups and sub-groups as your app needs.
|
|
226
237
|
|
|
@@ -228,17 +239,21 @@ Nesting is unlimited — organize routes into as many groups and sub-groups as y
|
|
|
228
239
|
const PATHS = defineRoutes({
|
|
229
240
|
SERVICES: {
|
|
230
241
|
ROOT: "/services",
|
|
231
|
-
|
|
232
|
-
DETAILS: "/services/
|
|
233
|
-
EDIT: "/services/
|
|
242
|
+
SUPPORT_CENTER: {
|
|
243
|
+
DETAILS: "/services/support-center/:id",
|
|
244
|
+
EDIT: "/services/support-center/edit/:id",
|
|
234
245
|
},
|
|
235
246
|
},
|
|
236
247
|
} as const);
|
|
237
248
|
|
|
238
249
|
PATHS.SERVICES.ROOT; // '/services'
|
|
239
|
-
PATHS.SERVICES.
|
|
240
|
-
PATHS.SERVICES.
|
|
241
|
-
|
|
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']
|
|
242
257
|
```
|
|
243
258
|
|
|
244
259
|
> Always pass `as const` to `defineRoutes()` — it preserves the literal string types that power `.build()`'s compile-time param checking.
|
|
@@ -247,7 +262,7 @@ PATHS.SERVICES.BENEFICIARY_CARE_CENTER.EDIT.paramNames; // ['id']
|
|
|
247
262
|
|
|
248
263
|
### `build(template, params, query?, options?)`
|
|
249
264
|
|
|
250
|
-
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).
|
|
251
266
|
|
|
252
267
|
```ts
|
|
253
268
|
import { build } from "react-routes-forge";
|
|
@@ -270,11 +285,32 @@ build("/page", {}, undefined, { hash: "section" });
|
|
|
270
285
|
// → '/page#section'
|
|
271
286
|
```
|
|
272
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'
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
See [Splat (`/*`) segments](#splat--segments) for details.
|
|
304
|
+
|
|
273
305
|
---
|
|
274
306
|
|
|
275
307
|
### `isActivePath(currentPath, template, options?)`
|
|
276
308
|
|
|
277
|
-
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).
|
|
278
314
|
|
|
279
315
|
```ts
|
|
280
316
|
import { isActivePath } from "react-routes-forge";
|
|
@@ -283,6 +319,9 @@ isActivePath("/users/42", "/users/:id"); // true
|
|
|
283
319
|
isActivePath("/users/42/posts", "/users/:id"); // false (exact match by default)
|
|
284
320
|
isActivePath("/users/42/posts", "/users/:id", { exact: false }); // true (prefix match)
|
|
285
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)
|
|
286
325
|
```
|
|
287
326
|
|
|
288
327
|
A common real-world use — highlighting the active nav link:
|
|
@@ -318,7 +357,7 @@ extractParamsFromPath("/a/:x/b/:y", "/a/foo/b/bar");
|
|
|
318
357
|
|
|
319
358
|
---
|
|
320
359
|
|
|
321
|
-
### `matchPath(template)`
|
|
360
|
+
### `matchPath(template, options?)`
|
|
322
361
|
|
|
323
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.
|
|
324
363
|
|
|
@@ -326,9 +365,19 @@ Converts a route template string into an anchored `RegExp` — useful when you n
|
|
|
326
365
|
import { matchPath } from "react-routes-forge";
|
|
327
366
|
|
|
328
367
|
const re = matchPath("/users/:id");
|
|
329
|
-
re.test("/users/42");
|
|
330
|
-
re.exec("/users/42");
|
|
331
|
-
re.test("/users/42/posts");
|
|
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
|
|
332
381
|
```
|
|
333
382
|
|
|
334
383
|
This is the building block used internally by `isActivePath` and `extractParamsFromPath`.
|
|
@@ -356,6 +405,7 @@ Returns the list of param names present in a template string.
|
|
|
356
405
|
import { getParamNames } from "react-routes-forge";
|
|
357
406
|
|
|
358
407
|
getParamNames("/users/:id/posts/:postId"); // → ['id', 'postId']
|
|
408
|
+
getParamNames("/files/*"); // → ['*'] (the splat param)
|
|
359
409
|
getParamNames("/users"); // → []
|
|
360
410
|
```
|
|
361
411
|
|
|
@@ -413,6 +463,7 @@ it("has no duplicate route paths", () => {
|
|
|
413
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.
|
|
414
464
|
|
|
415
465
|
Each breadcrumb entry contains:
|
|
466
|
+
|
|
416
467
|
- **`key`** — the dot-joined key from the route tree (e.g. `"USERS.EDIT"`)
|
|
417
468
|
- **`label`** — a human-readable label derived from the key (e.g. `"USERS.ROOT"` → `"Users"`, `"USERS.EDIT"` → `"Edit"`)
|
|
418
469
|
- **`path`** — the resolved breadcrumb path with params filled in (e.g. `"/users/edit/42"`)
|
|
@@ -447,11 +498,21 @@ getBreadcrumbs(PATHS, "/users/edit/42");
|
|
|
447
498
|
|
|
448
499
|
```ts
|
|
449
500
|
getBreadcrumbs(PATHS, "/users/edit/42", {
|
|
450
|
-
labelResolver: (key) =>
|
|
501
|
+
labelResolver: (key) =>
|
|
502
|
+
key.split(".").pop()!.replace(/_/g, " ").toUpperCase(),
|
|
451
503
|
});
|
|
452
504
|
// → [{ label: "HOME" }, { label: "ROOT" }, { label: "EDIT" }]
|
|
453
505
|
```
|
|
454
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
|
+
|
|
455
516
|
**Pre-flattened input** — pass a cached `flattenRoutes()` result instead of the tree:
|
|
456
517
|
|
|
457
518
|
```ts
|
|
@@ -461,16 +522,84 @@ getBreadcrumbs(flat, "/users/edit/42"); // same result as passing the tree
|
|
|
461
522
|
|
|
462
523
|
---
|
|
463
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
|
+
|
|
464
593
|
## React hooks
|
|
465
594
|
|
|
466
|
-
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`.
|
|
467
596
|
|
|
468
597
|
### `useRouteParams<T>()`
|
|
469
598
|
|
|
470
|
-
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:
|
|
471
600
|
|
|
472
601
|
```tsx
|
|
473
|
-
import { useRouteParams } from "react-routes-forge";
|
|
602
|
+
import { useRouteParams } from "react-routes-forge/hooks";
|
|
474
603
|
|
|
475
604
|
// Route: '/users/edit/:id'
|
|
476
605
|
function EditUser() {
|
|
@@ -485,6 +614,13 @@ function Comment() {
|
|
|
485
614
|
useRouteParams<"/posts/:postId/comments/:commentId">();
|
|
486
615
|
// ...
|
|
487
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
|
+
}
|
|
488
624
|
```
|
|
489
625
|
|
|
490
626
|
---
|
|
@@ -494,7 +630,7 @@ function Comment() {
|
|
|
494
630
|
Thin, typed wrapper around `useNavigate()` that accepts a resolved path (the output of `.build()`) along with the usual navigation options.
|
|
495
631
|
|
|
496
632
|
```tsx
|
|
497
|
-
import { useNavigateTo } from "react-routes-forge";
|
|
633
|
+
import { useNavigateTo } from "react-routes-forge/hooks";
|
|
498
634
|
import { PATHS } from "./paths";
|
|
499
635
|
|
|
500
636
|
function Component() {
|
|
@@ -515,10 +651,10 @@ navigateTo(PATHS.USERS.ROOT, { state: { from: "settings" } });
|
|
|
515
651
|
|
|
516
652
|
### `useResolvedPath(template, params, query?, options?)`
|
|
517
653
|
|
|
518
|
-
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).
|
|
519
655
|
|
|
520
656
|
```tsx
|
|
521
|
-
import { useResolvedPath } from "react-routes-forge";
|
|
657
|
+
import { useResolvedPath } from "react-routes-forge/hooks";
|
|
522
658
|
|
|
523
659
|
const path = useResolvedPath("/users/:id", { id: 42 });
|
|
524
660
|
// → '/users/42'
|
|
@@ -526,6 +662,10 @@ const path = useResolvedPath("/users/:id", { id: 42 });
|
|
|
526
662
|
const path = useResolvedPath("/users/:id", { id: 42 }, { tab: "info" });
|
|
527
663
|
// → '/users/42?tab=info'
|
|
528
664
|
|
|
665
|
+
// Splat segments are preserved
|
|
666
|
+
const path = useResolvedPath("/files/*", { "*": "a/b/c" });
|
|
667
|
+
// → '/files/a/b/c'
|
|
668
|
+
|
|
529
669
|
// Strict mode — throws RangeError instead of warning on missing params
|
|
530
670
|
const path = useResolvedPath("/users/:id", {}, undefined, { strict: true });
|
|
531
671
|
|
|
@@ -536,6 +676,107 @@ const path = useResolvedPath("/page", {}, undefined, { hash: "section" });
|
|
|
536
676
|
|
|
537
677
|
---
|
|
538
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
|
+
});
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
---
|
|
779
|
+
|
|
539
780
|
## Query string support
|
|
540
781
|
|
|
541
782
|
Every path-resolving function — `.build()`, `build()`, and `useResolvedPath()` — accepts an optional query object as its second-to-last argument.
|
|
@@ -554,6 +795,13 @@ build("/search", {}, { tags: ["admin", "moderator"] });
|
|
|
554
795
|
// → '/search?tags=admin&tags=moderator'
|
|
555
796
|
```
|
|
556
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
|
+
|
|
557
805
|
**`null` and `undefined` values are dropped**, so you can pass optional filters without conditionally building the object:
|
|
558
806
|
|
|
559
807
|
```ts
|
|
@@ -561,15 +809,20 @@ build("/users", {}, { sort: "asc", filter: undefined });
|
|
|
561
809
|
// → '/users?sort=asc'
|
|
562
810
|
```
|
|
563
811
|
|
|
564
|
-
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:
|
|
565
813
|
|
|
566
814
|
```ts
|
|
567
|
-
|
|
815
|
+
PATHS.USERS.ROOT.build({ sort: "asc", page: 2 });
|
|
816
|
+
// → '/users?sort=asc&page=2'
|
|
568
817
|
|
|
818
|
+
// The standalone build() util works the same way for raw templates:
|
|
819
|
+
import { build } from "react-routes-forge";
|
|
569
820
|
build(PATHS.USERS.ROOT, {}, { sort: "asc", page: 2 });
|
|
570
821
|
// → '/users?sort=asc&page=2'
|
|
571
822
|
```
|
|
572
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
|
+
|
|
573
826
|
---
|
|
574
827
|
|
|
575
828
|
## Hash fragment support
|
|
@@ -590,7 +843,12 @@ build("/page", {}, undefined, { hash: "section" });
|
|
|
590
843
|
// → '/page#section'
|
|
591
844
|
|
|
592
845
|
// Via useResolvedPath
|
|
593
|
-
useResolvedPath(
|
|
846
|
+
useResolvedPath(
|
|
847
|
+
"/users/:id",
|
|
848
|
+
{ id: 5 },
|
|
849
|
+
{ tab: "billing" },
|
|
850
|
+
{ hash: "invoice" },
|
|
851
|
+
);
|
|
594
852
|
// → '/users/5?tab=billing#invoice'
|
|
595
853
|
```
|
|
596
854
|
|
|
@@ -672,30 +930,30 @@ Everywhere the template string itself was used (e.g. `<Route path={PATHS.SERVICE
|
|
|
672
930
|
|
|
673
931
|
## Known behaviours & gotchas
|
|
674
932
|
|
|
675
|
-
###
|
|
933
|
+
### Routes are `String` objects, not primitives
|
|
676
934
|
|
|
677
|
-
`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:
|
|
678
936
|
|
|
679
937
|
```ts
|
|
680
938
|
// ✓ These all work as expected
|
|
681
|
-
String(PATHS.
|
|
939
|
+
String(PATHS.HOME); // '/'
|
|
682
940
|
`${PATHS.USERS.EDIT}`; // '/users/edit/:id'
|
|
683
941
|
PATHS.USERS.EDIT == "/users/edit/:id"; // true (loose equality)
|
|
684
942
|
|
|
685
943
|
// ✗ Watch out for these
|
|
686
|
-
typeof PATHS.
|
|
687
|
-
PATHS.
|
|
944
|
+
typeof PATHS.HOME; // 'object' ← not 'string'
|
|
945
|
+
PATHS.HOME === "/"; // false ← strict equality fails
|
|
688
946
|
```
|
|
689
947
|
|
|
690
|
-
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.
|
|
691
949
|
|
|
692
950
|
### `useResolvedPath` vs. the library's own `buildPath`
|
|
693
951
|
|
|
694
|
-
`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).
|
|
695
953
|
|
|
696
|
-
### ESM
|
|
954
|
+
### ESM + CommonJS builds
|
|
697
955
|
|
|
698
|
-
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.
|
|
699
957
|
|
|
700
958
|
---
|
|
701
959
|
|
|
@@ -711,15 +969,29 @@ PATHS.USERS.EDIT.build({ userId: 42 }); // ✗ compile error — 'id' expected,
|
|
|
711
969
|
|
|
712
970
|
`.paramNames` is similarly typed as a literal array of the exact param names in the template, not a generic `string[]`.
|
|
713
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
|
+
|
|
714
985
|
---
|
|
715
986
|
|
|
716
987
|
## Testing
|
|
717
988
|
|
|
718
|
-
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.
|
|
719
990
|
|
|
720
991
|
```bash
|
|
721
|
-
npm test
|
|
722
|
-
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)
|
|
723
995
|
|
|
724
996
|
# equivalent with other package managers
|
|
725
997
|
pnpm test / pnpm test:watch
|
|
@@ -727,6 +999,8 @@ yarn test / yarn test:watch
|
|
|
727
999
|
bun test / bun test:watch
|
|
728
1000
|
```
|
|
729
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
|
+
|
|
730
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.
|
|
731
1005
|
|
|
732
1006
|
---
|
|
@@ -734,7 +1008,7 @@ If you're contributing, new behaviour should come with a matching test — the e
|
|
|
734
1008
|
## Requirements
|
|
735
1009
|
|
|
736
1010
|
- **React** ≥ 17 (peer dependency)
|
|
737
|
-
- **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)
|
|
738
1012
|
- **Node.js** ≥ 18
|
|
739
1013
|
- **TypeScript** ≥ 5 recommended for full type inference (the package works with plain JavaScript too, just without compile-time param checking)
|
|
740
1014
|
|