@troyio/vite-file-router 0.1.0 → 0.2.1

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
@@ -4,30 +4,17 @@ A Vite plugin that generates a route tree from a file-based page directory.
4
4
 
5
5
  ## Install
6
6
 
7
- The package is published to GitHub Packages on every push to `main`, under
8
- `@OWNER/vite-file-router`. Configure npm to use GitHub Packages for that scope
9
- and authenticate with a GitHub token that has `read:packages` access:
10
-
11
- ```ini
12
- @OWNER:registry=https://npm.pkg.github.com
13
- //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
14
- ```
15
-
16
- Then install it with:
17
-
18
7
  ```sh
19
- npm install @OWNER/vite-file-router
8
+ npm install @troyio/vite-file-router
20
9
  ```
21
10
 
22
- Each main-branch build is published with a unique prerelease version.
23
-
24
11
  Vite is a peer dependency and must also be installed in your project.
25
12
 
26
13
  ## Configure
27
14
 
28
15
  ```ts
29
16
  import { defineConfig } from "vite";
30
- import { routes } from "@OWNER/vite-file-router";
17
+ import { routes } from "@troyio/vite-file-router";
31
18
 
32
19
  export default defineConfig({
33
20
  plugins: [routes()],
@@ -41,14 +28,20 @@ project root to use another directory:
41
28
  plugins: [routes("src/routes")];
42
29
  ```
43
30
 
44
- Each `page.tsx` becomes a route page, and each `layout.tsx` becomes a lazy
45
- layout for its directory. Directories become nested routes. For example:
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:
46
37
 
47
38
  ```text
48
39
  src/pages/
49
- layout.tsx
50
- page.tsx
40
+ layout.jsx
41
+ loading.jsx
42
+ page.jsx
51
43
  users/
44
+ loading.tsx
52
45
  page.tsx
53
46
  [id]/
54
47
  page.tsx
@@ -72,3 +65,9 @@ components.
72
65
  ```sh
73
66
  npm run build
74
67
  ```
68
+
69
+ ## Test
70
+
71
+ ```sh
72
+ npm test
73
+ ```
@@ -1,2 +1,7 @@
1
1
  import type { Node } from "./scanner";
2
- export declare function generate(node: Node, root?: boolean): string;
2
+ export type GeneratedRoutes = {
3
+ imports: string[];
4
+ helpers: string[];
5
+ routes: string;
6
+ };
7
+ export declare function generate(node: Node, root?: boolean, projectRoot?: string): GeneratedRoutes;
package/dist/index.js CHANGED
@@ -1,7 +1,85 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
- import { genArrayFromRaw, genObjectFromRaw } from "knitwork";
4
- import { normalizePath } from "vite";
2
+ import { genArrayFromRaw, genObjectFromRaw, genSafeVariableName } from "knitwork";
3
+ import { format } from "prettier";
4
+ import fs from "node:fs";
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
+ }
10
+ function getRoutePath(directory) {
11
+ const segment = path.basename(directory);
12
+ if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
13
+ return segment.replace(/^\[(.+)\]$/, ":$1");
14
+ }
15
+ function generateImport(file, projectRoot) {
16
+ return `async () => ({
17
+ Component: (await import(${JSON.stringify(getImportPath(file, projectRoot))})).default
18
+ })`;
19
+ }
20
+ function generateLazyComponent(file, projectRoot) {
21
+ return `lazy(() => import(${JSON.stringify(getImportPath(file, projectRoot))}))`;
22
+ }
23
+ function generateNode(node, root, context, inheritedBoundary) {
24
+ const route = {};
25
+ const hasBoundary = inheritedBoundary || Boolean(node.loading);
26
+ if (!root) route.path = JSON.stringify(getRoutePath(node.directory));
27
+ let loadingComponent;
28
+ if (node.loading) {
29
+ loadingComponent = genSafeVariableName(`Loading${context.nextLoadingId++}`);
30
+ context.loadingImports.push(`import ${loadingComponent} from ${JSON.stringify(getImportPath(node.loading, context.projectRoot))};`);
31
+ context.hasBoundaries = true;
32
+ }
33
+ if (loadingComponent) {
34
+ const child = node.layout ? generateLazyComponent(node.layout, context.projectRoot) : "Outlet";
35
+ route.Component = `createBoundary(${loadingComponent}, ${child})`;
36
+ } else if (node.layout) {
37
+ if (hasBoundary) route.Component = generateLazyComponent(node.layout, context.projectRoot);
38
+ else route.lazy = generateImport(node.layout, context.projectRoot);
39
+ }
40
+ const children = [];
41
+ if (node.page) {
42
+ const page = hasBoundary ? {
43
+ index: "true",
44
+ Component: generateLazyComponent(node.page, context.projectRoot)
45
+ } : {
46
+ index: "true",
47
+ lazy: generateImport(node.page, context.projectRoot)
48
+ };
49
+ children.push(genObjectFromRaw(page));
50
+ }
51
+ for (const child of node.children) children.push(generateNode(child, false, context, hasBoundary));
52
+ if (children.length) route.children = genArrayFromRaw(children);
53
+ return genObjectFromRaw(route);
54
+ }
55
+ function generate(node, root = false, projectRoot = process.cwd()) {
56
+ const context = {
57
+ projectRoot,
58
+ loadingImports: [],
59
+ nextLoadingId: 0,
60
+ hasBoundaries: false
61
+ };
62
+ const routes = generateNode(node, root, context, false);
63
+ const imports = [...context.loadingImports];
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) {
68
+ return function RouteBoundary() {
69
+ return createElement(
70
+ Suspense,
71
+ { fallback: createElement(Fallback) },
72
+ createElement(Component),
73
+ );
74
+ };
75
+ }`);
76
+ }
77
+ return {
78
+ imports,
79
+ helpers,
80
+ routes
81
+ };
82
+ }
5
83
  // src/scanner.ts
