@troyio/vite-file-router 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,536 +1,704 @@
1
- # vite-file-router
2
-
3
- A Vite plugin that generates a route tree from a file-based page directory.
4
-
5
- ## Install
6
-
7
- ```sh
8
- npm install @troyio/vite-file-router
9
- ```
10
-
11
- Vite is a peer dependency and must also be installed in your project.
12
-
13
- ## Configure
14
-
15
- ```ts
16
- import { defineConfig } from "vite";
17
- import { routes } from "@troyio/vite-file-router";
18
-
19
- export default defineConfig({
20
- plugins: [routes()],
21
- });
22
- ```
23
-
24
- By default, the plugin scans `src/pages` and recognizes the file conventions
25
- listed below. Pass a path relative to the Vite project root to use another
26
- directory:
27
-
28
- ```ts
29
- plugins: [routes("src/routes")];
30
- ```
31
-
32
- You can also configure the directory and special filenames together.
33
- Convention values are filename stems without extensions. Page and component
34
- conventions use `.jsx` / `.tsx` files (`.tsx` takes precedence); `loader` and
35
- `action` conventions use `.js` / `.ts` files (`.ts` takes precedence):
36
-
37
- ```ts
38
- plugins: [
39
- routes({
40
- directory: "app/views",
41
- conventions: {
42
- page: "route",
43
- layout: "shell",
44
- loading: "pending",
45
- notFound: "missing",
46
- error: "fault",
47
- loader: "load",
48
- action: "mutate",
49
- },
50
- }),
51
- ];
52
- ```
53
-
54
- Omitted convention values keep their defaults: `page`, `layout`, `loading`,
55
- `not-found`, `error`, `loader`, and `action`. Each convention must have a
56
- unique filename stem.
57
-
58
- The router scans the pages directory recursively. Each directory becomes a
59
- nested route, and the following files are recognized:
60
-
61
- | File | Purpose |
62
- | --- | --- |
63
- | `page.jsx` / `page.tsx` | The page component for the directory's URL. |
64
- | `layout.jsx` / `layout.tsx` | A layout around the directory's page and child routes. |
65
- | `loading.jsx` / `loading.tsx` | A Suspense fallback for the directory's page and child routes. |
66
- | `not-found.jsx` / `not-found.tsx` | A not-found page for unmatched URLs within the directory's route subtree. |
67
- | `error.jsx` / `error.tsx` | A React Router error boundary for the directory's route subtree. |
68
- | `loader.js` / `loader.ts` | A React Router loader for the directory's route and its descendants. |
69
- | `action.js` / `action.ts` | A React Router action selected when that directory route is the submission target. |
70
-
71
- Component files must default-export a React component. Loader and action
72
- modules must named-export their handler as `loader` or `action`; if both
73
- supported extensions exist for the same convention in one directory, the
74
- TypeScript file takes precedence.
75
-
76
- For example, this structure creates a home page, user pages, and two auth
77
- pages:
78
-
79
- ```text
80
- src/pages/
81
- page.jsx
82
- layout.tsx
83
- loading.tsx
84
- loader.ts
85
- users/
86
- page.tsx
87
- action.ts
88
- [id]/
89
- page.tsx
90
- docs/
91
- [[lang]]/
92
- page.tsx
93
- (auth)/
94
- layout.tsx
95
- sign-in/
96
- page.tsx
97
- sign-up/
98
- page.tsx
99
- [...slug]/
100
- page.tsx
101
- ```
102
-
103
- This maps to `/`, `/users`, `/users/:id`, `/docs/:lang?`, `/sign-in`,
104
- `/sign-up`, and `/*`. Square-bracket segments are dynamic parameters: `[id]`
105
- becomes `:id`; double-bracket segments are optional dynamic parameters, so
106
- `[[lang]]` becomes `:lang?` and matches both `/docs` and `/docs/en`.
107
- Catch-all segments such as `[...slug]` become `*`. Parenthesized directories
108
- are route groups; they organize files and can have their own page, layout,
109
- loading files, loaders, and actions, but their names do not add a URL segment.
110
-
111
- Directories whose names start with an underscore, such as `_components` or
112
- `_utils`, are private folders. The plugin skips them and everything nested
113
- inside them, so you can colocate components, hooks, and helpers next to the
114
- routes that use them without creating routes. A `_components/page.tsx` file is
115
- never routed.
116
-
117
- ### Layouts
118
-
119
- The plugin adapts React Router's matched child route to a `children` prop. A
120
- layout can render that prop directly and does not need to import or render
121
- `Outlet`:
122
-
123
- ```tsx
124
- import type { PropsWithChildren } from "react";
125
-
126
- export default function Layout({ children }: PropsWithChildren) {
127
- return <div className="app-shell">{children}</div>;
128
- }
129
- ```
130
-
131
- ### Loading fallbacks
132
-
133
- Add a `loading.jsx` or `loading.tsx` beside a page or layout to show a Suspense
134
- fallback while that route subtree loads:
135
-
136
- ```tsx
137
- export default function Loading() {
138
- return <p>Loading...</p>;
139
- }
140
- ```
141
-
142
- A nested loading file creates a more specific boundary for that directory.
143
-
144
- ### Not-found pages
145
-
146
- Add a `not-found.jsx` or `not-found.tsx` file to handle unmatched URLs. The plugin generates
147
- a catch-all child route, and the most specific matching route subtree's
148
- not-found page is used:
149
-
150
- ```text
151
- src/pages/
152
- not-found.tsx
153
- users/
154
- not-found.tsx
155
- page.tsx
156
- ```
157
-
158
- The root not-found page handles unknown URLs generally, while `users/not-found.tsx`
159
- handles unmatched URLs under `/users`.
160
-
161
- ### Route error boundaries
162
-
163
- Add an `error.jsx` or `error.tsx` file to handle errors from that route and its
164
- descendants. The nearest route boundary is used, following React Router's error
165
- handling behavior. Use React Router's `useRouteError` hook to read the error:
166
-
167
- ```tsx
168
- import { useRouteError } from "react-router";
169
-
170
- export default function ErrorPage() {
171
- const error = useRouteError();
172
- return <main>Something went wrong: {String(error)}</main>;
173
- }
174
- ```
175
-
176
- ### Loaders, actions, and route guards
177
-
178
- Add a `loader.ts` / `loader.js` or `action.ts` / `action.js` beside the route
179
- files and export a named handler. The generated React Router route calls these
180
- handlers with React Router's loader/action arguments. Their returned values,
181
- thrown errors, and redirect responses use React Router's normal data-router
182
- behavior. A loader on a directory route runs when that route matches, including
183
- when a descendant URL matches. An action runs only when its route is selected
184
- as the submission target; a parent action does not automatically handle
185
- submissions targeting child routes.
186
-
187
- A loader module may also export `shouldRevalidate(args)` to control the route's
188
- React Router `shouldRevalidate` option. It receives the same arguments as
189
- `shouldRevalidate` and must synchronously return a boolean:
190
-
191
- ```ts
192
- import type { LoaderFunctionArgs, ShouldRevalidateFunctionArgs } from "react-router";
193
-
194
- export function shouldRevalidate({ nextUrl, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
195
- if (nextUrl.searchParams.get("refresh") === "never") return false;
196
- return defaultShouldRevalidate;
197
- }
198
-
199
- export async function loader({ request }: LoaderFunctionArgs) {
200
- return getDashboardData(request);
201
- }
202
- ```
203
-
204
- Use a loader to redirect users who should not enter a protected route subtree.
205
- For example, `src/pages/dashboard/loader.ts` can guard `/dashboard` and all of
206
- its nested pages:
207
-
208
- ```ts
209
- import { redirect, type LoaderFunctionArgs } from "react-router";
210
- import { getUserForRequest } from "../../lib/auth.server";
211
-
212
- export async function loader({ request }: LoaderFunctionArgs) {
213
- const user = await getUserForRequest(request);
214
- if (!user) {
215
- const returnTo = new URL(request.url).pathname;
216
- throw redirect(`/sign-in?returnTo=${encodeURIComponent(returnTo)}`);
217
- }
218
- return null;
219
- }
220
- ```
221
-
222
- Actions use the same pattern and receive React Router's `ActionFunctionArgs`:
223
-
224
- ```ts
225
- import { redirect, type ActionFunctionArgs } from "react-router";
226
-
227
- export async function action({ request }: ActionFunctionArgs) {
228
- const formData = await request.formData();
229
- await updateProfile(formData);
230
- return redirect("/profile");
231
- }
232
- ```
233
-
234
- When a directory has both a page and a loader, that page is generated as an
235
- index child route, while the loader remains on the directory route so it also
236
- runs for descendant pages. Read the directory loader's result in the page with
237
- React Router's `useRouteLoaderData`. The generated route ID is
238
- `file-route:` followed by the directory path relative to the Vite project
239
- root, with `/` separators:
240
-
241
- ```tsx
242
- import { useRouteLoaderData } from "react-router";
243
-
244
- export default function DashboardPage() {
245
- const data = useRouteLoaderData<{ message: string }>(
246
- "file-route:src/pages/dashboard",
247
- );
248
- return <h1>{data?.message}</h1>;
249
- }
250
- ```
251
-
252
- To get the type from the loader module instead of writing it by hand, use
253
- `useRouteData` from `virtual:route-helpers`. See
254
- [Typed params and loader data](#typed-params-and-loader-data).
255
-
256
- The authentication and mutation functions above are application code. A
257
- client-side route guard controls navigation and presentation; it is not a
258
- security boundary. Protect data and mutations in the server/API layer as well,
259
- and do not rely on a parent loader running before descendant loaders.
260
-
261
- ### Dynamic routes and route groups
262
-
263
- Two pages must not resolve to the same URL. This includes pages with the same
264
- path in separate route groups, such as `(auth)/sign-in/page.tsx` and
265
- `(guest)/sign-in/page.tsx`. During development, the router displays a diagnostic
266
- page listing the conflicting files; production builds fail with the same
267
- diagnostic until the conflict is fixed. Dynamic parameter names do not make
268
- otherwise identical patterns distinct (`users/[id]` and `users/[slug]`
269
- conflict). Static and dynamic siblings such as `users/new` and `users/[id]`
270
- are distinct; React Router's normal route ranking selects the static route
271
- first.
272
-
273
- ## Use the generated routes
274
-
275
- Import `routes` from the virtual module and pass it to React Router:
276
-
277
- ```tsx
278
- import { createBrowserRouter } from "react-router";
279
- import { RouterProvider } from "react-router";
280
- import { routes } from "virtual:routes";
281
-
282
- const router = createBrowserRouter(routes);
283
-
284
- export default function App() {
285
- return <RouterProvider router={router} />;
286
- }
287
- ```
288
-
289
- ### Route manifest
290
-
291
- Import `routeManifest` from `virtual:route-manifest` for a stable, nested
292
- description of the scanned routes. Source files are relative to the Vite
293
- project root and use forward slashes. Each node includes its URL `fullPath`,
294
- the parameters introduced by that segment, any `optionalParams`, and its
295
- recognized files:
296
-
297
- ```ts
298
- import { routeManifest } from "virtual:route-manifest";
299
-
300
- console.log(routeManifest.children);
301
- ```
302
-
303
- The root node has `fullPath: "/"`; route-group nodes have no URL segment but
304
- remain in the tree. React Router's generated `routes` remain the runtime source
305
- of truth.
306
-
307
- The development server watches the configured directory. Adding, removing,
308
- renaming, or editing route files invalidates all generated virtual modules and
309
- reloads the app without requiring a server restart.
310
-
311
- ### Route tree inspection
312
-
313
- The route tree can be inspected in Vite DevTools. This requires Vite 8.3 or
314
- later and the DevTools package:
315
-
316
- ```sh
317
- npm install -D @vitejs/devtools
318
- ```
319
-
320
- Enable the dock in your Vite configuration:
321
-
322
- ```ts
323
- export default defineConfig({
324
- devtools: true,
325
- plugins: [routes()],
326
- });
327
- ```
328
-
329
- Open the **Routes** dock to use the panel. It's development-only and needs
330
- Vite DevTools enabled. In the panel you can:
331
-
332
- - **Browse the route tree.** Each route shows its URL segment, full path, and
333
- badges for its files: page, layout, loading, error, not-found (`404`),
334
- loader, and action. You can filter by path, param, or file, collapse
335
- branches, and move between routes with the arrow keys.
336
- - **See route details.** Select a route to see its params and files.
337
- - **Open files in your editor.** Click any file path. This uses Vite's built-in
338
- `/__open-in-editor` endpoint, so it opens the editor Vite detects or the one
339
- named in `LAUNCH_EDITOR`.
340
- - **Test a URL.** Enter a path such as `/users/42?tab=posts`. The panel shows
341
- the matched routes, the params taken from the URL, and the page or
342
- not-found page that renders. It also lists the layouts, loaders, loading
343
- fallback, and error boundary that apply. Matching uses React Router's
344
- `matchRoutes`, so the result agrees with the app. The matched routes are
345
- highlighted in the tree.
346
- - **Spot route conflicts.** Pages that resolve to the same URL pattern are
347
- listed at the top, with links to their files.
348
- - **Follow the current page.** The **Current page** card shows the URL your
349
- app is on, its params, and the page that renders it. The matched routes get
350
- a green dot in the tree, and the card updates as the app navigates. With
351
- several app tabs open, pick which one to follow. When a tab closes, it drops
352
- out of the list.
353
- - **See loader and action calls.** The **Timeline** tab lists each loader and
354
- action call your app makes, newest first. Each entry shows the route, URL,
355
- start time, duration, and outcome: returned data, a redirect with its status
356
- and location, a returned response, or a thrown error. Calls of 500 ms or
357
- more are highlighted. It also shows each `shouldRevalidate` decision, so
358
- you can see when a loader was skipped. You can filter by call type or text,
359
- and expand an entry to see a short preview of returned data and a link to
360
- the loader or action file. The dev server keeps the last 200 calls, so a
361
- panel opened later still sees recent history.
362
-
363
- The panel updates on its own when route files are added, removed, or renamed.
364
- The dev server pushes changes over a Server-Sent Events stream instead of the
365
- panel polling. If the dev server restarts, the panel reconnects by itself. The
366
- refresh button reloads the data by hand.
367
-
368
- The current page and the timeline come from a small client that
369
- `virtual:routes` includes on the dev server only. It reports the app's URL and
370
- wraps the generated loader, action, and `shouldRevalidate` handlers. It sends
371
- these over Vite's HMR connection. Handler results pass through unchanged, and
372
- only a short text preview of returned data is sent. It tracks navigation by
373
- wrapping `history.pushState` and `history.replaceState` and listening for
374
- `popstate`, and it also supports hash URLs (`#/path`). Production builds
375
- include none of this code.
376
-
377
- ### Typed route helpers
378
-
379
- Import `routePath` from `virtual:route-helpers` to build paths for discovered
380
- pages. The helper is compatible with React Router's `Link`, `NavLink`,
381
- `navigate`, and `redirect` APIs. Dynamic parameter names and required values are
382
- checked from the path literal; values are URL-encoded. Splat values use the
383
- React Router `"*"` parameter and are encoded segment-by-segment so `/`
384
- separators are preserved:
385
-
386
- ```tsx
387
- import { Link } from "react-router";
388
- import { routePath } from "virtual:route-helpers";
389
-
390
- export function UserLink({ userId }: { userId: string }) {
391
- return (
392
- <Link to={routePath("/users/:userId", { userId })}>
393
- View user
394
- </Link>
395
- );
396
- }
397
-
398
- const docsPath = routePath("/docs/*", { "*": "guides/getting-started" });
399
- const localizedDocsPath = routePath("/docs/:lang?", { lang: "en" });
400
- const defaultDocsPath = routePath("/docs/:lang?");
401
- ```
402
-
403
- Static routes and routes with only optional parameters need no parameter object.
404
- An optional parameter can also be omitted from the params object; its URL
405
- segment is removed. Unknown route patterns and missing required or extra
406
- parameters are rejected at runtime. Routes whose optional segment overlaps
407
- another page's URL pattern are reported as conflicts. The generated
408
- `routePatterns` export contains the discovered page paths.
409
-
410
- ### Generated route types
411
-
412
- In TypeScript projects, the plugin writes `src/route-types.d.ts` when the dev
413
- server starts and at the start of each build. It counts a project as
414
- TypeScript when the Vite project root has a `tsconfig.json` or
415
- `jsconfig.json`. JavaScript projects without either file get no extra file.
416
-
417
- The plugin rewrites the file whenever route files are added, removed, or
418
- renamed, and leaves it alone when nothing changed. The file lists every
419
- discovered page pattern and loader module. With it in your TypeScript project,
420
- `routePath`, `routePatterns`, and the hooks below only accept routes that
421
- exist, and your editor autocompletes them:
422
-
423
- ```ts
424
- routePath("/users/:userId", { userId }); // OK
425
- routePath("/usres/:userId", { userId }); // Type error: unknown route pattern
426
- ```
427
-
428
- The `dts` option controls this. It defaults to `"auto"`, which is the behavior
429
- above. A path, relative to the Vite project root and ending in `.d.ts`, always
430
- writes the file there, even without a TypeScript config. `false` never writes
431
- it:
432
-
433
- ```ts
434
- plugins: [routes({ dts: "types/route-types.d.ts" })];
435
- plugins: [routes({ dts: false })];
436
- ```
437
-
438
- If your `tsconfig.json` isn't in the Vite project root, for example when Vite's
439
- `root` points at a subfolder, set an explicit path.
440
-
441
- Without the file, route patterns are typed as `string` and loader data as
442
- `unknown`, the same as before. You can commit the file so types work before
443
- the first dev run, or add it to `.gitignore`.
444
-
445
- ### Typed params and loader data
446
-
447
- `virtual:route-helpers` also exports two hooks that wrap React Router's hooks
448
- with types taken from the route tree:
449
-
450
- - `useRouteParams(pattern)` returns `useParams()`, typed for that pattern.
451
- Required params are `string`, optional params are `string | undefined`, and
452
- a splat is a `string` under the `"*"` key.
453
- - `useRouteData(key)` returns `useRouteLoaderData()`, typed from the loader
454
- module. The key is either a page pattern, which reads the loader in that
455
- page's directory, or a loader route ID such as
456
- `"file-route:src/pages/users/[userId]"`, which can read any matched loader,
457
- including a parent's. It returns `undefined` when that route isn't matched,
458
- just like React Router's hook.
459
-
460
- ```tsx
461
- // src/pages/users/[userId]/loader.ts
462
- import type { LoaderFunctionArgs } from "react-router";
463
-
464
- export async function loader({ params }: LoaderFunctionArgs) {
465
- return { name: await getUserName(params.userId!) };
466
- }
467
-
468
- // src/pages/users/[userId]/page.tsx
469
- import { useRouteData, useRouteParams } from "virtual:route-helpers";
470
-
471
- export default function UserPage() {
472
- const { userId } = useRouteParams("/users/:userId"); // { userId: string }
473
- const data = useRouteData("/users/:userId"); // { name: string } | undefined
474
- return <h1>{data?.name ?? userId}</h1>;
475
- }
476
- ```
477
-
478
- The `RouteParams<"/users/:userId">`, `RouteLoaderData<key>`, `RoutePattern`,
479
- and `RouteLoaderKey` types are exported from `@troyio/vite-file-router` for
480
- your own helpers. Loader data is typed the same way as React Router's
481
- `useRouteLoaderData<typeof loader>()`. JavaScript loader modules are typed as
482
- `any`. Both hooks throw if they're passed a pattern or key the plugin didn't
483
- discover.
484
-
485
- ### Route preloading
486
-
487
- `virtual:routes` exports `preloadRoute(to, basename?)`. It matches a URL
488
- against the route tree and imports the page, layout, and loader modules of
489
- every matched route, so the next navigation there doesn't wait for those
490
- downloads. Error and action modules are left out, since they're only needed
491
- after an error or a form submission. Importing modules doesn't run loaders.
492
- Each module downloads once, however often you call it. The promise resolves to
493
- `false` when no route matches the URL.
494
-
495
- Call it when a user is likely to navigate, such as on link hover or focus:
496
-
497
- ```tsx
498
- import { Link, type LinkProps } from "react-router";
499
- import { preloadRoute } from "virtual:routes";
500
-
501
- export function PreloadLink(props: LinkProps & { to: string }) {
502
- const preload = () => void preloadRoute(props.to);
503
- return <Link {...props} onMouseEnter={preload} onFocus={preload} />;
504
- }
505
- ```
506
-
507
- If the router uses a `basename`, pass it as the second argument. `to` also
508
- accepts a partial location such as `{ pathname: "/users/42" }`.
509
-
510
- ### TypeScript setup
511
-
512
- Add Vite's client types and the package's virtual-module declaration to your
513
- application TypeScript project. For example, create `src/vite-env.d.ts`:
514
-
515
- ```ts
516
- /// <reference types="vite/client" />
517
- /// <reference types="@troyio/vite-file-router" />
518
- ```
519
-
520
- If `tsconfig.json` sets `compilerOptions.types`, include both `"vite/client"`
521
- and `"@troyio/vite-file-router"` in that list. This lets TypeScript resolve
522
- `virtual:routes`. Make sure the generated `src/route-types.d.ts`, or the path
523
- you set with `dts`, is included by your `tsconfig.json`. Restart your editor's
524
- TypeScript server if the declarations aren't picked up immediately.
525
-
526
- ## Build
527
-
528
- ```sh
529
- npm run build
530
- ```
531
-
532
- ## Test
533
-
534
- ```sh
535
- npm test
536
- ```
1
+ # vite-file-router
2
+
3
+ A Vite plugin that generates a route tree from a file-based page directory.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @troyio/vite-file-router
9
+ ```
10
+
11
+ Vite is a peer dependency and must also be installed in your project.
12
+
13
+ ## Configure
14
+
15
+ ```ts
16
+ import { defineConfig } from "vite";
17
+ import { routes } from "@troyio/vite-file-router";
18
+
19
+ export default defineConfig({
20
+ plugins: [routes()],
21
+ });
22
+ ```
23
+
24
+ By default, the plugin scans `src/pages` and recognizes the file conventions
25
+ listed below. Pass a path relative to the Vite project root to use another
26
+ directory:
27
+
28
+ ```ts
29
+ plugins: [routes("src/routes")];
30
+ ```
31
+
32
+ You can also configure the directory and special filenames together.
33
+ Convention values are filename stems without extensions. Page and component
34
+ conventions use `.jsx` / `.tsx` files (`.tsx` takes precedence); `loader`,
35
+ `action`, and `middleware` conventions use `.js` / `.ts` files (`.ts` takes
36
+ precedence):
37
+
38
+ ```ts
39
+ plugins: [
40
+ routes({
41
+ directory: "app/views",
42
+ conventions: {
43
+ page: "route",
44
+ layout: "shell",
45
+ loading: "pending",
46
+ notFound: "missing",
47
+ error: "fault",
48
+ loader: "load",
49
+ action: "mutate",
50
+ middleware: "guard",
51
+ hydrateFallback: "booting",
52
+ },
53
+ }),
54
+ ];
55
+ ```
56
+
57
+ Omitted convention values keep their defaults: `page`, `layout`, `loading`,
58
+ `not-found`, `error`, `loader`, `action`, `middleware`, and
59
+ `hydrate-fallback`. Each convention must have a unique filename stem.
60
+
61
+ The router scans the pages directory recursively. Each directory becomes a
62
+ nested route, and the following files are recognized:
63
+
64
+ | File | Purpose |
65
+ | --- | --- |
66
+ | `page.jsx` / `page.tsx` | The page component for the directory's URL. |
67
+ | `layout.jsx` / `layout.tsx` | A layout around the directory's page and child routes. |
68
+ | `loading.jsx` / `loading.tsx` | A Suspense fallback for the directory's page and child routes. |
69
+ | `not-found.jsx` / `not-found.tsx` | A not-found page for unmatched URLs within the directory's route subtree. |
70
+ | `error.jsx` / `error.tsx` | A React Router error boundary for the directory's route subtree. |
71
+ | `loader.js` / `loader.ts` | A React Router loader for the directory's route and its descendants. |
72
+ | `action.js` / `action.ts` | A React Router action selected when that directory route is the submission target. |
73
+ | `middleware.js` / `middleware.ts` | React Router middleware that runs before every loader and action at or below the directory. |
74
+ | `hydrate-fallback.jsx` / `hydrate-fallback.tsx` | What renders while the first page load's loaders run. |
75
+
76
+ Component files must default-export a React component. Loader and action
77
+ modules must named-export their handler as `loader` or `action`; if both
78
+ supported extensions exist for the same convention in one directory, the
79
+ TypeScript file takes precedence.
80
+
81
+ For example, this structure creates a home page, user pages, and two auth
82
+ pages:
83
+
84
+ ```text
85
+ src/pages/
86
+ page.jsx
87
+ layout.tsx
88
+ loading.tsx
89
+ loader.ts
90
+ users/
91
+ page.tsx
92
+ action.ts
93
+ [id]/
94
+ page.tsx
95
+ docs/
96
+ [[lang]]/
97
+ page.tsx
98
+ (auth)/
99
+ layout.tsx
100
+ sign-in/
101
+ page.tsx
102
+ sign-up/
103
+ page.tsx
104
+ [...slug]/
105
+ page.tsx
106
+ ```
107
+
108
+ This maps to `/`, `/users`, `/users/:id`, `/docs/:lang?`, `/sign-in`,
109
+ `/sign-up`, and `/*`. Square-bracket segments are dynamic parameters: `[id]`
110
+ becomes `:id`; double-bracket segments are optional dynamic parameters, so
111
+ `[[lang]]` becomes `:lang?` and matches both `/docs` and `/docs/en`.
112
+ Catch-all segments such as `[...slug]` become `*`. Parenthesized directories
113
+ are route groups; they organize files and can have their own page, layout,
114
+ loading files, loaders, and actions, but their names do not add a URL segment.
115
+
116
+ Directories whose names start with an underscore, such as `_components` or
117
+ `_utils`, are private folders. The plugin skips them and everything nested
118
+ inside them, so you can colocate components, hooks, and helpers next to the
119
+ routes that use them without creating routes. A `_components/page.tsx` file is
120
+ never routed.
121
+
122
+ ### Layouts
123
+
124
+ The plugin adapts React Router's matched child route to a `children` prop. A
125
+ layout can render that prop directly and does not need to import or render
126
+ `Outlet`:
127
+
128
+ ```tsx
129
+ import type { PropsWithChildren } from "react";
130
+
131
+ export default function Layout({ children }: PropsWithChildren) {
132
+ return <div className="app-shell">{children}</div>;
133
+ }
134
+ ```
135
+
136
+ ### Loading fallbacks
137
+
138
+ Add a `loading.jsx` or `loading.tsx` beside a page or layout to show a Suspense
139
+ fallback while that route subtree loads:
140
+
141
+ ```tsx
142
+ export default function Loading() {
143
+ return <p>Loading...</p>;
144
+ }
145
+ ```
146
+
147
+ A nested loading file creates a more specific boundary for that directory.
148
+
149
+ Loading files cover lazy page code, not loader time. On later navigations,
150
+ React Router keeps the current page on screen while loaders run, so show
151
+ pending UI with `useNavigation()` in a layout.
152
+
153
+ ### Hydrate fallbacks
154
+
155
+ On the first page load, nothing renders until the matched routes' loaders and
156
+ lazy modules are ready. Add a `hydrate-fallback.jsx` or `hydrate-fallback.tsx`
157
+ to show something in the meantime:
158
+
159
+ ```tsx
160
+ export default function HydrateFallback() {
161
+ return <p>Loading the app...</p>;
162
+ }
163
+ ```
164
+
165
+ The file becomes the directory route's React Router `HydrateFallback`.
166
+ React Router renders the nearest one at or above the first route that is
167
+ still loading, below the layouts of the routes above it. A layout in the same
168
+ directory as the fallback doesn't wrap it. It only shows on the first load;
169
+ later navigations keep the current page on screen instead.
170
+
171
+ Hydrate fallbacks are imported up front rather than lazily, because they
172
+ render before any route module has loaded. Keep them small: they're part of
173
+ your app's initial bundle.
174
+
175
+ ### Not-found pages
176
+
177
+ Add a `not-found.jsx` or `not-found.tsx` file to handle unmatched URLs. The plugin generates
178
+ a catch-all child route, and the most specific matching route subtree's
179
+ not-found page is used:
180
+
181
+ ```text
182
+ src/pages/
183
+ not-found.tsx
184
+ users/
185
+ not-found.tsx
186
+ page.tsx
187
+ ```
188
+
189
+ The root not-found page handles unknown URLs generally, while `users/not-found.tsx`
190
+ handles unmatched URLs under `/users`.
191
+
192
+ ### Route error boundaries
193
+
194
+ Add an `error.jsx` or `error.tsx` file to handle errors from that route and its
195
+ descendants. The nearest route boundary is used, following React Router's error
196
+ handling behavior. Use React Router's `useRouteError` hook to read the error:
197
+
198
+ ```tsx
199
+ import { useRouteError } from "react-router";
200
+
201
+ export default function ErrorPage() {
202
+ const error = useRouteError();
203
+ return <main>Something went wrong: {String(error)}</main>;
204
+ }
205
+ ```
206
+
207
+ ### Loaders, actions, and route guards
208
+
209
+ Add a `loader.ts` / `loader.js` or `action.ts` / `action.js` beside the route
210
+ files and export a named handler. The generated React Router route calls these
211
+ handlers with React Router's loader/action arguments. Their returned values,
212
+ thrown errors, and redirect responses use React Router's normal data-router
213
+ behavior. A loader on a directory route runs when that route matches, including
214
+ when a descendant URL matches. An action runs only when its route is selected
215
+ as the submission target; a parent action does not automatically handle
216
+ submissions targeting child routes.
217
+
218
+ A loader module may also export `shouldRevalidate(args)` to control the route's
219
+ React Router `shouldRevalidate` option. It receives the same arguments as
220
+ `shouldRevalidate` and must synchronously return a boolean:
221
+
222
+ ```ts
223
+ import type { LoaderFunctionArgs, ShouldRevalidateFunctionArgs } from "react-router";
224
+
225
+ export function shouldRevalidate({ nextUrl, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs) {
226
+ if (nextUrl.searchParams.get("refresh") === "never") return false;
227
+ return defaultShouldRevalidate;
228
+ }
229
+
230
+ export async function loader({ request }: LoaderFunctionArgs) {
231
+ return getDashboardData(request);
232
+ }
233
+ ```
234
+
235
+ Use a loader to redirect users who should not enter a protected route subtree.
236
+ For example, `src/pages/dashboard/loader.ts` can guard `/dashboard` and all of
237
+ its nested pages:
238
+
239
+ ```ts
240
+ import { redirect, type LoaderFunctionArgs } from "react-router";
241
+ import { getUserForRequest } from "../../lib/auth.server";
242
+
243
+ export async function loader({ request }: LoaderFunctionArgs) {
244
+ const user = await getUserForRequest(request);
245
+ if (!user) {
246
+ const returnTo = new URL(request.url).pathname;
247
+ throw redirect(`/sign-in?returnTo=${encodeURIComponent(returnTo)}`);
248
+ }
249
+ return null;
250
+ }
251
+ ```
252
+
253
+ Actions use the same pattern and receive React Router's `ActionFunctionArgs`:
254
+
255
+ ```ts
256
+ import { redirect, type ActionFunctionArgs } from "react-router";
257
+
258
+ export async function action({ request }: ActionFunctionArgs) {
259
+ const formData = await request.formData();
260
+ await updateProfile(formData);
261
+ return redirect("/profile");
262
+ }
263
+ ```
264
+
265
+ When a directory has both a page and a loader, that page is generated as an
266
+ index child route, while the loader remains on the directory route so it also
267
+ runs for descendant pages. Read the directory loader's result in the page with
268
+ React Router's `useRouteLoaderData`. The generated route ID is
269
+ `file-route:` followed by the directory path relative to the Vite project
270
+ root, with `/` separators:
271
+
272
+ ```tsx
273
+ import { useRouteLoaderData } from "react-router";
274
+
275
+ export default function DashboardPage() {
276
+ const data = useRouteLoaderData<{ message: string }>(
277
+ "file-route:src/pages/dashboard",
278
+ );
279
+ return <h1>{data?.message}</h1>;
280
+ }
281
+ ```
282
+
283
+ To get the type from the loader module instead of writing it by hand, use
284
+ `useRouteData` from `virtual:route-helpers`. See
285
+ [Typed params and loader data](#typed-params-and-loader-data).
286
+
287
+ The authentication and mutation functions above are application code. A
288
+ client-side route guard controls navigation and presentation; it is not a
289
+ security boundary. Protect data and mutations in the server/API layer as well,
290
+ and do not rely on a parent loader running before descendant loaders.
291
+
292
+ ### Middleware
293
+
294
+ Add a `middleware.ts` (or `.js`) file to run code before the loaders and
295
+ actions at or below its directory, for example to check a session, log
296
+ requests, or share data through React Router's `context`. Export a function,
297
+ or an array of functions, as `middleware` or as the default export:
298
+
299
+ ```ts
300
+ // src/pages/context.ts
301
+ import { createContext } from "react-router";
302
+
303
+ export const userContext = createContext<User | null>(null);
304
+ ```
305
+
306
+ ```ts
307
+ // src/pages/(app)/middleware.ts
308
+ import { redirect, type MiddlewareFunction } from "react-router";
309
+ import { userContext } from "../context";
310
+
311
+ export const middleware: MiddlewareFunction[] = [
312
+ async ({ request, context }, next) => {
313
+ const user = await getUser(request);
314
+ if (!user) throw redirect("/sign-in");
315
+ context.set(userContext, user);
316
+ await next();
317
+ },
318
+ ];
319
+ ```
320
+
321
+ ```ts
322
+ // src/pages/(app)/dashboard/loader.ts
323
+ import type { LoaderFunctionArgs } from "react-router";
324
+ import { userContext } from "../../context";
325
+
326
+ export function loader({ context }: LoaderFunctionArgs) {
327
+ return { user: context.get(userContext) };
328
+ }
329
+ ```
330
+
331
+ The plugin attaches the file to the directory's route through React Router's
332
+ route `middleware` option, so React Router's rules apply: parent middleware
333
+ runs before child middleware, `next()` runs the rest of the chain and the
334
+ loaders or action, and a function that doesn't call `next()` has it called
335
+ for it afterward. Throwing a redirect stops the navigation, and other thrown
336
+ errors render the nearest error file. Middleware runs on every navigation,
337
+ even when no route has a loader. React Router 8 supports route middleware
338
+ without a future flag.
339
+
340
+ React Router can't load middleware through a route's `lazy()` function, so
341
+ the generated route imports the file the first time its middleware runs.
342
+ `preloadRoute()` imports it ahead of time along with the route's other
343
+ modules.
344
+
345
+ Middleware runs in the browser here, like loaders and actions. As with route
346
+ guards, it isn't a security boundary: protect data in your server or API too.
347
+
348
+ ### Dynamic routes and route groups
349
+
350
+ Two pages must not resolve to the same URL. This includes pages with the same
351
+ path in separate route groups, such as `(auth)/sign-in/page.tsx` and
352
+ `(guest)/sign-in/page.tsx`. During development, the router displays a diagnostic
353
+ page listing the conflicting files; production builds fail with the same
354
+ diagnostic until the conflict is fixed. Dynamic parameter names do not make
355
+ otherwise identical patterns distinct (`users/[id]` and `users/[slug]`
356
+ conflict). Static and dynamic siblings such as `users/new` and `users/[id]`
357
+ are distinct; React Router's normal route ranking selects the static route
358
+ first.
359
+
360
+ ## Use the generated routes
361
+
362
+ Import `routes` from the virtual module and pass it to React Router:
363
+
364
+ ```tsx
365
+ import { createBrowserRouter } from "react-router";
366
+ import { RouterProvider } from "react-router";
367
+ import { routes } from "virtual:routes";
368
+
369
+ const router = createBrowserRouter(routes);
370
+
371
+ export default function App() {
372
+ return <RouterProvider router={router} />;
373
+ }
374
+ ```
375
+
376
+ ### Route manifest
377
+
378
+ Import `routeManifest` from `virtual:route-manifest` for a stable, nested
379
+ description of the scanned routes. Source files are relative to the Vite
380
+ project root and use forward slashes. Each node includes its URL `fullPath`,
381
+ the parameters introduced by that segment, any `optionalParams`, and its
382
+ recognized files:
383
+
384
+ ```ts
385
+ import { routeManifest } from "virtual:route-manifest";
386
+
387
+ console.log(routeManifest.children);
388
+ ```
389
+
390
+ The root node has `fullPath: "/"`; route-group nodes have no URL segment but
391
+ remain in the tree. React Router's generated `routes` remain the runtime source
392
+ of truth.
393
+
394
+ The development server watches the configured directory. Adding, removing,
395
+ renaming, or editing route files invalidates all generated virtual modules and
396
+ reloads the app without requiring a server restart.
397
+
398
+ ### Route tree inspection
399
+
400
+ The route tree can be inspected in Vite DevTools. This requires Vite 8.3 or
401
+ later and the DevTools package:
402
+
403
+ ```sh
404
+ npm install -D @vitejs/devtools
405
+ ```
406
+
407
+ Enable the dock in your Vite configuration:
408
+
409
+ ```ts
410
+ export default defineConfig({
411
+ devtools: true,
412
+ plugins: [routes()],
413
+ });
414
+ ```
415
+
416
+ Open the **Routes** dock to use the panel. It's development-only and needs
417
+ Vite DevTools enabled. The panel has three pages, listed in a sidebar (or a
418
+ top bar when the dock is narrow): **Routes**, **Timeline**, and **Generated
419
+ code**. The page you're on is kept in the URL hash, so reloading the panel
420
+ stays on it. In the panel you can:
421
+
422
+ - **Browse the route tree.** Each route shows its URL segment, full path, and
423
+ badges for its files: page, layout, loading, error, not-found (`404`),
424
+ loader, action, middleware (`mw`), and hydrate fallback (`hydrate`). You
425
+ can filter by path, param, or file, collapse
426
+ branches, and move between routes with the arrow keys.
427
+ - **See route details.** Select a route to see its params, files, and
428
+ warnings. For a route with a loader or action, the **Data calls** section
429
+ shows how many calls it made, their average and slowest duration, errors,
430
+ and the last outcome, with a link to those calls in the timeline.
431
+ - **Open files in your editor.** Click any file path. This uses Vite's built-in
432
+ `/__open-in-editor` endpoint, so it opens the editor Vite detects or the one
433
+ named in `LAUNCH_EDITOR`.
434
+ - **Test a URL.** Enter a path such as `/users/42?tab=posts`. The panel shows
435
+ the matched routes, the params taken from the URL, and the page or
436
+ not-found page that renders. It also lists the layouts, loaders, loading
437
+ fallback, and error boundary that apply. Matching uses React Router's
438
+ `matchRoutes`, so the result agrees with the app. The matched routes are
439
+ highlighted in the tree.
440
+ - **Open a route in your app.** Route details have an **Open in app** button.
441
+ For routes with params, it's a short form with a field for each param
442
+ (optional params can be left empty, and a catch-all takes the rest of the
443
+ path). The panel remembers the values you used last for each route. A
444
+ button on each tree row does the same, and the URL tester has one for the
445
+ tested URL. The app tab you follow navigates with React Router, without a
446
+ page reload, and Back returns to where it was. If no app tab is connected,
447
+ the URL opens in a new browser tab. URLs are paths in your app; if your
448
+ router uses a `basename`, include it.
449
+ - **Spot route conflicts.** Pages that resolve to the same URL pattern are
450
+ listed at the top, with links to their files.
451
+ - **Catch missing files.** The warnings list at the top, and a `warn` badge in
452
+ the tree, flag:
453
+ - a route with a loader, action, or middleware but no `error` file at or
454
+ above it, so errors it throws show React Router's default error screen;
455
+ - a missing root `not-found` file (a hint when a root `error` file handles
456
+ unknown URLs instead);
457
+ - a loader or action file that doesn't export a function named `loader` or
458
+ `action`, or a middleware file with no default or `middleware` export.
459
+
460
+ Loaders the timeline measures at 500 ms or more get a hint as well.
461
+ Loading files only cover lazy page code, not loader time, so show pending
462
+ UI with `useNavigation()` in a layout instead. Warnings update when route
463
+ files change.
464
+ - **Follow the current page.** The **Current page** card shows the URL your
465
+ app is on, its params, and the page that renders it. The matched routes get
466
+ a green dot in the tree, and the card updates as the app navigates. With
467
+ several app tabs open, pick which one to follow. When a tab closes, it drops
468
+ out of the list.
469
+ - **See loader and action calls.** The **Timeline** tab lists each loader,
470
+ action, and middleware call your app makes, newest first. Middleware
471
+ entries show the time spent in the middleware itself, not counting what
472
+ `next()` runs. Each entry shows the route, URL, start time, duration, and
473
+ outcome: returned data, a redirect with its status and location, a returned
474
+ response, a thrown error, or `next()` for middleware that passed control
475
+ on. Calls of 500 ms or
476
+ more are highlighted. It also shows each `shouldRevalidate` decision, so
477
+ you can see when a loader was skipped. You can filter by call type, text,
478
+ or route. The dev server keeps the last 200 calls, so a panel opened later
479
+ still sees recent history.
480
+ - **Read the generated code.** The **Generated code** page shows
481
+ `virtual:routes`, `virtual:route-manifest`, `virtual:route-helpers`, and
482
+ the route types declaration file exactly as the plugin generates them, with
483
+ line numbers, size, simple highlighting, and a copy button. For
484
+ `virtual:routes`, switch between the dev server version, which includes the
485
+ DevTools client, and the build version. The page refreshes when route files
486
+ change, and a module that can't be generated (for example during a route
487
+ conflict) shows its error.
488
+ - **Inspect a call.** Expand a timeline entry to see its route params, the
489
+ request, and for actions the submitted form fields or body. Returned data
490
+ appears as a collapsible JSON tree with a copy button. Responses show their
491
+ status, headers, and body, and errors show their name, message, and stack.
492
+ `shouldRevalidate` entries show the URL being left and the next URL, the
493
+ form method, and the action status.
494
+
495
+ The panel updates on its own when route files are added, removed, or renamed.
496
+ The dev server pushes changes over a Server-Sent Events stream instead of the
497
+ panel polling. If the dev server restarts, the panel reconnects by itself. The
498
+ refresh button reloads the data by hand.
499
+
500
+ The current page and the timeline come from a small client that
501
+ `virtual:routes` includes on the dev server only. It reports the app's URL and
502
+ wraps the generated loader, action, middleware, and `shouldRevalidate`
503
+ handlers. It sends these over Vite's HMR connection, and receives the panel's
504
+ **Open in app** requests over it. Handler results pass through unchanged. The
505
+ panel gets a capped copy of each call: up to 8 KB of returned data, and up to
506
+ 4 KB each for bodies and stacks. Circular references, `Map`, `Set`, and
507
+ `bigint` values are converted so serialization doesn't fail. Request and
508
+ response bodies are read from clones, so your handlers and React Router can
509
+ still read them. It tracks navigation by wrapping `history.pushState` and
510
+ `history.replaceState` and listening for `popstate`, and it also supports hash
511
+ URLs (`#/path`). Production builds include none of this code.
512
+
513
+ ### Build-time route warnings
514
+
515
+ `vite build` prints the same missing-file warnings the DevTools panel shows,
516
+ once per build, grouped by route:
517
+
518
+ ```text
519
+ vite-file-router found 2 route warnings:
520
+
521
+ /users
522
+ - This route has a loader but no error file at or above it, so errors it throws show React Router's default error screen. [no-error-boundary]
523
+ src/pages/users/loader.ts
524
+ - This loader file doesn't export a function named "loader", so the route loads no data. [missing-export]
525
+ src/pages/users/loader.ts
526
+ ```
527
+
528
+ Hints, such as a root `error` file handling unknown URLs in place of a
529
+ `not-found` page, only appear in the panel. The dev server doesn't print
530
+ warnings either; the panel shows them there.
531
+
532
+ Use the `warnings` option to change this:
533
+
534
+ ```ts
535
+ routes({ warnings: "error" }); // fail the build, for example in CI
536
+ routes({ warnings: false }); // don't check
537
+ routes({
538
+ warnings: { level: "error", ignore: ["no-not-found"] },
539
+ });
540
+ ```
541
+
542
+ The warning kinds are `no-error-boundary`, `no-not-found`, and
543
+ `missing-export`.
544
+
545
+ ### Typed route helpers
546
+
547
+ Import `routePath` from `virtual:route-helpers` to build paths for discovered
548
+ pages. The helper is compatible with React Router's `Link`, `NavLink`,
549
+ `navigate`, and `redirect` APIs. Dynamic parameter names and required values are
550
+ checked from the path literal; values are URL-encoded. Splat values use the
551
+ React Router `"*"` parameter and are encoded segment-by-segment so `/`
552
+ separators are preserved:
553
+
554
+ ```tsx
555
+ import { Link } from "react-router";
556
+ import { routePath } from "virtual:route-helpers";
557
+
558
+ export function UserLink({ userId }: { userId: string }) {
559
+ return (
560
+ <Link to={routePath("/users/:userId", { userId })}>
561
+ View user
562
+ </Link>
563
+ );
564
+ }
565
+
566
+ const docsPath = routePath("/docs/*", { "*": "guides/getting-started" });
567
+ const localizedDocsPath = routePath("/docs/:lang?", { lang: "en" });
568
+ const defaultDocsPath = routePath("/docs/:lang?");
569
+ ```
570
+
571
+ Static routes and routes with only optional parameters need no parameter object.
572
+ An optional parameter can also be omitted from the params object; its URL
573
+ segment is removed. Unknown route patterns and missing required or extra
574
+ parameters are rejected at runtime. Routes whose optional segment overlaps
575
+ another page's URL pattern are reported as conflicts. The generated
576
+ `routePatterns` export contains the discovered page paths.
577
+
578
+ ### Generated route types
579
+
580
+ In TypeScript projects, the plugin writes `src/route-types.d.ts` when the dev
581
+ server starts and at the start of each build. It counts a project as
582
+ TypeScript when the Vite project root has a `tsconfig.json` or
583
+ `jsconfig.json`. JavaScript projects without either file get no extra file.
584
+
585
+ The plugin rewrites the file whenever route files are added, removed, or
586
+ renamed, and leaves it alone when nothing changed. The file lists every
587
+ discovered page pattern and loader module. With it in your TypeScript project,
588
+ `routePath`, `routePatterns`, and the hooks below only accept routes that
589
+ exist, and your editor autocompletes them:
590
+
591
+ ```ts
592
+ routePath("/users/:userId", { userId }); // OK
593
+ routePath("/usres/:userId", { userId }); // Type error: unknown route pattern
594
+ ```
595
+
596
+ The `dts` option controls this. It defaults to `"auto"`, which is the behavior
597
+ above. A path, relative to the Vite project root and ending in `.d.ts`, always
598
+ writes the file there, even without a TypeScript config. `false` never writes
599
+ it:
600
+
601
+ ```ts
602
+ plugins: [routes({ dts: "types/route-types.d.ts" })];
603
+ plugins: [routes({ dts: false })];
604
+ ```
605
+
606
+ If your `tsconfig.json` isn't in the Vite project root, for example when Vite's
607
+ `root` points at a subfolder, set an explicit path.
608
+
609
+ Without the file, route patterns are typed as `string` and loader data as
610
+ `unknown`, the same as before. You can commit the file so types work before
611
+ the first dev run, or add it to `.gitignore`.
612
+
613
+ ### Typed params and loader data
614
+
615
+ `virtual:route-helpers` also exports two hooks that wrap React Router's hooks
616
+ with types taken from the route tree:
617
+
618
+ - `useRouteParams(pattern)` returns `useParams()`, typed for that pattern.
619
+ Required params are `string`, optional params are `string | undefined`, and
620
+ a splat is a `string` under the `"*"` key.
621
+ - `useRouteData(key)` returns `useRouteLoaderData()`, typed from the loader
622
+ module. The key is either a page pattern, which reads the loader in that
623
+ page's directory, or a loader route ID such as
624
+ `"file-route:src/pages/users/[userId]"`, which can read any matched loader,
625
+ including a parent's. It returns `undefined` when that route isn't matched,
626
+ just like React Router's hook.
627
+
628
+ ```tsx
629
+ // src/pages/users/[userId]/loader.ts
630
+ import type { LoaderFunctionArgs } from "react-router";
631
+
632
+ export async function loader({ params }: LoaderFunctionArgs) {
633
+ return { name: await getUserName(params.userId!) };
634
+ }
635
+
636
+ // src/pages/users/[userId]/page.tsx
637
+ import { useRouteData, useRouteParams } from "virtual:route-helpers";
638
+
639
+ export default function UserPage() {
640
+ const { userId } = useRouteParams("/users/:userId"); // { userId: string }
641
+ const data = useRouteData("/users/:userId"); // { name: string } | undefined
642
+ return <h1>{data?.name ?? userId}</h1>;
643
+ }
644
+ ```
645
+
646
+ The `RouteParams<"/users/:userId">`, `RouteLoaderData<key>`, `RoutePattern`,
647
+ and `RouteLoaderKey` types are exported from `@troyio/vite-file-router` for
648
+ your own helpers. Loader data is typed the same way as React Router's
649
+ `useRouteLoaderData<typeof loader>()`. JavaScript loader modules are typed as
650
+ `any`. Both hooks throw if they're passed a pattern or key the plugin didn't
651
+ discover.
652
+
653
+ ### Route preloading
654
+
655
+ `virtual:routes` exports `preloadRoute(to, basename?)`. It matches a URL
656
+ against the route tree and imports the page, layout, and loader modules of
657
+ every matched route, so the next navigation there doesn't wait for those
658
+ downloads. Error and action modules are left out, since they're only needed
659
+ after an error or a form submission. Importing modules doesn't run loaders.
660
+ Each module downloads once, however often you call it. The promise resolves to
661
+ `false` when no route matches the URL.
662
+
663
+ Call it when a user is likely to navigate, such as on link hover or focus:
664
+
665
+ ```tsx
666
+ import { Link, type LinkProps } from "react-router";
667
+ import { preloadRoute } from "virtual:routes";
668
+
669
+ export function PreloadLink(props: LinkProps & { to: string }) {
670
+ const preload = () => void preloadRoute(props.to);
671
+ return <Link {...props} onMouseEnter={preload} onFocus={preload} />;
672
+ }
673
+ ```
674
+
675
+ If the router uses a `basename`, pass it as the second argument. `to` also
676
+ accepts a partial location such as `{ pathname: "/users/42" }`.
677
+
678
+ ### TypeScript setup
679
+
680
+ Add Vite's client types and the package's virtual-module declaration to your
681
+ application TypeScript project. For example, create `src/vite-env.d.ts`:
682
+
683
+ ```ts
684
+ /// <reference types="vite/client" />
685
+ /// <reference types="@troyio/vite-file-router" />
686
+ ```
687
+
688
+ If `tsconfig.json` sets `compilerOptions.types`, include both `"vite/client"`
689
+ and `"@troyio/vite-file-router"` in that list. This lets TypeScript resolve
690
+ `virtual:routes`. Make sure the generated `src/route-types.d.ts`, or the path
691
+ you set with `dts`, is included by your `tsconfig.json`. Restart your editor's
692
+ TypeScript server if the declarations aren't picked up immediately.
693
+
694
+ ## Build
695
+
696
+ ```sh
697
+ npm run build
698
+ ```
699
+
700
+ ## Test
701
+
702
+ ```sh
703
+ npm test
704
+ ```