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