├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── createRouter.test.ts
├── createRouter.ts
├── index.ts
├── inferRouteObject.test.ts
└── inferRouteObject.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Arutiunian Artem
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Router Typed Object
2 |
3 | Bringing full typesafety to your React Router configurations
4 |
5 | ## Introduction
6 |
7 | **React Router Typed Object** is a helper library for [React Router](https://reactrouter.com) that brings complete typesafety to your route configurations. It enables you to define your routes in a way that TypeScript can infer all the necessary types, ensuring that all router references are consistent and safe across your codebase. This is especially beneficial in large applications with complex routing structures, where maintaining typesafety can greatly reduce errors and improve developer productivity.
8 |
9 | By using React Router Typed Object, you can leverage the power of TypeScript to catch errors at compile time, assist in refactoring, and provide better autocompletion and documentation within your code editor. Check more details and examples in the [Typesafe references and refactorings](#typesafe-references-and-refactorings) docs section.
10 |
11 | [](https://stackblitz.com/edit/react-router-typed-object?file=src%2Frouter.tsx,src%2Fcomponents.tsx)
12 |
13 | ## Features
14 |
15 | - **Seamless Integration**: Works with exact `react-router`'s `RouteObject` type and `react-router-dom`'s "createRouter\*" functions.
16 | - **Typesafe Route Definitions**: Automatically infer types from your route configurations.
17 | - **Path Parameter Handling**: Define routes with dynamic parameters and get type-checked path generation.
18 | - **Search Parameter Validation**: Use any validation library you like to define and validate search parameters.
19 | - **Type-Safe useParams Hook**: Access route parameters with full type safety using the built-in `useParams` hook.
20 |
21 | ## Installation
22 |
23 | To install React Router Typed Object, use npm or yarn:
24 |
25 | ```bash
26 | npm install react-router-typed-object @remix-run/router
27 | ```
28 |
29 | > `@remix-run/router` explicit installation is needed to allow correct type inference.
30 |
31 | ## Usage
32 |
33 | Here's how you can use React Router Typed Object in your project.
34 |
35 | ### Defining Routes with `inferRouteObject`
36 |
37 | The `inferRouteObject` function allows you to define your routes and automatically infer their types.
38 |
39 | ```tsx
40 | import { inferRouteObject } from "react-router-typed-object";
41 |
42 | export const ROUTES = inferRouteObject({
43 | path: "home",
44 | children: [
45 | { path: "products" },
46 | {
47 | path: "categories",
48 | children: [{ children: [{ children: [{ path: "electronics" }] }] }],
49 | },
50 | ],
51 | });
52 | ```
53 |
54 | This will create a `ROUTES` object that contains typed paths for all nested routes.
55 |
56 | ### Generating Paths with Parameters
57 |
58 | You can define routes with path parameters, and `inferRouteObject` will ensure that you provide the correct parameters when generating paths.
59 |
60 | ```tsx
61 | export const ROUTES = inferRouteObject({
62 | path: "products/:productId",
63 | children: [{ path: "reviews/:reviewId" }],
64 | });
65 |
66 | const productReviewPath = ROUTES["/products/:productId/reviews/:reviewId"].path(
67 | { productId: "123", reviewId: "456" }
68 | );
69 | // productReviewPath is "/products/123/reviews/456"
70 | ```
71 |
72 | If you try to omit required parameters or provide incorrect ones, TypeScript will show an error.
73 |
74 | ### Handling Search Parameters with Validation
75 |
76 | You can define search parameters using any type predicate, which give you both typesafety and runtime validation.
77 |
78 | ```tsx
79 | import { z } from "zod";
80 |
81 | export const ROUTES = inferRouteObject({
82 | path: "products/:productId",
83 | children: [
84 | {
85 | path: "reviews/:reviewId",
86 | searchParams: z.object({
87 | sortBy: z.enum(["date", "rating"]).default("date"),
88 | search: z.string().optional(),
89 | }).parse, // NOTE the `.parse`, we need only a validation function
90 | },
91 | ],
92 | });
93 |
94 | const productReviewPath = ROUTES["/products/:productId/reviews/:reviewId"].path(
95 | { productId: "123", reviewId: "456", sortBy: "rating", search: "best" }
96 | );
97 | // productReviewPath is "/products/123/reviews/456?sortBy=rating&search=best"
98 | ```
99 |
100 | If you provide invalid search parameters, the validation function will throw an error at runtime, ensuring your app only navigates to valid URLs.
101 |
102 | ### Using typesafe paths with a router
103 |
104 | "ROUTES" is your source of truth. You can use it to get a typesafe access to your routes in all other related APIs.
105 |
106 | ```tsx
107 |
108 | ```
109 |
110 | OR, of course:
111 |
112 | ```tsx
113 | import { createBrowserRouter } from "react-router-dom";
114 |
115 | export const router = createBrowserRouter([ROUTES]);
116 | ```
117 |
118 | ```tsx
119 | const navigate = useNavigate();
120 | const goToProductReview = (productId: string, reviewId: string) => {
121 | navigate(
122 | ROUTES["/products/:productId/reviews/:reviewId"].path({
123 | productId,
124 | reviewId,
125 | })
126 | );
127 | };
128 |
129 | goToProductReview("123", "456")}>Go to Review;
130 | ```
131 |
132 | ### Using the useParams hook
133 |
134 | The `useParams` hook allows you to access route parameters with full type safety:
135 |
136 | ```tsx
137 | function MyComponent() {
138 | // Get parameters with type safety
139 | const params =
140 | ROUTES["/products/:productId/reviews/:reviewId"].path.useParams();
141 |
142 | // params.b and params.d are typed as string
143 | // If the route has search parameters, they are also included in the params object
144 |
145 | return (
146 |
147 |
Product ID: {params.productId}
148 | Review ID: {params.reviewId}
149 |
150 | );
151 | }
152 | ```
153 |
154 | You can also provide fallback values for missing parameters:
155 |
156 | ```tsx
157 | function MyComponent() {
158 | // Provide fallback values for missing parameters
159 | const params = ROUTES[
160 | "/products/:productId/reviews/:reviewId"
161 | ].path.useParams({
162 | productId: "default-product",
163 | reviewId: "default-review",
164 | });
165 |
166 | return (
167 |
168 |
Product: {params.productId}
169 | Review: {params.reviewId}
170 |
171 | );
172 | }
173 | ```
174 |
175 | The `useParams` hook will:
176 |
177 | 1. Return the current route parameters with full type safety
178 | 2. Throw a runtime error if required parameters are missing and not provided in the fallback
179 | 3. Provide TypeScript compile-time errors if you try to access parameters that don't exist
180 |
181 | ### Navigating with built in `createRouter`
182 |
183 | You can use your "ROUTES" object to get a typesafe access to your routes
184 |
185 | The `createRouter` function creates a router instance with typesafe `navigate` method which added to every "path".
186 |
187 | ```tsx
188 | import { createRouter } from "react-router-typed-object";
189 | import { z } from "zod";
190 |
191 | const ROUTER = createRouter([
192 | {
193 | path: "a",
194 | children: [
195 | {
196 | path: ":productId/reviews/:reviewId",
197 | searchParams: z.object({
198 | sortBy: z.enum(["date", "rating"]).default("date"),
199 | }).parse,
200 | },
201 | ],
202 | },
203 | ]);
204 |
205 | ROUTER["/products/:productId/reviews/:reviewId"].path.navigate({
206 | productId: "123",
207 | reviewId: "456",
208 | sortBy: "rating",
209 | });
210 | // `location.href` is "/products/123/reviews/456?sortBy=rating"
211 | ```
212 |
213 | The `.navigate()` method of a router path is just a tiny bind function from the path to router `navigate` method.
214 |
215 | ## Typesafe references and refactorings
216 |
217 | The motivation behind creating this library stemmed from working on a large legacy project with a massive route configuration exceeding 1,000 lines of code. Managing and maintaining such a large configuration was challenging. It was easy to make mistakes like creating duplicate paths or unintentionally removing or modifying routes that were used elsewhere in the application.
218 |
219 | React Router Typed Object addresses these issues by allowing developers to define a strict list of all routes with full typesafety. The "**path**" property becomes a crucial element to synchronize type references between route usages and route definitions. With this library, you can use TypeScript's powerful tooling to find all route usages from the configuration or locate the relevant configuration part from a usage point. This ensures consistency and reduces the likelihood of errors in your routing logic.
220 |
221 | Open this example on [StackBlitz](https://stackblitz.com):
222 |
223 | [](https://stackblitz.com/edit/react-router-typed-object?file=src%2Frouter.tsx,src%2Fcomponents.tsx)
224 |
225 | 
226 |
227 | 
228 |
229 | ## API Reference
230 |
231 | ### `inferRouteObject(routeConfig, basename = '')`
232 |
233 | Generates a typesafe routes object from the given route configuration. The configuration is exactly `import { type RouteObject } from "react-router"`, but with additional `searchParams` property with a validation function.
234 |
235 | - **Parameters**:
236 | - `routeConfig`: An object representing the route configuration. It is same object from original React Router (`import { type RouteObject } from "react-router"`). Each route can include `path`, `children`, and additional `searchParams` validation function.
237 | - `basename`: optional starting path.
238 | - **Returns**: The same route object with additional "`\${string}`" properties which includes full routes paths in any depth of the config.
239 |
240 | ### `createRouter(routeConfig, options)`
241 |
242 | Creates a router instance with typesafe navigation methods.
243 |
244 | - **Parameters**:
245 | - `routeConfig`: The same route configuration used in `inferRouteObject`.
246 | - `options`:
247 | - all original options from `createBrowserRouter`.
248 | - `basename`: optional starting path.
249 | - `createRouter`: optional router creation function, defaults to `createBrowserRouter`.
250 | - **Returns**: A router instance with navigation methods and all "`\${string}`" routes from `inferRouteObject`
251 |
252 | ### `path.useParams(fallback?)`
253 |
254 | A hook that returns the current route parameters with full type safety.
255 |
256 | - **Parameters**:
257 | - `fallback`: Optional object containing fallback values for missing parameters.
258 | - **Returns**: An object containing all route parameters (path and search parameters) with proper types.
259 | - **Throws**: Runtime error if required parameters are missing and not provided in the fallback.
260 | - **Type Safety**: Provides TypeScript compile-time errors if you try to access parameters that don't exist.
261 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router-typed-object",
3 | "version": "1.0.5",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "react-router-typed-object",
9 | "version": "1.0.5",
10 | "license": "MIT",
11 | "devDependencies": {
12 | "@remix-run/router": "1.20.0",
13 | "react-router": "^6.27.0",
14 | "react-router-dom": "^6.27.0",
15 | "smartbundle": "^0.7.3",
16 | "typescript": "^5.6.3",
17 | "vitest": "^2.1.2",
18 | "zod": "^3.23.8"
19 | },
20 | "peerDependencies": {
21 | "@remix-run/router": "^1.8.0",
22 | "react-router": "^6.27.0",
23 | "react-router-dom": "^6.27.0"
24 | }
25 | },
26 | "node_modules/@esbuild/aix-ppc64": {
27 | "version": "0.21.5",
28 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
29 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
30 | "cpu": [
31 | "ppc64"
32 | ],
33 | "dev": true,
34 | "optional": true,
35 | "os": [
36 | "aix"
37 | ],
38 | "engines": {
39 | "node": ">=12"
40 | }
41 | },
42 | "node_modules/@esbuild/android-arm": {
43 | "version": "0.21.5",
44 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
45 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
46 | "cpu": [
47 | "arm"
48 | ],
49 | "dev": true,
50 | "optional": true,
51 | "os": [
52 | "android"
53 | ],
54 | "engines": {
55 | "node": ">=12"
56 | }
57 | },
58 | "node_modules/@esbuild/android-arm64": {
59 | "version": "0.21.5",
60 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
61 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
62 | "cpu": [
63 | "arm64"
64 | ],
65 | "dev": true,
66 | "optional": true,
67 | "os": [
68 | "android"
69 | ],
70 | "engines": {
71 | "node": ">=12"
72 | }
73 | },
74 | "node_modules/@esbuild/android-x64": {
75 | "version": "0.21.5",
76 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
77 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
78 | "cpu": [
79 | "x64"
80 | ],
81 | "dev": true,
82 | "optional": true,
83 | "os": [
84 | "android"
85 | ],
86 | "engines": {
87 | "node": ">=12"
88 | }
89 | },
90 | "node_modules/@esbuild/darwin-arm64": {
91 | "version": "0.21.5",
92 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
93 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
94 | "cpu": [
95 | "arm64"
96 | ],
97 | "dev": true,
98 | "optional": true,
99 | "os": [
100 | "darwin"
101 | ],
102 | "engines": {
103 | "node": ">=12"
104 | }
105 | },
106 | "node_modules/@esbuild/darwin-x64": {
107 | "version": "0.21.5",
108 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
109 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
110 | "cpu": [
111 | "x64"
112 | ],
113 | "dev": true,
114 | "optional": true,
115 | "os": [
116 | "darwin"
117 | ],
118 | "engines": {
119 | "node": ">=12"
120 | }
121 | },
122 | "node_modules/@esbuild/freebsd-arm64": {
123 | "version": "0.21.5",
124 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
125 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
126 | "cpu": [
127 | "arm64"
128 | ],
129 | "dev": true,
130 | "optional": true,
131 | "os": [
132 | "freebsd"
133 | ],
134 | "engines": {
135 | "node": ">=12"
136 | }
137 | },
138 | "node_modules/@esbuild/freebsd-x64": {
139 | "version": "0.21.5",
140 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
141 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
142 | "cpu": [
143 | "x64"
144 | ],
145 | "dev": true,
146 | "optional": true,
147 | "os": [
148 | "freebsd"
149 | ],
150 | "engines": {
151 | "node": ">=12"
152 | }
153 | },
154 | "node_modules/@esbuild/linux-arm": {
155 | "version": "0.21.5",
156 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
157 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
158 | "cpu": [
159 | "arm"
160 | ],
161 | "dev": true,
162 | "optional": true,
163 | "os": [
164 | "linux"
165 | ],
166 | "engines": {
167 | "node": ">=12"
168 | }
169 | },
170 | "node_modules/@esbuild/linux-arm64": {
171 | "version": "0.21.5",
172 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
173 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
174 | "cpu": [
175 | "arm64"
176 | ],
177 | "dev": true,
178 | "optional": true,
179 | "os": [
180 | "linux"
181 | ],
182 | "engines": {
183 | "node": ">=12"
184 | }
185 | },
186 | "node_modules/@esbuild/linux-ia32": {
187 | "version": "0.21.5",
188 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
189 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
190 | "cpu": [
191 | "ia32"
192 | ],
193 | "dev": true,
194 | "optional": true,
195 | "os": [
196 | "linux"
197 | ],
198 | "engines": {
199 | "node": ">=12"
200 | }
201 | },
202 | "node_modules/@esbuild/linux-loong64": {
203 | "version": "0.21.5",
204 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
205 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
206 | "cpu": [
207 | "loong64"
208 | ],
209 | "dev": true,
210 | "optional": true,
211 | "os": [
212 | "linux"
213 | ],
214 | "engines": {
215 | "node": ">=12"
216 | }
217 | },
218 | "node_modules/@esbuild/linux-mips64el": {
219 | "version": "0.21.5",
220 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
221 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
222 | "cpu": [
223 | "mips64el"
224 | ],
225 | "dev": true,
226 | "optional": true,
227 | "os": [
228 | "linux"
229 | ],
230 | "engines": {
231 | "node": ">=12"
232 | }
233 | },
234 | "node_modules/@esbuild/linux-ppc64": {
235 | "version": "0.21.5",
236 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
237 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
238 | "cpu": [
239 | "ppc64"
240 | ],
241 | "dev": true,
242 | "optional": true,
243 | "os": [
244 | "linux"
245 | ],
246 | "engines": {
247 | "node": ">=12"
248 | }
249 | },
250 | "node_modules/@esbuild/linux-riscv64": {
251 | "version": "0.21.5",
252 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
253 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
254 | "cpu": [
255 | "riscv64"
256 | ],
257 | "dev": true,
258 | "optional": true,
259 | "os": [
260 | "linux"
261 | ],
262 | "engines": {
263 | "node": ">=12"
264 | }
265 | },
266 | "node_modules/@esbuild/linux-s390x": {
267 | "version": "0.21.5",
268 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
269 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
270 | "cpu": [
271 | "s390x"
272 | ],
273 | "dev": true,
274 | "optional": true,
275 | "os": [
276 | "linux"
277 | ],
278 | "engines": {
279 | "node": ">=12"
280 | }
281 | },
282 | "node_modules/@esbuild/linux-x64": {
283 | "version": "0.21.5",
284 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
285 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
286 | "cpu": [
287 | "x64"
288 | ],
289 | "dev": true,
290 | "optional": true,
291 | "os": [
292 | "linux"
293 | ],
294 | "engines": {
295 | "node": ">=12"
296 | }
297 | },
298 | "node_modules/@esbuild/netbsd-x64": {
299 | "version": "0.21.5",
300 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
301 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
302 | "cpu": [
303 | "x64"
304 | ],
305 | "dev": true,
306 | "optional": true,
307 | "os": [
308 | "netbsd"
309 | ],
310 | "engines": {
311 | "node": ">=12"
312 | }
313 | },
314 | "node_modules/@esbuild/openbsd-x64": {
315 | "version": "0.21.5",
316 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
317 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
318 | "cpu": [
319 | "x64"
320 | ],
321 | "dev": true,
322 | "optional": true,
323 | "os": [
324 | "openbsd"
325 | ],
326 | "engines": {
327 | "node": ">=12"
328 | }
329 | },
330 | "node_modules/@esbuild/sunos-x64": {
331 | "version": "0.21.5",
332 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
333 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
334 | "cpu": [
335 | "x64"
336 | ],
337 | "dev": true,
338 | "optional": true,
339 | "os": [
340 | "sunos"
341 | ],
342 | "engines": {
343 | "node": ">=12"
344 | }
345 | },
346 | "node_modules/@esbuild/win32-arm64": {
347 | "version": "0.21.5",
348 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
349 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
350 | "cpu": [
351 | "arm64"
352 | ],
353 | "dev": true,
354 | "optional": true,
355 | "os": [
356 | "win32"
357 | ],
358 | "engines": {
359 | "node": ">=12"
360 | }
361 | },
362 | "node_modules/@esbuild/win32-ia32": {
363 | "version": "0.21.5",
364 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
365 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
366 | "cpu": [
367 | "ia32"
368 | ],
369 | "dev": true,
370 | "optional": true,
371 | "os": [
372 | "win32"
373 | ],
374 | "engines": {
375 | "node": ">=12"
376 | }
377 | },
378 | "node_modules/@esbuild/win32-x64": {
379 | "version": "0.21.5",
380 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
381 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
382 | "cpu": [
383 | "x64"
384 | ],
385 | "dev": true,
386 | "optional": true,
387 | "os": [
388 | "win32"
389 | ],
390 | "engines": {
391 | "node": ">=12"
392 | }
393 | },
394 | "node_modules/@jridgewell/sourcemap-codec": {
395 | "version": "1.5.0",
396 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
397 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
398 | "dev": true
399 | },
400 | "node_modules/@remix-run/router": {
401 | "version": "1.20.0",
402 | "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
403 | "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==",
404 | "dev": true,
405 | "engines": {
406 | "node": ">=14.0.0"
407 | }
408 | },
409 | "node_modules/@rollup/rollup-android-arm-eabi": {
410 | "version": "4.24.0",
411 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
412 | "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
413 | "cpu": [
414 | "arm"
415 | ],
416 | "dev": true,
417 | "optional": true,
418 | "os": [
419 | "android"
420 | ]
421 | },
422 | "node_modules/@rollup/rollup-android-arm64": {
423 | "version": "4.24.0",
424 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
425 | "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
426 | "cpu": [
427 | "arm64"
428 | ],
429 | "dev": true,
430 | "optional": true,
431 | "os": [
432 | "android"
433 | ]
434 | },
435 | "node_modules/@rollup/rollup-darwin-arm64": {
436 | "version": "4.24.0",
437 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
438 | "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
439 | "cpu": [
440 | "arm64"
441 | ],
442 | "dev": true,
443 | "optional": true,
444 | "os": [
445 | "darwin"
446 | ]
447 | },
448 | "node_modules/@rollup/rollup-darwin-x64": {
449 | "version": "4.24.0",
450 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
451 | "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
452 | "cpu": [
453 | "x64"
454 | ],
455 | "dev": true,
456 | "optional": true,
457 | "os": [
458 | "darwin"
459 | ]
460 | },
461 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
462 | "version": "4.24.0",
463 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
464 | "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
465 | "cpu": [
466 | "arm"
467 | ],
468 | "dev": true,
469 | "optional": true,
470 | "os": [
471 | "linux"
472 | ]
473 | },
474 | "node_modules/@rollup/rollup-linux-arm-musleabihf": {
475 | "version": "4.24.0",
476 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
477 | "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
478 | "cpu": [
479 | "arm"
480 | ],
481 | "dev": true,
482 | "optional": true,
483 | "os": [
484 | "linux"
485 | ]
486 | },
487 | "node_modules/@rollup/rollup-linux-arm64-gnu": {
488 | "version": "4.24.0",
489 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
490 | "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
491 | "cpu": [
492 | "arm64"
493 | ],
494 | "dev": true,
495 | "optional": true,
496 | "os": [
497 | "linux"
498 | ]
499 | },
500 | "node_modules/@rollup/rollup-linux-arm64-musl": {
501 | "version": "4.24.0",
502 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
503 | "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
504 | "cpu": [
505 | "arm64"
506 | ],
507 | "dev": true,
508 | "optional": true,
509 | "os": [
510 | "linux"
511 | ]
512 | },
513 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
514 | "version": "4.24.0",
515 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
516 | "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
517 | "cpu": [
518 | "ppc64"
519 | ],
520 | "dev": true,
521 | "optional": true,
522 | "os": [
523 | "linux"
524 | ]
525 | },
526 | "node_modules/@rollup/rollup-linux-riscv64-gnu": {
527 | "version": "4.24.0",
528 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
529 | "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
530 | "cpu": [
531 | "riscv64"
532 | ],
533 | "dev": true,
534 | "optional": true,
535 | "os": [
536 | "linux"
537 | ]
538 | },
539 | "node_modules/@rollup/rollup-linux-s390x-gnu": {
540 | "version": "4.24.0",
541 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
542 | "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
543 | "cpu": [
544 | "s390x"
545 | ],
546 | "dev": true,
547 | "optional": true,
548 | "os": [
549 | "linux"
550 | ]
551 | },
552 | "node_modules/@rollup/rollup-linux-x64-gnu": {
553 | "version": "4.24.0",
554 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
555 | "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
556 | "cpu": [
557 | "x64"
558 | ],
559 | "dev": true,
560 | "optional": true,
561 | "os": [
562 | "linux"
563 | ]
564 | },
565 | "node_modules/@rollup/rollup-linux-x64-musl": {
566 | "version": "4.24.0",
567 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
568 | "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
569 | "cpu": [
570 | "x64"
571 | ],
572 | "dev": true,
573 | "optional": true,
574 | "os": [
575 | "linux"
576 | ]
577 | },
578 | "node_modules/@rollup/rollup-win32-arm64-msvc": {
579 | "version": "4.24.0",
580 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
581 | "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
582 | "cpu": [
583 | "arm64"
584 | ],
585 | "dev": true,
586 | "optional": true,
587 | "os": [
588 | "win32"
589 | ]
590 | },
591 | "node_modules/@rollup/rollup-win32-ia32-msvc": {
592 | "version": "4.24.0",
593 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
594 | "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
595 | "cpu": [
596 | "ia32"
597 | ],
598 | "dev": true,
599 | "optional": true,
600 | "os": [
601 | "win32"
602 | ]
603 | },
604 | "node_modules/@rollup/rollup-win32-x64-msvc": {
605 | "version": "4.24.0",
606 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
607 | "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
608 | "cpu": [
609 | "x64"
610 | ],
611 | "dev": true,
612 | "optional": true,
613 | "os": [
614 | "win32"
615 | ]
616 | },
617 | "node_modules/@types/estree": {
618 | "version": "1.0.6",
619 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
620 | "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
621 | "dev": true
622 | },
623 | "node_modules/@vitest/expect": {
624 | "version": "2.1.2",
625 | "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz",
626 | "integrity": "sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==",
627 | "dev": true,
628 | "dependencies": {
629 | "@vitest/spy": "2.1.2",
630 | "@vitest/utils": "2.1.2",
631 | "chai": "^5.1.1",
632 | "tinyrainbow": "^1.2.0"
633 | },
634 | "funding": {
635 | "url": "https://opencollective.com/vitest"
636 | }
637 | },
638 | "node_modules/@vitest/mocker": {
639 | "version": "2.1.2",
640 | "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz",
641 | "integrity": "sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==",
642 | "dev": true,
643 | "dependencies": {
644 | "@vitest/spy": "^2.1.0-beta.1",
645 | "estree-walker": "^3.0.3",
646 | "magic-string": "^0.30.11"
647 | },
648 | "funding": {
649 | "url": "https://opencollective.com/vitest"
650 | },
651 | "peerDependencies": {
652 | "@vitest/spy": "2.1.2",
653 | "msw": "^2.3.5",
654 | "vite": "^5.0.0"
655 | },
656 | "peerDependenciesMeta": {
657 | "msw": {
658 | "optional": true
659 | },
660 | "vite": {
661 | "optional": true
662 | }
663 | }
664 | },
665 | "node_modules/@vitest/pretty-format": {
666 | "version": "2.1.2",
667 | "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz",
668 | "integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==",
669 | "dev": true,
670 | "dependencies": {
671 | "tinyrainbow": "^1.2.0"
672 | },
673 | "funding": {
674 | "url": "https://opencollective.com/vitest"
675 | }
676 | },
677 | "node_modules/@vitest/runner": {
678 | "version": "2.1.2",
679 | "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz",
680 | "integrity": "sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==",
681 | "dev": true,
682 | "dependencies": {
683 | "@vitest/utils": "2.1.2",
684 | "pathe": "^1.1.2"
685 | },
686 | "funding": {
687 | "url": "https://opencollective.com/vitest"
688 | }
689 | },
690 | "node_modules/@vitest/snapshot": {
691 | "version": "2.1.2",
692 | "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz",
693 | "integrity": "sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==",
694 | "dev": true,
695 | "dependencies": {
696 | "@vitest/pretty-format": "2.1.2",
697 | "magic-string": "^0.30.11",
698 | "pathe": "^1.1.2"
699 | },
700 | "funding": {
701 | "url": "https://opencollective.com/vitest"
702 | }
703 | },
704 | "node_modules/@vitest/spy": {
705 | "version": "2.1.2",
706 | "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz",
707 | "integrity": "sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==",
708 | "dev": true,
709 | "dependencies": {
710 | "tinyspy": "^3.0.0"
711 | },
712 | "funding": {
713 | "url": "https://opencollective.com/vitest"
714 | }
715 | },
716 | "node_modules/@vitest/utils": {
717 | "version": "2.1.2",
718 | "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz",
719 | "integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==",
720 | "dev": true,
721 | "dependencies": {
722 | "@vitest/pretty-format": "2.1.2",
723 | "loupe": "^3.1.1",
724 | "tinyrainbow": "^1.2.0"
725 | },
726 | "funding": {
727 | "url": "https://opencollective.com/vitest"
728 | }
729 | },
730 | "node_modules/ansi-regex": {
731 | "version": "5.0.1",
732 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
733 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
734 | "dev": true,
735 | "engines": {
736 | "node": ">=8"
737 | }
738 | },
739 | "node_modules/ansi-styles": {
740 | "version": "4.3.0",
741 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
742 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
743 | "dev": true,
744 | "dependencies": {
745 | "color-convert": "^2.0.1"
746 | },
747 | "engines": {
748 | "node": ">=8"
749 | },
750 | "funding": {
751 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
752 | }
753 | },
754 | "node_modules/assertion-error": {
755 | "version": "2.0.1",
756 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
757 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
758 | "dev": true,
759 | "engines": {
760 | "node": ">=12"
761 | }
762 | },
763 | "node_modules/cac": {
764 | "version": "6.7.14",
765 | "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
766 | "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
767 | "dev": true,
768 | "engines": {
769 | "node": ">=8"
770 | }
771 | },
772 | "node_modules/chai": {
773 | "version": "5.1.1",
774 | "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
775 | "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
776 | "dev": true,
777 | "dependencies": {
778 | "assertion-error": "^2.0.1",
779 | "check-error": "^2.1.1",
780 | "deep-eql": "^5.0.1",
781 | "loupe": "^3.1.0",
782 | "pathval": "^2.0.0"
783 | },
784 | "engines": {
785 | "node": ">=12"
786 | }
787 | },
788 | "node_modules/check-error": {
789 | "version": "2.1.1",
790 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
791 | "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
792 | "dev": true,
793 | "engines": {
794 | "node": ">= 16"
795 | }
796 | },
797 | "node_modules/cliui": {
798 | "version": "8.0.1",
799 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
800 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
801 | "dev": true,
802 | "dependencies": {
803 | "string-width": "^4.2.0",
804 | "strip-ansi": "^6.0.1",
805 | "wrap-ansi": "^7.0.0"
806 | },
807 | "engines": {
808 | "node": ">=12"
809 | }
810 | },
811 | "node_modules/color-convert": {
812 | "version": "2.0.1",
813 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
814 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
815 | "dev": true,
816 | "dependencies": {
817 | "color-name": "~1.1.4"
818 | },
819 | "engines": {
820 | "node": ">=7.0.0"
821 | }
822 | },
823 | "node_modules/color-name": {
824 | "version": "1.1.4",
825 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
826 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
827 | "dev": true
828 | },
829 | "node_modules/debug": {
830 | "version": "4.3.7",
831 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
832 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
833 | "dev": true,
834 | "dependencies": {
835 | "ms": "^2.1.3"
836 | },
837 | "engines": {
838 | "node": ">=6.0"
839 | },
840 | "peerDependenciesMeta": {
841 | "supports-color": {
842 | "optional": true
843 | }
844 | }
845 | },
846 | "node_modules/deep-eql": {
847 | "version": "5.0.2",
848 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
849 | "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
850 | "dev": true,
851 | "engines": {
852 | "node": ">=6"
853 | }
854 | },
855 | "node_modules/emoji-regex": {
856 | "version": "8.0.0",
857 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
858 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
859 | "dev": true
860 | },
861 | "node_modules/esbuild": {
862 | "version": "0.21.5",
863 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
864 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
865 | "dev": true,
866 | "hasInstallScript": true,
867 | "bin": {
868 | "esbuild": "bin/esbuild"
869 | },
870 | "engines": {
871 | "node": ">=12"
872 | },
873 | "optionalDependencies": {
874 | "@esbuild/aix-ppc64": "0.21.5",
875 | "@esbuild/android-arm": "0.21.5",
876 | "@esbuild/android-arm64": "0.21.5",
877 | "@esbuild/android-x64": "0.21.5",
878 | "@esbuild/darwin-arm64": "0.21.5",
879 | "@esbuild/darwin-x64": "0.21.5",
880 | "@esbuild/freebsd-arm64": "0.21.5",
881 | "@esbuild/freebsd-x64": "0.21.5",
882 | "@esbuild/linux-arm": "0.21.5",
883 | "@esbuild/linux-arm64": "0.21.5",
884 | "@esbuild/linux-ia32": "0.21.5",
885 | "@esbuild/linux-loong64": "0.21.5",
886 | "@esbuild/linux-mips64el": "0.21.5",
887 | "@esbuild/linux-ppc64": "0.21.5",
888 | "@esbuild/linux-riscv64": "0.21.5",
889 | "@esbuild/linux-s390x": "0.21.5",
890 | "@esbuild/linux-x64": "0.21.5",
891 | "@esbuild/netbsd-x64": "0.21.5",
892 | "@esbuild/openbsd-x64": "0.21.5",
893 | "@esbuild/sunos-x64": "0.21.5",
894 | "@esbuild/win32-arm64": "0.21.5",
895 | "@esbuild/win32-ia32": "0.21.5",
896 | "@esbuild/win32-x64": "0.21.5"
897 | }
898 | },
899 | "node_modules/escalade": {
900 | "version": "3.2.0",
901 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
902 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
903 | "dev": true,
904 | "engines": {
905 | "node": ">=6"
906 | }
907 | },
908 | "node_modules/estree-walker": {
909 | "version": "3.0.3",
910 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
911 | "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
912 | "dev": true,
913 | "dependencies": {
914 | "@types/estree": "^1.0.0"
915 | }
916 | },
917 | "node_modules/fsevents": {
918 | "version": "2.3.3",
919 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
920 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
921 | "dev": true,
922 | "hasInstallScript": true,
923 | "optional": true,
924 | "os": [
925 | "darwin"
926 | ],
927 | "engines": {
928 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
929 | }
930 | },
931 | "node_modules/get-caller-file": {
932 | "version": "2.0.5",
933 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
934 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
935 | "dev": true,
936 | "engines": {
937 | "node": "6.* || 8.* || >= 10.*"
938 | }
939 | },
940 | "node_modules/is-fullwidth-code-point": {
941 | "version": "3.0.0",
942 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
943 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
944 | "dev": true,
945 | "engines": {
946 | "node": ">=8"
947 | }
948 | },
949 | "node_modules/js-tokens": {
950 | "version": "4.0.0",
951 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
952 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
953 | "dev": true,
954 | "peer": true
955 | },
956 | "node_modules/loose-envify": {
957 | "version": "1.4.0",
958 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
959 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
960 | "dev": true,
961 | "peer": true,
962 | "dependencies": {
963 | "js-tokens": "^3.0.0 || ^4.0.0"
964 | },
965 | "bin": {
966 | "loose-envify": "cli.js"
967 | }
968 | },
969 | "node_modules/loupe": {
970 | "version": "3.1.2",
971 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
972 | "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
973 | "dev": true
974 | },
975 | "node_modules/magic-string": {
976 | "version": "0.30.12",
977 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
978 | "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
979 | "dev": true,
980 | "dependencies": {
981 | "@jridgewell/sourcemap-codec": "^1.5.0"
982 | }
983 | },
984 | "node_modules/ms": {
985 | "version": "2.1.3",
986 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
987 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
988 | "dev": true
989 | },
990 | "node_modules/nanoid": {
991 | "version": "3.3.7",
992 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
993 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
994 | "dev": true,
995 | "funding": [
996 | {
997 | "type": "github",
998 | "url": "https://github.com/sponsors/ai"
999 | }
1000 | ],
1001 | "bin": {
1002 | "nanoid": "bin/nanoid.cjs"
1003 | },
1004 | "engines": {
1005 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1006 | }
1007 | },
1008 | "node_modules/pathe": {
1009 | "version": "1.1.2",
1010 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
1011 | "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
1012 | "dev": true
1013 | },
1014 | "node_modules/pathval": {
1015 | "version": "2.0.0",
1016 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
1017 | "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
1018 | "dev": true,
1019 | "engines": {
1020 | "node": ">= 14.16"
1021 | }
1022 | },
1023 | "node_modules/picocolors": {
1024 | "version": "1.1.0",
1025 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
1026 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
1027 | "dev": true
1028 | },
1029 | "node_modules/postcss": {
1030 | "version": "8.4.47",
1031 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
1032 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
1033 | "dev": true,
1034 | "funding": [
1035 | {
1036 | "type": "opencollective",
1037 | "url": "https://opencollective.com/postcss/"
1038 | },
1039 | {
1040 | "type": "tidelift",
1041 | "url": "https://tidelift.com/funding/github/npm/postcss"
1042 | },
1043 | {
1044 | "type": "github",
1045 | "url": "https://github.com/sponsors/ai"
1046 | }
1047 | ],
1048 | "dependencies": {
1049 | "nanoid": "^3.3.7",
1050 | "picocolors": "^1.1.0",
1051 | "source-map-js": "^1.2.1"
1052 | },
1053 | "engines": {
1054 | "node": "^10 || ^12 || >=14"
1055 | }
1056 | },
1057 | "node_modules/react": {
1058 | "version": "18.3.1",
1059 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1060 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1061 | "dev": true,
1062 | "peer": true,
1063 | "dependencies": {
1064 | "loose-envify": "^1.1.0"
1065 | },
1066 | "engines": {
1067 | "node": ">=0.10.0"
1068 | }
1069 | },
1070 | "node_modules/react-dom": {
1071 | "version": "18.3.1",
1072 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1073 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1074 | "dev": true,
1075 | "peer": true,
1076 | "dependencies": {
1077 | "loose-envify": "^1.1.0",
1078 | "scheduler": "^0.23.2"
1079 | },
1080 | "peerDependencies": {
1081 | "react": "^18.3.1"
1082 | }
1083 | },
1084 | "node_modules/react-router": {
1085 | "version": "6.27.0",
1086 | "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz",
1087 | "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==",
1088 | "dev": true,
1089 | "dependencies": {
1090 | "@remix-run/router": "1.20.0"
1091 | },
1092 | "engines": {
1093 | "node": ">=14.0.0"
1094 | },
1095 | "peerDependencies": {
1096 | "react": ">=16.8"
1097 | }
1098 | },
1099 | "node_modules/react-router-dom": {
1100 | "version": "6.27.0",
1101 | "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz",
1102 | "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==",
1103 | "dev": true,
1104 | "dependencies": {
1105 | "@remix-run/router": "1.20.0",
1106 | "react-router": "6.27.0"
1107 | },
1108 | "engines": {
1109 | "node": ">=14.0.0"
1110 | },
1111 | "peerDependencies": {
1112 | "react": ">=16.8",
1113 | "react-dom": ">=16.8"
1114 | }
1115 | },
1116 | "node_modules/require-directory": {
1117 | "version": "2.1.1",
1118 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1119 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1120 | "dev": true,
1121 | "engines": {
1122 | "node": ">=0.10.0"
1123 | }
1124 | },
1125 | "node_modules/rollup": {
1126 | "version": "4.24.0",
1127 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
1128 | "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
1129 | "dev": true,
1130 | "dependencies": {
1131 | "@types/estree": "1.0.6"
1132 | },
1133 | "bin": {
1134 | "rollup": "dist/bin/rollup"
1135 | },
1136 | "engines": {
1137 | "node": ">=18.0.0",
1138 | "npm": ">=8.0.0"
1139 | },
1140 | "optionalDependencies": {
1141 | "@rollup/rollup-android-arm-eabi": "4.24.0",
1142 | "@rollup/rollup-android-arm64": "4.24.0",
1143 | "@rollup/rollup-darwin-arm64": "4.24.0",
1144 | "@rollup/rollup-darwin-x64": "4.24.0",
1145 | "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
1146 | "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
1147 | "@rollup/rollup-linux-arm64-gnu": "4.24.0",
1148 | "@rollup/rollup-linux-arm64-musl": "4.24.0",
1149 | "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
1150 | "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
1151 | "@rollup/rollup-linux-s390x-gnu": "4.24.0",
1152 | "@rollup/rollup-linux-x64-gnu": "4.24.0",
1153 | "@rollup/rollup-linux-x64-musl": "4.24.0",
1154 | "@rollup/rollup-win32-arm64-msvc": "4.24.0",
1155 | "@rollup/rollup-win32-ia32-msvc": "4.24.0",
1156 | "@rollup/rollup-win32-x64-msvc": "4.24.0",
1157 | "fsevents": "~2.3.2"
1158 | }
1159 | },
1160 | "node_modules/scheduler": {
1161 | "version": "0.23.2",
1162 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1163 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1164 | "dev": true,
1165 | "peer": true,
1166 | "dependencies": {
1167 | "loose-envify": "^1.1.0"
1168 | }
1169 | },
1170 | "node_modules/siginfo": {
1171 | "version": "2.0.0",
1172 | "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
1173 | "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
1174 | "dev": true
1175 | },
1176 | "node_modules/smartbundle": {
1177 | "version": "0.7.3",
1178 | "resolved": "https://registry.npmjs.org/smartbundle/-/smartbundle-0.7.3.tgz",
1179 | "integrity": "sha512-EQyM6jSmTpUJ3ajk13OcKA9AkTubjfUhS06P7DAzZqLZREiMMWcOiovPd8FeVkqyiLaZV90nFI4DjrfdrrIRHw==",
1180 | "dev": true,
1181 | "dependencies": {
1182 | "vite": "^5.4.9",
1183 | "yargs": "^17.7.2",
1184 | "zod": "^3.23.8"
1185 | },
1186 | "bin": {
1187 | "smartbundle": "__bin__/smartbundle.js"
1188 | },
1189 | "optionalDependencies": {
1190 | "typescript": "^5.0.0"
1191 | }
1192 | },
1193 | "node_modules/source-map-js": {
1194 | "version": "1.2.1",
1195 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1196 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1197 | "dev": true,
1198 | "engines": {
1199 | "node": ">=0.10.0"
1200 | }
1201 | },
1202 | "node_modules/stackback": {
1203 | "version": "0.0.2",
1204 | "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
1205 | "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
1206 | "dev": true
1207 | },
1208 | "node_modules/std-env": {
1209 | "version": "3.7.0",
1210 | "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
1211 | "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==",
1212 | "dev": true
1213 | },
1214 | "node_modules/string-width": {
1215 | "version": "4.2.3",
1216 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1217 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1218 | "dev": true,
1219 | "dependencies": {
1220 | "emoji-regex": "^8.0.0",
1221 | "is-fullwidth-code-point": "^3.0.0",
1222 | "strip-ansi": "^6.0.1"
1223 | },
1224 | "engines": {
1225 | "node": ">=8"
1226 | }
1227 | },
1228 | "node_modules/strip-ansi": {
1229 | "version": "6.0.1",
1230 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1231 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1232 | "dev": true,
1233 | "dependencies": {
1234 | "ansi-regex": "^5.0.1"
1235 | },
1236 | "engines": {
1237 | "node": ">=8"
1238 | }
1239 | },
1240 | "node_modules/tinybench": {
1241 | "version": "2.9.0",
1242 | "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
1243 | "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
1244 | "dev": true
1245 | },
1246 | "node_modules/tinyexec": {
1247 | "version": "0.3.0",
1248 | "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
1249 | "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
1250 | "dev": true
1251 | },
1252 | "node_modules/tinypool": {
1253 | "version": "1.0.1",
1254 | "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
1255 | "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==",
1256 | "dev": true,
1257 | "engines": {
1258 | "node": "^18.0.0 || >=20.0.0"
1259 | }
1260 | },
1261 | "node_modules/tinyrainbow": {
1262 | "version": "1.2.0",
1263 | "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
1264 | "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
1265 | "dev": true,
1266 | "engines": {
1267 | "node": ">=14.0.0"
1268 | }
1269 | },
1270 | "node_modules/tinyspy": {
1271 | "version": "3.0.2",
1272 | "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
1273 | "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
1274 | "dev": true,
1275 | "engines": {
1276 | "node": ">=14.0.0"
1277 | }
1278 | },
1279 | "node_modules/typescript": {
1280 | "version": "5.6.3",
1281 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
1282 | "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
1283 | "dev": true,
1284 | "bin": {
1285 | "tsc": "bin/tsc",
1286 | "tsserver": "bin/tsserver"
1287 | },
1288 | "engines": {
1289 | "node": ">=14.17"
1290 | }
1291 | },
1292 | "node_modules/vite": {
1293 | "version": "5.4.10",
1294 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
1295 | "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
1296 | "dev": true,
1297 | "license": "MIT",
1298 | "dependencies": {
1299 | "esbuild": "^0.21.3",
1300 | "postcss": "^8.4.43",
1301 | "rollup": "^4.20.0"
1302 | },
1303 | "bin": {
1304 | "vite": "bin/vite.js"
1305 | },
1306 | "engines": {
1307 | "node": "^18.0.0 || >=20.0.0"
1308 | },
1309 | "funding": {
1310 | "url": "https://github.com/vitejs/vite?sponsor=1"
1311 | },
1312 | "optionalDependencies": {
1313 | "fsevents": "~2.3.3"
1314 | },
1315 | "peerDependencies": {
1316 | "@types/node": "^18.0.0 || >=20.0.0",
1317 | "less": "*",
1318 | "lightningcss": "^1.21.0",
1319 | "sass": "*",
1320 | "sass-embedded": "*",
1321 | "stylus": "*",
1322 | "sugarss": "*",
1323 | "terser": "^5.4.0"
1324 | },
1325 | "peerDependenciesMeta": {
1326 | "@types/node": {
1327 | "optional": true
1328 | },
1329 | "less": {
1330 | "optional": true
1331 | },
1332 | "lightningcss": {
1333 | "optional": true
1334 | },
1335 | "sass": {
1336 | "optional": true
1337 | },
1338 | "sass-embedded": {
1339 | "optional": true
1340 | },
1341 | "stylus": {
1342 | "optional": true
1343 | },
1344 | "sugarss": {
1345 | "optional": true
1346 | },
1347 | "terser": {
1348 | "optional": true
1349 | }
1350 | }
1351 | },
1352 | "node_modules/vite-node": {
1353 | "version": "2.1.2",
1354 | "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz",
1355 | "integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==",
1356 | "dev": true,
1357 | "dependencies": {
1358 | "cac": "^6.7.14",
1359 | "debug": "^4.3.6",
1360 | "pathe": "^1.1.2",
1361 | "vite": "^5.0.0"
1362 | },
1363 | "bin": {
1364 | "vite-node": "vite-node.mjs"
1365 | },
1366 | "engines": {
1367 | "node": "^18.0.0 || >=20.0.0"
1368 | },
1369 | "funding": {
1370 | "url": "https://opencollective.com/vitest"
1371 | }
1372 | },
1373 | "node_modules/vitest": {
1374 | "version": "2.1.2",
1375 | "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz",
1376 | "integrity": "sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==",
1377 | "dev": true,
1378 | "dependencies": {
1379 | "@vitest/expect": "2.1.2",
1380 | "@vitest/mocker": "2.1.2",
1381 | "@vitest/pretty-format": "^2.1.2",
1382 | "@vitest/runner": "2.1.2",
1383 | "@vitest/snapshot": "2.1.2",
1384 | "@vitest/spy": "2.1.2",
1385 | "@vitest/utils": "2.1.2",
1386 | "chai": "^5.1.1",
1387 | "debug": "^4.3.6",
1388 | "magic-string": "^0.30.11",
1389 | "pathe": "^1.1.2",
1390 | "std-env": "^3.7.0",
1391 | "tinybench": "^2.9.0",
1392 | "tinyexec": "^0.3.0",
1393 | "tinypool": "^1.0.0",
1394 | "tinyrainbow": "^1.2.0",
1395 | "vite": "^5.0.0",
1396 | "vite-node": "2.1.2",
1397 | "why-is-node-running": "^2.3.0"
1398 | },
1399 | "bin": {
1400 | "vitest": "vitest.mjs"
1401 | },
1402 | "engines": {
1403 | "node": "^18.0.0 || >=20.0.0"
1404 | },
1405 | "funding": {
1406 | "url": "https://opencollective.com/vitest"
1407 | },
1408 | "peerDependencies": {
1409 | "@edge-runtime/vm": "*",
1410 | "@types/node": "^18.0.0 || >=20.0.0",
1411 | "@vitest/browser": "2.1.2",
1412 | "@vitest/ui": "2.1.2",
1413 | "happy-dom": "*",
1414 | "jsdom": "*"
1415 | },
1416 | "peerDependenciesMeta": {
1417 | "@edge-runtime/vm": {
1418 | "optional": true
1419 | },
1420 | "@types/node": {
1421 | "optional": true
1422 | },
1423 | "@vitest/browser": {
1424 | "optional": true
1425 | },
1426 | "@vitest/ui": {
1427 | "optional": true
1428 | },
1429 | "happy-dom": {
1430 | "optional": true
1431 | },
1432 | "jsdom": {
1433 | "optional": true
1434 | }
1435 | }
1436 | },
1437 | "node_modules/why-is-node-running": {
1438 | "version": "2.3.0",
1439 | "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
1440 | "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
1441 | "dev": true,
1442 | "dependencies": {
1443 | "siginfo": "^2.0.0",
1444 | "stackback": "0.0.2"
1445 | },
1446 | "bin": {
1447 | "why-is-node-running": "cli.js"
1448 | },
1449 | "engines": {
1450 | "node": ">=8"
1451 | }
1452 | },
1453 | "node_modules/wrap-ansi": {
1454 | "version": "7.0.0",
1455 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1456 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1457 | "dev": true,
1458 | "dependencies": {
1459 | "ansi-styles": "^4.0.0",
1460 | "string-width": "^4.1.0",
1461 | "strip-ansi": "^6.0.0"
1462 | },
1463 | "engines": {
1464 | "node": ">=10"
1465 | },
1466 | "funding": {
1467 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1468 | }
1469 | },
1470 | "node_modules/y18n": {
1471 | "version": "5.0.8",
1472 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1473 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1474 | "dev": true,
1475 | "engines": {
1476 | "node": ">=10"
1477 | }
1478 | },
1479 | "node_modules/yargs": {
1480 | "version": "17.7.2",
1481 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
1482 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
1483 | "dev": true,
1484 | "dependencies": {
1485 | "cliui": "^8.0.1",
1486 | "escalade": "^3.1.1",
1487 | "get-caller-file": "^2.0.5",
1488 | "require-directory": "^2.1.1",
1489 | "string-width": "^4.2.3",
1490 | "y18n": "^5.0.5",
1491 | "yargs-parser": "^21.1.1"
1492 | },
1493 | "engines": {
1494 | "node": ">=12"
1495 | }
1496 | },
1497 | "node_modules/yargs-parser": {
1498 | "version": "21.1.1",
1499 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
1500 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
1501 | "dev": true,
1502 | "engines": {
1503 | "node": ">=12"
1504 | }
1505 | },
1506 | "node_modules/zod": {
1507 | "version": "3.23.8",
1508 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
1509 | "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
1510 | "dev": true,
1511 | "funding": {
1512 | "url": "https://github.com/sponsors/colinhacks"
1513 | }
1514 | }
1515 | }
1516 | }
1517 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router-typed-object",
3 | "version": "1.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": "./src/index.ts",
7 | "scripts": {
8 | "publish": "tsc && vitest run && smartbundle && cd dist && npm publish --access=public",
9 | "test": "vitest run"
10 | },
11 | "peerDependencies": {
12 | "react-router": "^6.27.0",
13 | "react-router-dom": "^6.27.0",
14 | "@remix-run/router": "^1.8.0"
15 | },
16 | "devDependencies": {
17 | "smartbundle": "^0.7.3",
18 | "typescript": "^5.6.3",
19 | "vitest": "^2.1.2",
20 | "zod": "^3.23.8",
21 | "react-router": "^6.27.0",
22 | "react-router-dom": "^6.27.0",
23 | "@remix-run/router": "1.20.0"
24 | },
25 | "author": "artalar",
26 | "contributors": [
27 | {
28 | "name": "artalar",
29 | "url": "https://github.com/artalar"
30 | }
31 | ],
32 | "license": "MIT",
33 | "repository": {
34 | "type": "git",
35 | "url": "git+ssh://git@github.com/artalar/react-router-typed-object.git"
36 | },
37 | "bugs": {
38 | "url": "https://github.com/artalar/react-router-typed-object/issues"
39 | },
40 | "keywords": [
41 | "react",
42 | "react-router",
43 | "typescript",
44 | "react-typed-router"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/src/createRouter.test.ts:
--------------------------------------------------------------------------------
1 | import { createMemoryRouter } from "react-router-dom";
2 | import { it, expect } from "vitest";
3 | import { z } from "zod";
4 |
5 | import { createRouter } from "./createRouter.js";
6 |
7 | it("should apply navigation to the history", () => {
8 | const ROUTER = createRouter(
9 | [
10 | {
11 | path: "a",
12 | children: [
13 | {
14 | path: ":b/c/:d",
15 | searchParams: z.object({ z: z.string() }).parse,
16 | },
17 | ],
18 | },
19 | ],
20 | {
21 | createRouter: createMemoryRouter,
22 | }
23 | );
24 |
25 | expect(ROUTER.state.location.pathname).toBe("/");
26 |
27 | ROUTER["/a/:b/c/:d"].path.navigate({ b: "B", d: "D", z: "Z" });
28 | expect(ROUTER.state.location.pathname + ROUTER.state.location.search).toBe(
29 | "/a/B/c/D?z=Z"
30 | );
31 | });
32 |
33 | it("should apply basename", () => {
34 | const ROUTER = createRouter(
35 | [
36 | {
37 | path: "a",
38 | children: [
39 | {
40 | path: ":b/c/:d",
41 | searchParams: z.object({ z: z.string() }).parse,
42 | },
43 | ],
44 | },
45 | ],
46 | {
47 | createRouter: createMemoryRouter,
48 | basename: "/base",
49 | }
50 | );
51 |
52 | expect(ROUTER.state.location.pathname).toBe("/");
53 |
54 | ROUTER["/base/a/:b/c/:d"].path.navigate({ b: "B", d: "D", z: "Z" });
55 | expect(ROUTER.state.location.pathname + ROUTER.state.location.search).toBe(
56 | "/base/a/B/c/D?z=Z"
57 | );
58 | });
59 |
--------------------------------------------------------------------------------
/src/createRouter.ts:
--------------------------------------------------------------------------------
1 | import { type Router } from "@remix-run/router";
2 | import { type RouteObject } from "react-router";
3 | import { createBrowserRouter } from "react-router-dom";
4 |
5 | import {
6 | InferRoute,
7 | PathRoute,
8 | Pattern,
9 | inferRouteObject,
10 | isRoutePattern,
11 | } from "./inferRouteObject.js";
12 |
13 | export type NavigationOptions = Parameters[1];
14 |
15 | export interface Go> {
16 | /** Make SPA transition to the route with relative parameters */
17 | navigate(params: Params, opts?: NavigationOptions): void;
18 | }
19 |
20 | type BindRoutes> = {
21 | [K in keyof T]: T[K] & {
22 | path: T[K]["path"] & Go[0]>;
23 | };
24 | };
25 |
26 | export const bindRouter = <
27 | Router extends {
28 | navigate: (url: string, opts?: NavigationOptions) => void;
29 | },
30 | T extends Record
31 | >(
32 | router: Router,
33 | routes: T,
34 | basename = ""
35 | ): BindRoutes => {
36 | for (const pattern in routes) {
37 | if (isRoutePattern(pattern)) {
38 | const path = routes[pattern]?.path as PathRoute & Go;
39 | path.navigate = (params, opts) => {
40 | const target = path(params as unknown as Parameters[0]);
41 | router.navigate(
42 | path(params as unknown as Parameters[0]).slice(
43 | basename.length
44 | ),
45 | opts
46 | );
47 | };
48 | }
49 | }
50 |
51 | return routes as BindRoutes;
52 | };
53 |
54 | export type DOMRouterOpts = Parameters<
55 | typeof createBrowserRouter
56 | >[1] & {
57 | basename?: Basename;
58 | };
59 |
60 | export const createRouter = <
61 | const T extends RouteObject,
62 | Basename extends Pattern = "/"
63 | >(
64 | routeObject: Array,
65 | options: DOMRouterOpts & {
66 | createRouter?: typeof createBrowserRouter;
67 | } = {}
68 | ): Router & BindRoutes> => {
69 | const { createRouter = createBrowserRouter, ...opts } = options;
70 | const router = createRouter(routeObject, opts);
71 | const routes = inferRouteObject(
72 | { children: routeObject },
73 | opts?.basename
74 | ) as unknown as InferRoute<{ children: [T] }, Basename>;
75 | return Object.assign(router, bindRouter(router, routes, opts?.basename));
76 | };
77 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | type NavigationOptions,
3 | bindRouter,
4 | type DOMRouterOpts,
5 | createRouter,
6 | } from "./createRouter.js";
7 |
8 | export {
9 | type SearchParamsContract,
10 | type Pattern,
11 | type Routes,
12 | type PathRoute,
13 | type PathRouteParams,
14 | type PathRoutePathParams,
15 | type InferRoute,
16 | type FixRoot,
17 | type InferRoutePath,
18 | type UseParamsHook,
19 | isRoutePattern,
20 | inferRouteObject,
21 | } from "./inferRouteObject.js";
22 |
23 | // export { useTypedSearchParams } from './useTypedSearchParams';
24 |
--------------------------------------------------------------------------------
/src/inferRouteObject.test.ts:
--------------------------------------------------------------------------------
1 | import { it, expect, vi, beforeEach, afterEach } from "vitest";
2 | import { z } from "zod";
3 |
4 | // Mock react-router-dom
5 | vi.mock("react-router-dom", () => ({
6 | useLocation: vi.fn(),
7 | }));
8 |
9 | import { useLocation } from "react-router-dom";
10 |
11 | import { inferRouteObject } from "./inferRouteObject.js";
12 |
13 | it("should infer the list of all nested paths", () => {
14 | const ROUTES = inferRouteObject({
15 | path: "a",
16 | children: [
17 | { path: "b" },
18 | {
19 | path: "c",
20 | children: [{ children: [{ children: [{ path: "d" }] }] }],
21 | },
22 | ],
23 | });
24 |
25 | expect(ROUTES["/a"].path.pattern).toBe("/a");
26 | expect(ROUTES["/a/b"].path.pattern).toBe("/a/b");
27 | expect(ROUTES["/a/c"].path.pattern).toBe("/a/c");
28 | expect(ROUTES["/a/c/d"].path.pattern).toBe("/a/c/d");
29 | });
30 |
31 | it("should handle path params", () => {
32 | const ROUTES = inferRouteObject({
33 | path: "a/:b",
34 | children: [{ path: "c/:d" }],
35 | });
36 |
37 | expect(ROUTES["/a/:b/c/:d"].path({ b: "B", d: "D" })).toBe("/a/B/c/D");
38 | });
39 |
40 | it("should handle search params", () => {
41 | const ROUTES = inferRouteObject({
42 | path: "a/:b",
43 | children: [
44 | {
45 | path: "c/:d",
46 | searchParams: z.object({
47 | z: z.string(),
48 | q: z.string().optional(),
49 | }).parse,
50 | },
51 | ],
52 | });
53 |
54 | expect(ROUTES["/a/:b/c/:d"].path({ b: "B", d: "D", z: "Z" })).toBe(
55 | "/a/B/c/D?z=Z"
56 | );
57 | expect(
58 | ROUTES["/a/:b/c/:d"].path({ b: "B", d: "D", z: "Z", q: undefined })
59 | ).toBe("/a/B/c/D?z=Z");
60 | expect(ROUTES["/a/:b/c/:d"].path({ b: "B", d: "D", z: "Z", q: "Q" })).toBe(
61 | "/a/B/c/D?z=Z&q=Q"
62 | );
63 | });
64 |
65 | it("should handle both optional params and search params", () => {
66 | const ROUTES = inferRouteObject({
67 | path: "a",
68 | children: [
69 | {
70 | path: ":b/c/:d?",
71 | searchParams: z.object({ z: z.string() }).parse,
72 | },
73 | {
74 | path: "e",
75 | children: [
76 | {
77 | path: ":f?",
78 | searchParams: z.object({ z: z.string() }).parse,
79 | },
80 | ],
81 | },
82 | ],
83 | });
84 |
85 | expect(ROUTES["/a/:b/c/:d?"].path({ b: "B", d: "D", z: "Z" })).toBe(
86 | "/a/B/c/D?z=Z"
87 | );
88 | expect(ROUTES["/a/:b/c/:d?"].path({ b: "B", z: "Z" })).toBe("/a/B/c/?z=Z");
89 | expect(ROUTES["/a/e/:f?"].path({ z: "Z" })).toBe("/a/e/?z=Z");
90 | expect(ROUTES["/a/e/:f?"].path({ f: "F", z: "Z" })).toBe("/a/e/F?z=Z");
91 | });
92 |
93 | it("should handle optional search params", () => {
94 | const ROUTES = inferRouteObject({
95 | path: "a",
96 | searchParams: z.object({ q: z.string().optional() }).parse,
97 | });
98 |
99 | expect(ROUTES["/a"].path({})).toBe("/a");
100 | expect(ROUTES["/a"].path()).toBe("/a");
101 | });
102 |
103 | // Basic useParams tests
104 | it("should retrieve parameters with useParams hook", () => {
105 | // Setup mocks
106 | const mockLocation = {
107 | pathname: "/a/B/c/D",
108 | search: "?z=Z"
109 | };
110 |
111 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
112 |
113 | const ROUTES = inferRouteObject({
114 | path: "a/:b",
115 | children: [
116 | {
117 | path: "c/:d",
118 | searchParams: z.object({
119 | z: z.string(),
120 | q: z.string().optional(),
121 | }).parse,
122 | },
123 | ],
124 | });
125 |
126 | // Test with no fallback
127 | const params = ROUTES["/a/:b/c/:d"].path.useParams();
128 | expect(params).toEqual({ b: "B", d: "D", z: "Z" });
129 |
130 | // Test with fallback
131 | const paramsWithFallback = ROUTES["/a/:b/c/:d"].path.useParams({ q: "Q" });
132 | expect(paramsWithFallback).toEqual({ b: "B", d: "D", z: "Z", q: "Q" });
133 | });
134 |
135 | it("should throw error when required parameters are missing", () => {
136 | // Setup mocks
137 | const mockLocation = {
138 | pathname: "/a//c/", // Missing required parameters
139 | search: ""
140 | };
141 |
142 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
143 |
144 | const ROUTES = inferRouteObject({
145 | path: "a/:b",
146 | children: [
147 | {
148 | path: "c/:d",
149 | },
150 | ],
151 | });
152 |
153 | // Should throw error when required parameters are missing
154 | expect(() => {
155 | ROUTES["/a/:b/c/:d"].path.useParams();
156 | }).toThrow("Missing parameter");
157 |
158 | // Should not throw error when fallback provides required parameters
159 | const params = ROUTES["/a/:b/c/:d"].path.useParams({ b: "B", d: "D" });
160 | expect(params).toEqual({ b: "B", d: "D" });
161 | });
162 |
163 | it("should handle optional parameters correctly", () => {
164 | // Setup mocks
165 | const mockLocation = {
166 | pathname: "/a/B/c/", // Missing optional parameter
167 | search: ""
168 | };
169 |
170 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
171 |
172 | const ROUTES = inferRouteObject({
173 | path: "a/:b",
174 | children: [
175 | {
176 | path: "c/:d?", // d is optional
177 | },
178 | ],
179 | });
180 |
181 | // Should not throw error when optional parameter is missing
182 | const params = ROUTES["/a/:b/c/:d?"].path.useParams();
183 | expect(params).toEqual({ b: "B" });
184 |
185 | // Should use fallback for optional parameter
186 | const paramsWithFallback = ROUTES["/a/:b/c/:d?"].path.useParams({ d: "D" });
187 | expect(paramsWithFallback).toEqual({ b: "B", d: "D" });
188 | });
189 |
190 | // Edge case tests
191 | it("should handle empty parameters when route expects some", () => {
192 | // Setup mocks with empty parameters
193 | vi.mocked(useLocation).mockReturnValue({
194 | pathname: "/users/", // Missing required parameter
195 | search: ""
196 | } as any);
197 |
198 | const ROUTES = inferRouteObject({
199 | path: "users/:userId",
200 | });
201 |
202 | // Should throw error when no parameters are provided
203 | expect(() => {
204 | ROUTES["/users/:userId"].path.useParams();
205 | }).toThrow("Missing parameter");
206 |
207 | // Should use fallback when provided
208 | const params = ROUTES["/users/:userId"].path.useParams({ userId: "default-user" });
209 | expect(params).toEqual({ userId: "default-user" });
210 | });
211 |
212 | it("should handle invalid search parameters", () => {
213 | // Setup mocks
214 | const mockLocation = {
215 | pathname: "/products/123",
216 | search: "?minPrice=invalid" // Invalid search parameter (should be a number)
217 | };
218 |
219 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
220 |
221 | const ROUTES = inferRouteObject({
222 | path: "products/:productId",
223 | searchParams: z.object({
224 | minPrice: z.number(), // Expecting a number
225 | maxPrice: z.number().optional(),
226 | }).parse,
227 | });
228 |
229 | // Should throw error when search parameters fail validation
230 | expect(() => {
231 | ROUTES["/products/:productId"].path.useParams();
232 | }).toThrow();
233 |
234 | // Should use fallback when provided
235 | const params = ROUTES["/products/:productId"].path.useParams({
236 | minPrice: 10,
237 | maxPrice: 100
238 | });
239 | expect(params).toEqual({
240 | productId: "123",
241 | minPrice: 10,
242 | maxPrice: 100
243 | });
244 | });
245 |
246 | it("should handle complex nested routes with multiple parameters", () => {
247 | // Setup mocks for a complex route
248 | const mockLocation = {
249 | pathname: "/organizations/org-123/projects/proj-456/tasks/task-789",
250 | search: "?assignee=user-123&priority=high"
251 | };
252 |
253 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
254 |
255 | const ROUTES = inferRouteObject({
256 | path: "organizations/:orgId",
257 | children: [
258 | {
259 | path: "projects/:projectId",
260 | children: [
261 | {
262 | path: "tasks/:taskId",
263 | searchParams: z.object({
264 | assignee: z.string(),
265 | priority: z.enum(["low", "medium", "high"]),
266 | dueDate: z.string().optional(),
267 | }).parse,
268 | },
269 | ],
270 | },
271 | ],
272 | });
273 |
274 | // Should correctly parse all parameters
275 | const params = ROUTES["/organizations/:orgId/projects/:projectId/tasks/:taskId"].path.useParams();
276 | expect(params).toEqual({
277 | orgId: "org-123",
278 | projectId: "proj-456",
279 | taskId: "task-789",
280 | assignee: "user-123",
281 | priority: "high",
282 | });
283 |
284 | // Should merge with fallback
285 | const paramsWithFallback = ROUTES["/organizations/:orgId/projects/:projectId/tasks/:taskId"].path.useParams({
286 | dueDate: "2023-12-31",
287 | });
288 | expect(paramsWithFallback).toEqual({
289 | orgId: "org-123",
290 | projectId: "proj-456",
291 | taskId: "task-789",
292 | assignee: "user-123",
293 | priority: "high",
294 | dueDate: "2023-12-31",
295 | });
296 | });
297 |
298 | it("should handle special characters in parameters", () => {
299 | // Setup mocks with special characters
300 | const mockLocation = {
301 | pathname: "/users/user%2Fwith%2Fslashes/section%23with%23hashes",
302 | search: "?query=special+characters&filter=a%26b"
303 | };
304 |
305 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
306 |
307 | const ROUTES = inferRouteObject({
308 | path: "users/:userId",
309 | children: [
310 | {
311 | path: ":section",
312 | searchParams: z.object({
313 | query: z.string(),
314 | filter: z.string().optional(),
315 | }).parse,
316 | },
317 | ],
318 | });
319 |
320 | // Should correctly handle special characters
321 | const params = ROUTES["/users/:userId/:section"].path.useParams();
322 | expect(params).toEqual({
323 | userId: "user/with/slashes", // Our implementation decodes URL-encoded characters
324 | section: "section#with#hashes", // Our implementation decodes URL-encoded characters
325 | query: "special characters", // URLSearchParams decodes the values
326 | filter: "a&b", // URLSearchParams decodes the values
327 | });
328 | });
329 |
330 | it("should handle empty search string when search parameters are expected", () => {
331 | // Setup mocks with empty search string
332 | const mockLocation = {
333 | pathname: "/categories/electronics",
334 | search: "" // Empty search string
335 | };
336 |
337 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
338 |
339 | const ROUTES = inferRouteObject({
340 | path: "categories/:categoryId",
341 | searchParams: z.object({
342 | sort: z.string().optional(),
343 | filter: z.string().optional(),
344 | }).parse,
345 | });
346 |
347 | // Should not throw error when search string is empty but parameters are optional
348 | const params = ROUTES["/categories/:categoryId"].path.useParams();
349 | expect(params).toEqual({ categoryId: "electronics" });
350 |
351 | // Should merge with fallback
352 | const paramsWithFallback = ROUTES["/categories/:categoryId"].path.useParams({
353 | sort: "price-asc",
354 | filter: "in-stock",
355 | });
356 | expect(paramsWithFallback).toEqual({
357 | categoryId: "electronics",
358 | sort: "price-asc",
359 | filter: "in-stock",
360 | });
361 | });
362 |
363 | it("should prioritize URL parameters over fallback values", () => {
364 | // Setup mocks
365 | const mockLocation = {
366 | pathname: "/users/actual-user",
367 | search: "?role=admin"
368 | };
369 |
370 | vi.mocked(useLocation).mockReturnValue(mockLocation as any);
371 |
372 | const ROUTES = inferRouteObject({
373 | path: "users/:userId",
374 | searchParams: z.object({
375 | role: z.string(),
376 | }).parse,
377 | });
378 |
379 | // URL parameters should take precedence over fallback
380 | const params = ROUTES["/users/:userId"].path.useParams({
381 | userId: "fallback-user",
382 | role: "fallback-role",
383 | });
384 |
385 | expect(params).toEqual({
386 | userId: "actual-user", // From URL, not fallback
387 | role: "admin", // From URL, not fallback
388 | });
389 | });
390 |
--------------------------------------------------------------------------------
/src/inferRouteObject.ts:
--------------------------------------------------------------------------------
1 | // `{}` is not equal to `Record` in some cases
2 | // `void` need to describe an optional argument
3 | /* eslint-disable @typescript-eslint/ban-types */
4 |
5 | import { type RouteObject } from "react-router";
6 | import { useLocation } from "react-router-dom";
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
9 | export interface SearchParamsContract<
10 | T extends Record = Record
11 | > {
12 | (value: unknown): T;
13 | }
14 |
15 | export type SearchParamsType = Record | undefined;
16 |
17 | declare module "react-router" {
18 | export interface IndexRouteObject {
19 | searchParams?: SearchParamsContract;
20 | }
21 | export interface NonIndexRouteObject {
22 | searchParams?: SearchParamsContract;
23 | }
24 | }
25 |
26 | /** This generic helps format an intersection type for a more readable view */
27 | export type Shallow = T extends object
28 | ? {
29 | [Key in keyof T]: T[Key];
30 | }
31 | : T;
32 |
33 | export type Pattern = `/${string}`;
34 |
35 | export type Routes = {
36 | [key in Pattern]?: { path: PathRoute };
37 | };
38 |
39 | // Type for useParams hook
40 | export type UseParamsHook<
41 | Path extends string = string,
42 | SearchParams extends SearchParamsType = undefined
43 | > = (
44 | fallback?: Partial>
45 | ) => PathRouteParams;
46 |
47 | export interface PathRoute<
48 | Path extends string = string,
49 | SearchParams extends SearchParamsType = undefined
50 | > {
51 | /** Get the real path for this route based on the needed parameters */
52 | (params: PathRouteParams): string;
53 |
54 | pattern: Path;
55 |
56 | /**
57 | * Hook to get the current route parameters with type safety
58 | * @param fallback Optional fallback values for missing parameters
59 | * @returns The current route parameters
60 | */
61 | useParams: UseParamsHook;
62 | }
63 |
64 | export type PathRouteParams<
65 | Path extends string = string,
66 | SearchParams extends SearchParamsType = undefined
67 | > = Path extends `${string}:${string}`
68 | ? PathRoutePathParams &
69 | (SearchParams extends Record ? SearchParams : {})
70 | : SearchParams extends Record
71 | ? SearchParams
72 | : void;
73 |
74 | export type PathRoutePathParams =
75 | Path extends `:${infer Param}/${infer Rest}`
76 | ? { [key in Param]: string } & PathRoutePathParams
77 | : Path extends `:${infer MaybeOptionalParam}`
78 | ? MaybeOptionalParam extends `${infer OptionalParam}?`
79 | ? { [key in OptionalParam]?: string }
80 | : { [key in MaybeOptionalParam]: string }
81 | : Path extends `${string}/${infer Rest}`
82 | ? PathRoutePathParams
83 | : {};
84 |
85 | export type InferRoute<
86 | T extends RouteObject,
87 | Parent extends string
88 | > = T extends {
89 | children: [
90 | infer Child extends RouteObject,
91 | ...infer Children extends Array
92 | ];
93 | }
94 | ? (Child extends { path: infer Path extends string }
95 | ? InferRoutePath>
96 | : InferRoute) &
97 | InferRoute<{ children: Children }, Parent>
98 | : {};
99 |
100 | export type FixRoot = T extends `///${infer Path}`
101 | ? `/${Path}`
102 | : T extends `//${infer Path}`
103 | ? `/${Path}`
104 | : T;
105 |
106 | export type InferRoutePath = Record<
107 | Path,
108 | Shallow<
109 | Pick & {
110 | path: PathRoute<
111 | Path,
112 | T extends {
113 | searchParams: SearchParamsContract;
114 | }
115 | ? {} extends SearchParams
116 | ? SearchParams | undefined
117 | : SearchParams
118 | : undefined
119 | >;
120 | }
121 | >
122 | > &
123 | InferRoute;
124 | export const isRoutePattern = (pattern: string): pattern is Pattern =>
125 | pattern.startsWith("/");
126 |
127 | // Helper functions for parameter handling
128 | const extractParamNames = (pattern: string): string[] => {
129 | const paths = pattern.split("/");
130 | return paths
131 | .filter((part) => part.startsWith(":"))
132 | .map((name) => name.slice(1));
133 | };
134 |
135 | const validateParams = (
136 | params: Record | undefined,
137 | paramsNames: string[],
138 | pattern: string
139 | ): void => {
140 | if (!params) {
141 | if (paramsNames.length > 0 && paramsNames[0] && !paramsNames[0].endsWith("?")) {
142 | throw new Error(`Missing parameters for route "${pattern}"`);
143 | }
144 | return;
145 | }
146 |
147 | const missedParam = paramsNames.find(
148 | (name) => !name.endsWith("?") && !(name in params)
149 | );
150 |
151 | if (missedParam) {
152 | throw new Error(
153 | `Missing parameter "${missedParam}" for route "${pattern}"`
154 | );
155 | }
156 | };
157 |
158 | const buildPath = (
159 | pattern: string,
160 | paramsNames: string[],
161 | params: Record | undefined
162 | ): string => {
163 | let path = pattern;
164 |
165 | for (const name of paramsNames) {
166 | if (name.endsWith("?")) {
167 | path = path.replace(
168 | `:${name}`,
169 | params?.[name.slice(0, -1)] ?? ""
170 | );
171 | } else {
172 | path = path.replace(`:${name}`, params?.[name] ?? "");
173 | }
174 | }
175 |
176 | return path;
177 | };
178 |
179 | // Extract path parameters from URL by matching against pattern
180 | const extractPathParams = (
181 | pattern: string,
182 | pathname: string
183 | ): Record => {
184 | const patternParts = pattern.split('/');
185 | const pathParts = pathname.split('/');
186 | const params: Record = {};
187 |
188 | for (let i = 0; i < patternParts.length; i++) {
189 | const patternPart = patternParts[i]!;
190 | const pathPart = pathParts[i] || '';
191 |
192 | if (patternPart.startsWith(':')) {
193 | const paramName = patternPart.slice(1);
194 | const isOptional = paramName.endsWith('?');
195 | const cleanParamName = isOptional ? paramName.slice(0, -1) : paramName;
196 |
197 | // For non-optional parameters, empty values are considered missing
198 | if (pathPart) {
199 | // Decode URL-encoded characters
200 | params[cleanParamName] = decodeURIComponent(pathPart);
201 | } else if (!isOptional) {
202 | // Mark as undefined to trigger validation error later
203 | params[cleanParamName] = '';
204 | }
205 | }
206 | }
207 |
208 | return params;
209 | };
210 |
211 |
212 | const _inferRouteObject = <
213 | const T extends RouteObject,
214 | Parent extends string = ""
215 | >(
216 | routeObject: T,
217 | parent: Parent = "" as Parent,
218 | routes: Routes = {}
219 | ) => {
220 | for (const child of routeObject.children ?? []) {
221 | let path = parent as `/${string}`;
222 | if ("path" in child) {
223 | path = `${parent}/${child.path}` as `/${string}`;
224 | if (path.startsWith("//")) path = path.slice(1) as `/${string}`;
225 |
226 | const pattern = path;
227 |
228 | if (isRoutePattern(pattern)) {
229 | const searchParamsContract = child.searchParams;
230 | const paramsNames = extractParamNames(pattern);
231 |
232 | const get = (params: void | Record) => {
233 | validateParams(params as Record | undefined, paramsNames, pattern);
234 |
235 | let path = buildPath(pattern, paramsNames, params as Record | undefined);
236 |
237 | if (!searchParamsContract) {
238 | return path;
239 | }
240 |
241 | const searchParamsData = searchParamsContract(params ?? {});
242 | const searchParams = new URLSearchParams();
243 |
244 | for (const [k, v] of Object.entries(searchParamsData)) {
245 | if (v !== undefined) {
246 | searchParams.append(k, v);
247 | }
248 | }
249 |
250 | const searchParamsString = searchParams.toString();
251 | return `${path}${searchParamsString && `?${searchParamsString}`}`;
252 | };
253 |
254 | // Create the useParams hook
255 | const useParamsHook: UseParamsHook = (fallback) => {
258 | const location = useLocation();
259 |
260 | // Extract path parameters from the URL
261 | const pathParams = extractPathParams(pattern, location.pathname);
262 |
263 | // Check for missing required parameters
264 | const missingParams: string[] = [];
265 | paramsNames.forEach(name => {
266 | if (!name.endsWith("?") && (!pathParams[name] || pathParams[name] === '')) {
267 | missingParams.push(name);
268 | }
269 | });
270 |
271 | // If there are missing parameters, check if fallback provides them
272 | if (missingParams.length > 0) {
273 | if (fallback) {
274 | const stillMissing = missingParams.filter(name => !fallback[name]);
275 | if (stillMissing.length > 0) {
276 | throw new Error(`Missing parameter "${stillMissing[0]}" for route "${pattern}"`);
277 | }
278 | } else {
279 | throw new Error(`Missing parameter "${missingParams[0]}" for route "${pattern}"`);
280 | }
281 | }
282 |
283 | // Create a new object with fallback values first, then override with valid path params
284 | const mergedParams: Record = { ...(fallback || {}) };
285 |
286 | // Only add non-empty path params
287 | Object.entries(pathParams).forEach(([key, value]) => {
288 | if (value !== '') {
289 | mergedParams[key] = value;
290 | }
291 | });
292 |
293 | // Parse search params if needed
294 | if (searchParamsContract && location.search) {
295 | const searchParams = new URLSearchParams(location.search);
296 | const searchParamsObject: Record = {};
297 |
298 | for (const [key, value] of searchParams.entries()) {
299 | searchParamsObject[key] = value;
300 | }
301 |
302 | // Validate search params
303 | try {
304 | const validatedSearchParams = searchParamsContract(searchParamsObject);
305 | return { ...mergedParams, ...validatedSearchParams } as any;
306 | } catch (error) {
307 | // If validation fails and we have fallback search params, use those
308 | if (fallback) {
309 | try {
310 | const validatedFallbackParams = searchParamsContract(fallback);
311 | return { ...mergedParams, ...validatedFallbackParams } as any;
312 | } catch (fallbackError) {
313 | throw new Error(`Invalid search parameters and fallback: ${error}`);
314 | }
315 | }
316 | throw error;
317 | }
318 | }
319 |
320 | return mergedParams as any;
321 | };
322 |
323 | // eslint-disable-next-line no-param-reassign
324 | routes[pattern] = {
325 | path: Object.assign(get, {
326 | pattern,
327 | useParams: useParamsHook
328 | })
329 | };
330 | }
331 | }
332 | if ("children" in child) {
333 | _inferRouteObject(child, path, routes);
334 | }
335 | }
336 | };
337 |
338 | export const inferRouteObject = <
339 | const T extends RouteObject,
340 | Basename extends string = ""
341 | >(
342 | routeObject: T,
343 | basename: Basename = "" as Basename
344 | ): T & InferRoute<{ children: [T] }, Basename> => {
345 | const routes: Routes = {};
346 | _inferRouteObject({ children: [routeObject] }, basename, routes);
347 |
348 | return { ...routeObject, ...routes } as T &
349 | InferRoute<{ children: [T] }, Basename>;
350 | };
351 |
352 | /* eslint-enable @typescript-eslint/ban-types */
353 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src",
4 | "target": "ESNext",
5 | "module": "NodeNext",
6 | "moduleResolution": "NodeNext",
7 | "declaration": true,
8 | "strict": true,
9 | "allowSyntheticDefaultImports": true,
10 | "esModuleInterop": true,
11 | "noUncheckedIndexedAccess": true,
12 | "noEmit": true,
13 | "skipLibCheck": true
14 | },
15 | "exclude": ["node_modules", "build"]
16 | }
17 |
--------------------------------------------------------------------------------