@troyio/vite-file-router 0.2.0 → 0.2.2
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 +21 -0
- package/dist/generator.d.ts +2 -1
- package/dist/index.js +66 -46
- package/package.json +12 -13
package/README.md
CHANGED
|
@@ -60,8 +60,29 @@ import { routes } from "virtual:routes";
|
|
|
60
60
|
The virtual module exports a `routes` array with lazy-loaded page and layout
|
|
61
61
|
components.
|
|
62
62
|
|
|
63
|
+
### TypeScript
|
|
64
|
+
|
|
65
|
+
Make sure your application TypeScript project includes Vite's client types and
|
|
66
|
+
the router package's type declarations. For example, add this file as
|
|
67
|
+
`src/vite-env.d.ts`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
/// <reference types="vite/client" />
|
|
71
|
+
/// <reference types="@troyio/vite-file-router" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If your `tsconfig.json` sets `compilerOptions.types`, include both
|
|
75
|
+
`"vite/client"` and `"@troyio/vite-file-router"` in that list. Then restart
|
|
76
|
+
the TypeScript server in your editor.
|
|
77
|
+
|
|
63
78
|
## Build
|
|
64
79
|
|
|
65
80
|
```sh
|
|
66
81
|
npm run build
|
|
67
82
|
```
|
|
83
|
+
|
|
84
|
+
## Test
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm test
|
|
88
|
+
```
|
package/dist/generator.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Node } from "./scanner";
|
|
2
2
|
export type GeneratedRoutes = {
|
|
3
3
|
imports: string[];
|
|
4
|
+
helpers: string[];
|
|
4
5
|
routes: string;
|
|
5
6
|
};
|
|
6
|
-
export declare function generate(node: Node, root?: boolean): GeneratedRoutes;
|
|
7
|
+
export declare function generate(node: Node, root?: boolean, projectRoot?: string): GeneratedRoutes;
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,24 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { genArrayFromRaw, genObjectFromRaw } from "knitwork";
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
-
}
|
|
2
|
+
import { genArrayFromRaw, genObjectFromRaw, genSafeVariableName } from "knitwork";
|
|
3
|
+
import { format } from "prettier";
|
|
4
|
+
import fs from "node:fs";
|
|
28
5
|
// src/generator.ts
|
|
6
|
+
function getImportPath(file, projectRoot) {
|
|
7
|
+
const relativePath = path.relative(projectRoot, file).split(path.sep).join("/");
|
|
8
|
+
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
9
|
+
}
|
|
29
10
|
function getRoutePath(directory) {
|
|
30
11
|
const segment = path.basename(directory);
|
|
31
12
|
if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
|
|
32
13
|
return segment.replace(/^\[(.+)\]$/, ":$1");
|
|
33
14
|
}
|
|
34
|
-
function generateImport(file) {
|
|
15
|
+
function generateImport(file, projectRoot) {
|
|
35
16
|
return `async () => ({
|
|
36
|
-
Component: (await import(${JSON.stringify(
|
|
17
|
+
Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
|
|
37
18
|
})`;
|
|
38
19
|
}
|
|
39
|
-
function generateLazyComponent(file) {
|
|
40
|
-
return `lazy(() => import(${JSON.stringify(
|
|
20
|
+
function generateLazyComponent(file, projectRoot) {
|
|
21
|
+
return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}))`;
|
|
41
22
|
}
|
|
42
23
|
function generateNode(node, root, context, inheritedBoundary) {
|
|
43
24
|
const route = {};
|
|
@@ -45,25 +26,25 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
45
26
|
if (!root) route.path = JSON.stringify(getRoutePath(node.directory));
|
|
46
27
|
let loadingComponent;
|
|
47
28
|
if (node.loading) {
|
|
48
|
-
loadingComponent = `Loading${context.nextLoadingId++}
|
|
49
|
-
context.loadingImports.push(`import ${loadingComponent} from ${JSON.stringify(
|
|
29
|
+
loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
|
|
30
|
+
context.loadingImports.push(`import ${loadingComponent} from ${JSON.stringify(getImportPath(node.loading, context.projectRoot))};`);
|
|
50
31
|
context.hasBoundaries = true;
|
|
51
32
|
}
|
|
52
33
|
if (loadingComponent) {
|
|
53
|
-
const child = node.layout ? generateLazyComponent(node.layout) : "Outlet";
|
|
34
|
+
const child = node.layout ? generateLazyComponent(node.layout, context.projectRoot) : "Outlet";
|
|
54
35
|
route.Component = `createBoundary(${loadingComponent}, ${child})`;
|
|
55
36
|
} else if (node.layout) {
|
|
56
|
-
if (hasBoundary) route.Component = generateLazyComponent(node.layout);
|
|
57
|
-
else route.lazy = generateImport(node.layout);
|
|
37
|
+
if (hasBoundary) route.Component = generateLazyComponent(node.layout, context.projectRoot);
|
|
38
|
+
else route.lazy = generateImport(node.layout, context.projectRoot);
|
|
58
39
|
}
|
|
59
40
|
const children = [];
|
|
60
41
|
if (node.page) {
|
|
61
42
|
const page = hasBoundary ? {
|
|
62
43
|
index: "true",
|
|
63
|
-
Component: generateLazyComponent(node.page)
|
|
44
|
+
Component: generateLazyComponent(node.page, context.projectRoot)
|
|
64
45
|
} : {
|
|
65
46
|
index: "true",
|
|
66
|
-
lazy: generateImport(node.page)
|
|
47
|
+
lazy: generateImport(node.page, context.projectRoot)
|
|
67
48
|
};
|
|
68
49
|
children.push(genObjectFromRaw(page));
|
|
69
50
|
}
|
|
@@ -71,15 +52,19 @@ function generateNode(node, root, context, inheritedBoundary) {
|
|
|
71
52
|
if (children.length) route.children = genArrayFromRaw(children);
|
|
72
53
|
return genObjectFromRaw(route);
|
|
73
54
|
}
|
|
74
|
-
function generate(node, root = false) {
|
|
55
|
+
function generate(node, root = false, projectRoot = process.cwd()) {
|
|
75
56
|
const context = {
|
|
57
|
+
projectRoot,
|
|
76
58
|
loadingImports: [],
|
|
77
59
|
nextLoadingId: 0,
|
|
78
60
|
hasBoundaries: false
|
|
79
61
|
};
|
|
80
62
|
const routes = generateNode(node, root, context, false);
|
|
81
63
|
const imports = [...context.loadingImports];
|
|
82
|
-
|
|
64
|
+
const helpers = [];
|
|
65
|
+
if (context.hasBoundaries) {
|
|
66
|
+
imports.unshift(`import { createElement, lazy, Suspense } from "react";`, `import { Outlet } from "react-router";`);
|
|
67
|
+
helpers.push(`function createBoundary(Fallback, Component) {
|
|
83
68
|
return function RouteBoundary() {
|
|
84
69
|
return createElement(
|
|
85
70
|
Suspense,
|
|
@@ -88,27 +73,62 @@ function generate(node, root = false) {
|
|
|
88
73
|
);
|
|
89
74
|
};
|
|
90
75
|
}`);
|
|
76
|
+
}
|
|
91
77
|
return {
|
|
92
78
|
imports,
|
|
79
|
+
helpers,
|
|
93
80
|
routes
|
|
94
81
|
};
|
|
95
82
|
}
|
|
83
|
+
// src/scanner.ts
|
|
84
|
+
function scan(directory) {
|
|
85
|
+
const node = {
|
|
86
|
+
name: path.basename(directory),
|
|
87
|
+
directory,
|
|
88
|
+
children: []
|
|
89
|
+
};
|
|
90
|
+
if (!fs.existsSync(directory)) return node;
|
|
91
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const file = path.join(directory, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
node.children.push(scan(file));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const match = /^(page|layout|loading)\.(jsx|tsx)$/.exec(entry.name);
|
|
99
|
+
if (!match) continue;
|
|
100
|
+
const [, name, extension] = match;
|
|
101
|
+
const key = name;
|
|
102
|
+
if (!node[key] || extension === "tsx") node[key] = file;
|
|
103
|
+
}
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
96
106
|
// src/index.ts
|
|
97
107
|
var MODULE_ID = "virtual:routes";
|
|
98
108
|
var RESOLVED_ID = `\0${MODULE_ID}`;
|
|
99
109
|
function routes(directory = "src/pages") {
|
|
110
|
+
let projectRoot = "";
|
|
100
111
|
let root = "";
|
|
101
|
-
function execute() {
|
|
102
|
-
const generated = generate(scan(root), true);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
async function execute() {
|
|
113
|
+
const generated = generate(scan(root), true, projectRoot);
|
|
114
|
+
const sections = [
|
|
115
|
+
generated.imports.sort((left, right) => {
|
|
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"),
|
|
122
|
+
generated.helpers.join("\n"),
|
|
123
|
+
`export const routes = ${genArrayFromRaw([generated.routes])};`
|
|
124
|
+
].filter(Boolean);
|
|
125
|
+
return format(sections.join("\n\n"), { parser: "typescript" });
|
|
107
126
|
}
|
|
108
127
|
return {
|
|
109
128
|
name: "routes",
|
|
110
129
|
configResolved(config) {
|
|
111
|
-
|
|
130
|
+
projectRoot = config.root;
|
|
131
|
+
root = path.resolve(projectRoot, directory);
|
|
112
132
|
},
|
|
113
133
|
resolveId: (id) => id === MODULE_ID ? RESOLVED_ID : void 0,
|
|
114
134
|
load: (id) => id === RESOLVED_ID ? execute() : void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@troyio/vite-file-router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "File-system routing plugin for Vite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -26,27 +26,26 @@
|
|
|
26
26
|
"import": "./dist/index.js"
|
|
27
27
|
}
|
|
28
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
29
|
"dependencies": {
|
|
35
30
|
"knitwork": "^1.3.0",
|
|
36
|
-
"
|
|
37
|
-
"vite-file-router": "link:"
|
|
31
|
+
"prettier": "^3.9.10"
|
|
38
32
|
},
|
|
39
33
|
"peerDependencies": {
|
|
40
34
|
"react": ">=19.2.7",
|
|
35
|
+
"react-router": "^8.5.1",
|
|
41
36
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
42
37
|
},
|
|
43
38
|
"devDependencies": {
|
|
44
|
-
"@
|
|
39
|
+
"@biomejs/biome": "^2.6.4",
|
|
45
40
|
"@types/node": "^24.19.2",
|
|
46
|
-
"eslint": "^10.14.0",
|
|
47
|
-
"globals": "^17.14.0",
|
|
48
41
|
"typescript": "~6.0.3",
|
|
49
|
-
"typescript-eslint": "^8.72.1",
|
|
50
42
|
"vite": "^8.3.5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "vite build && tsc -p tsconfig.build.json",
|
|
46
|
+
"test": "pnpm build && node --test",
|
|
47
|
+
"lint": "biome check",
|
|
48
|
+
"format": "biome format --write",
|
|
49
|
+
"prepublish": "pnpm audit & pnpm build"
|
|
51
50
|
}
|
|
52
|
-
}
|
|
51
|
+
}
|