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 +625 -170
- package/dist/index.d.ts +61 -12
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
PATHS.USERS.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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)
|
|
8
|
+
[](#)
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
74
|
-
* const {
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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
|
+
"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
|
-
"@
|
|
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
|
}
|