@troyio/vite-file-router 0.2.4 → 0.2.5
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 +34 -0
- package/dist/index.js +43 -12
- package/dist/scanner.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ nested route, and the following files are recognized:
|
|
|
36
36
|
| `page.jsx` / `page.tsx` | The page component for the directory's URL. |
|
|
37
37
|
| `layout.jsx` / `layout.tsx` | A layout around the directory's page and child routes. |
|
|
38
38
|
| `loading.jsx` / `loading.tsx` | A Suspense fallback for the directory's page and child routes. |
|
|
39
|
+
| `not-found.jsx` / `not-found.tsx` | A not-found page for unmatched URLs within the directory's route subtree. |
|
|
40
|
+
| `error.jsx` / `error.tsx` | A React Router error boundary for the directory's route subtree. |
|
|
39
41
|
|
|
40
42
|
Each file must default-export a React component. If both extensions exist for
|
|
41
43
|
the same file type in one directory, `.tsx` takes precedence.
|
|
@@ -95,6 +97,38 @@ export default function Loading() {
|
|
|
95
97
|
|
|
96
98
|
A nested loading file creates a more specific boundary for that directory.
|
|
97
99
|
|
|
100
|
+
### Not-found pages
|
|
101
|
+
|
|
102
|
+
Add a `not-found.jsx` or `not-found.tsx` file to handle unmatched URLs. The plugin generates
|
|
103
|
+
a catch-all child route, and the most specific matching route subtree's
|
|
104
|
+
not-found page is used:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
src/pages/
|
|
108
|
+
not-found.tsx
|
|
109
|
+
users/
|
|
110
|
+
not-found.tsx
|
|
111
|
+
page.tsx
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The root not-found page handles unknown URLs generally, while `users/not-found.tsx`
|
|
115
|
+
handles unmatched URLs under `/users`.
|
|
116
|
+
|
|
117
|
+
### Route error boundaries
|
|
118
|
+
|
|
119
|
+
Add an `error.jsx` or `error.tsx` file to handle errors from that route and its
|
|
120
|
+
descendants. The nearest route boundary is used, following React Router's error
|
|
121
|
+
handling behavior. Use React Router's `useRouteError` hook to read the error:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useRouteError } from "react-router";
|
|
125
|
+
|
|
126
|
+
export default function ErrorPage() {
|
|
127
|
+
const error = useRouteError();
|
|
128
|
+
return <main>Something went wrong: {String(error)}</main>;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
98
132
|
### Dynamic routes and route groups
|
|
99
133
|
|
|
100
134
|
Two pages must not resolve to the same URL. This includes pages with the same
|
package/dist/index.js
CHANGED
|
@@ -33,15 +33,20 @@ function findPageRouteConflicts(node, pages = /* @__PURE__ */ new Map(), segment
|
|
|
33
33
|
files
|
|
34
34
|
}));
|
|
35
35
|
}
|
|
36
|
+
function validateRouteGroupNotFound(node, segments = [], root = true) {
|
|
37
|
+
const routeSegments = root || node.routeGroup ? segments : [...segments, getRoutePath(node.directory)];
|
|
38
|
+
if (node.routeGroup && node.notFound && routeSegments.length === 0) throw new Error(`Route-group not-found page "${node.notFound}" has no URL subtree to match. Move it into a directory with a URL segment so it is scoped to a distinct route subtree.`);
|
|
39
|
+
for (const child of node.children) validateRouteGroupNotFound(child, routeSegments, false);
|
|
40
|
+
}
|
|
36
41
|
function generateImport(file, projectRoot) {
|
|
37
42
|
return `async () => ({
|
|
38
43
|
Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
|
|
39
44
|
})`;
|
|
40
45
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
function generateRouteLazy(node, projectRoot, includeLayout) {
|
|
47
|
+
const properties = [];
|
|
48
|
+
if (includeLayout && node.layout) properties.push(`Component: withOutlet((await import(${JSON.stringify(getImportPath(node.layout, projectRoot))})).default)`);
|
|
49
|
+
return `async () => ({ ${properties.join(", ")} })`;
|
|
45
50
|
}
|
|
46
51
|
function generateLazyComponent(file, projectRoot) {
|
|
47
52
|
return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}))`;
|
|
@@ -54,6 +59,10 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
54
59
|
const hasBoundary = inheritedBoundary || Boolean(node.loading);
|
|
55
60
|
if (node.layout) context.hasLayouts = true;
|
|
56
61
|
if (!root && !node.routeGroup) route.path = JSON.stringify(getRoutePath(node.directory));
|
|
62
|
+
if (node.error) {
|
|
63
|
+
context.hasErrorBoundaries = true;
|
|
64
|
+
route.ErrorBoundary = `createErrorBoundary(${generateLazyComponent(node.error, context.projectRoot)})`;
|
|
65
|
+
}
|
|
57
66
|
let loadingComponent;
|
|
58
67
|
if (node.loading) {
|
|
59
68
|
loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
|
|
@@ -65,8 +74,8 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
65
74
|
route.Component = `createBoundary(${loadingComponent}, ${child})`;
|
|
66
75
|
} else if (node.layout) {
|
|
67
76
|
if (hasBoundary) route.Component = generateLazyLayout(node.layout, context.projectRoot);
|
|
68
|
-
else route.lazy = generateLayoutImport(node.layout, context.projectRoot);
|
|
69
77
|
}
|
|
78
|
+
if (node.layout && !hasBoundary) route.lazy = generateRouteLazy(node, context.projectRoot, true);
|
|
70
79
|
const children = [];
|
|
71
80
|
if (node.page) {
|
|
72
81
|
const page = hasBoundary ? {
|
|
@@ -79,10 +88,21 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
79
88
|
children.push(genObjectFromRaw(page));
|
|
80
89
|
}
|
|
81
90
|
for (const child of node.children) children.push(generateNode(child, false, context, hasBoundary));
|
|
91
|
+
if (node.notFound) {
|
|
92
|
+
const notFoundRoute = hasBoundary ? {
|
|
93
|
+
path: JSON.stringify("*"),
|
|
94
|
+
Component: generateLazyComponent(node.notFound, context.projectRoot)
|
|
95
|
+
} : {
|
|
96
|
+
path: JSON.stringify("*"),
|
|
97
|
+
lazy: generateImport(node.notFound, context.projectRoot)
|
|
98
|
+
};
|
|
99
|
+
children.push(genObjectFromRaw(notFoundRoute));
|
|
100
|
+
}
|
|
82
101
|
if (children.length) route.children = genArrayFromRaw(children);
|
|
83
102
|
return genObjectFromRaw(route);
|
|
84
103
|
}
|
|
85
104
|
function generate(node, root = false, projectRoot = process.cwd()) {
|
|
105
|
+
validateRouteGroupNotFound(node, [], root);
|
|
86
106
|
const conflicts = findPageRouteConflicts(node, /* @__PURE__ */ new Map(), [], root);
|
|
87
107
|
if (conflicts.length) throw new RouteConflictError(conflicts);
|
|
88
108
|
const context = {
|
|
@@ -90,15 +110,17 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
90
110
|
loadingImports: [],
|
|
91
111
|
nextLoadingId: 0,
|
|
92
112
|
hasBoundaries: false,
|
|
93
|
-
hasLayouts: false
|
|
113
|
+
hasLayouts: false,
|
|
114
|
+
hasErrorBoundaries: false
|
|
94
115
|
};
|
|
95
116
|
const routes = generateNode(node, root, context, false);
|
|
96
117
|
const imports = [...context.loadingImports];
|
|
97
118
|
const helpers = [];
|
|
98
|
-
if (context.hasBoundaries || context.hasLayouts) {
|
|
99
|
-
const reactImports = [
|
|
100
|
-
if (context.hasBoundaries || context.hasLayouts) reactImports.push("
|
|
101
|
-
if (context.hasBoundaries) reactImports.push("
|
|
119
|
+
if (context.hasBoundaries || context.hasLayouts || context.hasErrorBoundaries) {
|
|
120
|
+
const reactImports = [];
|
|
121
|
+
if (context.hasBoundaries || context.hasLayouts || context.hasErrorBoundaries) reactImports.push("createElement");
|
|
122
|
+
if (context.hasBoundaries || context.hasLayouts || context.hasErrorBoundaries) reactImports.push("lazy");
|
|
123
|
+
if (context.hasBoundaries || context.hasErrorBoundaries) reactImports.push("Suspense");
|
|
102
124
|
imports.unshift(`import { ${reactImports.join(", ")} } from "react";`);
|
|
103
125
|
if (context.hasBoundaries || context.hasLayouts) imports.push(`import { Outlet } from "react-router";`);
|
|
104
126
|
}
|
|
@@ -115,6 +137,15 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
115
137
|
return function LayoutRoute() {
|
|
116
138
|
return createElement(Layout, null, createElement(Outlet));
|
|
117
139
|
};
|
|
140
|
+
}`);
|
|
141
|
+
if (context.hasErrorBoundaries) helpers.push(`function createErrorBoundary(Component) {
|
|
142
|
+
return function RouteErrorBoundary() {
|
|
143
|
+
return createElement(
|
|
144
|
+
Suspense,
|
|
145
|
+
{ fallback: createElement("p", null, "Loading error page...") },
|
|
146
|
+
createElement(Component),
|
|
147
|
+
);
|
|
148
|
+
};
|
|
118
149
|
}`);
|
|
119
150
|
return {
|
|
120
151
|
imports,
|
|
@@ -138,10 +169,10 @@ function scan(directory) {
|
|
|
138
169
|
node.children.push(scan(file));
|
|
139
170
|
continue;
|
|
140
171
|
}
|
|
141
|
-
const match = /^(page|layout|loading)\.(jsx|tsx)$/.exec(entry.name);
|
|
172
|
+
const match = /^(page|layout|loading|not-found|error)\.(jsx|tsx)$/.exec(entry.name);
|
|
142
173
|
if (!match) continue;
|
|
143
174
|
const [, name, extension] = match;
|
|
144
|
-
const key = name;
|
|
175
|
+
const key = name === "not-found" ? "notFound" : name;
|
|
145
176
|
if (!node[key] || extension === "tsx") node[key] = file;
|
|
146
177
|
}
|
|
147
178
|
return node;
|
package/dist/scanner.d.ts
CHANGED