react-fs-router 1.0.13 → 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.
- package/README.md +724 -175
- package/dist/adapters/custom.d.ts +9 -0
- package/dist/adapters/custom.d.ts.map +1 -0
- package/dist/adapters/custom.js +11 -0
- package/dist/adapters/custom.js.map +1 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +14 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/react-router.d.ts +18 -0
- package/dist/adapters/react-router.d.ts.map +1 -0
- package/dist/adapters/react-router.js +58 -0
- package/dist/adapters/react-router.js.map +1 -0
- package/dist/adapters/tanstack.d.ts +16 -0
- package/dist/adapters/tanstack.d.ts.map +1 -0
- package/dist/adapters/tanstack.js +75 -0
- package/dist/adapters/tanstack.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/generator.d.ts +48 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +272 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +53 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +100 -0
- package/dist/render.js.map +1 -0
- package/dist/scanner.d.ts +27 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +38 -0
- package/dist/scanner.js.map +1 -0
- package/dist/tree.d.ts +11 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +83 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vite.d.ts +14 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +33 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpack.d.ts +32 -0
- package/dist/webpack.d.ts.map +1 -0
- package/dist/webpack.js +26 -0
- package/dist/webpack.js.map +1 -0
- package/package.json +103 -35
- package/.idea/git_toolbox_prj.xml +0 -15
- package/.idea/modules.xml +0 -8
- package/.idea/react-filesystem-router.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/genPath.js +0 -212
- package/index.js +0 -19
- package/lib/components/Routes.js +0 -0
- package/tsconfig.json +0 -7
package/README.md
CHANGED
|
@@ -1,175 +1,724 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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! 🚀
|