next-vite-router 1.2.3 → 1.3.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
@@ -4,7 +4,7 @@ File-based routing for React + Vite.
4
4
 
5
5
  Define routes with folders and let the package generate the
6
6
  [React Router](https://reactrouter.com/) `RouteObject[]`. It does not replace
7
- React Router, navigation, history, loaders, or application state.
7
+ React Router, navigation, history, or application state.
8
8
 
9
9
  ## Start here
10
10
 
@@ -31,6 +31,10 @@ export default defineConfig({
31
31
  });
32
32
  ```
33
33
 
34
+ The plugin ships both ESM and CJS, so this works when `package.json` has no
35
+ `"type": "module"` (Vite 5 loads `vite.config.ts` with `require()` in that
36
+ case). You can also rename the file to `vite.config.mts` to force ESM.
37
+
34
38
  Create your route tree:
35
39
 
36
40
  ```text
@@ -88,10 +92,12 @@ npm run dev
88
92
 
89
93
  ## Use React Router directly
90
94
 
91
- `Router` is only a convenience wrapper around `useRoutes(routes)`. Routes are
92
- generated lazily on the first render, so calls to `configureRouter` and
93
- `registerMiddleware` made during application setup are applied before route
94
- elements are created. You can keep using React Router APIs:
95
+ `Router` is only a convenience wrapper around `useRoutes(routes)`. The
96
+ lazy-loading behavior and `loadingFallback` configured through `configureRouter`
97
+ are read at render time, so they take effect even when `generateRoutes()` runs
98
+ at module scope before application setup finishes. Register middleware with
99
+ `registerMiddleware` *before* calling `generateRoutes()`, because route elements
100
+ are wrapped while the tree is built. You can keep using React Router APIs:
95
101
 
96
102
  ```tsx
97
103
  import {
@@ -115,10 +121,18 @@ export default function App() {
115
121
  }
116
122
  ```
117
123
 
118
- Use `useNavigate`, loaders, actions, and other React Router APIs from
119
- `react-router-dom`. The package `useParams` hook is also available from the
120
- virtual module and maps catch-all names such as `slug`. The package does not
121
- create competing navigation or data-loading APIs.
124
+ Keep using React Router's navigation and hooks from `react-router-dom`
125
+ (`useNavigate`, `useLocation`, `Link`, `Outlet`). For parameters, import
126
+ `useParams` from the virtual module instead of `react-router-dom`: dynamic
127
+ `[id]` segments work with either hook, but named catch-all segments
128
+ (`[...slug]`, `[[...slug]]`) are only surfaced as `slug` through the package
129
+ `useParams`. React Router's own `useParams` returns them under `params["*"]`.
130
+
131
+ The generated `RouteObject[]` carries page, layout, and not-found elements only.
132
+ There is no file convention that produces React Router route `loader` or
133
+ `action` functions, and those data APIs run only under a data router
134
+ (`createBrowserRouter` + `RouterProvider`), not under `BrowserRouter` with
135
+ `useRoutes`. Fetch data inside your page components instead.
122
136
 
123
137
  ## File conventions
124
138
 
@@ -133,13 +147,17 @@ create competing navigation or data-loading APIs.
133
147
  | `(admin)` | Route group omitted from the URL |
134
148
 
135
149
  Catch-all pages and `not-found.tsx` at the same level both use React Router's
136
- `*` path and should not be declared together if their fallback behavior must
137
- be distinct. The catch-all parameter provider wraps the page route, so a
138
- layout cannot read that parameter through the package `useParams` hook.
150
+ `*` path and are rejected during route validation because their fallback
151
+ behavior would be ambiguous. The catch-all parameter provider wraps the page
152
+ route, so a layout cannot read that parameter through the package `useParams`
153
+ hook.
139
154
 
140
155
  The Vite plugin accepts custom `pageFile`, `layoutFile`, and `notFoundFile`
141
156
  names. `createRouteGenerator` accepts the same options, plus `pagesDir`, as
142
157
  its optional fourth argument when custom glob paths or extensions are used.
158
+ For the Vite plugin, `pagesDir` is relative to the Vite project root (the
159
+ default is `src/app`). The lower-level route path utility also accepts an
160
+ absolute `pagesDir`, which is useful for custom route generators.
143
161
 
144
162
  Layouts are the recommended way to add route-scoped navigation, providers, or
145
163
  authorization:
@@ -206,6 +224,9 @@ configureRouter({
206
224
  });
207
225
  ```
208
226
 
227
+ The fallback is resolved when a route suspends during render, so the order of
228
+ `configureRouter` relative to `generateRoutes()` does not matter.
229
+
209
230
  `enableLazyLoading: false` is available for compatibility and testing. It does
210
231
  not turn Vite's asynchronous imports into synchronous imports.
211
232
 
@@ -302,7 +323,12 @@ pnpm exec tsc -p test/fixture/tsconfig.json --noEmit
302
323
  pnpm run test:local
303
324
  ```
304
325
 
305
- The route matrix lives in [`test/routes.test.js`](./test/routes.test.js).
326
+ The route matrix lives in [`test/routes.test.js`](./test/routes.test.js) and
327
+ includes React Router `matchRoutes` assertions for dynamic parameters, route
328
+ groups, nested `not-found.tsx` fallbacks, and catch-all pages under layouts.
329
+ `test/test-route-types.js` covers generated TypeScript declarations and HMR
330
+ updates, while `test/test-compatibility.js` checks the supported dependency
331
+ ranges and generated production bootstrap.
306
332
 
307
333
  ## License
308
334
 
package/dist/cli.cjs CHANGED
@@ -548,7 +548,7 @@ const init = new commander.Command().name("init").description(
548
548
  Project "${projectName}" is ready!
549
549
  `);
550
550
  });
551
- const version = "1.2.3";
551
+ const version = "1.3.0";
552
552
  const pkg = {
553
553
  version
554
554
  };
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("./tree-hc1dSMdb.cjs"),l=require("react"),S=require("react-router-dom"),C={enableLazyLoading:!0};let f={...C};function v(e){f={...f,...e}}function F(){f={...C}}function T(){return f}let g=[];function x(e,t){g.push({pattern:e,middleware:t})}function A(){g=[]}function y(e,t){let r=t;for(const n of g)n.pattern.lastIndex=0,n.pattern.test(e)&&(r=n.middleware(r)),n.pattern.lastIndex=0;return r}const M=()=>l.createElement("div"),h=new Map;function N(){const e=T();if(e.enableLazyLoading===!1)return null;const t=e.loadingFallback||M;return l.createElement(t)}function $(){h.clear()}function p(e,t,r){let n=h.get(t);n||(n=l.lazy(e),h.set(t,n));const a=l.createElement(l.Suspense,{fallback:l.createElement(N)},l.createElement(n));return r?y(r,a):a}const P=l.createContext(void 0);function w({name:e,children:t}){return l.createElement(P.Provider,{value:e},t)}function k(){return l.useContext(P)}function E(e,t){return y(e,l.createElement(t))}function D(e){const t=c.parseRouteSegments(e)[0];return(t==null?void 0:t.kind)==="catchAll"&&t.optional}function d(e){const t=c.parseRouteSegments(e)[0];return t?c.routeSegmentToPath(t):""}function s(e,t){return m(e,t,!0)}function m(e,t,r=!1){const n=e.page;if(!n)throw new Error(`Route node "${e.fullPath}" has no page loader`);const a=p(async()=>({default:(await n()).default}),`page:${e.fullPath||t}`,t),o=c.parseRouteSegments(e.segment)[0],u=(o==null?void 0:o.kind)==="catchAll"?l.createElement(w,{name:o.name,children:a}):a;return r?{index:!0,element:u}:{path:d(e.segment),element:u}}function q(e){const t=e.notFound;if(!t)throw new Error(`Route node "${e.fullPath}" has no not-found loader`);const r=e.fullPath?`/${e.fullPath}`:"/";return{path:"*",element:p(async()=>({default:(await t()).default}),`notfound:${e.fullPath}`,r)}}function L(e){return e.notFound?[q(e)]:[]}function I(e,t,r,n){return{...t?{path:t}:{},element:E(e,r),children:n.length>0?n:void 0}}function b(e,t){const r=[];return[...e.children.entries()].sort(([n],[a])=>n.localeCompare(a)).forEach(([n,a])=>{const o=d(n),u=o?t?`${t}/${o}`:`/${o}`:t;r.push(...R(a,!1,u))}),r}function z(e,t){const r=[];return e.page&&r.push(s(e,t||"/")),r.push(...b(e,t)),r.push(...L(e)),r}function G(e,t,r,n,a,o){if(D(e.segment)){const u=[...a,...o];return[s(e,t),{path:n,element:m(e,r).element,children:u.length>0?u:void 0}]}return a.length>0||o.length>0?[{path:n,children:[s(e,r),...a,...o]}]:[{path:n,element:m(e,r).element}]}function O(e,t,r,n){const a=[...e.page?[s(e,t)]:[],...r,...n];return[{children:a.length>0?a:void 0}]}function j(e){const t=z(e,"");return e.layout?[{path:"/",element:E("/",e.layout),children:t}]:e.page?t.length>1?[{path:"/",children:t}]:[{path:"/",element:m(e,"/",!1).element}]:e.notFound?[{path:"/",children:t}]:t}function R(e,t=!1,r=""){if(t)return j(e);const n=d(e.segment),a=!n,o=a?r:r+"/"+n,u=b(e,o),i=L(e);return e.layout?[I(o,a?"":n,e.layout,z(e,o))]:e.page&&!a?G(e,r,o,n,u,i):e.page||e.notFound||!a?a?O(e,o,u,i):[{path:n,children:[...e.page?[s(e,o)]:[],...u,...i]}]:u}function B(e,t,r,n){return()=>{const a=c.buildRouteTree(e,t,r,n);return R(a,!0)}}function H(){const e=S.useParams(),t=k();return typeof t=="string"&&e["*"]!==void 0?{...e,[t]:e["*"]}:e}exports.assertNoDuplicateRoutes=c.assertNoDuplicateRoutes;exports.assertNoRouteConflicts=c.assertNoRouteConflicts;exports.buildRouteTree=c.buildRouteTree;exports.extractRoutePath=c.extractRoutePath;exports.normalizeSegment=c.normalizeSegment;exports.parseRouteSegments=c.parseRouteSegments;exports.routeSegmentToPath=c.routeSegmentToPath;exports.CatchAllParamProvider=w;exports.clearLazyCache=$;exports.clearMiddlewares=A;exports.configureRouter=v;exports.createLazyElement=p;exports.createRouteGenerator=B;exports.registerMiddleware=x;exports.resetRouterConfig=F;exports.treeToRoutes=R;exports.useParams=H;
@@ -0,0 +1,144 @@
1
+ import { ComponentType } from 'react';
2
+ import { default as default_2 } from 'react';
3
+ import { ReactElement } from 'react';
4
+ import { RouteObject } from 'react-router-dom';
5
+
6
+ export declare function assertNoDuplicateRoutes(filePaths: string[], fileOptions?: RouteFileOptions): void;
7
+
8
+ export declare function assertNoRouteConflicts(pageFiles: string[], notFoundFiles: string[], fileOptions?: RouteFileOptions): void;
9
+
10
+ export declare function buildRouteTree(pages: Record<string, () => Promise<any>>, layouts: Record<string, any>, notFounds: Record<string, () => Promise<any>>, fileOptions?: RouteFileOptions): RouteNode;
11
+
12
+ export declare function CatchAllParamProvider({ name, children, }: {
13
+ name: string;
14
+ children: default_2.ReactElement;
15
+ }): default_2.ReactElement;
16
+
17
+ /**
18
+ * Clear the module-level lazy component cache.
19
+ *
20
+ * `lazyComponentCache` is a package-wide singleton keyed by route path, so a
21
+ * `React.lazy` component created in one context (for example a previous test)
22
+ * is reused whenever another context requests the same key with a different
23
+ * loader. Exposing an explicit reset keeps that state from leaking, mirroring
24
+ * `clearMiddlewares` for the middleware registry.
25
+ */
26
+ export declare function clearLazyCache(): void;
27
+
28
+ export declare function clearMiddlewares(): void;
29
+
30
+ export declare function configureRouter(config: RouterConfig): void;
31
+
32
+ /**
33
+ * Create a lazy element wrapped with Suspense and apply middlewares
34
+ * @param loader - Function that loads the component
35
+ * @param cacheKey - Unique key for the cache
36
+ * @param routePath - Route path for middlewares
37
+ */
38
+ export declare function createLazyElement(loader: () => Promise<any>, cacheKey: string, routePath?: string): default_2.ReactElement;
39
+
40
+ /**
41
+ * Create a custom route generator function using your own files
42
+ *
43
+ * ⚠️ NOTE: This function is intended for advanced use only.
44
+ * For most cases, use the Vite plugin which automatically handles the paths.
45
+ *
46
+ * @example
47
+ * // In your configuration file
48
+ * const pages = import.meta.glob("../../pages/**\\/page.tsx");
49
+ * const layouts = import.meta.glob("../../pages/**\\/layout.tsx", { eager: true });
50
+ * const notFounds = import.meta.glob("../../pages/**\\/not-found.tsx");
51
+ *
52
+ * export const generateCustomRoutes = createRouteGenerator(pages, layouts, notFounds);
53
+ *
54
+ * // Then use it
55
+ * const routes = generateCustomRoutes();
56
+ */
57
+ export declare function createRouteGenerator(pages: Record<string, () => Promise<any>>, layouts: Record<string, any>, notFounds: Record<string, () => Promise<any>>, fileOptions?: RouteFileOptions): () => RouteObject[];
58
+
59
+ /**
60
+ * Extract the route path from a file path
61
+ * @example "../../app/blog/[id]/page.tsx" -> "blog/[id]"
62
+ * @example "../../app/page.tsx" -> ""
63
+ *
64
+ * `pagesDir` may be relative or absolute. The plugin resolves it relative to
65
+ * the Vite project root before passing it here.
66
+ * The prefix is stripped lazily so it matches the FIRST directory boundary
67
+ * that equals `pagesDir` (or the leading `app`/`pages` root marker when no
68
+ * `pagesDir` is supplied). A greedy match would instead consume up to the
69
+ * LAST such folder, which mangles routes that legitimately contain a nested
70
+ * folder named like the pages directory (e.g. `app/docs/app/page.tsx`).
71
+ */
72
+ export declare function extractRoutePath(filePath: string, { pagesDir, pageFile, layoutFile, notFoundFile }?: RouteFileOptions): string;
73
+
74
+ /**
75
+ * Normalize a Next.js segment to a React Router segment
76
+ * @example (admin) -> ""
77
+ * @example [id] -> :id
78
+ * @example [...slug] -> *
79
+ */
80
+ export declare function normalizeSegment(segment: string): string;
81
+
82
+ export declare function parseRouteSegments(routePath: string): RouteSegment[];
83
+
84
+ export declare function registerMiddleware(pattern: RegExp, middleware: RouteMiddleware): void;
85
+
86
+ /**
87
+ * Restore the module-level router configuration to its defaults.
88
+ *
89
+ * `configureRouter` merges, so it can never drop keys back to their initial
90
+ * state. A dedicated reset lets isolated contexts (for example tests) start
91
+ * from a known configuration instead of inheriting whatever was set globally
92
+ * before, matching the explicit reset offered by `clearMiddlewares`.
93
+ */
94
+ export declare function resetRouterConfig(): void;
95
+
96
+ export declare type RouteFileOptions = {
97
+ pagesDir?: string;
98
+ pageFile?: string;
99
+ layoutFile?: string;
100
+ notFoundFile?: string;
101
+ };
102
+
103
+ export declare type RouteMiddleware = (element: ReactElement) => ReactElement;
104
+
105
+ export declare type RouteModuleLoader = () => Promise<{
106
+ default: ComponentType;
107
+ }>;
108
+
109
+ export declare type RouteNode = {
110
+ segment: string;
111
+ fullPath: string;
112
+ page?: RouteModuleLoader;
113
+ layout?: ComponentType;
114
+ notFound?: RouteModuleLoader;
115
+ children: Map<string, RouteNode>;
116
+ };
117
+
118
+ export declare type RouterConfig = {
119
+ loadingFallback?: React.ComponentType;
120
+ enableLazyLoading?: boolean;
121
+ };
122
+
123
+ export declare type RouteSegment = {
124
+ kind: "group";
125
+ name: string;
126
+ } | {
127
+ kind: "static";
128
+ name: string;
129
+ } | {
130
+ kind: "dynamic";
131
+ name: string;
132
+ } | {
133
+ kind: "catchAll";
134
+ name: string;
135
+ optional: boolean;
136
+ };
137
+
138
+ export declare function routeSegmentToPath(segment: RouteSegment): string;
139
+
140
+ export declare function treeToRoutes(node: RouteNode, isRoot?: boolean, currentPath?: string): RouteObject[];
141
+
142
+ export declare function useParams<T extends Record<string, string | undefined> = Record<string, string | undefined>>(): T;
143
+
144
+ export { }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,8 @@ import { RouteObject } from 'react-router-dom';
5
5
 
6
6
  export declare function assertNoDuplicateRoutes(filePaths: string[], fileOptions?: RouteFileOptions): void;
7
7
 
8
+ export declare function assertNoRouteConflicts(pageFiles: string[], notFoundFiles: string[], fileOptions?: RouteFileOptions): void;
9
+
8
10
  export declare function buildRouteTree(pages: Record<string, () => Promise<any>>, layouts: Record<string, any>, notFounds: Record<string, () => Promise<any>>, fileOptions?: RouteFileOptions): RouteNode;
9
11
 
10
12
  export declare function CatchAllParamProvider({ name, children, }: {
@@ -12,6 +14,17 @@ export declare function CatchAllParamProvider({ name, children, }: {
12
14
  children: default_2.ReactElement;
13
15
  }): default_2.ReactElement;
14
16
 
17
+ /**
18
+ * Clear the module-level lazy component cache.
19
+ *
20
+ * `lazyComponentCache` is a package-wide singleton keyed by route path, so a
21
+ * `React.lazy` component created in one context (for example a previous test)
22
+ * is reused whenever another context requests the same key with a different
23
+ * loader. Exposing an explicit reset keeps that state from leaking, mirroring
24
+ * `clearMiddlewares` for the middleware registry.
25
+ */
26
+ export declare function clearLazyCache(): void;
27
+
15
28
  export declare function clearMiddlewares(): void;
16
29
 
17
30
  export declare function configureRouter(config: RouterConfig): void;
@@ -47,6 +60,14 @@ export declare function createRouteGenerator(pages: Record<string, () => Promise
47
60
  * Extract the route path from a file path
48
61
  * @example "../../app/blog/[id]/page.tsx" -> "blog/[id]"
49
62
  * @example "../../app/page.tsx" -> ""
63
+ *
64
+ * `pagesDir` may be relative or absolute. The plugin resolves it relative to
65
+ * the Vite project root before passing it here.
66
+ * The prefix is stripped lazily so it matches the FIRST directory boundary
67
+ * that equals `pagesDir` (or the leading `app`/`pages` root marker when no
68
+ * `pagesDir` is supplied). A greedy match would instead consume up to the
69
+ * LAST such folder, which mangles routes that legitimately contain a nested
70
+ * folder named like the pages directory (e.g. `app/docs/app/page.tsx`).
50
71
  */
51
72
  export declare function extractRoutePath(filePath: string, { pagesDir, pageFile, layoutFile, notFoundFile }?: RouteFileOptions): string;
52
73
 
@@ -58,8 +79,20 @@ export declare function extractRoutePath(filePath: string, { pagesDir, pageFile,
58
79
  */
59
80
  export declare function normalizeSegment(segment: string): string;
60
81
 
82
+ export declare function parseRouteSegments(routePath: string): RouteSegment[];
83
+
61
84
  export declare function registerMiddleware(pattern: RegExp, middleware: RouteMiddleware): void;
62
85
 
86
+ /**
87
+ * Restore the module-level router configuration to its defaults.
88
+ *
89
+ * `configureRouter` merges, so it can never drop keys back to their initial
90
+ * state. A dedicated reset lets isolated contexts (for example tests) start
91
+ * from a known configuration instead of inheriting whatever was set globally
92
+ * before, matching the explicit reset offered by `clearMiddlewares`.
93
+ */
94
+ export declare function resetRouterConfig(): void;
95
+
63
96
  export declare type RouteFileOptions = {
64
97
  pagesDir?: string;
65
98
  pageFile?: string;
@@ -84,10 +117,26 @@ export declare type RouteNode = {
84
117
 
85
118
  export declare type RouterConfig = {
86
119
  loadingFallback?: React.ComponentType;
87
- pagesDir?: string;
88
120
  enableLazyLoading?: boolean;
89
121
  };
90
122
 
123
+ export declare type RouteSegment = {
124
+ kind: "group";
125
+ name: string;
126
+ } | {
127
+ kind: "static";
128
+ name: string;
129
+ } | {
130
+ kind: "dynamic";
131
+ name: string;
132
+ } | {
133
+ kind: "catchAll";
134
+ name: string;
135
+ optional: boolean;
136
+ };
137
+
138
+ export declare function routeSegmentToPath(segment: RouteSegment): string;
139
+
91
140
  export declare function treeToRoutes(node: RouteNode, isRoot?: boolean, currentPath?: string): RouteObject[];
92
141
 
93
142
  export declare function useParams<T extends Record<string, string | undefined> = Record<string, string | undefined>>(): T;