@troyio/vite-file-router 0.0.0-stage → 0.2.0

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
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # vite-file-router
2
+
3
+ A Vite plugin that generates a route tree from a file-based page directory.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @troyio/vite-file-router
9
+ ```
10
+
11
+ Vite is a peer dependency and must also be installed in your project.
12
+
13
+ ## Configure
14
+
15
+ ```ts
16
+ import { defineConfig } from "vite";
17
+ import { routes } from "@troyio/vite-file-router";
18
+
19
+ export default defineConfig({
20
+ plugins: [routes()],
21
+ });
22
+ ```
23
+
24
+ By default, the plugin scans `src/pages`. Pass a path relative to the Vite
25
+ project root to use another directory:
26
+
27
+ ```ts
28
+ plugins: [routes("src/routes")];
29
+ ```
30
+
31
+ Each `page.jsx` or `page.tsx` becomes a route page, and each `layout.jsx` or
32
+ `layout.tsx` becomes a lazy layout for its directory. Directories become nested
33
+ routes. Add a `loading.jsx` or `loading.tsx` to provide a Suspense fallback for
34
+ that directory's page and nested routes; a nested loading file provides a more
35
+ specific fallback. Each file must default-export a React component. If both
36
+ extensions exist for the same component, `.tsx` takes precedence. For example:
37
+
38
+ ```text
39
+ src/pages/
40
+ layout.jsx
41
+ loading.jsx
42
+ page.jsx
43
+ users/
44
+ loading.tsx
45
+ page.tsx
46
+ [id]/
47
+ page.tsx
48
+ ```
49
+
50
+ Dynamic segments such as `[id]` become `:id`; catch-all segments such as
51
+ `[...slug]` become `*`.
52
+
53
+ Import the generated route configuration from the `virtual:routes` module in
54
+ your application:
55
+
56
+ ```ts
57
+ import { routes } from "virtual:routes";
58
+ ```
59
+
60
+ The virtual module exports a `routes` array with lazy-loaded page and layout
61
+ components.
62
+
63
+ ## Build
64
+
65
+ ```sh
66
+ npm run build
67
+ ```
@@ -0,0 +1,6 @@
1
+ import type { Node } from "./scanner";
2
+ export type GeneratedRoutes = {
3
+ imports: string[];
4
+ routes: string;
5
+ };
6
+ export declare function generate(node: Node, root?: boolean): GeneratedRoutes;
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function routes(directory?: string): Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { genArrayFromRaw, genObjectFromRaw } from "knitwork";
4
+ import { normalizePath } from "vite";
5
+ // src/scanner.ts
6
+ function scan(directory) {
7
+ const node = {
8
+ name: path.basename(directory),
9
+ directory,
10
+ children: []
11
+ };
12
+ if (!fs.existsSync(directory)) return node;
13
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const file = path.join(directory, entry.name);
16
+ if (entry.isDirectory()) {
17
+ node.children.push(scan(file));
18
+ continue;
19
+ }
20
+ const match = /^(page|layout|loading)\.(jsx|tsx)$/.exec(entry.name);
21
+ if (!match) continue;
22
+ const [, name, extension] = match;
23
+ const key = name;
24
+ if (!node[key] || extension === "tsx") node[key] = file;
25
+ }
26
+ return node;
27
+ }
28
+ // src/generator.ts
29
+ function getRoutePath(directory) {
30
+ const segment = path.basename(directory);
31
+ if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
32
+ return segment.replace(/^\[(.+)\]$/, ":$1");
33
+ }
34
+ function generateImport(file) {
35
+ return `async () => ({
36
+ Component: (await import(${JSON.stringify(normalizePath(file))})).default
37
+ })`;
38
+ }
39
+ function generateLazyComponent(file) {
40
+ return `lazy(() => import(${JSON.stringify(normalizePath(file))}))`;
41
+ }
42
+ function generateNode(node, root, context, inheritedBoundary) {
43
+ const route = {};
44
+ const hasBoundary = inheritedBoundary || Boolean(node.loading);
45
+ if (!root) route.path = JSON.stringify(getRoutePath(node.directory));
46
+ let loadingComponent;
47
+ if (node.loading) {
48
+ loadingComponent = `Loading${context.nextLoadingId++}`;
49
+ context.loadingImports.push(`import ${loadingComponent} from ${JSON.stringify(normalizePath(node.loading))};`);
50
+ context.hasBoundaries = true;
51
+ }
52
+ if (loadingComponent) {
53
+ const child = node.layout ? generateLazyComponent(node.layout) : "Outlet";
54
+ route.Component = `createBoundary(${loadingComponent}, ${child})`;
55
+ } else if (node.layout) {
56
+ if (hasBoundary) route.Component = generateLazyComponent(node.layout);
57
+ else route.lazy = generateImport(node.layout);
58
+ }
59
+ const children = [];
60
+ if (node.page) {
61
+ const page = hasBoundary ? {
62
+ index: "true",
63
+ Component: generateLazyComponent(node.page)
64
+ } : {
65
+ index: "true",
66
+ lazy: generateImport(node.page)
67
+ };
68
+ children.push(genObjectFromRaw(page));
69
+ }
70
+ for (const child of node.children) children.push(generateNode(child, false, context, hasBoundary));
71
+ if (children.length) route.children = genArrayFromRaw(children);
72
+ return genObjectFromRaw(route);
73
+ }
74
+ function generate(node, root = false) {
75
+ const context = {
76
+ loadingImports: [],
77
+ nextLoadingId: 0,
78
+ hasBoundaries: false
79
+ };
80
+ const routes = generateNode(node, root, context, false);
81
+ const imports = [...context.loadingImports];
82
+ if (context.hasBoundaries) imports.unshift(`import { createElement, lazy, Suspense } from "react";`, `import { Outlet } from "react-router";`, `function createBoundary(Fallback, Component) {
83
+ return function RouteBoundary() {
84
+ return createElement(
85
+ Suspense,
86
+ { fallback: createElement(Fallback) },
87
+ createElement(Component),
88
+ );
89
+ };
90
+ }`);
91
+ return {
92
+ imports,
93
+ routes
94
+ };
95
+ }
96
+ // src/index.ts
97
+ var MODULE_ID = "virtual:routes";
98
+ var RESOLVED_ID = `\0${MODULE_ID}`;
99
+ function routes(directory = "src/pages") {
100
+ let root = "";
101
+ function execute() {
102
+ const generated = generate(scan(root), true);
103
+ return `
104
+ ${generated.imports.join("\n")}
105
+ export const routes = ${genArrayFromRaw([generated.routes])};
106
+ `;
107
+ }
108
+ return {
109
+ name: "routes",
110
+ configResolved(config) {
111
+ root = path.resolve(config.root, directory);
112
+ },
113
+ resolveId: (id) => id === MODULE_ID ? RESOLVED_ID : void 0,
114
+ load: (id) => id === RESOLVED_ID ? execute() : void 0,
115
+ configureServer: (server) => {
116
+ server.watcher.add(root);
117
+ server.watcher.on("all", (event, file) => {
118
+ if (![
119
+ "add",
120
+ "unlink",
121
+ "addDir",
122
+ "unlinkDir",
123
+ "change"
124
+ ].includes(event)) return;
125
+ const location = path.resolve(file);
126
+ if (location !== root && !location.startsWith(root + path.sep)) return;
127
+ const module = server.moduleGraph.getModuleById(RESOLVED_ID);
128
+ if (module) server.moduleGraph.invalidateModule(module);
129
+ server.ws.send({ type: "full-reload" });
130
+ });
131
+ }
132
+ };
133
+ }
134
+ export { routes };
@@ -0,0 +1,9 @@
1
+ export type Node = {
2
+ name: string;
3
+ directory: string;
4
+ page?: string;
5
+ layout?: string;
6
+ loading?: string;
7
+ children: Node[];
8
+ };
9
+ export declare function scan(directory: string): Node;
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@troyio/vite-file-router",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "File-system routing plugin for Vite",
5
+ "type": "module",
6
+ "private": false,
7
+ "keywords": [
8
+ "vite",
9
+ "react",
10
+ "routing",
11
+ "react-router"
12
+ ],
13
+ "license": "MIT",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "types"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./types/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./types/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "build": "vite build && tsc -p tsconfig.build.json",
31
+ "lint": "eslint src vite.config.ts",
32
+ "prepublish": "pnpm build"
33
+ },
34
+ "dependencies": {
35
+ "knitwork": "^1.3.0",
36
+ "react-router": "^8.5.1",
37
+ "vite-file-router": "link:"
38
+ },
39
+ "peerDependencies": {
40
+ "react": ">=19.2.7",
41
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@types/node": "^24.19.2",
46
+ "eslint": "^10.14.0",
47
+ "globals": "^17.14.0",
48
+ "typescript": "~6.0.3",
49
+ "typescript-eslint": "^8.72.1",
50
+ "vite": "^8.3.5"
51
+ }
52
+ }
@@ -0,0 +1,3 @@
1
+ /// <reference path="./virtual-routes.d.ts" />
2
+
3
+ export { routes } from "../dist/index.js";
@@ -0,0 +1,4 @@
1
+ declare module "virtual:routes" {
2
+ import type { RouteObject } from "react-router";
3
+ export const routes: RouteObject[];
4
+ }