react-fs-router 1.0.12 → 2.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.
Files changed (66) hide show
  1. package/README.md +724 -175
  2. package/dist/adapters/custom.d.ts +9 -0
  3. package/dist/adapters/custom.d.ts.map +1 -0
  4. package/dist/adapters/custom.js +11 -0
  5. package/dist/adapters/custom.js.map +1 -0
  6. package/dist/adapters/index.d.ts +8 -0
  7. package/dist/adapters/index.d.ts.map +1 -0
  8. package/dist/adapters/index.js +14 -0
  9. package/dist/adapters/index.js.map +1 -0
  10. package/dist/adapters/react-router.d.ts +18 -0
  11. package/dist/adapters/react-router.d.ts.map +1 -0
  12. package/dist/adapters/react-router.js +58 -0
  13. package/dist/adapters/react-router.js.map +1 -0
  14. package/dist/adapters/tanstack.d.ts +16 -0
  15. package/dist/adapters/tanstack.d.ts.map +1 -0
  16. package/dist/adapters/tanstack.js +75 -0
  17. package/dist/adapters/tanstack.js.map +1 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +59 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/config.d.ts +24 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +81 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/generator.d.ts +48 -0
  27. package/dist/generator.d.ts.map +1 -0
  28. package/dist/generator.js +272 -0
  29. package/dist/generator.js.map +1 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +8 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/render.d.ts +53 -0
  35. package/dist/render.d.ts.map +1 -0
  36. package/dist/render.js +100 -0
  37. package/dist/render.js.map +1 -0
  38. package/dist/scanner.d.ts +27 -0
  39. package/dist/scanner.d.ts.map +1 -0
  40. package/dist/scanner.js +38 -0
  41. package/dist/scanner.js.map +1 -0
  42. package/dist/tree.d.ts +11 -0
  43. package/dist/tree.d.ts.map +1 -0
  44. package/dist/tree.js +83 -0
  45. package/dist/tree.js.map +1 -0
  46. package/dist/types.d.ts +98 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/vite.d.ts +14 -0
  51. package/dist/vite.d.ts.map +1 -0
  52. package/dist/vite.js +33 -0
  53. package/dist/vite.js.map +1 -0
  54. package/dist/webpack.d.ts +32 -0
  55. package/dist/webpack.d.ts.map +1 -0
  56. package/dist/webpack.js +26 -0
  57. package/dist/webpack.js.map +1 -0
  58. package/package.json +103 -35
  59. package/.idea/git_toolbox_prj.xml +0 -15
  60. package/.idea/modules.xml +0 -8
  61. package/.idea/react-filesystem-router.iml +0 -12
  62. package/.idea/vcs.xml +0 -6
  63. package/genPath.js +0 -212
  64. package/index.js +0 -19
  65. package/lib/components/Routes.js +0 -0
  66. package/tsconfig.json +0 -7
