@troyio/vite-file-router 0.2.2 → 0.2.3
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 +6 -1
- package/dist/generator.d.ts +9 -0
- package/dist/index.js +62 -9
- package/dist/scanner.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,12 @@ src/pages/
|
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Dynamic segments such as `[id]` become `:id`; catch-all segments such as
|
|
51
|
-
`[...slug]` become `*`.
|
|
51
|
+
`[...slug]` become `*`. Parenthesized directories are route groups and do not
|
|
52
|
+
add a URL segment. Route groups can contain `page`, `layout`, and `loading`
|
|
53
|
+
files just like other directories. For example, `(auth)/sign-in/page.tsx` maps
|
|
54
|
+
to `/sign-in`. If two pages resolve to the same URL, the dev server displays a
|
|
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.
|
|
52
57
|
|
|
53
58
|
Import the generated route configuration from the `virtual:routes` module in
|
|
54
59
|
your application:
|
package/dist/generator.d.ts
CHANGED
|
@@ -4,4 +4,13 @@ export type GeneratedRoutes = {
|
|
|
4
4
|
helpers: string[];
|
|
5
5
|
routes: string;
|
|
6
6
|
};
|
|
7
|
+
export type RouteConflict = {
|
|
8
|
+
routePath: string;
|
|
9
|
+
files: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare class RouteConflictError extends Error {
|
|
12
|
+
readonly conflicts: RouteConflict[];
|
|
13
|
+
constructor(conflicts: RouteConflict[]);
|
|
14
|
+
}
|
|
15
|
+
export declare function findPageRouteConflicts(node: Node, pages?: Map<string, string[]>, segments?: string[], root?: boolean): RouteConflict[];
|
|
7
16
|
export declare function generate(node: Node, root?: boolean, projectRoot?: string): GeneratedRoutes;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,15 @@ import { genArrayFromRaw, genObjectFromRaw, genSafeVariableName } from "knitwork
|
|
|
3
3
|
import { format } from "prettier";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
// src/generator.ts
|
|
6
|
+
var RouteConflictError = class extends Error {
|
|
7
|
+
conflicts;
|
|
8
|
+
constructor(conflicts) {
|
|
9
|
+
const details = conflicts.map(({ routePath, files }) => ` ${routePath}\n${files.map((file) => ` - ${file}`).join("\n")}`).join("\n");
|
|
10
|
+
super(`Multiple page files resolve to the same route:\n${details}\n\nGive each page a unique URL path. Parenthesized route-group names are omitted from URLs, so pages in different groups can still conflict.`);
|
|
11
|
+
this.conflicts = conflicts;
|
|
12
|
+
this.name = "RouteConflictError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
6
15
|
function getImportPath(file, projectRoot) {
|
|
7
16
|
const relativePath = path.relative(projectRoot, file).split(path.sep).join("/");
|
|
8
17
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
@@ -12,6 +21,18 @@ function getRoutePath(directory) {
|
|
|
12
21
|
if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
|
|
13
22
|
return segment.replace(/^\[(.+)\]$/, ":$1");
|
|
14
23
|
}
|
|
24
|
+
function findPageRouteConflicts(node, pages = /* @__PURE__ */ new Map(), segments = [], root = true) {
|
|
25
|
+
const routeSegments = root || node.routeGroup ? segments : [...segments, getRoutePath(node.directory)];
|
|
26
|
+
if (node.page) {
|
|
27
|
+
const routePath = `/${routeSegments.map((segment) => segment.startsWith(":") ? ":" : segment).join("/")}`;
|
|
28
|
+
pages.set(routePath, [...pages.get(routePath) ?? [], node.page]);
|
|
29
|
+
}
|
|
30
|
+
for (const child of node.children) findPageRouteConflicts(child, pages, routeSegments, false);
|
|
31
|
+
return [...pages.entries()].filter(([, files]) => files.length > 1).map(([routePath, files]) => ({
|
|
32
|
+
routePath,
|
|
33
|
+
files
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
15
36
|
function generateImport(file, projectRoot) {
|
|
16
37
|
return `async () => ({
|
|
17
38
|
Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
|
|
@@ -23,7 +44,7 @@ function generateLazyComponent(file, projectRoot) {
|
|
|
23
44
|
function generateNode(node, root, context, inheritedBoundary) {
|
|
24
45
|
const route = {};
|
|
25
46
|
const hasBoundary = inheritedBoundary || Boolean(node.loading);
|
|
26
|
-
if (!root) route.path = JSON.stringify(getRoutePath(node.directory));
|
|
47
|
+
if (!root && !node.routeGroup) route.path = JSON.stringify(getRoutePath(node.directory));
|
|
27
48
|
let loadingComponent;
|
|
28
49
|
if (node.loading) {
|
|
29
50
|
loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
|
|
@@ -53,6 +74,8 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
53
74
|
return genObjectFromRaw(route);
|
|
54
75
|
}
|
|
55
76
|
function generate(node, root = false, projectRoot = process.cwd()) {
|
|
77
|
+
const conflicts = findPageRouteConflicts(node, /* @__PURE__ */ new Map(), [], root);
|
|
78
|
+
if (conflicts.length) throw new RouteConflictError(conflicts);
|
|
56
79
|
const context = {
|
|
57
80
|
projectRoot,
|
|
58
81
|
loadingImports: [],
|
|
@@ -85,6 +108,7 @@ function scan(directory) {
|
|
|
85
108
|
const node = {
|
|
86
109
|
name: path.basename(directory),
|
|
87
110
|
directory,
|
|
111
|
+
routeGroup: /^\(.+\)$/.test(path.basename(directory)),
|
|
88
112
|
children: []
|
|
89
113
|
};
|
|
90
114
|
if (!fs.existsSync(directory)) return node;
|
|
@@ -109,16 +133,44 @@ var RESOLVED_ID = `\0${MODULE_ID}`;
|
|
|
109
133
|
function routes(directory = "src/pages") {
|
|
110
134
|
let projectRoot = "";
|
|
111
135
|
let root = "";
|
|
136
|
+
let dev = false;
|
|
137
|
+
function sortImports(imports) {
|
|
138
|
+
return imports.sort((left, right) => {
|
|
139
|
+
const leftSource = left.match(/from ["'](.+?)["']/)?.[1] ?? left;
|
|
140
|
+
const rightSource = right.match(/from ["'](.+?)["']/)?.[1] ?? right;
|
|
141
|
+
const leftIsRelative = leftSource.startsWith(".");
|
|
142
|
+
if (leftIsRelative !== rightSource.startsWith(".")) return leftIsRelative ? 1 : -1;
|
|
143
|
+
return leftSource.localeCompare(rightSource);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
112
146
|
async function execute() {
|
|
113
|
-
const
|
|
147
|
+
const tree = scan(root);
|
|
148
|
+
const conflicts = findPageRouteConflicts(tree);
|
|
149
|
+
if (conflicts.length) {
|
|
150
|
+
const error = new RouteConflictError(conflicts);
|
|
151
|
+
if (!dev) throw error;
|
|
152
|
+
const code = `
|
|
153
|
+
import { createElement } from "react";
|
|
154
|
+
|
|
155
|
+
const message = ${JSON.stringify(error.message)};
|
|
156
|
+
|
|
157
|
+
function RouteConflictPage() {
|
|
158
|
+
return createElement(
|
|
159
|
+
"main",
|
|
160
|
+
{ style: { fontFamily: "system-ui, sans-serif", margin: "3rem auto", maxWidth: "60rem", padding: "0 1.5rem" } },
|
|
161
|
+
createElement("h1", null, "Conflicting routes"),
|
|
162
|
+
createElement("p", null, "The file-based router found page files that resolve to the same URL. Fix these conflicts, then the page will reload."),
|
|
163
|
+
createElement("pre", { style: { background: "#f3f4f6", overflowX: "auto", padding: "1rem", whiteSpace: "pre-wrap" } }, message),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const routes = [{ path: "*", Component: RouteConflictPage }];
|
|
168
|
+
`;
|
|
169
|
+
return format(code, { parser: "typescript" });
|
|
170
|
+
}
|
|
171
|
+
const generated = generate(tree, true, projectRoot);
|
|
114
172
|
const sections = [
|
|
115
|
-
generated.imports.
|
|
116
|
-
const leftSource = left.match(/from ["'](.+?)["']/)?.[1] ?? left;
|
|
117
|
-
const rightSource = right.match(/from ["'](.+?)["']/)?.[1] ?? right;
|
|
118
|
-
const leftIsRelative = leftSource.startsWith(".");
|
|
119
|
-
if (leftIsRelative !== rightSource.startsWith(".")) return leftIsRelative ? 1 : -1;
|
|
120
|
-
return leftSource.localeCompare(rightSource);
|
|
121
|
-
}).join("\n"),
|
|
173
|
+
sortImports(generated.imports).join("\n"),
|
|
122
174
|
generated.helpers.join("\n"),
|
|
123
175
|
`export const routes = ${genArrayFromRaw([generated.routes])};`
|
|
124
176
|
].filter(Boolean);
|
|
@@ -129,6 +181,7 @@ function routes(directory = "src/pages") {
|
|
|
129
181
|
configResolved(config) {
|
|
130
182
|
projectRoot = config.root;
|
|
131
183
|
root = path.resolve(projectRoot, directory);
|
|
184
|
+
dev = config.command === "serve";
|
|
132
185
|
},
|
|
133
186
|
resolveId: (id) => id === MODULE_ID ? RESOLVED_ID : void 0,
|
|
134
187
|
load: (id) => id === RESOLVED_ID ? execute() : void 0,
|
package/dist/scanner.d.ts
CHANGED