@enhancd/react-file-router 1.0.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 +330 -0
- package/eslint.config.mjs +8 -0
- package/package.json +45 -0
- package/rollup.config.js +99 -0
- package/src/index.ts +1 -0
- package/src/lib/navigate-to-page/index.ts +1 -0
- package/src/lib/navigate-to-page/navigate-to-page.tsx +23 -0
- package/src/lib/reactFileRouter.ts +3 -0
- package/src/lib/route/route.ts +12 -0
- package/src/lib/router-schema/router-schema.ts +5 -0
- package/src/lib/vite-plugin/__tests__/reactFileRouterVitePlugin.spec.ts +16 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$deep$nested$path/DeepNestedPath.page.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$default/$arrow/DefaultArrow.page.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$default/DefaultDeclaration.page.tsx +3 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$lazy/Lazy.fallback.tsx +3 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$lazy/Lazy.layout.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$lazy/Lazy.lazy.page.tsx +3 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$named/$arrow/NamedArrowPage.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$named/$declaration/NamedDeclaration.page.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$query@idx/$nested@param/QueryNested.page.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$query@idx/Query.404.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$query@idx/Query.layout.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/$query@idx/Query.page.tsx +0 -0
- package/src/lib/vite-plugin/__tests__/src/$router/Index.404.tsx +3 -0
- package/src/lib/vite-plugin/__tests__/src/$router/Index.error.tsx +3 -0
- package/src/lib/vite-plugin/__tests__/src/$router/Index.layout.tsx +10 -0
- package/src/lib/vite-plugin/__tests__/src/$router/Index.page.tsx +3 -0
- package/src/lib/vite-plugin/compile-folder-to-schema/compileFolderToSchema.ts +125 -0
- package/src/lib/vite-plugin/compile-lazy-pages/compileLazyPages.ts +30 -0
- package/src/lib/vite-plugin/compile-schema-to-router-file/compileSchemaToRouterFile.ts +64 -0
- package/src/lib/vite-plugin/transpile-lazy-page/transpile-lazy-page.ts +29 -0
- package/src/lib/vite-plugin/vitePlugin.ts +69 -0
- package/src/types/reactFileRouterSchema.d.ts +14 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +32 -0
- package/tsconfig.spec.json +37 -0
- package/vite.config.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# @enhancd/react-file-router
|
|
2
|
+
|
|
3
|
+
File-based routing for Vite + React apps using react-router-dom V6. Automatically generates routes from a directory structure — no manual `RouteObject` configuration needed.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @enhancd/react-file-router react-router-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> `react` and `react-dom` are also required as peer dependencies if not already installed.
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### 1. Add the Vite plugin
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// vite.config.ts
|
|
19
|
+
import { defineConfig } from "vite";
|
|
20
|
+
import react from "@vitejs/plugin-react";
|
|
21
|
+
import { reactFileRouterVitePlugin } from "@enhancd/react-file-router/vite-plugin";
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [
|
|
26
|
+
react(),
|
|
27
|
+
reactFileRouterVitePlugin(),
|
|
28
|
+
],
|
|
29
|
+
resolve: {
|
|
30
|
+
alias: {
|
|
31
|
+
// optional but recommended — lets you import pages as `import { AboutPage } from "@router"`
|
|
32
|
+
"@router": path.resolve(__dirname, "./src/$router"),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Set up the router provider
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// src/main.tsx
|
|
42
|
+
import { StrictMode } from "react";
|
|
43
|
+
import * as ReactDOM from "react-dom/client";
|
|
44
|
+
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
|
45
|
+
import { routerSchema } from "@enhancd/react-file-router";
|
|
46
|
+
|
|
47
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
48
|
+
<StrictMode>
|
|
49
|
+
<RouterProvider router={createBrowserRouter(routerSchema)} />
|
|
50
|
+
</StrictMode>
|
|
51
|
+
);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Create the `$router` directory
|
|
55
|
+
|
|
56
|
+
Create `src/$router/` and add route files following the naming conventions below.
|
|
57
|
+
|
|
58
|
+
## File Naming Conventions
|
|
59
|
+
|
|
60
|
+
Route files live inside `src/$router/` and follow the pattern `Name.type.tsx`:
|
|
61
|
+
|
|
62
|
+
| Suffix | Purpose | Example filename | Example component name |
|
|
63
|
+
|--------|---------|-----------------|----------------------|
|
|
64
|
+
| `.page.tsx` | Page component | `Index.page.tsx` | `IndexPage` |
|
|
65
|
+
| `.lazy.page.tsx` | Lazy-loaded page (code split) | `Home.lazy.page.tsx` | `HomeLazyPage` |
|
|
66
|
+
| `.layout.tsx` | Layout wrapper (renders `<Outlet />`) | `Index.layout.tsx` | `IndexLayout` |
|
|
67
|
+
| `.404.tsx` | Not-found page for this route level | `Index.404.tsx` | `Index404` |
|
|
68
|
+
| `.error.tsx` | Error boundary for this route level | `Index.error.tsx` | `IndexError` |
|
|
69
|
+
| `.fallback.tsx` | Suspense fallback for a lazy page | `Home.fallback.tsx` | `HomeFallback` |
|
|
70
|
+
|
|
71
|
+
**Component naming rule:** each `.`-separated part of the filename (excluding the file extension) is PascalCased and concatenated. `my.product.page.tsx` → `MyProductPage`.
|
|
72
|
+
|
|
73
|
+
## Folder Naming Conventions
|
|
74
|
+
|
|
75
|
+
Sub-route folders inside `$router` must start with `$` or `@`:
|
|
76
|
+
|
|
77
|
+
| Prefix | Meaning | Folder name | Generated path |
|
|
78
|
+
|--------|---------|-------------|----------------|
|
|
79
|
+
| `$` | Static path segment | `$products` | `/products` |
|
|
80
|
+
| `@` | Dynamic URL parameter | `@productId` | `/:productId` |
|
|
81
|
+
|
|
82
|
+
Nesting maps directly to URL nesting:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
$router/
|
|
86
|
+
├── $products/ → /products
|
|
87
|
+
│ └── @productId/ → /products/:productId
|
|
88
|
+
└── $about/ → /about
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Directory Structure Example
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/$router/
|
|
95
|
+
├── index.ts ← re-exports all page components
|
|
96
|
+
├── Index.page.tsx ← /
|
|
97
|
+
├── Index.layout.tsx ← layout wrapping all children
|
|
98
|
+
├── Index.404.tsx ← shown for unmatched routes
|
|
99
|
+
├── Index.error.tsx ← error boundary for root
|
|
100
|
+
│
|
|
101
|
+
├── $about/
|
|
102
|
+
│ └── About.page.tsx ← /about
|
|
103
|
+
│
|
|
104
|
+
└── $products/
|
|
105
|
+
├── Products.page.tsx ← /products
|
|
106
|
+
├── Products.error.tsx ← error boundary for /products
|
|
107
|
+
│
|
|
108
|
+
└── @productId/
|
|
109
|
+
├── Product.lazy.page.tsx ← /products/:productId (lazy loaded)
|
|
110
|
+
├── Product.fallback.tsx ← shown while lazy chunk loads
|
|
111
|
+
└── Product.error.tsx ← error boundary for /products/:productId
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Route Types in Detail
|
|
115
|
+
|
|
116
|
+
### Page (`.page.tsx`)
|
|
117
|
+
|
|
118
|
+
The main component rendered at a route. For dynamic routes the URL parameters are passed as props:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// $router/$products/@productId/Product.page.tsx
|
|
122
|
+
export const ProductPage = (params: { productId: string }) => {
|
|
123
|
+
return <div>Product: {params.productId}</div>;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Lazy Page (`.lazy.page.tsx`)
|
|
128
|
+
|
|
129
|
+
Same as a page but code-split into a separate bundle chunk. The plugin automatically wraps it in `React.lazy` + `React.Suspense`. Use a `.fallback.tsx` in the same folder to show UI while the chunk loads:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// $router/$products/@productId/Product.lazy.page.tsx
|
|
133
|
+
export const ProductLazyPage = (params: { productId: string }) => {
|
|
134
|
+
return <div>Product: {params.productId}</div>;
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// $router/$products/@productId/Product.fallback.tsx
|
|
140
|
+
export const ProductFallback = () => <div>Loading...</div>;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If no `.fallback.tsx` is present, nothing is rendered while the chunk loads.
|
|
144
|
+
|
|
145
|
+
### Layout (`.layout.tsx`)
|
|
146
|
+
|
|
147
|
+
Wraps all child routes at the same level. Must render `<Outlet />` where children should appear:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// $router/Index.layout.tsx
|
|
151
|
+
import { Outlet } from "react-router-dom";
|
|
152
|
+
|
|
153
|
+
export const IndexLayout = () => (
|
|
154
|
+
<div>
|
|
155
|
+
<nav>My Nav</nav>
|
|
156
|
+
<Outlet />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 404 Page (`.404.tsx`)
|
|
162
|
+
|
|
163
|
+
Rendered when no child routes match. Compiled to a `{ path: "*" }` catch-all route:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// $router/Index.404.tsx
|
|
167
|
+
import { NavLink } from "react-router-dom";
|
|
168
|
+
|
|
169
|
+
export const Index404 = () => (
|
|
170
|
+
<div>
|
|
171
|
+
<h1>Page not found</h1>
|
|
172
|
+
<NavLink to="/">Go home</NavLink>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Error Page (`.error.tsx`)
|
|
178
|
+
|
|
179
|
+
Used as the `errorElement` for its route level. Catches render errors thrown by the page component or any of its children:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// $router/$products/Products.error.tsx
|
|
183
|
+
export const ProductsError = () => (
|
|
184
|
+
<div>Something went wrong loading products.</div>
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Type-Safe Navigation
|
|
189
|
+
|
|
190
|
+
All navigation APIs accept page components instead of string paths. TypeScript infers the required URL params directly from the component's props — omitting a required param is a compile-time error.
|
|
191
|
+
|
|
192
|
+
### `route(page, params?)`
|
|
193
|
+
|
|
194
|
+
Generates a path string from a page component reference.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { route } from "@enhancd/react-file-router";
|
|
198
|
+
import { AboutPage, ProductLazyPage } from "@router";
|
|
199
|
+
|
|
200
|
+
route(AboutPage) // → "/about"
|
|
201
|
+
route(ProductLazyPage, { productId: "1" }) // → "/products/1"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### `NavLinkToPage`
|
|
205
|
+
|
|
206
|
+
Wrapper around react-router-dom's `NavLink`. All `NavLink` props are supported, including the `className` function that receives `{ isActive }`.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { NavLinkToPage } from "@enhancd/react-file-router";
|
|
210
|
+
import { AboutPage, ProductLazyPage } from "@router";
|
|
211
|
+
|
|
212
|
+
<NavLinkToPage
|
|
213
|
+
to={AboutPage}
|
|
214
|
+
className={({ isActive }) => isActive ? "active" : ""}
|
|
215
|
+
>
|
|
216
|
+
About
|
|
217
|
+
</NavLinkToPage>
|
|
218
|
+
|
|
219
|
+
<NavLinkToPage to={ProductLazyPage} params={{ productId: "1" }}>
|
|
220
|
+
Product 1
|
|
221
|
+
</NavLinkToPage>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `LinkToPage`
|
|
225
|
+
|
|
226
|
+
Wrapper around react-router-dom's `Link`. Use when active-state styling is not needed.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { LinkToPage } from "@enhancd/react-file-router";
|
|
230
|
+
import { AboutPage, ProductLazyPage } from "@router";
|
|
231
|
+
|
|
232
|
+
<LinkToPage to={AboutPage}>About</LinkToPage>
|
|
233
|
+
<LinkToPage to={ProductLazyPage} params={{ productId: "1" }}>Product 1</LinkToPage>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `useNavigateToPage()`
|
|
237
|
+
|
|
238
|
+
Hook that returns a type-safe navigate function. Useful in event handlers, form submissions, and effects.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { useNavigateToPage } from "@enhancd/react-file-router";
|
|
242
|
+
import { AboutPage, ProductLazyPage } from "@router";
|
|
243
|
+
|
|
244
|
+
export const MyComponent = () => {
|
|
245
|
+
const navigateTo = useNavigateToPage();
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<>
|
|
249
|
+
<button onClick={() => navigateTo(AboutPage)}>Go to About</button>
|
|
250
|
+
<button onClick={() => navigateTo(ProductLazyPage, { params: { productId: "1" } })}>
|
|
251
|
+
Go to Product 1
|
|
252
|
+
</button>
|
|
253
|
+
</>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The second argument also accepts all standard `NavigateOptions` from react-router-dom (`replace`, `state`, etc.) alongside `params`.
|
|
259
|
+
|
|
260
|
+
### `NavigateToPage`
|
|
261
|
+
|
|
262
|
+
Declarative redirect component — a wrapper around react-router-dom's `Navigate`. Redirects immediately when rendered.
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { NavigateToPage } from "@enhancd/react-file-router";
|
|
266
|
+
import { AboutPage } from "@router";
|
|
267
|
+
|
|
268
|
+
// Redirect when a condition is met
|
|
269
|
+
if (isLoggedOut) return <NavigateToPage to={AboutPage} replace />;
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Re-exporting Pages (Recommended)
|
|
273
|
+
|
|
274
|
+
Add `index.ts` files to each folder to create a single import point for all page components:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// $router/index.ts
|
|
278
|
+
export * from "./Index.page";
|
|
279
|
+
export * from "./$about";
|
|
280
|
+
export * from "./$products";
|
|
281
|
+
|
|
282
|
+
// $router/$products/index.ts
|
|
283
|
+
export * from "./Products.page";
|
|
284
|
+
export * from "./@productId";
|
|
285
|
+
|
|
286
|
+
// $router/$products/@productId/index.ts
|
|
287
|
+
export * from "./Product.lazy.page";
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Then import everything from the `@router` alias:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { IndexPage, AboutPage, ProductsPage, ProductLazyPage } from "@router";
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Vite Plugin Options
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
reactFileRouterVitePlugin({
|
|
300
|
+
rootDir?: string, // project root (default: vite config root)
|
|
301
|
+
workDir?: string, // subdirectory inside root (default: "src")
|
|
302
|
+
routerDir?: string, // router folder name (default: "$router")
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The plugin looks for routes at `{rootDir}/{workDir}/{routerDir}/`.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Default: {project}/src/$router/
|
|
310
|
+
reactFileRouterVitePlugin()
|
|
311
|
+
|
|
312
|
+
// Custom folder name
|
|
313
|
+
reactFileRouterVitePlugin({ routerDir: "pages" })
|
|
314
|
+
// → {project}/src/pages/
|
|
315
|
+
|
|
316
|
+
// Custom work directory
|
|
317
|
+
reactFileRouterVitePlugin({ workDir: "app" })
|
|
318
|
+
// → {project}/app/$router/
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## How It Works
|
|
322
|
+
|
|
323
|
+
At startup (and on file changes in dev mode) the Vite plugin:
|
|
324
|
+
|
|
325
|
+
1. Scans the `$router` directory recursively to build a route schema from folder and file names.
|
|
326
|
+
2. Generates a virtual module (`virtual:react-file-router-schema`) containing the full `RouteObject[]` array and registers each page component in `window.__ENHANCD_REACT_FILE_ROUTER__` for use by `route()`.
|
|
327
|
+
3. For `.lazy.page.tsx` files, generates a wrapper module that uses `React.lazy` + `React.Suspense` so the actual page code is split into a separate chunk.
|
|
328
|
+
4. Hot-reloads automatically when route files are added, removed, or renamed.
|
|
329
|
+
|
|
330
|
+
The `routerSchema` import you pass to `createBrowserRouter()` resolves to this virtual module at build/dev time — no generated files are written to disk.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createConfig } from "../../eslint.config.mjs";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
export default createConfig(__dirname);
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enhancd/react-file-router",
|
|
3
|
+
"description": "File-based routing for Vite + React apps using react-router-dom V6. Automatically generates routes from a directory structure — no manual RouteObject configuration needed.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Alexandr Maliovaniy <alexandrmaliovaniy@gmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/alexandrmaliovaniy/enhancd/blob/master/packages/react-file-router/README.md"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/alexandrmaliovaniy/enhancd/blob/master/packages/react-file-router/README.md",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"build": "tsc && rollup -c"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.cjs.js",
|
|
18
|
+
"module": "./dist/index.esm.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.esm.js",
|
|
25
|
+
"require": "./dist/index.cjs.js"
|
|
26
|
+
},
|
|
27
|
+
"./vite-plugin": {
|
|
28
|
+
"types": "./dist/vite-plugin.d.ts",
|
|
29
|
+
"import": "./dist/vite-plugin.esm.js",
|
|
30
|
+
"require": "./dist/vite-plugin.cjs.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"react",
|
|
35
|
+
"router",
|
|
36
|
+
"file"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": "*",
|
|
40
|
+
"react-router-dom": "*",
|
|
41
|
+
"fs": "*",
|
|
42
|
+
"vite": "*",
|
|
43
|
+
"path": "*"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
2
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
3
|
+
import esbuild from "rollup-plugin-esbuild";
|
|
4
|
+
import terser from "@rollup/plugin-terser";
|
|
5
|
+
import peerDepsExternal from "rollup-plugin-peer-deps-external";
|
|
6
|
+
import dts from "rollup-plugin-dts";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
|
|
9
|
+
function logBundledModules() {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
name: "log-bundled-modules",
|
|
14
|
+
moduleParsed(info) {
|
|
15
|
+
if (seen.has(info.id)) return;
|
|
16
|
+
seen.add(info.id);
|
|
17
|
+
if (info.id.includes("\0")) return;
|
|
18
|
+
|
|
19
|
+
console.log("[bundled]", info.id);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));
|
|
25
|
+
|
|
26
|
+
const entries = {
|
|
27
|
+
index: "src/index.ts",
|
|
28
|
+
"vite-plugin": "src/lib/vite-plugin/vitePlugin.ts",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const createConfig = (input, output, format) => ({
|
|
32
|
+
input,
|
|
33
|
+
output: {
|
|
34
|
+
file: output,
|
|
35
|
+
format,
|
|
36
|
+
sourcemap: true,
|
|
37
|
+
exports: "named",
|
|
38
|
+
...(format === "esm" && { preserveModules: false }),
|
|
39
|
+
},
|
|
40
|
+
onwarn(warning, warn) {
|
|
41
|
+
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
42
|
+
throw new Error(warning.message);
|
|
43
|
+
}
|
|
44
|
+
warn(warning);
|
|
45
|
+
},
|
|
46
|
+
plugins: [
|
|
47
|
+
logBundledModules(),
|
|
48
|
+
peerDepsExternal(),
|
|
49
|
+
esbuild({
|
|
50
|
+
include: /\.[jt]sx?$/,
|
|
51
|
+
minify: false,
|
|
52
|
+
target: "es2015",
|
|
53
|
+
jsx: "automatic",
|
|
54
|
+
tsconfig: "tsconfig.json",
|
|
55
|
+
}),
|
|
56
|
+
resolve({
|
|
57
|
+
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
58
|
+
}),
|
|
59
|
+
commonjs(),
|
|
60
|
+
terser(),
|
|
61
|
+
],
|
|
62
|
+
external: [
|
|
63
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
64
|
+
/^virtual:/,
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const createDtsConfig = (input, output) => ({
|
|
69
|
+
input,
|
|
70
|
+
output: {
|
|
71
|
+
file: output,
|
|
72
|
+
format: "esm",
|
|
73
|
+
},
|
|
74
|
+
plugins: [
|
|
75
|
+
dts({
|
|
76
|
+
respectExternal: true,
|
|
77
|
+
compilerOptions: {
|
|
78
|
+
declarationMap: false,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
],
|
|
82
|
+
external: [
|
|
83
|
+
/node_modules/,
|
|
84
|
+
...Object.keys(pkg.peerDependencies || {}),
|
|
85
|
+
/^virtual:/,
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const configs = Object.entries(entries).flatMap(([name, input]) => {
|
|
90
|
+
const outputBase = name === "index" ? "dist/index" : `dist/${name}`;
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
createConfig(input, `${outputBase}.esm.js`, "esm"),
|
|
94
|
+
createConfig(input, `${outputBase}.cjs.js`, "cjs"),
|
|
95
|
+
createDtsConfig(input, `${outputBase}.d.ts`),
|
|
96
|
+
];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export default configs;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./lib/reactFileRouter";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./navigate-to-page";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { Navigate, useNavigate, NavigateProps, NavLink, Link, NavLinkProps, LinkProps } from "react-router-dom"
|
|
3
|
+
import { route } from "../reactFileRouter";
|
|
4
|
+
|
|
5
|
+
export const useNavigateToPage = () => {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
return <Page extends (...args: any[]) => ReactElement>(page: Page, params?: Page extends (...args: [infer Args]) => ReactElement ? { params?: Args } & NavigateProps : { params: {} }) => navigate(route(page, params?.params), params);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const NavigateToPage = <Page extends (...args: any[]) => ReactElement>(props: Omit<NavigateProps, "to"> & { to: Page, params?: Page extends (...args: [infer Args]) => ReactElement ? Args : { params: {} }}) => {
|
|
11
|
+
const {to, params: prms, ...prps} = props;
|
|
12
|
+
return <Navigate to={route(to, prms)} {...prps} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const NavLinkToPage = <Page extends (...args: any[]) => ReactElement>(props: Omit<NavLinkProps, "to"> & { to: Page, params?: Page extends (...args: [infer Args]) => ReactElement ? Args : { params: {} }}) => {
|
|
16
|
+
const {to, params: prms, ...prps} = props;
|
|
17
|
+
return <NavLink to={route(to, prms)} {...prps} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const LinkToPage = <Page extends (...args: any[]) => ReactElement>(props: Omit<LinkProps, "to"> & { to: Page, params?: Page extends (...args: [infer Args]) => ReactElement ? Args : { params: {} }}) => {
|
|
21
|
+
const {to, params: prms, ...prps} = props;
|
|
22
|
+
return <Link to={route(to, prms)} {...prps} />;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
import { href } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export const route = <Page extends (...args: any[]) => ReactElement>(page: Page, params?: Page extends (...args: [infer Args]) => ReactElement ? Args : never): string => {
|
|
5
|
+
if (!window) throw new Error("window object not found!");
|
|
6
|
+
if (!("__ENHANCD_REACT_FILE_ROUTER__" in window)) throw new Error("enhancd react router not found! Make sure vite plugin enabled");
|
|
7
|
+
if (!window.__ENHANCD_REACT_FILE_ROUTER__?.has(page)) throw new Error("Page not found!");
|
|
8
|
+
const value = window.__ENHANCD_REACT_FILE_ROUTER__.get(page);
|
|
9
|
+
if (!value) throw new Error("Path not setteled!");
|
|
10
|
+
if (!params) return value;
|
|
11
|
+
return href(value, params);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { reactFileRouterVitePlugin } from "../vitePlugin";
|
|
3
|
+
|
|
4
|
+
describe("Vite plugin", () => {
|
|
5
|
+
it("Init", () => {
|
|
6
|
+
|
|
7
|
+
const routerPluginObj = reactFileRouterVitePlugin();
|
|
8
|
+
|
|
9
|
+
expect(routerPluginObj.name).toEqual("enhancd-react-file-router");
|
|
10
|
+
expect(routerPluginObj.enforce).toEqual("pre");
|
|
11
|
+
expect(typeof routerPluginObj.configResolved).toBe("function");
|
|
12
|
+
expect(typeof routerPluginObj.resolveId).toBe("function");
|
|
13
|
+
expect(typeof routerPluginObj.load).toBe("function");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/lib/vite-plugin/__tests__/src/$router/$named/$declaration/NamedDeclaration.page.tsx
ADDED
|
File without changes
|
package/src/lib/vite-plugin/__tests__/src/$router/$query@idx/$nested@param/QueryNested.page.tsx
ADDED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
|
|
5
|
+
export type FolderSchema = {
|
|
6
|
+
folderPath: string;
|
|
7
|
+
files: {
|
|
8
|
+
page?: {
|
|
9
|
+
id: string;
|
|
10
|
+
path: string;
|
|
11
|
+
fileName: string;
|
|
12
|
+
componentName: string;
|
|
13
|
+
realtivePath: string;
|
|
14
|
+
importString: string;
|
|
15
|
+
export: "named" | "default";
|
|
16
|
+
}
|
|
17
|
+
"lazy.page"?: {
|
|
18
|
+
id: string;
|
|
19
|
+
path: string;
|
|
20
|
+
fileName: string;
|
|
21
|
+
componentName: string;
|
|
22
|
+
realtivePath: string;
|
|
23
|
+
importString: string;
|
|
24
|
+
export: "named" | "default";
|
|
25
|
+
},
|
|
26
|
+
layout?: {
|
|
27
|
+
id: string;
|
|
28
|
+
path: string;
|
|
29
|
+
fileName: string;
|
|
30
|
+
componentName: string;
|
|
31
|
+
importString: string;
|
|
32
|
+
export: "named" | "default";
|
|
33
|
+
},
|
|
34
|
+
error?: {
|
|
35
|
+
id: string;
|
|
36
|
+
path: string;
|
|
37
|
+
fileName: string;
|
|
38
|
+
componentName: string;
|
|
39
|
+
importString: string;
|
|
40
|
+
export: "named" | "default";
|
|
41
|
+
},
|
|
42
|
+
fallback?: {
|
|
43
|
+
id: string;
|
|
44
|
+
path: string;
|
|
45
|
+
fileName: string;
|
|
46
|
+
componentName: string;
|
|
47
|
+
importString: string;
|
|
48
|
+
export: "named" | "default";
|
|
49
|
+
},
|
|
50
|
+
"404"?: {
|
|
51
|
+
id: string;
|
|
52
|
+
path: string;
|
|
53
|
+
fileName: string;
|
|
54
|
+
componentName: string;
|
|
55
|
+
importString: string;
|
|
56
|
+
export: "named" | "default";
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
subroutes: FolderSchema[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const getFunctionExportType = (fnName: string, rawFile: string): "named" | "default" => {
|
|
64
|
+
if (new RegExp(`export\\s+function\\s+${fnName}`).test(rawFile)) return "named";
|
|
65
|
+
if (new RegExp(`export\\s+const\\s+${fnName}\\s*=\\s*\\(`).test(rawFile)) return "named";
|
|
66
|
+
if (new RegExp(`const\\s+${fnName}\\s*=\\s*function`).test(rawFile) && new RegExp(`export\\s*{[A-Za-z0-9\\s,]*${rawFile}`).test(rawFile)) return "named";
|
|
67
|
+
if (new RegExp(`const\\s+${fnName}\\s*=\\s*\\(`).test(rawFile) && new RegExp(`export\\s*{[A-Za-z0-9\\s,]*${rawFile}`).test(rawFile)) return "named";
|
|
68
|
+
|
|
69
|
+
if (new RegExp(`export\\s+default\\s+function\\s+${fnName}`).test(rawFile)) return "default";
|
|
70
|
+
if (new RegExp("export\\s+default\\s+function").test(rawFile)) return "default";
|
|
71
|
+
if (new RegExp(`const\\s+${fnName}\\s*=\\s*function`).test(rawFile) && new RegExp(`export\\s+default\\s+${rawFile}`).test(rawFile)) return "default";
|
|
72
|
+
if (new RegExp(`const\\s+${fnName}\\s*=\\s*\\(`).test(rawFile) && new RegExp(`export\\s+default\\s+${rawFile}`).test(rawFile)) return "default";
|
|
73
|
+
if (new RegExp(`function\\s+${fnName}`).test(rawFile) && new RegExp(`export\\s+default\\s+${rawFile}`).test(rawFile)) return "default";
|
|
74
|
+
|
|
75
|
+
return "default";
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fileNametoComponentName = (name: string) => {
|
|
79
|
+
return name.split(".").slice(0, -1).map(e => `${e.charAt(0).toUpperCase()}${e.slice(1)}`).join("");
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const fileImportString = (componentName: string, id: string, path: string, exportType: "named" | "default", lazy = false) => {
|
|
83
|
+
if (lazy && exportType === "named") return `const Lazy${componentName} = React.lazy(() => import("${path}").then(module => { return { default: module.${componentName} } }));`;
|
|
84
|
+
if (lazy && exportType === "default") return `const Lazy${componentName} = React.lazy(() => import("${path}"));`;
|
|
85
|
+
if (exportType === "named") return `import { ${componentName} as ${id} } from "${path}";`;
|
|
86
|
+
return `import ${id} from "${path}";`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const compileFolderToSchema = (folderPath: string, routerPath = folderPath): FolderSchema => {
|
|
90
|
+
if (!fs.existsSync(folderPath)) throw new Error(`Failed to run react-file-router vite plugin. Folder "${folderPath}" doesn't exist`);
|
|
91
|
+
const dirContent = fs.readdirSync(folderPath);
|
|
92
|
+
const subroutes = [];
|
|
93
|
+
const files: FolderSchema["files"] = { };
|
|
94
|
+
for (const itemName of dirContent) {
|
|
95
|
+
const itemPath = path.join(folderPath, itemName);
|
|
96
|
+
const itemStat = fs.statSync(itemPath);
|
|
97
|
+
if (itemStat.isDirectory() && (itemName.startsWith("@") || itemName.startsWith("$"))) subroutes.push(itemPath);
|
|
98
|
+
if (!itemStat.isFile()) continue;
|
|
99
|
+
const match = itemName.match(/\.(?<type>lazy.page|page|error|layout|fallback|404)\./);
|
|
100
|
+
if (!match || !match.groups) continue;
|
|
101
|
+
const type = match.groups.type as keyof FolderSchema["files"];
|
|
102
|
+
const componentName = fileNametoComponentName(itemName);
|
|
103
|
+
const rawFile = fs.readFileSync(itemPath, "utf-8");
|
|
104
|
+
const exportType = "named" as const;
|
|
105
|
+
|
|
106
|
+
const relativePath = path.relative(routerPath, folderPath).replace(/\\/g, "/").replace(/\\/g, "/").replace(/@/g, "/:").replace(/\$/g, "/").replaceAll(/\/+/g, "/") || "/";
|
|
107
|
+
const itemSchema = { path: itemPath.replace(/\\/g, "/"), id: `A${crypto.createHash("sha256").update(`${relativePath}/${itemName}`, "utf-8").digest("hex")}`, fileName: itemName, componentName, realtivePath: relativePath, importString: "", export: exportType };
|
|
108
|
+
|
|
109
|
+
itemSchema.importString = fileImportString(itemSchema.componentName, itemSchema.id, itemSchema.path, exportType);
|
|
110
|
+
if (type === "lazy.page") {
|
|
111
|
+
files["page"] = itemSchema;
|
|
112
|
+
files["lazy.page"] = { ...itemSchema, path: `virtual:react-file-router/${itemSchema.id}.tsx` };
|
|
113
|
+
files["lazy.page"].importString = fileImportString(files["lazy.page"].componentName, files["lazy.page"].id, files["lazy.page"].path, files["lazy.page"].export, true);
|
|
114
|
+
} else {
|
|
115
|
+
files[type] = itemSchema;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
folderPath,
|
|
122
|
+
files,
|
|
123
|
+
subroutes: subroutes.map(subroutePath => compileFolderToSchema(subroutePath, routerPath))
|
|
124
|
+
};
|
|
125
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { FolderSchema } from "../compile-folder-to-schema/compileFolderToSchema";
|
|
3
|
+
|
|
4
|
+
const generatePageFile = (file: FolderSchema["files"]) => {
|
|
5
|
+
return `import * as React from "react";
|
|
6
|
+
${file["fallback"] ? file["fallback"].importString : ""}
|
|
7
|
+
${file["lazy.page"]?.importString}
|
|
8
|
+
|
|
9
|
+
${file["page"]?.export === "default" ? "export default" : "export"} const ${file.page?.componentName} = (args = {}) => {
|
|
10
|
+
return React.createElement(
|
|
11
|
+
React.Suspense,
|
|
12
|
+
{ fallback: React.createElement(${file["fallback"] ? file["fallback"].id : "() => null"}, null) },
|
|
13
|
+
React.createElement(Lazy${file["page"]?.componentName}, { ...args })
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const compileLazyPages = (schema: FolderSchema, rootFolder: string): Record<string, { rawFile: string, lazy: boolean, linkedFile: string }> => {
|
|
21
|
+
const out: Record<string, { rawFile: string, lazy: boolean, linkedFile: string }> = {};
|
|
22
|
+
if (schema.files["lazy.page"] && schema.files["page"]) {
|
|
23
|
+
const rawFile = fs.readFileSync(schema.files["page"].path, "utf-8");
|
|
24
|
+
out[schema.files["lazy.page"].path] = { lazy: true, rawFile, linkedFile: schema.files["page"].path };
|
|
25
|
+
out[schema.files["page"].path] = { lazy: false, rawFile: generatePageFile(schema.files), linkedFile: schema.files["lazy.page"].path };
|
|
26
|
+
}
|
|
27
|
+
return schema.subroutes.reduce((acc, el) => {
|
|
28
|
+
return { ...acc, ...compileLazyPages(el, rootFolder) };
|
|
29
|
+
}, out);
|
|
30
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { FolderSchema } from "../compile-folder-to-schema/compileFolderToSchema";
|
|
4
|
+
import { routerSchema } from "src/lib/reactFileRouter";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const compileImports = (schema: FolderSchema, rootDir: string): string => {
|
|
8
|
+
const out: string[] = [];
|
|
9
|
+
const fileTypes = Object.keys(schema.files) as `${keyof FolderSchema["files"]}`[];
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
fileTypes.forEach(fileType => {
|
|
13
|
+
const file = schema.files[fileType];
|
|
14
|
+
if (["lazy.page", "fallback"].includes(fileType) || !file?.importString) return;
|
|
15
|
+
out.push(file?.importString);
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return [...out, ...schema.subroutes.map(subroutSchema => compileImports(subroutSchema, rootDir))].join("\n").replace(/^\n/gm, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const compileRoutes = (schema: FolderSchema, rootFolder: string): typeof routerSchema => {
|
|
23
|
+
if (!schema.files.page && !schema.files[404] && !schema.files.layout) return [];
|
|
24
|
+
|
|
25
|
+
const element = schema.files.page ? schema.files.page.realtivePath.search(":") > 0 ? `React.createElement(() => React.createElement(${schema.files.page.id}, useParams()), null)` : `React.createElement(${schema.files.page.id}, null)` : "null";
|
|
26
|
+
const notFoundPage = schema.files[404] ? { path: "*", element: `React.createElement(${schema.files[404].id}, null)` } : {};
|
|
27
|
+
if (!schema.files.layout) {
|
|
28
|
+
return [{
|
|
29
|
+
path: schema.files.page?.realtivePath || "",
|
|
30
|
+
errorElement: schema.files.error ? `React.createElement(${schema.files.error.id}, null)` : undefined,
|
|
31
|
+
element,
|
|
32
|
+
}, notFoundPage, ...schema.subroutes.map(subrouteSchema => compileRoutes(subrouteSchema, rootFolder)).flat(1)];
|
|
33
|
+
}
|
|
34
|
+
return [{
|
|
35
|
+
path: schema.files.page?.realtivePath || "",
|
|
36
|
+
element: `React.createElement(${schema.files.layout.id}, null)`,
|
|
37
|
+
errorElement: schema.files.error ? `React.createElement(${schema.files.error.id}, null)` : undefined,
|
|
38
|
+
children: [{ path: "", element }, notFoundPage, ...schema.subroutes.map(subrouteSchema => compileRoutes(subrouteSchema, rootFolder)).flat(1)]
|
|
39
|
+
}]
|
|
40
|
+
}
|
|
41
|
+
const compilePathInjects = (schema: FolderSchema, rootFolder: string): string => {
|
|
42
|
+
const out = [];
|
|
43
|
+
|
|
44
|
+
if (schema.files.page) out.push(`window.__ENHANCD_REACT_FILE_ROUTER__.set(${schema.files.page.id}, "${schema.files.page.realtivePath}");`);
|
|
45
|
+
|
|
46
|
+
return [...out, ...schema.subroutes.map(subroutSchema => compilePathInjects(subroutSchema, rootFolder))].join("\n").replace(/^\n/gm, "");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const compileSchemaToRouterFile = (schema: FolderSchema, routerFolder: string, rootFolder: string): string => {
|
|
50
|
+
if (!fs.existsSync(routerFolder)) throw new Error(`Failed to run react-file-router vite plugin. Folder "${routerFolder}" doesn't exist`);
|
|
51
|
+
|
|
52
|
+
const imports = compileImports(schema, rootFolder);
|
|
53
|
+
const routes = JSON.stringify(compileRoutes(schema, routerFolder), null, 2).replace(/"(React\.createElement\(.+, null\))"/g, "$1");
|
|
54
|
+
|
|
55
|
+
return `import * as React from "react";
|
|
56
|
+
import { useParams } from "react-router-dom"
|
|
57
|
+
${imports}
|
|
58
|
+
|
|
59
|
+
export default ${routes}
|
|
60
|
+
|
|
61
|
+
window.__ENHANCD_REACT_FILE_ROUTER__ = new Map();
|
|
62
|
+
${compilePathInjects(schema, rootFolder)}`;
|
|
63
|
+
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { transform } from "@babel/standalone";
|
|
2
|
+
|
|
3
|
+
export const transpileLazyPage = async (filename: string, source: string, resolve: (idString: string) => Promise<{ id: string } | null>) => {
|
|
4
|
+
const imports = Array.from(source.matchAll(/^\s*import.+"(?<id>.+)".+/mg));
|
|
5
|
+
const resolvedImports = await Promise.all(imports.map(async match => {
|
|
6
|
+
if (!match.groups?.id) return "";
|
|
7
|
+
const resolvedId = (await resolve(match.groups.id))?.id;
|
|
8
|
+
|
|
9
|
+
if (typeof resolvedId !== "string") return "";
|
|
10
|
+
return match[0].replace(match[1], resolvedId);
|
|
11
|
+
}));
|
|
12
|
+
const lastImportelement = imports.at(-1);
|
|
13
|
+
const codeWithoutImports = lastImportelement ? source.slice(lastImportelement.index + lastImportelement[0].length, -1) : source;
|
|
14
|
+
|
|
15
|
+
const transpiledCode = transform(codeWithoutImports, {
|
|
16
|
+
presets: [["react", {
|
|
17
|
+
pragma: "React.createElement",
|
|
18
|
+
pragmaFrag: "React.Fragment"
|
|
19
|
+
}],
|
|
20
|
+
"typescript"],
|
|
21
|
+
filename,
|
|
22
|
+
plugins: [["transform-react-jsx", { runtime: "automatic" }]]
|
|
23
|
+
}).code;
|
|
24
|
+
|
|
25
|
+
if (!transpiledCode) throw Error(`Babel could transpile file: ${filename}`);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
return resolvedImports.join("") + transpiledCode;
|
|
29
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { Plugin, ResolvedConfig, ViteDevServer } from "vite";
|
|
4
|
+
import { compileFolderToSchema } from "./compile-folder-to-schema/compileFolderToSchema";
|
|
5
|
+
import { compileSchemaToRouterFile } from "./compile-schema-to-router-file/compileSchemaToRouterFile";
|
|
6
|
+
import { compileLazyPages } from "./compile-lazy-pages/compileLazyPages";
|
|
7
|
+
import { transpileLazyPage } from "./transpile-lazy-page/transpile-lazy-page";
|
|
8
|
+
|
|
9
|
+
export function reactFileRouterVitePlugin(params?: { rootDir?: string, routerDir?: string, workDir?: string }): Plugin {
|
|
10
|
+
let routerFile: string;
|
|
11
|
+
let lazyPagesMap: Record<string, { rawFile: string, lazy: boolean, linkedFile: string }>;
|
|
12
|
+
let config: ResolvedConfig;
|
|
13
|
+
let server: ViteDevServer;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: "enhancd-react-file-router",
|
|
17
|
+
enforce: "pre" as const,
|
|
18
|
+
async configResolved(cfg) {
|
|
19
|
+
config = cfg;
|
|
20
|
+
const rootDir = params?.rootDir ?? cfg.root;
|
|
21
|
+
const routerDir = path.join(rootDir, params?.workDir ?? "src", params?.routerDir ?? "$router");
|
|
22
|
+
if (!fs.existsSync(routerDir)) throw new Error(`Folder "${routerDir}" doesn't exist!`);
|
|
23
|
+
const schema = compileFolderToSchema(routerDir);
|
|
24
|
+
routerFile = compileSchemaToRouterFile(schema, routerDir, rootDir);
|
|
25
|
+
lazyPagesMap = compileLazyPages(schema, rootDir);
|
|
26
|
+
},
|
|
27
|
+
configureServer(viteServer) {
|
|
28
|
+
server = viteServer;
|
|
29
|
+
},
|
|
30
|
+
resolveId(id: string) {
|
|
31
|
+
if (id.startsWith("virtual:react-file-router-schema")) return "\0" + id;
|
|
32
|
+
if (lazyPagesMap[id] && lazyPagesMap[id].lazy) return "\0" + id;
|
|
33
|
+
},
|
|
34
|
+
async load(id: string) {
|
|
35
|
+
if (id.includes("virtual:react-file-router-schema")) return routerFile;
|
|
36
|
+
const pagePath = id.replace("\0", "");
|
|
37
|
+
const page = lazyPagesMap[pagePath];
|
|
38
|
+
|
|
39
|
+
if (page && page.lazy) {
|
|
40
|
+
return transpileLazyPage(id, page.rawFile, (source: string) => this.resolve(source, page.linkedFile));
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
if (page && !page.lazy) return page.rawFile;
|
|
44
|
+
},
|
|
45
|
+
watchChange(id, change) {
|
|
46
|
+
const rootDir = params?.rootDir ?? config.root;
|
|
47
|
+
const routerDir = path.join(rootDir, params?.workDir ?? "src", params?.routerDir ?? "$router");
|
|
48
|
+
const relPath = path.relative(routerDir, id);
|
|
49
|
+
|
|
50
|
+
if (relPath.startsWith("..")) return;
|
|
51
|
+
const schema = compileFolderToSchema(routerDir);
|
|
52
|
+
routerFile = compileSchemaToRouterFile(schema, routerDir, rootDir);
|
|
53
|
+
lazyPagesMap = compileLazyPages(schema, rootDir);
|
|
54
|
+
const virtualModule = server.moduleGraph.getModuleById("\0virtual:react-file-router-schema");
|
|
55
|
+
if (virtualModule) server.moduleGraph.invalidateModule(virtualModule);
|
|
56
|
+
const module = lazyPagesMap[id]?.linkedFile ? server.moduleGraph.getModuleById(`\0${lazyPagesMap[id]?.linkedFile}`) : server.moduleGraph.getModuleById(`\0${id}`);
|
|
57
|
+
if (module) {
|
|
58
|
+
server.moduleGraph.invalidateModule(module);
|
|
59
|
+
module.importers.forEach(importer => {
|
|
60
|
+
server.moduleGraph.invalidateModule(importer);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
server.ws.send({
|
|
64
|
+
type: "full-reload",
|
|
65
|
+
path: "*"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
type RouteSchema = Array<{
|
|
3
|
+
path: string;
|
|
4
|
+
element?: React.ReactNode;
|
|
5
|
+
children: RouteSchema[];
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
declare module "virtual:react-file-router-schema" {
|
|
9
|
+
export default schema as RouteSchema;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare const window: {
|
|
13
|
+
__ENHANCD_REACT_FILE_ROUTER__?: Map<object, string>;
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
|
8
|
+
"emitDeclarationOnly": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"noEmit": false,
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
|
|
17
|
+
"references": [],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"vite.config.ts",
|
|
20
|
+
"vite.config.mts",
|
|
21
|
+
"vitest.config.ts",
|
|
22
|
+
"vitest.config.mts",
|
|
23
|
+
"src/**/*.test.ts",
|
|
24
|
+
"src/**/*.spec.ts",
|
|
25
|
+
"src/**/*.test.tsx",
|
|
26
|
+
"src/**/*.spec.tsx",
|
|
27
|
+
"src/**/*.test.js",
|
|
28
|
+
"src/**/*.spec.js",
|
|
29
|
+
"src/**/*.test.jsx",
|
|
30
|
+
"src/**/*.spec.jsx"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./out-tsc/vitest",
|
|
5
|
+
"types": [
|
|
6
|
+
"vitest/globals",
|
|
7
|
+
"vitest/importMeta",
|
|
8
|
+
"vite/client",
|
|
9
|
+
"node",
|
|
10
|
+
"vitest"
|
|
11
|
+
],
|
|
12
|
+
"module": "esnext",
|
|
13
|
+
"noEmit": false,
|
|
14
|
+
"moduleResolution": "bundler",
|
|
15
|
+
"forceConsistentCasingInFileNames": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"vite.config.ts",
|
|
19
|
+
"vite.config.mts",
|
|
20
|
+
"vitest.config.ts",
|
|
21
|
+
"vitest.config.mts",
|
|
22
|
+
"src/**/*.test.ts",
|
|
23
|
+
"src/**/*.spec.ts",
|
|
24
|
+
"src/**/*.test.tsx",
|
|
25
|
+
"src/**/*.spec.tsx",
|
|
26
|
+
"src/**/*.test.js",
|
|
27
|
+
"src/**/*.spec.js",
|
|
28
|
+
"src/**/*.test.jsx",
|
|
29
|
+
"src/**/*.spec.jsx",
|
|
30
|
+
"src/**/*.d.ts"
|
|
31
|
+
],
|
|
32
|
+
"references": [
|
|
33
|
+
{
|
|
34
|
+
"path": "./tsconfig.lib.json"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
export default defineConfig(() => ({
|
|
4
|
+
root: __dirname,
|
|
5
|
+
cacheDir: "../../node_modules/.vite/packages/react-file-router",
|
|
6
|
+
plugins: [],
|
|
7
|
+
test: {
|
|
8
|
+
name: "react-file-router",
|
|
9
|
+
watch: false,
|
|
10
|
+
globals: true,
|
|
11
|
+
environment: "node",
|
|
12
|
+
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
13
|
+
reporters: ["default"],
|
|
14
|
+
coverage: {
|
|
15
|
+
reportsDirectory: "./test-output/vitest/coverage",
|
|
16
|
+
provider: "v8" as const,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}));
|