@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 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 generateLayoutImport(file, projectRoot) {
42
- return `async () => ({
43
- Component: withOutlet((await import(${JSON.stringify(getImportPath(file, projectRoot))})).default)
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 = ["createElement"];
100
- if (context.hasBoundaries || context.hasLayouts) reactImports.push("lazy");
101
- if (context.hasBoundaries) reactImports.push("Suspense");
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
@@ -5,6 +5,8 @@ export type Node = {
5
5
  page?: string;
6
6
  layout?: string;
7
7
  loading?: string;
8
+ notFound?: string;
9
+ error?: string;
8
10
  children: Node[];
9
11
  };
10
12
  export declare function scan(directory: string): Node;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@troyio/vite-file-router",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "File-system routing plugin for Vite",
5
5
  "type": "module",
6
6
  "private": false,