├── .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 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/react-router-typed-object?file=src%2Frouter.tsx,src%2Fcomponents.tsx) 224 | 225 | ![image](https://github.com/user-attachments/assets/071f4a6b-f820-4cb6-8f52-0f0997faffa4) 226 | 227 | ![image](https://github.com/user-attachments/assets/596a372b-aa88-4be4-bd52-3099ff644622) 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 | --------------------------------------------------------------------------------