@troyio/vite-file-router 0.2.6 → 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 +30 -2
- package/dist/index.js +58 -1
- package/dist/scanner.d.ts +1 -0
- package/package.json +1 -1
- package/types/index.d.ts +44 -0
- package/types/virtual-routes.d.ts +10 -0
package/README.md
CHANGED
|
@@ -197,8 +197,36 @@ remain in the tree. React Router's generated `routes` remain the runtime source
|
|
|
197
197
|
of truth.
|
|
198
198
|
|
|
199
199
|
The development server watches the configured directory. Adding, removing,
|
|
200
|
-
renaming, or editing route files invalidates
|
|
201
|
-
the app without requiring a server restart.
|
|
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.
|
|
202
230
|
|
|
203
231
|
### TypeScript setup
|
|
204
232
|
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,12 @@ function getRouteSegment(name) {
|
|
|
14
14
|
if (name.startsWith("[...") && name.endsWith("]")) return "*";
|
|
15
15
|
return name.replace(/^\[(.+)\]$/, ":$1");
|
|
16
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
|
+
}
|
|
17
23
|
function scan(directory, conventions = DEFAULT_CONVENTIONS) {
|
|
18
24
|
const node = {
|
|
19
25
|
name: path.basename(directory),
|
|
@@ -210,6 +216,8 @@ var ROUTES_MODULE_ID = "virtual:routes";
|
|
|
210
216
|
var ROUTES_RESOLVED_ID = `\0${ROUTES_MODULE_ID}`;
|
|
211
217
|
var MANIFEST_MODULE_ID = "virtual:route-manifest";
|
|
212
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}`;
|
|
213
221
|
function resolveOptions(input) {
|
|
214
222
|
const options = typeof input === "string" ? { directory: input } : input;
|
|
215
223
|
if (!options || typeof options !== "object" || Array.isArray(options)) throw new TypeError("routes() expects a directory string or options object.");
|
|
@@ -281,6 +289,49 @@ function routes(input = "src/pages") {
|
|
|
281
289
|
const manifest = createRouteManifest(scan(root, options.conventions), projectRoot);
|
|
282
290
|
return format(`export const routeManifest = ${JSON.stringify(manifest, null, 2)};`, { parser: "typescript" });
|
|
283
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
|
+
}
|
|
284
335
|
return {
|
|
285
336
|
name: "routes",
|
|
286
337
|
configResolved(config) {
|
|
@@ -291,10 +342,12 @@ function routes(input = "src/pages") {
|
|
|
291
342
|
resolveId: (id) => {
|
|
292
343
|
if (id === ROUTES_MODULE_ID) return ROUTES_RESOLVED_ID;
|
|
293
344
|
if (id === MANIFEST_MODULE_ID) return MANIFEST_RESOLVED_ID;
|
|
345
|
+
if (id === ROUTE_HELPERS_MODULE_ID) return ROUTE_HELPERS_RESOLVED_ID;
|
|
294
346
|
},
|
|
295
347
|
load: (id) => {
|
|
296
348
|
if (id === ROUTES_RESOLVED_ID) return execute();
|
|
297
349
|
if (id === MANIFEST_RESOLVED_ID) return executeManifest();
|
|
350
|
+
if (id === ROUTE_HELPERS_RESOLVED_ID) return executeRouteHelpers();
|
|
298
351
|
},
|
|
299
352
|
configureServer: (server) => {
|
|
300
353
|
server.watcher.add(root);
|
|
@@ -309,7 +362,11 @@ function routes(input = "src/pages") {
|
|
|
309
362
|
const location = path.resolve(file);
|
|
310
363
|
const relative = path.relative(root, location);
|
|
311
364
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return;
|
|
312
|
-
for (const id of [
|
|
365
|
+
for (const id of [
|
|
366
|
+
ROUTES_RESOLVED_ID,
|
|
367
|
+
MANIFEST_RESOLVED_ID,
|
|
368
|
+
ROUTE_HELPERS_RESOLVED_ID
|
|
369
|
+
]) {
|
|
313
370
|
const module = server.moduleGraph.getModuleById(id);
|
|
314
371
|
if (module) server.moduleGraph.invalidateModule(module);
|
|
315
372
|
}
|
package/dist/scanner.d.ts
CHANGED
|
@@ -21,5 +21,6 @@ export type RouteManifestNode = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare const DEFAULT_CONVENTIONS: Record<RouteFileKind, string>;
|
|
23
23
|
export declare function getRouteSegment(name: string): string;
|
|
24
|
+
export declare function collectPageRoutePatterns(node: Node, segments?: string[], root?: boolean): string[];
|
|
24
25
|
export declare function scan(directory: string, conventions?: Record<RouteFileKind, string>): Node;
|
|
25
26
|
export declare function createRouteManifest(node: Node, projectRoot: string, parentPath?: string, root?: boolean): RouteManifestNode;
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -7,3 +7,47 @@ export type {
|
|
|
7
7
|
RouteManifestNode,
|
|
8
8
|
RoutesOptions,
|
|
9
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
|
+
: [];
|
|
@@ -7,3 +7,13 @@ declare module "virtual:route-manifest" {
|
|
|
7
7
|
import type { RouteManifestNode } from "@troyio/vite-file-router";
|
|
8
8
|
export const routeManifest: RouteManifestNode;
|
|
9
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
|
+
}
|