6
84
  function scan(directory) {
7
85
  const node = {
@@ -17,50 +95,40 @@ function scan(directory) {
17
95
  node.children.push(scan(file));
18
96
  continue;
19
97
  }
20
- if (entry.name === "page.tsx") node.page = file;
21
- if (entry.name === "layout.tsx") node.layout = file;
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;
22
103
  }
23
104
  return node;
24
105
  }
25
- // src/generator.ts
26
- function getRoutePath(directory) {
27
- const segment = path.basename(directory);
28
- if (segment.startsWith("[...") && segment.endsWith("]")) return "*";
29
- return segment.replace(/^\[(.+)\]$/, ":$1");
30
- }
31
- function generateImport(file) {
32
- return `async ()=>({
33
- Component: (await import(${JSON.stringify(normalizePath(file))})).default
34
- })`;
35
- }
36
- function generate(node, root = false) {
37
- const route = {};
38
- if (!root) route.path = JSON.stringify(getRoutePath(node.directory));
39
- if (node.layout) route.lazy = generateImport(node.layout);
40
- const children = [];
41
- if (node.page) children.push(genObjectFromRaw({
42
- index: "true",
43
- lazy: generateImport(node.page)
44
- }));
45
- for (const child of node.children) children.push(generate(child));
46
- if (children.length) route.children = genArrayFromRaw(children);
47
- return genObjectFromRaw(route);
48
- }
49
106
  // src/index.ts
50
107
  var MODULE_ID = "virtual:routes";
51
108
  var RESOLVED_ID = `\0${MODULE_ID}`;
52
109
  function routes(directory = "src/pages") {
110
+ let projectRoot = "";
53
111
  let root = "";
54
- function execute() {
55
- const routes = generate(scan(root), true);
56
- return `
57
- export const routes = ${genArrayFromRaw([routes])};
58
- `;
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" });
59
126
  }
60
127
  return {
61
128
  name: "routes",
62
129
  configResolved(config) {
63
- root = path.resolve(config.root, directory);
130
+ projectRoot = config.root;
131
+ root = path.resolve(projectRoot, directory);
64
132
  },
65
133
  resolveId: (id) => id === MODULE_ID ? RESOLVED_ID : void 0,
66
134
  load: (id) => id === RESOLVED_ID ? execute() : void 0,
package/dist/scanner.d.ts CHANGED
@@ -3,6 +3,7 @@ export type Node = {
3
3
  directory: string;
4
4
  page?: string;
5
5
  layout?: string;
6
+ loading?: string;
6
7
  children: Node[];
7
8
  };
8
9
  export declare function scan(directory: string): Node;
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@troyio/vite-file-router",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "File-system routing plugin for Vite",
5
5
  "type": "module",
6
6
  "private": false,
7
+ "keywords": [
8
+ "vite",
9
+ "react",
10
+ "routing",
11
+ "react-router"
12
+ ],
13
+ "license": "MIT",
7
14
  "publishConfig": {
8
15
  "access": "public"
9
16
  },
@@ -19,25 +26,26 @@
19
26
  "import": "./dist/index.js"
20
27
  }
21
28
  },
22
- "scripts": {
23
- "build": "vite build && tsc -p tsconfig.build.json",
24
- "lint": "eslint src vite.config.ts"
25
- },
26
29
  "dependencies": {
27
30
  "knitwork": "^1.3.0",
28
- "react-router": "^8.5.1",
29
- "vite-file-router": "link:"
31
+ "prettier": "^3.9.10"
30
32
  },
31
33
  "peerDependencies": {
34
+ "react": ">=19.2.7",
35
+ "react-router": "^8.5.1",
32
36
  "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
33
37
  },
34
38
  "devDependencies": {
35
- "@eslint/js": "^10.0.1",
39
+ "@biomejs/biome": "^2.6.4",
36
40
  "@types/node": "^24.19.2",
37
- "eslint": "^10.13.0",
38
- "globals": "^17.13.0",
39
41
  "typescript": "~6.0.3",
40
- "typescript-eslint": "^8.72.0",
41
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"
42
50
  }
43
- }
51
+ }