@troyio/vite-file-router 0.2.5 → 0.2.7
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 +79 -3
- package/dist/generator.d.ts +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +158 -37
- package/dist/scanner.d.ts +15 -1
- package/package.json +1 -1
- package/types/index.d.ts +50 -0
- package/types/virtual-routes.d.ts +15 -0
package/README.md
CHANGED
|
@@ -21,13 +21,36 @@ export default defineConfig({
|
|
|
21
21
|
});
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
By default, the plugin scans `src/pages
|
|
25
|
-
project root to use another
|
|
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:
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
plugins: [routes("src/routes")];
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
You can also configure the directory and special filenames together. Convention
|
|
33
|
+
values are filename stems without `.jsx` or `.tsx`; `.tsx` takes precedence when
|
|
34
|
+
both extensions exist:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
plugins: [
|
|
38
|
+
routes({
|
|
39
|
+
directory: "app/views",
|
|
40
|
+
conventions: {
|
|
41
|
+
page: "route",
|
|
42
|
+
layout: "shell",
|
|
43
|
+
loading: "pending",
|
|
44
|
+
notFound: "missing",
|
|
45
|
+
error: "fault",
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
];
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Omitted convention values keep their defaults: `page`, `layout`, `loading`,
|
|
52
|
+
`not-found`, and `error`. Each convention must have a unique filename stem.
|
|
53
|
+
|
|
31
54
|
The router scans the pages directory recursively. Each directory becomes a
|
|
32
55
|
nested route, and the following files are recognized:
|
|
33
56
|
|
|
@@ -135,7 +158,11 @@ Two pages must not resolve to the same URL. This includes pages with the same
|
|
|
135
158
|
path in separate route groups, such as `(auth)/sign-in/page.tsx` and
|
|
136
159
|
`(guest)/sign-in/page.tsx`. During development, the router displays a diagnostic
|
|
137
160
|
page listing the conflicting files; production builds fail with the same
|
|
138
|
-
diagnostic until the conflict is fixed.
|
|
161
|
+
diagnostic until the conflict is fixed. Dynamic parameter names do not make
|
|
162
|
+
otherwise identical patterns distinct (`users/[id]` and `users/[slug]`
|
|
163
|
+
conflict). Static and dynamic siblings such as `users/new` and `users/[id]`
|
|
164
|
+
are distinct; React Router's normal route ranking selects the static route
|
|
165
|
+
first.
|
|
139
166
|
|
|
140
167
|
## Use the generated routes
|
|
141
168
|
|
|
@@ -152,6 +179,55 @@ export default function App() {
|
|
|
152
179
|
}
|
|
153
180
|
```
|
|
154
181
|
|
|
182
|
+
### Route manifest
|
|
183
|
+
|
|
184
|
+
Import `routeManifest` from `virtual:route-manifest` for a stable, nested
|
|
185
|
+
description of the scanned routes. Source files are relative to the Vite
|
|
186
|
+
project root and use forward slashes. Each node includes its URL `fullPath`,
|
|
187
|
+
the parameters introduced by that segment, and its recognized files:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { routeManifest } from "virtual:route-manifest";
|
|
191
|
+
|
|
192
|
+
console.log(routeManifest.children);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The root node has `fullPath: "/"`; route-group nodes have no URL segment but
|
|
196
|
+
remain in the tree. React Router's generated `routes` remain the runtime source
|
|
197
|
+
of truth.
|
|
198
|
+
|
|
199
|
+
The development server watches the configured directory. Adding, removing,
|
|
200
|
+
renaming, or editing route files invalidates all generated virtual modules and
|
|
201
|
+
reloads the app without requiring a server restart.
|
|
202
|
+
|
|
203
|
+
### Typed route helpers
|
|
204
|
+
|
|
205
|
+
Import `routePath` from `virtual:route-helpers` to build paths for discovered
|
|
206
|
+
pages. The helper is compatible with React Router's `Link`, `NavLink`,
|
|
207
|
+
`navigate`, and `redirect` APIs. Dynamic parameter names and required values are
|
|
208
|
+
checked from the path literal; values are URL-encoded. Splat values use the
|
|
209
|
+
React Router `"*"` parameter and are encoded segment-by-segment so `/`
|
|
210
|
+
separators are preserved:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { Link } from "react-router";
|
|
214
|
+
import { routePath } from "virtual:route-helpers";
|
|
215
|
+
|
|
216
|
+
export function UserLink({ userId }: { userId: string }) {
|
|
217
|
+
return (
|
|
218
|
+
<Link to={routePath("/users/:userId", { userId })}>
|
|
219
|
+
View user
|
|
220
|
+
</Link>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const docsPath = routePath("/docs/*", { "*": "guides/getting-started" });
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Static routes need no parameter object: `routePath("/")`. Unknown route
|
|
228
|
+
patterns and missing or extra parameters are rejected at runtime. The generated
|
|
229
|
+
`routePatterns` export contains the discovered page paths.
|
|
230
|
+
|
|
155
231
|
### TypeScript setup
|
|
156
232
|
|
|
157
233
|
Add Vite's client types and the package's virtual-module declaration to your
|
package/dist/generator.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
-
|
|
2
|
+
import { type RouteConventions } from "./scanner";
|
|
3
|
+
export type RoutesOptions = {
|
|
4
|
+
directory?: string;
|
|
5
|
+
conventions?: RouteConventions;
|
|
6
|
+
};
|
|
7
|
+
export type { RouteConventions, RouteFileKind, RouteManifestNode, } from "./scanner";
|
|
8
|
+
export declare function routes(input?: string | RoutesOptions): Plugin;
|
package/dist/index.js
CHANGED
|
@@ -2,12 +2,72 @@ import path from "node:path";
|
|
|
2
2
|
import { genArrayFromRaw, genObjectFromRaw, genSafeVariableName } from "knitwork";
|
|
3
3
|
import { format } from "prettier";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
// src/scanner.ts
|
|
6
|
+
var DEFAULT_CONVENTIONS = {
|
|
7
|
+
page: "page",
|
|
8
|
+
layout: "layout",
|
|
9
|
+
loading: "loading",
|
|
10
|
+
notFound: "not-found",
|
|
11
|
+
error: "error"
|
|
12
|
+
};
|
|
13
|
+
function getRouteSegment(name) {
|
|
14
|
+
if (name.startsWith("[...") && name.endsWith("]")) return "*";
|
|
15
|
+
return name.replace(/^\[(.+)\]$/, ":$1");
|
|
16
|
+
}
|
|
17
|
+
function collectPageRoutePatterns(node, segments = [], root = true) {
|
|
18
|
+
const routeSegments = root || node.routeGroup ? segments : [...segments, getRouteSegment(node.name)];
|
|
19
|
+
const patterns = node.page ? [`/${routeSegments.join("/")}`] : [];
|
|
20
|
+
for (const child of node.children) patterns.push(...collectPageRoutePatterns(child, routeSegments, false));
|
|
21
|
+
return patterns;
|
|
22
|
+
}
|
|
23
|
+
function scan(directory, conventions = DEFAULT_CONVENTIONS) {
|
|
24
|
+
const node = {
|
|
25
|
+
name: path.basename(directory),
|
|
26
|
+
directory,
|
|
27
|
+
routeGroup: /^\(.+\)$/.test(path.basename(directory)),
|
|
28
|
+
children: []
|
|
29
|
+
};
|
|
30
|
+
if (!fs.existsSync(directory)) return node;
|
|
31
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const file = path.join(directory, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
node.children.push(scan(file, conventions));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const match = /^(.*)\.(jsx|tsx)$/.exec(entry.name);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
const [, basename, extension] = match;
|
|
41
|
+
const kind = Object.keys(conventions).find((candidate) => conventions[candidate] === basename);
|
|
42
|
+
if (!kind) continue;
|
|
43
|
+
if (!node[kind] || extension === "tsx") node[kind] = file;
|
|
44
|
+
}
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
function createRouteManifest(node, projectRoot, parentPath = "", root = true) {
|
|
48
|
+
const segment = root || node.routeGroup ? null : getRouteSegment(node.name);
|
|
49
|
+
const fullPath = segment === null ? parentPath || "/" : `${parentPath}/${segment}` || "/";
|
|
50
|
+
const params = !root && !node.routeGroup && node.name.startsWith("[") ? [node.name.startsWith("[...") ? "*" : node.name.slice(1, -1)] : [];
|
|
51
|
+
const files = {};
|
|
52
|
+
for (const kind of Object.keys(DEFAULT_CONVENTIONS)) {
|
|
53
|
+
const file = node[kind];
|
|
54
|
+
if (file) files[kind] = path.relative(projectRoot, file).split(path.sep).join("/");
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
segment,
|
|
58
|
+
fullPath,
|
|
59
|
+
params,
|
|
60
|
+
routeGroup: node.routeGroup,
|
|
61
|
+
files,
|
|
62
|
+
children: node.children.map((child) => createRouteManifest(child, projectRoot, fullPath === "/" ? "" : fullPath, false))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
5
65
|
// src/generator.ts
|
|
6
66
|
var RouteConflictError = class extends Error {
|
|
7
67
|
conflicts;
|
|
8
68
|
constructor(conflicts) {
|
|
9
69
|
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\
|
|
70
|
+
super(`Multiple page files resolve to the same route pattern:\n${details}\n\nReact Router treats dynamic parameters as equivalent regardless of their names, and parenthesized route-group names are omitted from URLs. Give each page a unique URL pattern.`);
|
|
11
71
|
this.conflicts = conflicts;
|
|
12
72
|
this.name = "RouteConflictError";
|
|
13
73
|
}
|
|
@@ -17,9 +77,7 @@ function getImportPath(file, projectRoot) {
|
|
|
17
77
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
18
78
|
}
|
|
19
79
|
function getRoutePath(directory) {
|
|
20
|
-
|
|
21
|
-
if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
|
|
22
|
-
return segment.replace(/^\[(.+)\]$/, ":$1");
|
|
80
|
+
return getRouteSegment(path.basename(directory));
|
|
23
81
|
}
|
|
24
82
|
function findPageRouteConflicts(node, pages = /* @__PURE__ */ new Map(), segments = [], root = true) {
|
|
25
83
|
const routeSegments = root || node.routeGroup ? segments : [...segments, getRoutePath(node.directory)];
|
|
@@ -153,34 +211,35 @@ function generate(node, root = false, projectRoot = process.cwd()) {
|
|
|
153
211
|
routes
|
|
154
212
|
};
|
|
155
213
|
}
|
|
156
|
-
// src/
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
214
|
+
// src/index.ts
|
|
215
|
+
var ROUTES_MODULE_ID = "virtual:routes";
|
|
216
|
+
var ROUTES_RESOLVED_ID = `\0${ROUTES_MODULE_ID}`;
|
|
217
|
+
var MANIFEST_MODULE_ID = "virtual:route-manifest";
|
|
218
|
+
var MANIFEST_RESOLVED_ID = `\0${MANIFEST_MODULE_ID}`;
|
|
219
|
+
var ROUTE_HELPERS_MODULE_ID = "virtual:route-helpers";
|
|
220
|
+
var ROUTE_HELPERS_RESOLVED_ID = `\0${ROUTE_HELPERS_MODULE_ID}`;
|
|
221
|
+
function resolveOptions(input) {
|
|
222
|
+
const options = typeof input === "string" ? { directory: input } : input;
|
|
223
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("routes() expects a directory string or options object.");
|
|
224
|
+
for (const key of Object.keys(options)) if (key !== "directory" && key !== "conventions") throw new TypeError(`Unknown routes() option "${key}".`);
|
|
225
|
+
const directory = options.directory ?? "src/pages";
|
|
226
|
+
if (typeof directory !== "string" || directory.trim() === "") throw new TypeError("routes() directory must be a non-empty path string.");
|
|
227
|
+
const conventions = { ...DEFAULT_CONVENTIONS };
|
|
228
|
+
if (options.conventions !== void 0 && (!options.conventions || typeof options.conventions !== "object" || Array.isArray(options.conventions))) throw new TypeError("routes() conventions must be an object.");
|
|
229
|
+
for (const [kind, basename] of Object.entries(options.conventions ?? {})) {
|
|
230
|
+
if (!Object.hasOwn(DEFAULT_CONVENTIONS, kind)) throw new TypeError(`Unknown route file convention "${kind}".`);
|
|
231
|
+
if (typeof basename !== "string" || basename.trim() === "" || basename === "." || basename === ".." || /[\\/]/.test(basename) || /\.[^.]+$/.test(basename)) throw new TypeError(`Route file convention "${kind}" must be a filename without an extension.`);
|
|
232
|
+
conventions[kind] = basename;
|
|
233
|
+
}
|
|
234
|
+
const names = Object.values(conventions);
|
|
235
|
+
if (new Set(names).size !== names.length) throw new TypeError("Route file convention names must be unique.");
|
|
236
|
+
return {
|
|
160
237
|
directory,
|
|
161
|
-
|
|
162
|
-
children: []
|
|
238
|
+
conventions
|
|
163
239
|
};
|
|
164
|
-
if (!fs.existsSync(directory)) return node;
|
|
165
|
-
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
const file = path.join(directory, entry.name);
|
|
168
|
-
if (entry.isDirectory()) {
|
|
169
|
-
node.children.push(scan(file));
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const match = /^(page|layout|loading|not-found|error)\.(jsx|tsx)$/.exec(entry.name);
|
|
173
|
-
if (!match) continue;
|
|
174
|
-
const [, name, extension] = match;
|
|
175
|
-
const key = name === "not-found" ? "notFound" : name;
|
|
176
|
-
if (!node[key] || extension === "tsx") node[key] = file;
|
|
177
|
-
}
|
|
178
|
-
return node;
|
|
179
240
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
var RESOLVED_ID = `\0${MODULE_ID}`;
|
|
183
|
-
function routes(directory = "src/pages") {
|
|
241
|
+
function routes(input = "src/pages") {
|
|
242
|
+
const options = resolveOptions(input);
|
|
184
243
|
let projectRoot = "";
|
|
185
244
|
let root = "";
|
|
186
245
|
let dev = false;
|
|
@@ -194,7 +253,7 @@ function routes(directory = "src/pages") {
|
|
|
194
253
|
});
|
|
195
254
|
}
|
|
196
255
|
async function execute() {
|
|
197
|
-
const tree = scan(root);
|
|
256
|
+
const tree = scan(root, options.conventions);
|
|
198
257
|
const conflicts = findPageRouteConflicts(tree);
|
|
199
258
|
if (conflicts.length) {
|
|
200
259
|
const error = new RouteConflictError(conflicts);
|
|
@@ -209,7 +268,7 @@ function routes(directory = "src/pages") {
|
|
|
209
268
|
"main",
|
|
210
269
|
{ style: { fontFamily: "system-ui, sans-serif", margin: "3rem auto", maxWidth: "60rem", padding: "0 1.5rem" } },
|
|
211
270
|
createElement("h1", null, "Conflicting routes"),
|
|
212
|
-
createElement("p", null, "The file-based router found
|
|
271
|
+
createElement("p", null, "The file-based router found pages with the same effective URL pattern. React Router treats dynamic parameter names as equivalent, and route-group names do not add URL segments. Fix these conflicts, then the page will reload."),
|
|
213
272
|
createElement("pre", { style: { background: "#f3f4f6", overflowX: "auto", padding: "1rem", whiteSpace: "pre-wrap" } }, message),
|
|
214
273
|
);
|
|
215
274
|
}
|
|
@@ -226,15 +285,70 @@ function routes(directory = "src/pages") {
|
|
|
226
285
|
].filter(Boolean);
|
|
227
286
|
return format(sections.join("\n\n"), { parser: "typescript" });
|
|
228
287
|
}
|
|
288
|
+
async function executeManifest() {
|
|
289
|
+
const manifest = createRouteManifest(scan(root, options.conventions), projectRoot);
|
|
290
|
+
return format(`export const routeManifest = ${JSON.stringify(manifest, null, 2)};`, { parser: "typescript" });
|
|
291
|
+
}
|
|
292
|
+
async function executeRouteHelpers() {
|
|
293
|
+
const tree = scan(root, options.conventions);
|
|
294
|
+
const patterns = [...new Set(collectPageRoutePatterns(tree))].sort();
|
|
295
|
+
const code = `
|
|
296
|
+
export const routePatterns = ${JSON.stringify(patterns)};
|
|
297
|
+
|
|
298
|
+
export function routePath(pattern, ...args) {
|
|
299
|
+
if (!routePatterns.includes(pattern)) {
|
|
300
|
+
throw new TypeError(\`Unknown route pattern "\${pattern}".\`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const params = args[0] ?? {};
|
|
304
|
+
const parameterNames = [...pattern.matchAll(/:([^/]+)|\\*/g)].map(
|
|
305
|
+
([, name]) => name ?? "*",
|
|
306
|
+
);
|
|
307
|
+
for (const key of Object.keys(params)) {
|
|
308
|
+
if (!parameterNames.includes(key)) {
|
|
309
|
+
throw new TypeError(\`Unknown route parameter "\${key}" for "\${pattern}".\`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return pattern.replace(/:([^/]+)|\\*/g, (token, name) => {
|
|
313
|
+
const key = name ?? "*";
|
|
314
|
+
if (!Object.hasOwn(params, key)) {
|
|
315
|
+
throw new TypeError(\`Missing route parameter "\${key}" for "\${pattern}".\`);
|
|
316
|
+
}
|
|
317
|
+
const value = String(params[key]);
|
|
318
|
+
if (key !== "*" && value.length === 0) {
|
|
319
|
+
throw new TypeError(
|
|
320
|
+
'Route parameter "' +
|
|
321
|
+
key +
|
|
322
|
+
'" for "' +
|
|
323
|
+
pattern +
|
|
324
|
+
'" cannot be empty.',
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return key === "*"
|
|
328
|
+
? value.split("/").map(encodeURIComponent).join("/")
|
|
329
|
+
: encodeURIComponent(value);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
return format(code, { parser: "typescript" });
|
|
334
|
+
}
|
|
229
335
|
return {
|
|
230
336
|
name: "routes",
|
|
231
337
|
configResolved(config) {
|
|
232
338
|
projectRoot = config.root;
|
|
233
|
-
root = path.resolve(projectRoot, directory);
|
|
339
|
+
root = path.resolve(projectRoot, options.directory);
|
|
234
340
|
dev = config.command === "serve";
|
|
235
341
|
},
|
|
236
|
-
resolveId: (id) =>
|
|
237
|
-
|
|
342
|
+
resolveId: (id) => {
|
|
343
|
+
if (id === ROUTES_MODULE_ID) return ROUTES_RESOLVED_ID;
|
|
344
|
+
if (id === MANIFEST_MODULE_ID) return MANIFEST_RESOLVED_ID;
|
|
345
|
+
if (id === ROUTE_HELPERS_MODULE_ID) return ROUTE_HELPERS_RESOLVED_ID;
|
|
346
|
+
},
|
|
347
|
+
load: (id) => {
|
|
348
|
+
if (id === ROUTES_RESOLVED_ID) return execute();
|
|
349
|
+
if (id === MANIFEST_RESOLVED_ID) return executeManifest();
|
|
350
|
+
if (id === ROUTE_HELPERS_RESOLVED_ID) return executeRouteHelpers();
|
|
351
|
+
},
|
|
238
352
|
configureServer: (server) => {
|
|
239
353
|
server.watcher.add(root);
|
|
240
354
|
server.watcher.on("all", (event, file) => {
|
|
@@ -246,9 +360,16 @@ function routes(directory = "src/pages") {
|
|
|
246
360
|
"change"
|
|
247
361
|
].includes(event)) return;
|
|
248
362
|
const location = path.resolve(file);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
363
|
+
const relative = path.relative(root, location);
|
|
364
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return;
|
|
365
|
+
for (const id of [
|
|
366
|
+
ROUTES_RESOLVED_ID,
|
|
367
|
+
MANIFEST_RESOLVED_ID,
|
|
368
|
+
ROUTE_HELPERS_RESOLVED_ID
|
|
369
|
+
]) {
|
|
370
|
+
const module = server.moduleGraph.getModuleById(id);
|
|
371
|
+
if (module) server.moduleGraph.invalidateModule(module);
|
|
372
|
+
}
|
|
252
373
|
server.ws.send({ type: "full-reload" });
|
|
253
374
|
});
|
|
254
375
|
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -9,4 +9,18 @@ export type Node = {
|
|
|
9
9
|
error?: string;
|
|
10
10
|
children: Node[];
|
|
11
11
|
};
|
|
12
|
-
export
|
|
12
|
+
export type RouteFileKind = "page" | "layout" | "loading" | "notFound" | "error";
|
|
13
|
+
export type RouteConventions = Partial<Record<RouteFileKind, string>>;
|
|
14
|
+
export type RouteManifestNode = {
|
|
15
|
+
segment: string | null;
|
|
16
|
+
fullPath: string;
|
|
17
|
+
params: string[];
|
|
18
|
+
routeGroup: boolean;
|
|
19
|
+
files: Partial<Record<RouteFileKind, string>>;
|
|
20
|
+
children: RouteManifestNode[];
|
|
21
|
+
};
|
|
22
|
+
export declare const DEFAULT_CONVENTIONS: Record<RouteFileKind, string>;
|
|
23
|
+
export declare function getRouteSegment(name: string): string;
|
|
24
|
+
export declare function collectPageRoutePatterns(node: Node, segments?: string[], root?: boolean): string[];
|
|
25
|
+
export declare function scan(directory: string, conventions?: Record<RouteFileKind, string>): Node;
|
|
26
|
+
export declare function createRouteManifest(node: Node, projectRoot: string, parentPath?: string, root?: boolean): RouteManifestNode;
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
1
|
/// <reference path="./virtual-routes.d.ts" />
|
|
2
2
|
|
|
3
3
|
export { routes } from "../dist/index.js";
|
|
4
|
+
export type {
|
|
5
|
+
RouteConventions,
|
|
6
|
+
RouteFileKind,
|
|
7
|
+
RouteManifestNode,
|
|
8
|
+
RoutesOptions,
|
|
9
|
+
} from "../dist/index.js";
|
|
10
|
+
|
|
11
|
+
export type RouteParameterValue = string | number;
|
|
12
|
+
|
|
13
|
+
type RouteParametersForSegment<Segment extends string> =
|
|
14
|
+
Segment extends `:${infer Name}`
|
|
15
|
+
? { [Key in Name]: RouteParameterValue }
|
|
16
|
+
: Segment extends "*"
|
|
17
|
+
? { "*": RouteParameterValue }
|
|
18
|
+
: {};
|
|
19
|
+
|
|
20
|
+
export type RouteParameters<Path extends string> =
|
|
21
|
+
Path extends `${infer Segment}/${infer Rest}`
|
|
22
|
+
? RouteParametersForSegment<Segment> & RouteParameters<Rest>
|
|
23
|
+
: RouteParametersForSegment<Path>;
|
|
24
|
+
|
|
25
|
+
type RouteParameterShapes<Path extends string> =
|
|
26
|
+
Path extends unknown ? RouteParameters<Path> : never;
|
|
27
|
+
|
|
28
|
+
type UnionToIntersection<Union> = (
|
|
29
|
+
Union extends unknown ? (value: Union) => void : never
|
|
30
|
+
) extends (value: infer Intersection) => void
|
|
31
|
+
? Intersection
|
|
32
|
+
: never;
|
|
33
|
+
|
|
34
|
+
type RouteRequiresParameters<Path extends string> =
|
|
35
|
+
Path extends unknown
|
|
36
|
+
? keyof RouteParameters<Path> extends never
|
|
37
|
+
? false
|
|
38
|
+
: true
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
type HasParameterShapeMismatch<Path extends string> =
|
|
42
|
+
[RouteParameterShapes<Path>] extends [
|
|
43
|
+
UnionToIntersection<RouteParameterShapes<Path>>,
|
|
44
|
+
]
|
|
45
|
+
? false
|
|
46
|
+
: true;
|
|
47
|
+
|
|
48
|
+
export type RoutePathArguments<Path extends string> =
|
|
49
|
+
true extends RouteRequiresParameters<Path>
|
|
50
|
+
? HasParameterShapeMismatch<Path> extends true
|
|
51
|
+
? [params: never]
|
|
52
|
+
: [params: RouteParameters<Path>]
|
|
53
|
+
: [];
|
|
@@ -2,3 +2,18 @@ declare module "virtual:routes" {
|
|
|
2
2
|
import type { RouteObject } from "react-router";
|
|
3
3
|
export const routes: RouteObject[];
|
|
4
4
|
}
|
|
5
|
+
|
|
6
|
+
declare module "virtual:route-manifest" {
|
|
7
|
+
import type { RouteManifestNode } from "@troyio/vite-file-router";
|
|
8
|
+
export const routeManifest: RouteManifestNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "virtual:route-helpers" {
|
|
12
|
+
import type { RoutePathArguments } from "@troyio/vite-file-router";
|
|
13
|
+
|
|
14
|
+
export const routePatterns: readonly string[];
|
|
15
|
+
export function routePath<const Path extends string>(
|
|
16
|
+
pattern: Path,
|
|
17
|
+
...args: RoutePathArguments<Path>
|
|
18
|
+
): string;
|
|
19
|
+
}
|