@troyio/vite-file-router 0.2.3 → 0.2.4
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 +82 -29
- package/dist/index.js +27 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,57 +28,110 @@ project root to use another directory:
|
|
|
28
28
|
plugins: [routes("src/routes")];
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
The router scans the pages directory recursively. Each directory becomes a
|
|
32
|
+
nested route, and the following files are recognized:
|
|
33
|
+
|
|
34
|
+
| File | Purpose |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `page.jsx` / `page.tsx` | The page component for the directory's URL. |
|
|
37
|
+
| `layout.jsx` / `layout.tsx` | A layout around the directory's page and child routes. |
|
|
38
|
+
| `loading.jsx` / `loading.tsx` | A Suspense fallback for the directory's page and child routes. |
|
|
39
|
+
|
|
40
|
+
Each file must default-export a React component. If both extensions exist for
|
|
41
|
+
the same file type in one directory, `.tsx` takes precedence.
|
|
42
|
+
|
|
43
|
+
For example, this structure creates a home page, user pages, and two auth
|
|
44
|
+
pages:
|
|
37
45
|
|
|
38
46
|
```text
|
|
39
47
|
src/pages/
|
|
40
|
-
layout.jsx
|
|
41
|
-
loading.jsx
|
|
42
48
|
page.jsx
|
|
49
|
+
layout.tsx
|
|
50
|
+
loading.tsx
|
|
43
51
|
users/
|
|
44
|
-
loading.tsx
|
|
45
52
|
page.tsx
|
|
46
53
|
[id]/
|
|
47
54
|
page.tsx
|
|
55
|
+
(auth)/
|
|
56
|
+
layout.tsx
|
|
57
|
+
sign-in/
|
|
58
|
+
page.tsx
|
|
59
|
+
sign-up/
|
|
60
|
+
page.tsx
|
|
61
|
+
[...slug]/
|
|
62
|
+
page.tsx
|
|
48
63
|
```
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
files
|
|
54
|
-
|
|
55
|
-
diagnostic page listing the conflicting files and explaining how to fix them.
|
|
56
|
-
Production builds fail with the same diagnostic until the conflicts are fixed.
|
|
65
|
+
This maps to `/`, `/users`, `/users/:id`, `/sign-in`, `/sign-up`, and `/*`.
|
|
66
|
+
Square-bracket segments are dynamic parameters: `[id]` becomes `:id`.
|
|
67
|
+
Catch-all segments such as `[...slug]` become `*`. Parenthesized directories
|
|
68
|
+
are route groups; they organize files and can have their own page, layout, and
|
|
69
|
+
loading files, but their names do not add a URL segment.
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
your application:
|
|
71
|
+
### Layouts
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
The plugin adapts React Router's matched child route to a `children` prop. A
|
|
74
|
+
layout can render that prop directly and does not need to import or render
|
|
75
|
+
`Outlet`:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import type { PropsWithChildren } from "react";
|
|
79
|
+
|
|
80
|
+
export default function Layout({ children }: PropsWithChildren) {
|
|
81
|
+
return <div className="app-shell">{children}</div>;
|
|
82
|
+
}
|
|
63
83
|
```
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
85
|
+
### Loading fallbacks
|
|
86
|
+
|
|
87
|
+
Add a `loading.jsx` or `loading.tsx` beside a page or layout to show a Suspense
|
|
88
|
+
fallback while that route subtree loads:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
export default function Loading() {
|
|
92
|
+
return <p>Loading...</p>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A nested loading file creates a more specific boundary for that directory.
|
|
97
|
+
|
|
98
|
+
### Dynamic routes and route groups
|
|
99
|
+
|
|
100
|
+
Two pages must not resolve to the same URL. This includes pages with the same
|
|
101
|
+
path in separate route groups, such as `(auth)/sign-in/page.tsx` and
|
|
102
|
+
`(guest)/sign-in/page.tsx`. During development, the router displays a diagnostic
|
|
103
|
+
page listing the conflicting files; production builds fail with the same
|
|
104
|
+
diagnostic until the conflict is fixed.
|
|
105
|
+
|
|
106
|
+
## Use the generated routes
|
|
107
|
+
|
|
108
|
+
Import `routes` from the virtual module and pass it to React Router:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { createBrowserRouter, RouterProvider } from "react-router";
|
|
112
|
+
import { routes } from "virtual:routes";
|
|
113
|
+
|
|
114
|
+
const router = createBrowserRouter(routes);
|
|
115
|
+
|
|
116
|
+
export default function App() {
|
|
117
|
+
return <RouterProvider router={router} />;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
67
120
|
|
|
68
|
-
### TypeScript
|
|
121
|
+
### TypeScript setup
|
|
69
122
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`src/vite-env.d.ts`:
|
|
123
|
+
Add Vite's client types and the package's virtual-module declaration to your
|
|
124
|
+
application TypeScript project. For example, create `src/vite-env.d.ts`:
|
|
73
125
|
|
|
74
126
|
```ts
|
|
75
127
|
/// <reference types="vite/client" />
|
|
76
128
|
/// <reference types="@troyio/vite-file-router" />
|
|
77
129
|
```
|
|
78
130
|
|
|
79
|
-
If
|
|
80
|
-
|
|
81
|
-
|
|
131
|
+
If `tsconfig.json` sets `compilerOptions.types`, include both `"vite/client"`
|
|
132
|
+
and `"@troyio/vite-file-router"` in that list. This lets TypeScript resolve
|
|
133
|
+
`virtual:routes`. Restart your editor's TypeScript server if the declaration
|
|
134
|
+
isn't picked up immediately.
|
|
82
135
|
|
|
83
136
|
## Build
|
|
84
137
|
|
package/dist/index.js
CHANGED
|
@@ -38,12 +38,21 @@ function generateImport(file, projectRoot) {
|
|
|
38
38
|
Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
|
|
39
39
|
})`;
|
|
40
40
|
}
|
|
41
|
+
function generateLayoutImport(file, projectRoot) {
|
|
42
|
+
return `async () => ({
|
|
43
|
+
Component: withOutlet((await import(${JSON.stringify(getImportPath(file, projectRoot))})).default)
|
|
44
|
+
})`;
|
|
45
|
+
}
|
|
41
46
|
function generateLazyComponent(file, projectRoot) {
|
|
42
47
|
return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}))`;
|
|
43
48
|
}
|
|
49
|
+
function generateLazyLayout(file, projectRoot) {
|
|
50
|
+
return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}).then(({ default: Layout }) => ({ default: withOutlet(Layout) })))`;
|
|
51
|
+
}
|
|
44
52
|
function generateNode(node, root, context, inheritedBoundary) {
|
|
45
53
|
const route = {};
|
|
46
54
|
const hasBoundary = inheritedBoundary || Boolean(node.loading);
|
|
55
|
+
if (node.layout) context.hasLayouts = true;
|
|
47
56
|
if (!root && !node.routeGroup) route.path = JSON.stringify(getRoutePath(node.directory));
|
|
48
57
|
let loadingComponent;
|
|
49
58
|
if (node.loading) {
|
|
@@ -52,11 +61,11 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
52
61
|
context.hasBoundaries = true;
|
|
53
62
|
}
|
|
54
63
|
if (loadingComponent) {
|
|
55
|
-
const child = node.layout ?
|
|
64
|
+
const child = node.layout ? generateLazyLayout(node.layout, context.projectRoot) : "Outlet";
|
|
56
65
|
route.Component = `createBoundary(${loadingComponent}, ${child})`;
|
|
57
66
|
} else if (node.layout) {
|
|
58
|
-
if (hasBoundary) route.Component =
|
|
59
|
-
else route.lazy =
|
|
67
|
+
if (hasBoundary) route.Component = generateLazyLayout(node.layout, context.projectRoot);
|
|
68
|
+
else route.lazy = generateLayoutImport(node.layout, context.projectRoot);
|
|
60
69
|
}
|
|
61
70
|
const children = [];
|
|
62
71
|
if (node.page) {
|
|
@@ -80,14 +89,20 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
80
89
|
projectRoot,
|
|
81
90
|
loadingImports: [],
|
|
82
91
|
nextLoadingId: 0,
|
|
83
|
-
hasBoundaries: false
|
|
92
|
+
hasBoundaries: false,
|
|
93
|
+
hasLayouts: false
|
|
84
94
|
};
|
|
85
95
|
const routes = generateNode(node, root, context, false);
|
|
86
96
|
const imports = [...context.loadingImports];
|
|
87
97
|
const helpers = [];
|
|
88
|
-
if (context.hasBoundaries) {
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
if (context.hasBoundaries || context.hasLayouts) {
|
|
99
|
+
const reactImports = ["createElement"];
|
|
100
|
+
if (context.hasBoundaries || context.hasLayouts) reactImports.push("lazy");
|
|
101
|
+
if (context.hasBoundaries) reactImports.push("Suspense");
|
|
102
|
+
imports.unshift(`import { ${reactImports.join(", ")} } from "react";`);
|
|
103
|
+
if (context.hasBoundaries || context.hasLayouts) imports.push(`import { Outlet } from "react-router";`);
|
|
104
|
+
}
|
|
105
|
+
if (context.hasBoundaries) helpers.push(`function createBoundary(Fallback, Component) {
|
|
91
106
|
return function RouteBoundary() {
|
|
92
107
|
return createElement(
|
|
93
108
|
Suspense,
|
|
@@ -96,7 +111,11 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
96
111
|
);
|
|
97
112
|
};
|
|
98
113
|
}`);
|
|
99
|
-
|
|
114
|
+
if (context.hasLayouts) helpers.push(`function withOutlet(Layout) {
|
|
115
|
+
return function LayoutRoute() {
|
|
116
|
+
return createElement(Layout, null, createElement(Outlet));
|
|
117
|
+
};
|
|
118
|
+
}`);
|
|
100
119
|
return {
|
|
101
120
|
imports,
|
|
102
121
|
helpers,
|