next-vite-router 1.0.7 → 1.1.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,248 +1,279 @@
1
- # Next Vite Router
1
+ # Next Vite Router
2
2
 
3
- Next Vite Router is an npm package that provides Next.js-style, file-based routing for React applications built with Vite. It generates React Router route objects from a filesystem layout (pages, layouts, not-found), and it exposes a Vite plugin that injects a virtual module to generate routes at build/dev time.
3
+ File-based routing for React + Vite.
4
4
 
5
- ## Installation
5
+ Define routes with folders and let the package generate the
6
+ [React Router](https://reactrouter.com/) `RouteObject[]`. It does not replace
7
+ React Router, navigation, history, loaders, or application state.
6
8
 
7
- Install with your package manager of choice:
9
+ ## Start here
8
10
 
9
11
  ```bash
10
- pnpm add next-vite-router
11
- # or
12
- npm install next-vite-router
13
- # or
14
- yarn add next-vite-router
12
+ npm create vite@latest my-app -- --template react-ts
13
+ cd my-app
14
+ npm install next-vite-router react-router-dom
15
15
  ```
16
16
 
17
- The package has peer dependencies: React, React DOM and React Router DOM. Make sure your project already includes compatible versions (React 18+ is recommended).
17
+ Add the plugin to `vite.config.ts`:
18
18
 
19
- ## CLI / Getting Started
20
-
21
- The quickest way to start a new project with `next-vite-router` is to use the interactive CLI:
19
+ ```ts
20
+ import { defineConfig } from "vite";
21
+ import react from "@vitejs/plugin-react";
22
+ import { nextViteRouter } from "next-vite-router/plugin";
22
23
 
23
- ```bash
24
- npx next-vite-router init
25
- # or
26
- pnpm dlx next-vite-router init
27
- # or
28
- yarn dlx next-vite-router init
29
- # or
30
- bunx next-vite-router init
24
+ export default defineConfig({
25
+ plugins: [react(), nextViteRouter()],
26
+ });
31
27
  ```
32
28
 
33
- The CLI will prompt you for:
34
-
35
- 1. **Project name** name of the folder to create
36
- 2. **Template** which React Vite variant to use:
37
- - `react-ts` (React + TypeScript)
38
- - `react-swc-ts` (React + SWC + TypeScript)
39
- - `react-compiler-ts` (React Compiler + TypeScript)
40
- 3. **Src folder** whether to keep source files in `src/` (recommended) or place them at the project root
41
- 4. **Tailwind CSS** whether to add and configure Tailwind CSS (with `@tailwindcss/vite`)
29
+ Create your route tree:
30
+
31
+ ```text
32
+ src/app/
33
+ ├── page.tsx
34
+ ├── about/
35
+ │ └── page.tsx
36
+ └── users/
37
+ ├── page.tsx
38
+ └── [id]/
39
+ └── page.tsx
40
+ ```
42
41
 
43
- ### What the CLI does
42
+ This generates:
44
43
 
45
- The CLI automates the entire setup:
44
+ ```text
45
+ /
46
+ /about
47
+ /users
48
+ /users/:id
49
+ ```
46
50
 
47
- - Scaffolds a new Vite + React project using `create-vite`
48
- - Installs `next-vite-router` and `react-router-dom@latest`
49
- - Configures the Vite plugin in `vite.config.ts`
50
- - Sets up path aliases (`@/*`) in `tsconfig.json` and Vite config
51
- - Creates TypeScript declarations for the virtual module (`virtual:next-vite-router`)
52
- - Generates example pages (`app/page.tsx`, `app/layout.tsx`, `app/example/page.tsx`)
53
- - Optionally configures Tailwind CSS with `@tailwindcss/vite` and creates `global.css`
51
+ Use the generated router inside a normal React Router provider:
54
52
 
55
- After the CLI finishes:
53
+ ```tsx
54
+ // src/App.tsx
55
+ import { Router } from "virtual:next-vite-router";
56
56
 
57
- ```bash
58
- cd <your-project-name>
59
- pnpm dev
57
+ export default function App() {
58
+ return <Router />;
59
+ }
60
60
  ```
61
61
 
62
- Your app is ready with file-based routing!
63
-
64
- ## Quick Start Vite plugin
65
-
66
- The recommended and easiest way to use this library in a Vite app is via the Vite plugin. The plugin exposes a virtual module `virtual:next-vite-router` that provides a `generateRoutes()` function and re-exports `useRoutes` from `react-router-dom` for convenience.
62
+ ```tsx
63
+ // src/main.tsx
64
+ import { StrictMode } from "react";
65
+ import { createRoot } from "react-dom/client";
66
+ import { BrowserRouter } from "react-router-dom";
67
+ import App from "./App";
67
68
 
68
- Example `vite.config.ts`:
69
+ createRoot(document.getElementById("root")!).render(
70
+ <StrictMode>
71
+ <BrowserRouter>
72
+ <App />
73
+ </BrowserRouter>
74
+ </StrictMode>,
75
+ );
76
+ ```
69
77
 
70
- ```ts
71
- import { defineConfig } from 'vite';
72
- import react from '@vitejs/plugin-react';
73
- import { nextViteRouter } from 'next-vite-router';
78
+ That is the complete setup. Run:
74
79
 
75
- export default defineConfig({
76
- plugins: [
77
- react(),
78
- nextViteRouter({ pagesDir: 'src/pages' })
79
- ]
80
- });
80
+ ```bash
81
+ npm run dev
81
82
  ```
82
83
 
83
- Example usage inside your app. **Important: generate routes once at module level** (not inside the component) to avoid infinite re-renders.
84
+ ## Use React Router directly
84
85
 
85
- File: `src/App.tsx`
86
+ `Router` is only a convenience wrapper around
87
+ `useRoutes(generateRoutes())`. You can keep using React Router APIs:
86
88
 
87
89
  ```tsx
88
- import { useRoutes } from 'react-router-dom';
89
- import { generateRoutes } from 'virtual:next-vite-router';
90
+ import {
91
+ Link,
92
+ generateRoutes,
93
+ useRoutes,
94
+ } from "virtual:next-vite-router";
95
+ import { useNavigate, useParams } from "react-router-dom";
90
96
 
91
97
  const routes = generateRoutes();
92
98
 
93
99
  export default function App() {
94
100
  const element = useRoutes(routes);
95
- return element;
101
+ return (
102
+ <>
103
+ <Link to="/users">Users</Link>
104
+ {element}
105
+ </>
106
+ );
96
107
  }
97
108
  ```
98
109
 
99
- File: `src/main.tsx`
110
+ Use `useNavigate`, `useParams`, loaders, actions, and other React Router APIs
111
+ from `react-router-dom`. The package intentionally does not create competing
112
+ navigation or data-loading APIs.
113
+
114
+ ## File conventions
115
+
116
+ | File or folder | Result |
117
+ | --- | --- |
118
+ | `page.tsx` | Route page |
119
+ | `layout.tsx` | Nested layout rendered with React Router's `Outlet` |
120
+ | `not-found.tsx` | `*` route for that subtree |
121
+ | `[id]` | Dynamic segment `:id` |
122
+ | `[...slug]` | Catch-all `*` segment |
123
+ | `[[...slug]]` | Optional catch-all `*` segment |
124
+ | `(admin)` | Route group omitted from the URL |
125
+
126
+ Layouts are the recommended way to add route-scoped navigation, providers, or
127
+ authorization:
128
+
129
+ ```text
130
+ src/app/
131
+ ├── layout.tsx
132
+ └── dashboard/
133
+ ├── layout.tsx
134
+ └── page.tsx
135
+ ```
100
136
 
101
137
  ```tsx
102
- import { StrictMode } from 'react';
103
- import { createRoot } from 'react-dom/client';
104
- import { BrowserRouter } from 'react-router-dom';
105
- import App from './App';
106
-
107
- createRoot(document.getElementById('root')!).render(
108
- <StrictMode>
109
- <BrowserRouter>
110
- <App />
111
- </BrowserRouter>
112
- </StrictMode>
113
- );
138
+ import { Outlet } from "react-router-dom";
139
+
140
+ export default function DashboardLayout() {
141
+ return (
142
+ <AuthGuard>
143
+ <Outlet />
144
+ </AuthGuard>
145
+ );
146
+ }
114
147
  ```
115
148
 
116
- **Key points:**
149
+ ## Type-safe paths
117
150
 
118
- - Call `generateRoutes()` once at module level, not inside the component (prevents infinite re-renders).
119
- - Wrap your app with `BrowserRouter` at the entry point (`main.tsx`), not inside `App`.
120
- - `pagesDir` (default: `src/app`) is the directory where you organize your **route files** (`page.tsx`, `layout.tsx`, `not-found.tsx`). It has nothing to do with the `App.tsx` component.
121
- - The virtual module is `virtual:next-vite-router` (the plugin resolves it internally).
151
+ The Vite plugin generates `virtual-next-vite-router.d.ts` automatically:
122
152
 
123
- **Example file structure:**
153
+ ```tsx
154
+ import type {
155
+ RouteParams,
156
+ RoutePath,
157
+ } from "virtual:next-vite-router";
124
158
 
125
- ```
126
- src/
127
- ├── App.tsx ← Your main app component
128
- ├── main.tsx ← Entry point (with BrowserRouter)
129
- └── app/ ← pagesDir (routes are organized here)
130
- ├── page.tsx ← Home page
131
- ├── layout.tsx ← Root layout
132
- └── dashboard/
133
- ├── page.tsx ← /dashboard page
134
- └── layout.tsx ← Dashboard layout
159
+ const path = "/users/:id" satisfies RoutePath;
160
+ const params: RouteParams<typeof path> = { id: "123" };
135
161
  ```
136
162
 
137
- TypeScript note:
163
+ Invalid paths and missing dynamic parameters are reported by TypeScript.
164
+ No manual declaration for the virtual module is required.
138
165
 
139
- If you're using TypeScript in a consuming project, add a declaration file so the virtual module is recognized by the compiler. Create a file such as `src/virtual.d.ts` (or any `*.d.ts` included by your `tsconfig.json`) with the following content:
166
+ ## Lazy loading
140
167
 
141
- ```ts
142
- declare module 'virtual:next-vite-router' {
143
- import type { RouteObject } from 'react-router-dom';
168
+ Pages and `not-found.tsx` files use Vite's non-eager `import.meta.glob`, so
169
+ Vite can emit separate chunks for pages. Layouts are eager because they wrap
170
+ the generated route tree. Lazy loading is enabled by default:
144
171
 
145
- export const generateRoutes: () => RouteObject[];
146
- export { useRoutes } from 'react-router-dom';
147
- }
148
- ```
149
-
150
- This ensures TypeScript knows the shape of the virtual module provided by the Vite plugin and avoids `Cannot find module 'virtual:next-vite-router'` errors in editors and builds.
172
+ ```tsx
173
+ import { configureRouter } from "next-vite-router";
151
174
 
152
- ## Programmatic route generation
175
+ configureRouter({
176
+ loadingFallback: () => <div>Loading route...</div>,
177
+ });
178
+ ```
153
179
 
154
- If you prefer to generate routes manually (for custom setups), the package exports helpers you can use with `import.meta.glob`.
180
+ `enableLazyLoading: false` is available for compatibility and testing. It does
181
+ not turn Vite's asynchronous imports into synchronous imports.
155
182
 
156
- Example:
183
+ ## Optional middleware
157
184
 
158
- ```ts
159
- import { createRouteGenerator } from 'next-vite-router';
185
+ Prefer layouts for route-scoped behavior. `registerMiddleware` is an optional
186
+ escape hatch for cross-cutting wrappers:
160
187
 
161
- // in a Vite environment
162
- const pages = import.meta.glob('../../pages/**/page.tsx');
163
- const layouts = import.meta.glob('../../pages/**/layout.tsx', { eager: true });
164
- const notFounds = import.meta.glob('../../pages/**/not-found.tsx');
188
+ ```tsx
189
+ import { registerMiddleware } from "next-vite-router";
165
190
 
166
- const generateRoutes = createRouteGenerator(pages, layouts, notFounds);
167
- const routes = generateRoutes();
191
+ registerMiddleware(/^\/dashboard/, (element) => (
192
+ <AuthGuard>{element}</AuthGuard>
193
+ ));
168
194
  ```
169
195
 
170
- Use this when you need a fully custom glob layout or want to call the generator at runtime.
196
+ Middleware wraps generated route elements; it does not create routes or
197
+ replace React Router.
198
+
199
+ ## CLI
171
200
 
172
- ## API Reference
201
+ For an interactive setup:
173
202
 
174
- Exports from the package (see `src/index.ts`):
203
+ ```bash
204
+ npx create-next-vite-router init
205
+ ```
175
206
 
176
- - `createRouteGenerator(pages, layouts, notFounds)` returns a function that builds `RouteObject[]` using your globs.
177
- - `buildRouteTree` builds the intermediate route tree from page/layout/not-found globs.
178
- - `treeToRoutes` converts the route tree to React Router routes.
179
- - `registerMiddleware`, `clearMiddlewares` middleware helpers to manage registered middlewares.
180
- - `configureRouter` configure router-level options.
181
- - `nextViteRouter(options)` Vite plugin factory. Options:
182
- - `pagesDir?: string` (default: `src/app`)
183
- - `pageFile?: string` (default: `page.tsx`)
184
- - `layoutFile?: string` (default: `layout.tsx`)
185
- - `notFoundFile?: string` (default: `not-found.tsx`)
186
- - Types exported: `RouterConfig`, `RouteNode`, `RouteMiddleware`, `NextViteRouterPluginOptions`.
207
+ The CLI creates a Vite React project, installs the package and
208
+ `react-router-dom@^7.0.0`, configures the plugin, and adds an example route
209
+ tree.
187
210
 
188
- ### `configureRouter(config: RouterConfig)`
211
+ ## Compatibility
189
212
 
190
- Use `configureRouter` to set global router options for runtime behavior. The function merges the provided `config` with any existing configuration.
213
+ | Package | Supported versions |
214
+ | --- | --- |
215
+ | React / React DOM | `>=18 <20` |
216
+ | React Router DOM | `>=6 <8` |
217
+ | Vite | `>=5 <9` |
191
218
 
192
- Common `RouterConfig` fields:
219
+ The compatibility workflow tests React 18 and 19 with React Router 6 and 7
220
+ across Vite 5, 6, 7, and 8.
193
221
 
194
- - `loadingFallback?: React.ComponentType` component shown while lazy-loaded page components load.
195
- - `pagesDir?: string` path used by the generator to resolve pages (useful if you need to override defaults at runtime).
196
- - `enableLazyLoading?: boolean` when true, pages are wrapped to load lazily.
222
+ ## Advanced API
197
223
 
198
- Example:
224
+ For custom glob layouts, the package exports:
199
225
 
200
226
  ```ts
201
- import { configureRouter } from 'next-vite-router';
202
-
203
- configureRouter({
204
- loadingFallback: () => <div>Loading...</div>,
205
- enableLazyLoading: true,
206
- });
227
+ import {
228
+ buildRouteTree,
229
+ createRouteGenerator,
230
+ treeToRoutes,
231
+ } from "next-vite-router";
207
232
  ```
208
233
 
209
- Call `configureRouter` during app bootstrap before calling `generateRoutes()`.
234
+ The normal pipeline is:
210
235
 
211
- ### `registerMiddleware(pattern: RegExp, middleware: RouteMiddleware)`
212
-
213
- `registerMiddleware` lets you register middleware functions that will be applied to route elements whose path matches the provided `pattern` (a `RegExp`). Middlewares receive the route element (`ReactElement`) and should return a new element (for example wrapping it with providers, guards, or layout components).
236
+ ```text
237
+ filesystem -> buildRouteTree() -> treeToRoutes() -> RouteObject[] -> React Router
238
+ ```
214
239
 
215
- Important details:
240
+ ## Examples
216
241
 
217
- - Middlewares are stored in insertion order and applied sequentially.
218
- - To remove all registered middlewares, use `clearMiddlewares()`.
219
- - The internal helper `applyMiddlewares(path, element)` is used when constructing the final route element.
242
+ The repository includes a runnable showcase in [`examples/`](./examples/):
220
243
 
221
- Example:
244
+ - `basic`
245
+ - `layouts`
246
+ - `dynamic-routes`
247
+ - `middleware`
248
+ - `typed-routes`
222
249
 
223
- ```ts
224
- import { registerMiddleware } from 'next-vite-router';
250
+ Run it from the examples directory:
225
251
 
226
- registerMiddleware(/^\/dashboard/, (element) => (
227
- <AuthGuard>{element}</AuthGuard>
228
- ));
252
+ ```bash
253
+ cd examples
254
+ pnpm install
255
+ pnpm dev
229
256
  ```
230
257
 
231
- ## Configuration options (plugin)
258
+ The app opens a small navigation menu so each example can be tested
259
+ immediately. The `predev` script builds the local package automatically.
232
260
 
233
- The plugin takes an options object. Defaults are:
261
+ ## Development
234
262
 
235
- ```ts
236
- {
237
- pagesDir: 'src/app',
238
- pageFile: 'page.tsx',
239
- layoutFile: 'layout.tsx',
240
- notFoundFile: 'not-found.tsx',
241
- }
263
+ From this repository:
264
+
265
+ ```bash
266
+ pnpm install
267
+ pnpm run build
268
+ pnpm run test:generator
269
+ pnpm run test:routes
270
+ pnpm run test:compatibility
271
+ pnpm exec tsc -p test/fixture/tsconfig.json --noEmit
272
+ pnpm run test:local
242
273
  ```
243
274
 
244
- Pass a custom `pagesDir` if your project uses `src/pages` or another convention.
275
+ The route matrix lives in [`test/routes.test.js`](./test/routes.test.js).
245
276
 
246
277
  ## License
247
278
 
248
- MIT
279
+ MIT
package/dist/cli.cjs CHANGED
@@ -200,10 +200,18 @@ function createVirtualDTS(projectDir, useSrc) {
200
200
  }
201
201
  const filePath = path.join(targetDir, "virtual-next-vite-router.d.ts");
202
202
  const content = `declare module 'virtual:next-vite-router' {
203
+ import type { ReactElement } from 'react';
203
204
  import type { RouteObject } from 'react-router-dom';
204
205
 
206
+ export type RoutePath = string;
207
+ export type RoutePathParams = Record<string, Record<string, string>>;
208
+ export type RouteParams<P extends RoutePath> = P extends keyof RoutePathParams
209
+ ? RoutePathParams[P]
210
+ : never;
211
+
205
212
  export const generateRoutes: () => RouteObject[];
206
- export { useRoutes } from 'react-router-dom';
213
+ export function Router(): ReactElement | null;
214
+ export { Link, useRoutes } from 'react-router-dom';
207
215
  }
208
216
  `;
209
217
  fs.writeFileSync(filePath, content, "utf-8");
@@ -221,7 +229,7 @@ import { generateRoutes, useRoutes } from 'virtual:next-vite-router';
221
229
 
222
230
  const routes = generateRoutes();
223
231
 
224
- function App() {
232
+ export default function App() {
225
233
  const element = useRoutes(routes);
226
234
  return element;
227
235
  }
@@ -518,7 +526,7 @@ const init = new commander.Command().name("init").description(
518
526
  );
519
527
  const addDepsCmd = pm.addDepsCommand([
520
528
  "next-vite-router",
521
- "react-router-dom@latest"
529
+ "react-router-dom@^7.0.0"
522
530
  ]);
523
531
  child_process.execSync(addDepsCmd, { stdio: "inherit", cwd: projectName });
524
532
  logger.info("Configuring next-vite-router in the project...");
@@ -531,7 +539,7 @@ const init = new commander.Command().name("init").description(
531
539
  Project "${projectName}" is ready!
532
540
  `);
533
541
  });
534
- const version = "1.0.7";
542
+ const version = "1.1.0";
535
543
  const pkg = {
536
544
  version
537
545
  };
package/dist/index.js CHANGED
@@ -1,86 +1,84 @@
1
- import i, { Suspense as g, lazy as z } from "react";
2
- import { nextViteRouter as D } from "./plugin.js";
3
- function f(e) {
4
- return /^\([^)]+\)$/.test(e) ? "" : /^\[\.\.\.(.+)\]$/.test(e) ? "*" : /^\[([^\]]+)\]$/.test(e) ? e.replace(/^\[([^\]]+)\]$/, ":$1") : e;
5
- }
6
- function F(e) {
7
- return e.replace("../../app/", "").replace(/\/?(page|layout|not-found)\.tsx$/, "");
8
- }
9
- let m = {};
10
- function k(e) {
11
- m = { ...m, ...e };
1
+ import { e as C, n as f } from "./index.plugin--ecyiAem.js";
2
+ import { a as G } from "./index.plugin--ecyiAem.js";
3
+ import i, { Suspense as y, lazy as F } from "react";
4
+ const L = {
5
+ enableLazyLoading: !0
6
+ };
7
+ let d = { ...L };
8
+ function x(e) {
9
+ d = { ...d, ...e };
12
10
  }
13
- function C() {
14
- return m;
11
+ function $() {
12
+ return d;
15
13
  }
16
- let d = [];
17
- function S(e, a) {
18
- d.push({ pattern: e, middleware: a });
14
+ let E = [];
15
+ function T(e, r) {
16
+ E.push({ pattern: e, middleware: r });
19
17
  }
20
- function T() {
21
- d = [];
18
+ function j() {
19
+ E = [];
22
20
  }
23
- function E(e, a) {
24
- let r = a;
25
- for (const n of d)
26
- n.pattern.test(e) && (r = n.middleware(r));
27
- return r;
21
+ function z(e, r) {
22
+ let n = r;
23
+ for (const t of E)
24
+ t.pattern.test(e) && (n = t.middleware(n));
25
+ return n;
28
26
  }
29
- const x = () => i.createElement("div"), y = /* @__PURE__ */ new Map();
30
- function R(e, a, r) {
31
- const n = C();
32
- if (n.enableLazyLoading === !1) {
33
- const u = i.lazy(e), c = i.createElement(
34
- g,
27
+ const M = () => i.createElement("div"), b = /* @__PURE__ */ new Map();
28
+ function R(e, r, n) {
29
+ const t = $();
30
+ if (t.enableLazyLoading === !1) {
31
+ const c = i.lazy(e), u = i.createElement(
32
+ y,
35
33
  { fallback: null },
36
34
  // No loading fallback when disabled
37
- i.createElement(u)
35
+ i.createElement(c)
38
36
  );
39
- return r ? E(r, c) : c;
37
+ return n ? z(n, u) : u;
40
38
  }
41
- let t = y.get(a);
42
- t || (t = z(e), y.set(a, t));
43
- const l = n.loadingFallback || x, o = i.createElement(
44
- g,
45
- { fallback: i.createElement(l) },
46
- i.createElement(t)
39
+ let l = b.get(r);
40
+ l || (l = F(e), b.set(r, l));
41
+ const o = t.loadingFallback || M, a = i.createElement(
42
+ y,
43
+ { fallback: i.createElement(o) },
44
+ i.createElement(l)
47
45
  );
48
- return r ? E(r, o) : o;
46
+ return n ? z(n, a) : a;
49
47
  }
50
- function p(e, a, r, n) {
51
- const t = F(a), l = t ? t.split("/") : [];
52
- let o = e, u = "";
53
- l.forEach((c, w) => {
54
- u = u ? `${u}/${c}` : c, o.children.has(c) || o.children.set(c, {
55
- segment: c,
56
- fullPath: u,
48
+ function p(e, r, n, t) {
49
+ const l = C(r), o = l ? l.split("/") : [];
50
+ let a = e, c = "";
51
+ o.forEach((u, h) => {
52
+ c = c ? `${c}/${u}` : u, a.children.has(u) || a.children.set(u, {
53
+ segment: u,
54
+ fullPath: c,
57
55
  children: /* @__PURE__ */ new Map()
58
- }), o = o.children.get(c), w === l.length - 1 && (o[n] = r);
59
- }), l.length === 0 && (e[n] = r);
56
+ }), a = a.children.get(u), h === o.length - 1 && (a[t] = n);
57
+ }), o.length === 0 && (e[t] = n);
60
58
  }
61
- function L(e, a, r) {
62
- const n = {
59
+ function S(e, r, n) {
60
+ const t = {
63
61
  segment: "",
64
62
  fullPath: "",
65
63
  children: /* @__PURE__ */ new Map()
66
64
  };
67
- return Object.entries(a).forEach(([t, l]) => {
68
- p(n, t, l.default, "layout");
69
- }), Object.entries(e).forEach(([t, l]) => {
70
- p(n, t, l, "page");
71
- }), Object.entries(r).forEach(([t, l]) => {
72
- p(n, t, l, "notFound");
73
- }), n;
65
+ return Object.entries(r).forEach(([l, o]) => {
66
+ p(t, l, o.default, "layout");
67
+ }), Object.entries(e).forEach(([l, o]) => {
68
+ p(t, l, o, "page");
69
+ }), Object.entries(n).forEach(([l, o]) => {
70
+ p(t, l, o, "notFound");
71
+ }), t;
74
72
  }
75
- function h(e, a, r = !1) {
76
- const n = R(
73
+ function m(e, r, n = !1) {
74
+ const t = R(
77
75
  async () => ({ default: (await e.page()).default }),
78
- `page:${e.fullPath || a}`,
79
- a
76
+ `page:${e.fullPath || r}`,
77
+ r
80
78
  );
81
- return r ? { index: !0, element: n } : { path: f(e.segment), element: n };
79
+ return n ? { index: !0, element: t } : { path: f(e.segment), element: t };
82
80
  }
83
- function $(e) {
81
+ function g(e) {
84
82
  return {
85
83
  path: "*",
86
84
  element: R(
@@ -89,62 +87,70 @@ function $(e) {
89
87
  )
90
88
  };
91
89
  }
92
- function s(e, a) {
93
- const r = [];
94
- return e.children.forEach((n, t) => {
95
- const l = f(t), o = l ? a ? `${a}/${l}` : `/${l}` : a;
96
- r.push(...b(n, !1, o));
97
- }), r;
98
- }
99
- function b(e, a = !1, r = "") {
90
+ function s(e, r) {
100
91
  const n = [];
101
- if (a && e.layout) {
102
- const t = [];
103
- return e.page && t.push(h(e, "/", !0)), t.push(...s(e, "")), e.notFound && t.push($(e)), n.push({
92
+ return [...e.children.entries()].sort(([t], [l]) => t.localeCompare(l)).forEach(([t, l]) => {
93
+ const o = f(t), a = o ? r ? `${r}/${o}` : `/${o}` : r;
94
+ n.push(...w(l, !1, a));
95
+ }), n;
96
+ }
97
+ function w(e, r = !1, n = "") {
98
+ const t = [];
99
+ if (r && e.layout) {
100
+ const a = [];
101
+ return e.page && a.push(m(e, "/", !0)), a.push(...s(e, "")), e.notFound && a.push(g(e)), t.push({
104
102
  path: "/",
105
103
  element: i.createElement(e.layout),
106
- children: t
107
- }), n;
104
+ children: a
105
+ }), t;
108
106
  }
109
107
  if (e.layout) {
110
- const t = f(e.segment);
111
- if (!t)
112
- return s(e, r);
113
- const l = r + "/" + t, o = [];
114
- return e.page && o.push(h(e, l, !0)), o.push(...s(e, l)), e.notFound && o.push($(e)), n.push({
115
- path: t,
108
+ const a = f(e.segment);
109
+ if (!a)
110
+ return s(e, n);
111
+ const c = n + "/" + a, u = [];
112
+ return e.page && u.push(m(e, c, !0)), u.push(...s(e, c)), e.notFound && u.push(g(e)), t.push({
113
+ path: a,
116
114
  element: i.createElement(e.layout),
117
- children: o.length > 0 ? o : void 0
118
- }), n;
115
+ children: u.length > 0 ? u : void 0
116
+ }), t;
119
117
  }
120
118
  if (e.page) {
121
- const t = f(e.segment);
122
- if (!t)
123
- return s(e, r);
124
- const l = r + "/" + t, o = s(e, l), u = h(e, l, !1);
125
- return n.push({
126
- path: u.path,
127
- element: u.element,
128
- children: o.length > 0 ? o : void 0
129
- }), n;
119
+ const a = f(e.segment);
120
+ if (!a)
121
+ return r ? (t.push({
122
+ path: "/",
123
+ element: m(e, "/", !0).element,
124
+ children: s(e, n).length > 0 ? s(e, n) : void 0
125
+ }), e.notFound && t.push(g(e)), t) : s(e, n);
126
+ const c = n + "/" + a, u = s(e, c), h = m(e, c, !1);
127
+ return t.push({
128
+ path: h.path,
129
+ element: h.element,
130
+ children: u.length > 0 ? u : void 0
131
+ }), t;
130
132
  }
131
- return s(e, r);
133
+ const l = f(e.segment), o = s(e, n);
134
+ return l ? [{
135
+ path: l,
136
+ children: o.length > 0 ? o : void 0
137
+ }] : o;
132
138
  }
133
- function j(e, a, r) {
139
+ function O(e, r, n) {
134
140
  return () => {
135
- const n = L(e, a, r);
136
- return b(n, !0);
141
+ const t = S(e, r, n);
142
+ return w(t, !0);
137
143
  };
138
144
  }
139
145
  export {
140
- L as buildRouteTree,
141
- T as clearMiddlewares,
142
- k as configureRouter,
146
+ S as buildRouteTree,
147
+ j as clearMiddlewares,
148
+ x as configureRouter,
143
149
  R as createLazyElement,
144
- j as createRouteGenerator,
145
- F as extractRoutePath,
146
- D as nextViteRouter,
150
+ O as createRouteGenerator,
151
+ C as extractRoutePath,
152
+ G as nextViteRouter,
147
153
  f as normalizeSegment,
148
- S as registerMiddleware,
149
- b as treeToRoutes
154
+ T as registerMiddleware,
155
+ w as treeToRoutes
150
156
  };
@@ -0,0 +1,132 @@
1
+ import p from "node:fs";
2
+ import i from "node:path";
3
+ function y(e) {
4
+ return /^\([^)]+\)$/.test(e) ? "" : /^\[\[\.\.\.(.+)\]\]$/.test(e) || /^\[\.\.\.(.+)\]$/.test(e) ? "*" : /^\[([^\]]+)\]$/.test(e) ? e.replace(/^\[([^\]]+)\]$/, ":$1") : e;
5
+ }
6
+ function b(e) {
7
+ return e.replace(/\\/g, "/").replace("../../app/", "").replace(/\/?(page|layout|not-found)\.tsx$/, "");
8
+ }
9
+ function R(e) {
10
+ const t = e.split(i.sep).filter(Boolean).map(y).filter(Boolean);
11
+ return t.length > 0 ? `/${t.join("/")}` : "/";
12
+ }
13
+ function h(e, t) {
14
+ const s = /* @__PURE__ */ new Set();
15
+ function n(a) {
16
+ for (const u of p.readdirSync(a, { withFileTypes: !0 })) {
17
+ const r = i.join(a, u.name);
18
+ u.isDirectory() ? n(r) : u.isFile() && u.name === t && s.add(R(i.relative(e, a)));
19
+ }
20
+ }
21
+ return p.existsSync(e) && n(e), [...s].sort();
22
+ }
23
+ function x(e) {
24
+ const t = e.match(/^\/(?:\[\.\.\.|:)([^/\]]+)\]?$/);
25
+ return t == null ? void 0 : t[1];
26
+ }
27
+ function $(e) {
28
+ const t = e.map((n) => ` | "${n}"`).join(`
29
+ `), s = e.map((n) => {
30
+ const a = n.split("/").map((r) => x(`/${r}`)).filter((r) => !!r), u = a.length > 0 ? a.map((r) => ` ${r}: string;`).join(`
31
+ `) : " [key: string]: never;";
32
+ return ` "${n}": {
33
+ ${u}
34
+ };`;
35
+ }).join(`
36
+ `);
37
+ return `declare module 'virtual:next-vite-router' {
38
+ import type { ReactElement } from 'react';
39
+ import type { RouteObject } from 'react-router-dom';
40
+
41
+ export type RoutePath =
42
+ ${t || ' | "/"'};
43
+
44
+ export type RouteParams<P extends RoutePath> = P extends keyof RoutePathParams
45
+ ? RoutePathParams[P]
46
+ : never;
47
+
48
+ export interface RoutePathParams {
49
+ ${s}
50
+ }
51
+
52
+ export const generateRoutes: () => RouteObject[];
53
+ export function Router(): ReactElement | null;
54
+ export { Link, useRoutes } from 'react-router-dom';
55
+ }
56
+ `;
57
+ }
58
+ function g(e = {}) {
59
+ const {
60
+ pagesDir: t = "src/app",
61
+ pageFile: s = "page.tsx",
62
+ layoutFile: n = "layout.tsx",
63
+ notFoundFile: a = "not-found.tsx"
64
+ } = e, u = "virtual:next-vite-router", r = "\0" + u;
65
+ let l = "", m = "";
66
+ function f() {
67
+ const o = $(h(l, s));
68
+ p.writeFileSync(m, o, "utf-8");
69
+ }
70
+ return {
71
+ name: "next-vite-router",
72
+ configResolved(o) {
73
+ l = i.resolve(o.root, t), m = i.join(i.dirname(l), "virtual-next-vite-router.d.ts"), f();
74
+ },
75
+ resolveId(o) {
76
+ if (o === u)
77
+ return r;
78
+ },
79
+ load(o) {
80
+ if (o === r)
81
+ return `
82
+ import { buildRouteTree, treeToRoutes, normalizeSegment, createLazyElement } from 'next-vite-router';
83
+ import { Link, useRoutes } from 'react-router-dom';
84
+
85
+ const pagesGlob = import.meta.glob('/${t}/**/${s}');
86
+ const layoutsGlob = import.meta.glob('/${t}/**/${n}', { eager: true });
87
+ const notFoundsGlob = import.meta.glob('/${t}/**/${a}');
88
+
89
+ const transformPath = (path) => path.replace('/${t}/', '');
90
+
91
+ const pages = Object.fromEntries(
92
+ Object.entries(pagesGlob).map(([key, value]) => [transformPath(key), value])
93
+ );
94
+ const layouts = Object.fromEntries(
95
+ Object.entries(layoutsGlob).map(([key, value]) => [transformPath(key), value])
96
+ );
97
+ const notFounds = Object.fromEntries(
98
+ Object.entries(notFoundsGlob).map(([key, value]) => [transformPath(key), value])
99
+ );
100
+
101
+ export function generateRoutes() {
102
+ const tree = buildRouteTree(pages, layouts, notFounds);
103
+ const routes = treeToRoutes(tree, true);
104
+ return routes;
105
+ }
106
+
107
+ export function Router() {
108
+ return useRoutes(generateRoutes());
109
+ }
110
+
111
+ export { Link, useRoutes };
112
+ `;
113
+ },
114
+ handleHotUpdate({ file: o, server: c }) {
115
+ if (o.includes(t) && (o.endsWith(s) || o.endsWith(n) || o.endsWith(a))) {
116
+ f();
117
+ const d = c.moduleGraph.getModuleById(
118
+ r
119
+ );
120
+ d && (c.moduleGraph.invalidateModule(d), c.ws.send({
121
+ type: "full-reload",
122
+ path: "*"
123
+ }));
124
+ }
125
+ }
126
+ };
127
+ }
128
+ export {
129
+ g as a,
130
+ b as e,
131
+ y as n
132
+ };
package/dist/plugin.js CHANGED
@@ -1,59 +1,6 @@
1
- function d(i = {}) {
2
- const {
3
- pagesDir: t = "src/app",
4
- pageFile: n = "page.tsx",
5
- layoutFile: a = "layout.tsx",
6
- notFoundFile: u = "not-found.tsx"
7
- } = i, s = "virtual:next-vite-router", o = "\0" + s;
8
- return {
9
- name: "next-vite-router",
10
- resolveId(e) {
11
- if (e === s)
12
- return o;
13
- },
14
- load(e) {
15
- if (e === o)
16
- return `
17
- import { buildRouteTree, treeToRoutes, normalizeSegment, createLazyElement } from 'next-vite-router';
18
-
19
- const pagesGlob = import.meta.glob('/${t}/**/${n}');
20
- const layoutsGlob = import.meta.glob('/${t}/**/${a}', { eager: true });
21
- const notFoundsGlob = import.meta.glob('/${t}/**/${u}');
22
-
23
- const transformPath = (path) => path.replace('/${t}/', '');
24
-
25
- const pages = Object.fromEntries(
26
- Object.entries(pagesGlob).map(([key, value]) => [transformPath(key), value])
27
- );
28
- const layouts = Object.fromEntries(
29
- Object.entries(layoutsGlob).map(([key, value]) => [transformPath(key), value])
30
- );
31
- const notFounds = Object.fromEntries(
32
- Object.entries(notFoundsGlob).map(([key, value]) => [transformPath(key), value])
33
- );
34
-
35
- export function generateRoutes() {
36
- const tree = buildRouteTree(pages, layouts, notFounds);
37
- const routes = treeToRoutes(tree, true);
38
- return routes;
39
- }
40
-
41
- export { useRoutes } from 'react-router-dom';
42
- `;
43
- },
44
- handleHotUpdate({ file: e, server: r }) {
45
- if (e.includes(t) && (e.endsWith(n) || e.endsWith(a) || e.endsWith(u))) {
46
- const l = r.moduleGraph.getModuleById(
47
- o
48
- );
49
- l && (r.moduleGraph.invalidateModule(l), r.ws.send({
50
- type: "full-reload",
51
- path: "*"
52
- }));
53
- }
54
- }
55
- };
56
- }
1
+ import "node:fs";
2
+ import "node:path";
3
+ import { a as i } from "./index.plugin--ecyiAem.js";
57
4
  export {
58
- d as nextViteRouter
5
+ i as nextViteRouter
59
6
  };
package/package.json CHANGED
@@ -1,70 +1,72 @@
1
- {
2
- "name": "next-vite-router",
3
- "version": "1.0.7",
4
- "description": "Next.js-like file-based routing for React + Vite",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "bin": {
9
- "create-next-vite-router": "./dist/cli.cjs"
10
- },
11
- "exports": {
12
- ".": {
13
- "import": "./dist/index.js",
14
- "types": "./dist/index.d.ts"
15
- },
16
- "./plugin": {
17
- "import": "./dist/plugin.js",
18
- "types": "./dist/plugin.d.ts"
19
- }
20
- },
21
- "files": [
22
- "dist",
23
- "README.md",
24
- "LICENSE"
25
- ],
26
- "scripts": {
27
- "dev": "vite build --watch",
28
- "build": "vite build && vite build -c vite.config.cli.ts",
29
- "test:local": "node test/test-package.js",
30
- "preview": "pnpm pack --dry-run",
31
- "prepublishOnly": "pnpm run build && pnpm run test:local",
32
- "cli": "cd test && node ../dist/cli.cjs"
33
- },
34
- "keywords": [
35
- "react",
36
- "vite",
37
- "router",
38
- "nextjs",
39
- "file-based-routing",
40
- "filesystem-router",
41
- "react-router"
42
- ],
43
- "author": "Rlzee",
44
- "repository": {
45
- "type": "git",
46
- "url": "https://github.com/Rlzee/next-vite-router"
47
- },
48
- "homepage": "https://github.com/Rlzee/next-vite-router#readme",
49
- "license": "MIT",
50
- "peerDependencies": {
51
- "react": "^18.0.0",
52
- "react-dom": "^18.0.0",
53
- "react-router-dom": "^6.0.0",
54
- "vite": "^5.0.0"
55
- },
56
- "devDependencies": {
57
- "@types/node": "^24.10.1",
58
- "@types/prompts": "^2.4.9",
59
- "@types/react": "^18.0.0",
60
- "@types/react-dom": "^18.0.0",
61
- "typescript": "^5.0.0",
62
- "vite": "^5.0.0",
63
- "vite-plugin-dts": "^3.0.0"
64
- },
65
- "dependencies": {
66
- "commander": "^14.0.2",
67
- "kleur": "^4.1.5",
68
- "prompts": "^2.4.2"
69
- }
70
- }
1
+ {
2
+ "name": "next-vite-router",
3
+ "version": "1.1.0",
4
+ "description": "Next.js-like file-based routing for React + Vite",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "create-next-vite-router": "./dist/cli.cjs"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./plugin": {
17
+ "import": "./dist/plugin.js",
18
+ "types": "./dist/plugin.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "keywords": [
27
+ "react",
28
+ "vite",
29
+ "router",
30
+ "nextjs",
31
+ "file-based-routing",
32
+ "filesystem-router",
33
+ "react-router"
34
+ ],
35
+ "author": "Rlzee",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Rlzee/next-vite-router"
39
+ },
40
+ "homepage": "https://github.com/Rlzee/next-vite-router#readme",
41
+ "license": "MIT",
42
+ "peerDependencies": {
43
+ "react": ">=18 <20",
44
+ "react-dom": ">=18 <20",
45
+ "react-router-dom": ">=6 <8",
46
+ "vite": ">=5 <9"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.10.1",
50
+ "@types/prompts": "^2.4.9",
51
+ "@types/react": "^18.0.0",
52
+ "@types/react-dom": "^18.0.0",
53
+ "typescript": "^5.0.0",
54
+ "vite": "^5.0.0",
55
+ "vite-plugin-dts": "^3.0.0"
56
+ },
57
+ "dependencies": {
58
+ "commander": "^14.0.2",
59
+ "kleur": "^4.1.5",
60
+ "prompts": "^2.4.2"
61
+ },
62
+ "scripts": {
63
+ "dev": "vite build --watch",
64
+ "build": "vite build && vite build -c vite.config.cli.ts",
65
+ "test:local": "node test/test-package.js",
66
+ "test:routes": "node test/test-route-types.js",
67
+ "test:generator": "node --test test/routes.test.js",
68
+ "test:compatibility": "node test/test-compatibility.js",
69
+ "preview": "pnpm pack --dry-run",
70
+ "cli": "cd test && node ../dist/cli.cjs"
71
+ }
72
+ }