package/README.md CHANGED
@@ -1,175 +1,724 @@
1
- # React-fs-router
2
-
3
- A cli library for implementing file-system based routing in React applications.
4
- ## Installation
5
-
6
- Use yarn or npm to install react-fs-router
7
-
8
- ```bash
9
- yarn add react-fs-router
10
- ```
11
-
12
- ```bash
13
- npm install react-fs-router
14
- ```
15
-
16
- ## Usage
17
-
18
- The position of a file in the folder structure defines it's path. Files named index are treated as the root and default file in each folder.
19
- You can define a dynamic route by placing the name of the file in brackets. ie: [id].js ==> :id
20
-
21
- | Arguments | Result |
22
- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
23
- | -i --input <directory> | Path to watch for changes. Required. |
24
- | -o --output <output> | Optional argument that takes a path to place output files in. Defaults to the input directory. |
25
- | -ext --extention <extention> | Optional argument to change the file extension of the output files. Defaults to .js . |
26
- | -p --pageProperties [type] | Optional boolean argument to disable generating page_properties file. Defaults to false. |
27
- | -b --build [type] | Optional boolean argument that tells the program to run once. Defaults to false. Intended for build commands. |
28
-
29
- ```JSON
30
- "scripts": {
31
- "rfr": "rfr -d <path to pages folder>"
32
- }
33
- ```
34
-
35
- Run the development server
36
-
37
- ```bash
38
- npm run start
39
- ```
40
-
41
- Run react-fs-router
42
-
43
- ```bash
44
- npm run rfr
45
- ```
46
-
47
- The program will then generate two files one containing imports and exports of all the files inside the provided path and one containing an array of objects describing each file. The properties in the mentioned object are as follows:
48
-
49
- ```javascript
50
-
51
- component: // Name of the default export of the file
52
- name: // Name of the component mentioned in the file as // name: string
53
- icon: // Name of the icon mentioned in the file as // icon: string
54
- index: // A number by which the objects in this array are sorted by. mentioned in the file as // index: number
55
- file: // The name of the file this object is for
56
- path: // The path of this file in relation to its position in the folder structure. index files are always "/"
57
-
58
- ```
59
-
60
- Name, icon and index can be written in each file as comments and react-fs-router will read and place them in the output file.
61
-
62
- ```javascript
63
- // name: Home
64
- // icon: HomeOutlined
65
- // index: 0
66
- ```
67
-
68
- To use the generated files with react-router:
69
-
70
- ```javascript xml react
71
- function App() {
72
-
73
- function renderComponent(key, module) {
74
- return module[key] || module["default"] || module;
75
- }
76
-
77
- function renderConponents(imports: Record<string, ReactNode>) {
78
- let renders = [];
79
- // Looping through the Pages import
80
- for (const [key, Value] of Object.entries(imports)) {
81
- const Component = renderComponent(key, Value);
82
- // Finding the correct route object.
83
- const route = routes.filter(
84
- (config) => config.component === Component.name
85
- )[0];
86
-
87
- renders.push(
88
- <Route exact={true} key={route.path} path={route?.path || ""}>
89
- <Component />
90
- </Route>
91
- );
92
- }
93
- return renders;
94
- }
95
- return <Switch>{setPages(renderConponents(Pages))}</Switch>;
96
- }
97
- ```
98
-
99
- Place this component inside BrowserRouter
100
-
101
- ```xml
102
- <BrowserRouter>
103
- <App />
104
- </BrowserRouter>
105
- ```
106
-
107
- ## Using with Ant Pro Layout
108
-
109
- The routes array generated by this library can be easily used with Ant Designs pro layout component.
110
- The following is an example using an hoc:
111
-
112
- ```javascript xml
113
- function Layout(Component) {
114
- return class extends React.Component {
115
- state = {
116
- path: "",
117
- };
118
-
119
- render() {
120
- const makeIcon = (Icon) => <Icon />;
121
- const loopMenuItem = (menus) =>
122
- // If name and icon are found in the file of the component, they will appear in the
123
- // pro layout menu with the specified name and icon.
124
- menus.map(({ icon, children, ...item }) => {
125
- return {
126
- ...item,
127
- icon: icon && makeIcon(Icons[icon]),
128
- children: children && loopMenuItem(children),
129
- };
130
- });
131
-
132
- return (
133
- <ProLayout
134
- menuItemRender={(item, dom) => (
135
- <Link
136
- onClick={() => this.setState({ path: item.path || "/" })}
137
- to={item.path}
138
- >
139
- {dom}
140
- </Link>
141
- )}
142
- menu={{ request: async () => loopMenuItem(routes) }}
143
- style={{ height: "100vh" }}
144
- location={{ pathname: this.state.path || window.location.pathname }}
145
- >
146
- <PageContainer>
147
- <Component {...this.props} />
148
- </PageContainer>
149
- </ProLayout>
150
- );
151
- }
152
- };
153
- }
154
- ```
155
-
156
- ## Usage with Concurrently
157
-
158
- You can use [concurrently](https://www.npmjs.com/package/concurrently) to run rfr with react at the same time.
159
-
160
- ```bash
161
- npm install -g concurrently
162
- ```
163
-
164
- ```bash
165
- concurrently --kill-others "npm run start" "npm run rfr"
166
- ```
167
-
168
- ## Caveats
169
-
170
- 1. Since all of the files are imported and re-exported, having two files with the same name as default export will cause an error.
171
- 2. Running this package along with the react start script is not recommended as it interferes with the react script. Consider using a package like concurrently for this purpose.
172
-
173
- ## License
174
-
175
- [MIT](https://choosealicense.com/licenses/mit/)
1
+ # 🗺️ react-fs-router
2
+
3
+ > Config-driven, adapter-pluggable file-system routing for React.
4
+
5
+ Stop hand-writing route tables. Put a file in your pages folder, run the
6
+ generator, and boom 💥 you have a route. `react-fs-router` scans a directory
7
+ of page files, builds a route tree for you, and writes out a single module that
8
+ plugs straight into your favorite routing solution — React Router, TanStack
9
+ Router, or whatever router you like.
10
+
11
+ The best part? **You stay in control.** The generated module is plain, readable
12
+ code that imports *your* files and exports the pieces you need. No runtime
13
+ magic, no hidden conventions.
14
+
15
+ ---
16
+
17
+ ## 📚 What's inside
18
+
19
+ - [Why would I use this?](#-why-would-i-use-this)
20
+ - [Installation](#-installation)
21
+ - [Quick start](#-quick-start)
22
+ - [How it works](#-how-it-works)
23
+ - [File conventions](#-file-conventions)
24
+ - [Dynamic segments & route syntax](#-dynamic-segments--route-syntax)
25
+ - [Layouts](#-layouts)
26
+ - [Data functions (`loader` / `action`)](#-data-functions-loader--action)
27
+ - [Custom function wrappers](#-custom-function-wrappers)
28
+ - [Generated output](#-generated-output)
29
+ - [The `ROUTES` navigation map](#-the-routes-navigation-map)
30
+ - [Router adapters](#-router-adapters)
31
+ - [CLI & plugins](#-cli--plugins)
32
+ ---
33
+
34
+ ## 🤔 Why would I use this?
35
+
36
+ Routing by filesystem is a lovely idea on paper, but most routers need you to
37
+ hand-write a matching route tree somewhere — and then keep it in sync forever.
38
+ `react-fs-router` flips that around:
39
+
40
+ - 📂 **Your files are the source of truth.** `pages/about.tsx` *is* `/about`.
41
+ - 🔌 **Bring your own router.** Works with React Router (default), TanStack
42
+ Router, or any custom solution through small adapters.
43
+ - 🧩 **Composable wrappers.** `loading`, `error`, `guard`, `shell` — any file
44
+ name can become a wrapper around your page instead of a page itself.
45
+ - 📦 **One generated module.** Imports everything, exports routes + a ready
46
+ `Router` component you can drop into `main.tsx` in seconds.
47
+ - 🧭 **Typed-feeling navigation.** The generated `ROUTES` map gives you
48
+ constants like `ROUTES.ABOUT` and dynamic builders like
49
+ `ROUTES.USERS_ID(14)` — no more stringly-typed URLs scattered around.
50
+ - 🚧 **Sensible ignore rules.** Files starting with `_` (like `_private.tsx` or
51
+ a `_components` folder) are never turned into routes configurable, of
52
+ course.
53
+ - **Works with your toolchain.** Use the CLI, a Vite plugin, or a webpack
54
+ plugin including watch mode while you develop.
55
+
56
+ ---
57
+
58
+ ## 📦 Installation
59
+
60
+ ```bash
61
+ npm install react-fs-router
62
+ ```
63
+
64
+ Then install the router you plan to use. `react-fs-router` treats routers as
65
+ optional peers, so it never forces one on you.
66
+
67
+ ### If you're using React Router (the default) 🧭
68
+
69
+ React Router v7+ — note that v7 merged `react-router-dom` into
70
+ `react-router`, so that's the package you want:
71
+
72
+ ```bash
73
+ npm install react-router
74
+ ```
75
+
76
+ ### If you're using TanStack Router ⚡
77
+
78
+ ```bash
79
+ npm install @tanstack/react-router
80
+ ```
81
+
82
+ That's it. Each router package is an **optional** peer dependency — install
83
+ only the ones you actually use.
84
+
85
+ > 💡 **Tip:** If you load your config from a `.ts` file, you'll need Node
86
+ > **20.6 or later** (it uses native TypeScript loading). On older Node, write
87
+ > `rfr.config.js` instead.
88
+
89
+ ---
90
+
91
+ ## 🚀 Quick start
92
+
93
+ Let's build a tiny app together. This is the whole setup:
94
+
95
+ ### 1. Create a config file
96
+
97
+ ```ts
98
+ // rfr.config.ts
99
+ import { defineConfig } from "react-fs-router";
100
+
101
+ export default defineConfig({
102
+ pages: "src/pages", // 📂 where your page files live
103
+ outDir: "src", // 📤 where the generated file goes
104
+ outFileName: "routes.tsx", // 📄 name of the generated file
105
+ });
106
+ ```
107
+
108
+ Don't worry about the options yet — the defaults are sensible and everything is
109
+ explained below.
110
+
111
+ ### 2. Write some pages 📝
112
+
113
+ ```
114
+ src/pages/
115
+ ├── index.tsx → /
116
+ ├── about.tsx → /about
117
+ ├── layout.tsx → wraps every page (renders your nav/header)
118
+ └── users/
119
+ ├── index.tsx → /users
120
+ └── [id].tsx → /users/:id
121
+ ```
122
+
123
+ ```tsx
124
+ // src/pages/about.tsx
125
+ export default function About() {
126
+ return <h1>About us 🎉</h1>;
127
+ }
128
+ ```
129
+
130
+ ### 3. Generate 🪄
131
+
132
+ ```bash
133
+ npx rfr
134
+ ```
135
+
136
+ This scans `src/pages` and writes `src/routes.tsx`. Run it once, or use watch
137
+ mode while developing (`npx rfr --watch`).
138
+
139
+ ### 4. Render 🖥️
140
+
141
+ The generated file has a default `Router` component that already wires up a
142
+ React Router data router. Just render it:
143
+
144
+ ```tsx
145
+ // src/main.tsx
146
+ import { createRoot } from "react-dom/client";
147
+ import Router from "./routes";
148
+
149
+ createRoot(document.getElementById("root")!).render(<Router />);
150
+ ```
151
+
152
+ And you're live! 🎊 Navigate to `/about` and your page shows up. If you prefer
153
+ TanStack Router, only two lines change — jump to
154
+ [Router adapters](#-router-adapters) to see all the options.
155
+
156
+ ---
157
+
158
+ ## ⚙️ How it works
159
+
160
+ Under the hood there are just three steps:
161
+
162
+ 1. **Scan** 🔎 — a file system walker reads your pages directory using the
163
+ conventions you configure (file extensions, ignore rules, function names).
164
+ 2. **Build a tree** 🌳 — files and folders become a route tree. Directories
165
+ become groups, `index` files become their folder's page, `layout` files
166
+ become wrappers, and so on.
167
+ 3. **Generate** ✍️ — one module is written (default `routes.tsx`) that imports
168
+ your pages/layouts/loaders and exports the pieces documented in
169
+ [Generated output](#-generated-output).
170
+
171
+ The generator is fully deterministic: the same folder plus the same config
172
+ always produces the same file. If anything looks surprising, just open the
173
+ generated file — it's your code to read and tweak.
174
+
175
+ ---
176
+
177
+ ## 📁 File conventions
178
+
179
+ | File | Purpose |
180
+ | --- | --- |
181
+ | `index.tsx` | The page **for its folder**. `pages/index.tsx` → `/`. |
182
+ | `layout.tsx` | A **layout wrapper** for the folder. Must render an `<Outlet />` (or its `children`). |
183
+ | `loading.tsx`, `error.tsx`, or any mapped function | A **wrapper component**. Must render its `children`. |
184
+ | `loader.ts`, `action.ts` | **Data functions** attached to the route (export a function, not a component). |
185
+ | `about.tsx` | A **leaf route** → `/about`. |
186
+ | `users/[id].tsx` | A **dynamic segment** → `/users/:id` with React Router syntax. |
187
+ | `docs/[...slug].tsx` | A **catch-all segment** → `/docs/*slug` with React Router syntax. |
188
+
189
+ A few things worth knowing:
190
+
191
+ - A single page file is also a route path (`pages/about.tsx` → `/about`).
192
+ - Folders define hierarchy — **every directory under `pages` is a route path**.
193
+ - A folder's `index.tsx` is the route for the folder itself; other files inside
194
+ become its children. You can have a page *and* children at the same URL — the
195
+ `index` page renders at the folder's path while child routes render deeper.
196
+
197
+ ### 🚫 Ignoring files
198
+
199
+ - Files and directories starting with `_` are ignored **by default**
200
+ (`_private.tsx`, `_components`). The prefix is configurable via
201
+ `ignorePrefix`; set it to `""` to disable ignoring entirely.
202
+ - Dot-prefixed files/directories (`.hidden`) are also ignored by default —
203
+ turn that off with `ignoreDotFiles: false`.
204
+ - Ignore extra names with `ignore: ["helpers"]`.
205
+
206
+ The resolved prefix is exported from the generated module, so your app can
207
+ always read the actual "ignore marker" without guessing:
208
+
209
+ - `routingMeta` contains the full resolved settings (`ignorePrefix`, adapter,
210
+ and friends).
211
+ - `ignoreIdentifier` is the standalone ignore prefix (`"_"` by default).
212
+
213
+ ### 📋 Configuration reference
214
+
215
+ Create `rfr.config.ts` (or `.js`) at your project root. Here's every option
216
+ with a friendly explanation:
217
+
218
+ ```ts
219
+ import { defineConfig } from "react-fs-router";
220
+
221
+ export default defineConfig({
222
+ pages: "src/pages", // directory to scan 📂
223
+ outDir: "src", // where to write the generated routes file 📤
224
+ outFileName: "routes.tsx", // generated file name 📄
225
+ adapter: "react-router", // "react-router" | "tanstack-router" | "custom" 🔌
226
+
227
+ // Map file names to wrapper functions. A file whose base name maps to a
228
+ // function is treated as an add-on instead of a route.
229
+ functions: {
230
+ layout: "layout",
231
+ loading: "loading",
232
+ error: "error",
233
+ // your own: guard: "guard"
234
+ },
235
+
236
+ // Map file names to data functions (loader, action, ...). These files export
237
+ // a function that is attached to the route rather than a wrapper component.
238
+ loaders: {
239
+ loader: "loader",
240
+ action: "action",
241
+ },
242
+
243
+ // Whether a parent's layout cascades to layout-less descendants. Default: true.
244
+ inheritLayout: true,
245
+ // Generate the legacy declarative <BrowserRouter> component instead of the
246
+ // default data-router route objects. Default: false.
247
+ legacyBrowserRouter: false,
248
+
249
+ // Files/dirs starting with this prefix are ignored. Default: "_".
250
+ ignorePrefix: "_",
251
+ // Whether dot-prefixed files/dirs are ignored. Default: true.
252
+ ignoreDotFiles: true,
253
+ extensions: [".js", ".jsx", ".ts", ".tsx"],
254
+ ignore: ["helpers"],
255
+ importPrefix: "@/pages", // optional import alias (replaces relative imports)
256
+
257
+ // Customize how dynamic/catch-all segments are rendered. Defaults come from
258
+ // the selected adapter (react-router: `:id` / `*rest`, tanstack: `$id` / `*rest`).
259
+ formatDynamicSegment: (name) => `:${name}`,
260
+ formatCatchAllSegment: (name) => `*${name}`,
261
+ });
262
+ ```
263
+
264
+ `functions` and `loaders` may also be **functions** returning a name or `null`
265
+ — handy for fancy rules:
266
+
267
+ ```ts
268
+ export default {
269
+ pages: "src/pages",
270
+ functions: (fileName) => (fileName === "shell" ? "shell" : null),
271
+ };
272
+ ```
273
+
274
+ > 💡 **Note:** `loaders`/`actions` only run in React Router's data-router mode
275
+ > (the default). The legacy `<BrowserRouter>` mode and the TanStack adapter
276
+ > don't translate them — see the adapter sections for details.
277
+
278
+ ---
279
+
280
+ ## 🔀 Dynamic segments & route syntax
281
+
282
+ Square brackets in file names mark *dynamic* data, and the path syntax adapts
283
+ to whatever router you selected:
284
+
285
+ | File | React Router | TanStack Router |
286
+ | --- | --- | --- |
287
+ | `pages/users/[id].tsx` | `/users/:id` | `/users/$id` |
288
+ | `pages/docs/[...slug].tsx` | `/docs/*slug` | `/docs/*slug` → `$` splat route |
289
+
290
+ Every dynamic segment becomes a **parameter** you can read inside your page
291
+ with the router's normal hooks (`useParams`, `useLoaderData`, and friends). For
292
+ TanStack, catch-alls are converted to TanStack's `$` splat route automatically
293
+ by the adapter — you don't need to think about it. 🎩
294
+
295
+ Want different syntax? Override it with `formatDynamicSegment` /
296
+ `formatCatchAllSegment` in your config.
297
+
298
+ ---
299
+
300
+ ## 🖼️ Layouts
301
+
302
+ A `layout` file wraps the folder's **child routes**. Think: header + footer +
303
+ sidebar that stays mounted while you navigate between the pages inside that
304
+ folder.
305
+
306
+ Child routes render through the layout's outlet, so **your layout must render
307
+ an `<Outlet />`** — or, equivalently, render its `children` prop (the library
308
+ passes the outlet element through as `children`):
309
+
310
+ ```tsx
311
+ // pages/layout.tsx
312
+ import { Outlet } from "react-router";
313
+
314
+ export default function RootLayout() {
315
+ return (
316
+ <div>
317
+ <header>My app 🧡</header>
318
+ <Outlet /> {/* child routes render here */}
319
+ </div>
320
+ );
321
+ }
322
+ ```
323
+
324
+ The same thing using `children`:
325
+
326
+ ```tsx
327
+ export default function RootLayout({ children }: { children?: React.ReactNode }) {
328
+ return (
329
+ <div>
330
+ <header>My app 🧡</header>
331
+ {children}
332
+ </div>
333
+ );
334
+ }
335
+ ```
336
+
337
+ If the layout never renders an outlet (or `children`), child pages will not
338
+ appear — so don't forget it! 😉
339
+
340
+ ### 🪆 Layout inheritance
341
+
342
+ By default, a parent's layout **cascades** to descendants that don't define
343
+ their own layout. A descendant that *does* define its own layout replaces
344
+ (overrides) the parent's rather than nesting inside it.
345
+
346
+ Set `inheritLayout: false` to disable cascading — then a layout only wraps its
347
+ own `index` page and never its descendants.
348
+
349
+ ---
350
+
351
+ ## 🔋 Data functions (`loader` / `action`)
352
+
353
+ Files mapped in `loaders` export a plain **function** (not a component). They
354
+ are attached to the route's `loader`/`action` and executed by a React Router
355
+ data router. The result is available in the page via `useLoaderData()` /
356
+ `useActionData()` — perfect for data fetching with loading states:
357
+
358
+ ```ts
359
+ // pages/users/loader.ts
360
+ export async function loader({ params }: { params: { id: string } }) {
361
+ return { id: params.id };
362
+ }
363
+ ```
364
+
365
+ ```tsx
366
+ // pages/users/[id].tsx
367
+ import { useLoaderData } from "react-router";
368
+
369
+ export default function User() {
370
+ const data = useLoaderData() as { id: string };
371
+ return <div>{data.id}</div>;
372
+ }
373
+ ```
374
+
375
+ A directory's `loader`/`action` is attached to its `index` page so
376
+ `useLoaderData()` works there. If the directory has no `index`, the data
377
+ functions are attached to the group route so they still run for child
378
+ navigations.
379
+
380
+ ---
381
+
382
+ ## 🧩 Custom function wrappers
383
+
384
+ The `functions` map turns file names into **wrapper components**. `layout`,
385
+ `loading`, and `error` are enabled by default; add names such as `guard` or
386
+ `shell`, and files like `pages/account/guard.tsx` are treated as wrappers
387
+ instead of routes.
388
+
389
+ ### What a wrapper can do
390
+
391
+ - 🎨 Render surrounding UI — spinners, banners, error panels, or content chrome
392
+ around the page.
393
+ - 🚪 **Gate access**: return `<Navigate to={ROUTES.LOGIN} replace />` (or
394
+ `null`) instead of rendering `children` when a guard fails.
395
+ - 🧠 Provide context to the page, e.g.
396
+ `<SettingsContext.Provider value={settings}>{children}</SettingsContext.Provider>`.
397
+ - 🧭 Observe navigation with hooks such as `useLocation()` / `useParams()` and
398
+ change what it renders per location.
399
+ - 🛡️ Act as an error boundary (a class component) or suspense boundary by
400
+ wrapping `children`.
401
+
402
+ Here's a classic auth guard:
403
+
404
+ ```tsx
405
+ // pages/account/guard.tsx
406
+ // config: functions: { layout: "layout", guard: "guard" }
407
+ import { Navigate, useLocation } from "react-router";
408
+ import { ROUTES } from "./routes";
409
+
410
+ export default function Guard({ children }: { children?: React.ReactNode }) {
411
+ const location = useLocation();
412
+ if (!isAuthenticated()) {
413
+ return <Navigate to={ROUTES.LOGIN} state={{ from: location }} replace />;
414
+ }
415
+ return <>{children}</>;
416
+ }
417
+ ```
418
+
419
+ ### 📨 Props a wrapper receives
420
+
421
+ A wrapper receives exactly one prop: `children: ReactNode`. `children` is the
422
+ page element — or, when several wrappers exist on the same route, the page
423
+ already wrapped by the inner wrappers. A wrapper **must render `children`**
424
+ unless it intentionally replaces it (as the guard above does).
425
+
426
+ Wrappers do *not* receive route params, `request`, or `context`; read those
427
+ with hooks (`useParams`, `useLocation`, `useLoaderData`, ...) instead. And
428
+ remember: `loading.tsx`/`error.tsx` don't automatically react to router loading
429
+ or error state — they're ordinary wrappers. Use `useNavigation` /
430
+ `useRouteError` inside them to respond to those states.
431
+
432
+ ### 🪢 Wrapper chain
433
+
434
+ Multiple mapped files for the same route nest as a **chain** around the page,
435
+ in scan order: the last file processed becomes the outermost wrapper. Each
436
+ wrapper only needs to render its `children`, so chains compose naturally.
437
+
438
+ ### 🧱 Layouts vs wrappers
439
+
440
+ A `layout` also comes from the `functions` map, but it's special-cased: a
441
+ layout wraps the folder's *child routes* through an outlet and stays mounted
442
+ while you navigate between those children. A custom wrapper wraps a single
443
+ *page* (a directory `index` or a leaf file) only.
444
+
445
+ ### 🔄 Lifecycle
446
+
447
+ 1. In the default data-router mode, the router runs the route's
448
+ `loader`/`action` before rendering (see data functions above).
449
+ 2. When a route matches, React mounts the component tree from the outside in:
450
+ parent layouts render first, each page renders inside its parent's outlet,
451
+ and the page renders inside its wrapper chain — outermost layout →
452
+ wrapper(s) → page.
453
+ 3. A wrapper mounts together with the page it wraps and unmounts when you
454
+ navigate away from that page. Per-page state (form input, scroll position,
455
+ timers) belongs here and resets on leave. State that should survive
456
+ navigation between sibling pages belongs in a `layout` or a context
457
+ provider higher up.
458
+ 4. Navigating between dynamic values of the same route (`/users/1` →
459
+ `/users/2`) **re-renders** the same mounted component instead of remounting
460
+ it, so write effects against `useParams()` / `useLocation()`. If you need a
461
+ full remount per param change, wrap `children` in a keyed element from
462
+ inside the wrapper, e.g.
463
+ `<Fragment key={params.id}>{children}</Fragment>`.
464
+
465
+ > ⚠️ Wrappers render *inside* the layout's outlet, never before it — a wrapper
466
+ > cannot prevent its layout from rendering. To guard an entire subtree before
467
+ > any of its content shows, put the wrapper higher up the tree or in the root
468
+ > `layout`.
469
+
470
+ ---
471
+
472
+ ## 📦 Generated output
473
+
474
+ The generated file (default `routes.tsx`) exports:
475
+
476
+ - `routes` — the runtime route entries used by the component.
477
+ - `routeObjects` — (React Router only) resolved route objects with
478
+ `loader`/`action` attached. In the default mode this is ready for
479
+ `createBrowserRouter`; in legacy mode it is ready for `useRoutes`.
480
+ - `routeTree` — (TanStack Router only) the resolved TanStack Router route tree,
481
+ ready for `createRouter`; the default `Router` component renders it.
482
+ - `routesMeta` — a serializable route tree (paths plus function/loader file
483
+ specifiers).
484
+ - `routingMeta` — the resolved routing settings (adapter, `ignorePrefix`,
485
+ `ignoreDotFiles`, `ignore`, `inheritLayout`, `legacyBrowserRouter`).
486
+ - `ignoreIdentifier` — the resolved ignore prefix (the leading character that
487
+ marks a file as ignored; `"_"` by default).
488
+ - `ROUTES` — a navigation map of every addressable page path. Static paths are
489
+ string constants; dynamic paths are functions that build the URL from their
490
+ arguments (see below). 🧭
491
+ - A `default` export — the single `FileSystemRouter` component.
492
+
493
+ ---
494
+
495
+ ## 🧭 The `ROUTES` navigation map
496
+
497
+ Stop hard-coding URLs. Use `ROUTES` instead of stringly-typed paths for links
498
+ and programmatic navigation. Keys are the uppercase path segments joined with
499
+ `_`; the root index route is `HOME`. Dynamic segments (and their catch-alls)
500
+ become function arguments in path order:
501
+
502
+ | File | Generated entry |
503
+ | --- | --- |
504
+ | `pages/index.tsx` | `ROUTES.HOME === "/"` |
505
+ | `pages/about.tsx` | `ROUTES.ABOUT === "/about"` |
506
+ | `pages/users/index.tsx` | `ROUTES.USERS === "/users"` |
507
+ | `pages/users/[id].tsx` | `ROUTES.USERS_ID(14) === "/users/14"` |
508
+ | `pages/docs/[...slug].tsx` | `ROUTES.DOCS_SLUG("guides", "intro") === "/docs/guides/intro"` |
509
+
510
+ ```tsx
511
+ import { Link } from "react-router";
512
+ import { ROUTES } from "./routes";
513
+
514
+ <Link to={ROUTES.USERS_ID(14)}>User 14</Link>;
515
+ ```
516
+
517
+ Beautiful, type-safe-ish navigation with zero extra tooling. ✨
518
+
519
+ ---
520
+
521
+ ## 🔌 Router adapters
522
+
523
+ This is where `react-fs-router` shines: the *same* page folder can target
524
+ different routers, and the generated module changes accordingly.
525
+
526
+ ### 🧭 React Router (default)
527
+
528
+ By default the generated module targets React Router's **data router** API. It
529
+ exports `routeObjects` — ready for `createBrowserRouter` — and a default
530
+ `Router` component that renders a data router directly. Loaders/actions work
531
+ out of the box. 🎉
532
+
533
+ ```tsx
534
+ // main.tsx
535
+ import { createRoot } from "react-dom/client";
536
+ import { createBrowserRouter, RouterProvider } from "react-router";
537
+ import { routeObjects } from "./routes";
538
+
539
+ const router = createBrowserRouter(routeObjects);
540
+
541
+ createRoot(document.getElementById("root")!).render(
542
+ <RouterProvider router={router} />,
543
+ );
544
+ ```
545
+
546
+ Or use the generated default component (it already wraps a data router):
547
+
548
+ ```tsx
549
+ // main.tsx
550
+ import { createRoot } from "react-dom/client";
551
+ import Router from "./routes";
552
+
553
+ createRoot(document.getElementById("root")!).render(<Router />);
554
+ ```
555
+
556
+ #### Legacy: `<BrowserRouter>` (opt-in)
557
+
558
+ Prefer the older declarative API (`<BrowserRouter>` + `useRoutes`)? Set
559
+ `legacyBrowserRouter: true` in config. The generated default component then
560
+ renders via `useRoutes` and must be placed inside a `<BrowserRouter>`:
561
+
562
+ ```tsx
563
+ // main.tsx
564
+ import { createRoot } from "react-dom/client";
565
+ import { BrowserRouter } from "react-router";
566
+ import Router from "./routes";
567
+
568
+ createRoot(document.getElementById("root")!).render(
569
+ <BrowserRouter>
570
+ <Router />
571
+ </BrowserRouter>,
572
+ );
573
+ ```
574
+
575
+ > ⚠️ Note: the legacy declarative mode ignores `loader`/`action` data
576
+ > functions. Use the default data-router mode when you need loaders.
577
+
578
+ ### ⚡ TanStack Router
579
+
580
+ TanStack Router is wired up the same easy way. Set
581
+ `adapter: "tanstack-router"` in your config. Files are still written with
582
+ square brackets; dynamic files map to TanStack Router's `$param` routes and
583
+ catch-all files map to TanStack Router's `$` splat routes:
584
+
585
+ | File | TanStack route |
586
+ | --- | --- |
587
+ | `pages/index.tsx` | `/` |
588
+ | `pages/about.tsx` | `/about` |
589
+ | `pages/users/[id].tsx` | `/users/$id` |
590
+ | `pages/docs/[...slug].tsx` | `/docs/$` |
591
+
592
+ Internally the resolved tree keeps the adapter-neutral `*rest` catch-all syntax
593
+ (also used by `ROUTES`); `toTanstackRouteTree` rewrites it to TanStack's `$`
594
+ splat route for you. 🎩
595
+
596
+ ```ts
597
+ // rfr.config.ts
598
+ import { defineConfig } from "react-fs-router";
599
+
600
+ export default defineConfig({
601
+ pages: "src/pages",
602
+ adapter: "tanstack-router",
603
+ });
604
+ ```
605
+
606
+ The generated module exports `routes`, `routesMeta`, `ROUTES`, `routingMeta`,
607
+ `ignoreIdentifier`, and `routeTree` — a ready-made TanStack Router route tree —
608
+ plus a default `Router` component that builds a TanStack Router from it and
609
+ renders it. First install the router peer package:
610
+
611
+ ```bash
612
+ npm install @tanstack/react-router
613
+ ```
614
+
615
+ Then render the default component:
616
+
617
+ ```tsx
618
+ // main.tsx
619
+ import { createRoot } from "react-dom/client";
620
+ import Router from "./routes";
621
+
622
+ createRoot(document.getElementById("root")!).render(<Router />);
623
+ ```
624
+
625
+ Want the router instance yourself? Use the exported `routeTree` with TanStack
626
+ Router's `createRouter` and `RouterProvider`:
627
+
628
+ ```tsx
629
+ // main.tsx
630
+ import { createRoot } from "react-dom/client";
631
+ import { RouterProvider, createHashHistory, createRouter } from "@tanstack/react-router";
632
+ import { routeTree } from "./routes";
633
+
634
+ const router = createRouter({ routeTree, history: createHashHistory() });
635
+
636
+ createRoot(document.getElementById("root")!).render(
637
+ <RouterProvider router={router} />,
638
+ );
639
+ ```
640
+
641
+ `toTanstackRouteTree` (exported from `react-fs-router/adapters/tanstack`) is the
642
+ conversion behind `routeTree`: it maps resolved routes into TanStack
643
+ `createRoute` entries, applies root/layout wrappers, converts dynamic segments
644
+ (`[id]` → `$id`) and catch-alls (`*rest` → `$` splat), and renders through a
645
+ `RouterProvider`.
646
+
647
+ > 💡 `loader`/`action` files are not translated to TanStack loaders — those
648
+ > belong on your own TanStack route definitions. The tree is built at runtime,
649
+ > so `params`/`search` are not statically typed; register file routes with
650
+ > TanStack's codegen (`routeTree.gen.ts` + `createFileRoute`) when you want
651
+ > full type safety.
652
+
653
+ ### 🎨 Bring your own router (`custom`)
654
+
655
+ For any other routing solution, use the generated `routes` plus a
656
+ `renderRoutes` callback (or `createCustomAdapter` from
657
+ `react-fs-router/adapters/custom`):
658
+
659
+ ```tsx
660
+ import Router from "./routes";
661
+
662
+ <Router
663
+ renderRoutes={(resolvedRoutes) => (
664
+ <MyRouter routes={resolvedRoutes} />
665
+ )}
666
+ />;
667
+ ```
668
+
669
+ Register your own adapter:
670
+
671
+ ```ts
672
+ import { defineAdapter } from "react-fs-router";
673
+
674
+ defineAdapter("my-router", ({ routes }) => <MyRoutes routes={routes} />);
675
+ ```
676
+
677
+ ---
678
+
679
+ ## 🛠️ CLI & plugins
680
+
681
+ ### CLI (any toolchain)
682
+
683
+ ```bash
684
+ npx rfr --input src/pages --output src --adapter react-router
685
+ ```
686
+
687
+ Options:
688
+
689
+ | Flag | Description |
690
+ | --- | --- |
691
+ | `-i, --input <dir>` | Pages directory (overrides config) |
692
+ | `-c, --config <file>` | Config file path (default `rfr.config.ts`) |
693
+ | `-o, --output <dir>` | Output directory (overrides config) |
694
+ | `-a, --adapter <name>` | Routing adapter to use |
695
+ | `-w, --watch` | Watch and regenerate on changes |
696
+ | `-b, --build` | Generate once and exit (default) |
697
+
698
+ ### ⚡ Vite plugin
699
+
700
+ ```ts
701
+ // vite.config.ts
702
+ import { reactFsRouter } from "react-fs-router/vite";
703
+
704
+ export default {
705
+ plugins: [reactFsRouter({ userConfig: { pages: "src/pages" } })],
706
+ };
707
+ ```
708
+
709
+ ### 📦 Webpack plugin
710
+
711
+ ```js
712
+ // webpack.config.js
713
+ const { ReactFsRouterWebpackPlugin } = require("react-fs-router/webpack");
714
+
715
+ module.exports = {
716
+ plugins: [new ReactFsRouterWebpackPlugin({ userConfig: { pages: "src/pages" } })],
717
+ };
718
+ ```
719
+
720
+ ---
721
+
722
+ ## 📜 License
723
+
724
+ [MIT](https://choosealicense.com/licenses/mit/) — go build something awesome! 🚀