@maestro-js/react-router-file-routes 1.0.0-alpha.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 +332 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +195 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Route File Naming (Polaris Adventures)
|
|
2
|
+
|
|
3
|
+
Adapted from https://remix.run/docs/en/main/file-conventions/route-files-v2
|
|
4
|
+
|
|
5
|
+
## Basic Routes
|
|
6
|
+
|
|
7
|
+
Any JavaScript or TypeScript files in the `app/routes/` directory will become routes in your application. The filename maps to the route's URL pathname, except for `index.tsx` which is the [index route](https://remix.run/docs/en/main/guides/routing#index-routes) for the [root route](https://remix.run/docs/en/main/file-conventions/route-files-v2#root-route).
|
|
8
|
+
|
|
9
|
+
<!-- prettier-ignore -->
|
|
10
|
+
```diff
|
|
11
|
+
app/
|
|
12
|
+
├── routes/
|
|
13
|
+
+ │ ├── index.tsx
|
|
14
|
+
+ │ └── about.tsx
|
|
15
|
+
└── root.tsx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| URL | Matched Routes |
|
|
19
|
+
| -------- | -------------- |
|
|
20
|
+
| `/` | `index.tsx` |
|
|
21
|
+
| `/about` | `about.tsx` |
|
|
22
|
+
|
|
23
|
+
Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested-routing](https://remix.run/docs/en/main/guides/routing#what-is-nested-routing).
|
|
24
|
+
|
|
25
|
+
### Dot Delimiters
|
|
26
|
+
|
|
27
|
+
Adding a `.` to a route filename will create a `/` in the URL.
|
|
28
|
+
|
|
29
|
+
<!-- prettier-ignore -->
|
|
30
|
+
```diff
|
|
31
|
+
app/
|
|
32
|
+
├── routes/
|
|
33
|
+
│ ├── index.tsx
|
|
34
|
+
│ ├── about.tsx
|
|
35
|
+
+ │ ├── concerts.trending.tsx
|
|
36
|
+
+ │ ├── concerts.salt-lake-city.tsx
|
|
37
|
+
+ │ └── concerts.san-diego.tsx
|
|
38
|
+
└── root.tsx
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| URL | Matched Route |
|
|
42
|
+
| -------------------------- | ----------------------------- |
|
|
43
|
+
| `/concerts/trending` | `concerts.trending.tsx` |
|
|
44
|
+
| `/concerts/salt-lake-city` | `concerts.salt-lake-city.tsx` |
|
|
45
|
+
| `/concerts/san-diego` | `concerts.san-diego.tsx` |
|
|
46
|
+
|
|
47
|
+
### Folder Structure
|
|
48
|
+
|
|
49
|
+
Adding a parent folder to a route file will also create a `/` in the URL.
|
|
50
|
+
|
|
51
|
+
<!-- prettier-ignore -->
|
|
52
|
+
```diff
|
|
53
|
+
app/
|
|
54
|
+
├── routes/
|
|
55
|
+
│ ├── index.tsx
|
|
56
|
+
│ ├── about.tsx
|
|
57
|
+
+ │ └── concerts
|
|
58
|
+
+ │ ├── trending.tsx
|
|
59
|
+
+ │ ├── salt-lake-city.tsx
|
|
60
|
+
+ │ └── san-diego.tsx
|
|
61
|
+
└── root.tsx
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| URL | Matched Route |
|
|
65
|
+
| -------------------------- | ----------------------------- |
|
|
66
|
+
| `/concerts/trending` | `concerts/trending.tsx` |
|
|
67
|
+
| `/concerts/salt-lake-city` | `concerts/salt-lake-city.tsx` |
|
|
68
|
+
| `/concerts/san-diego` | `concerts/san-diego.tsx` |
|
|
69
|
+
|
|
70
|
+
### Dynamic Segments
|
|
71
|
+
|
|
72
|
+
Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
|
|
73
|
+
|
|
74
|
+
<!-- prettier-ignore -->
|
|
75
|
+
```diff
|
|
76
|
+
app/
|
|
77
|
+
├── routes/
|
|
78
|
+
│ ├── index.tsx
|
|
79
|
+
│ ├── about.tsx
|
|
80
|
+
+ │ └── concerts
|
|
81
|
+
+ │ ├── trending.tsx
|
|
82
|
+
+ │ └── $city.tsx
|
|
83
|
+
└── root.tsx
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| URL | Matched Route |
|
|
87
|
+
| -------------------------- | ----------------------- |
|
|
88
|
+
| `/concerts/trending` | `concerts/trending.tsx` |
|
|
89
|
+
| `/concerts/salt-lake-city` | `concerts/$city.tsx` |
|
|
90
|
+
| `/concerts/san-diego` | `concerts/$city.tsx` |
|
|
91
|
+
|
|
92
|
+
### Nested Routes
|
|
93
|
+
|
|
94
|
+
You can create nested routes with nested folders. By default, a file with the name `_layout` will become the layout for all routes in that folder.
|
|
95
|
+
|
|
96
|
+
```diff
|
|
97
|
+
app/
|
|
98
|
+
├── routes/
|
|
99
|
+
│ ├── index.tsx
|
|
100
|
+
│ ├── about.tsx
|
|
101
|
+
+ │ └── concerts
|
|
102
|
+
+ │ ├── _layout.tsx
|
|
103
|
+
+ │ ├── $city.tsx
|
|
104
|
+
+ │ ├── index.tsx
|
|
105
|
+
+ │ └── trending.tsx
|
|
106
|
+
└── root.tsx
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
All the routes in the `concerts` folder will be child routes of `concerts/_layout.tsx` and render inside the parent route's outlet.
|
|
110
|
+
|
|
111
|
+
| URL | Matched Route | Layout |
|
|
112
|
+
| ------------------------ | --------------------- | ----------------------------------- |
|
|
113
|
+
| / | index.tsx | `root.tsx` |
|
|
114
|
+
| /about | about.tsx | `root.tsx` |
|
|
115
|
+
| /concerts | concerts/index.tsx | `root.tsx` > `concerts/_layout.tsx` |
|
|
116
|
+
| /concerts/trending | concerts/trending.tsx | `root.tsx` > `concerts/_layout.tsx` |
|
|
117
|
+
| /concerts/salt-lake-city | concerts/$city.tsx | `root.tsx` > `concerts/_layout.tsx` |
|
|
118
|
+
|
|
119
|
+
Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
|
|
120
|
+
|
|
121
|
+
### Nested Layouts without Nested URLs
|
|
122
|
+
|
|
123
|
+
We call these **Pathless Routes**
|
|
124
|
+
|
|
125
|
+
Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore in the folder name.
|
|
126
|
+
|
|
127
|
+
<!-- prettier-ignore -->
|
|
128
|
+
```diff
|
|
129
|
+
app/
|
|
130
|
+
├── routes/
|
|
131
|
+
+ │ ├── _auth
|
|
132
|
+
+ │ │ ├── login.tsx
|
|
133
|
+
+ │ │ ├── _layout.tsx
|
|
134
|
+
+ │ │ └── register.tsx
|
|
135
|
+
│ ├── index.tsx
|
|
136
|
+
│ └── concerts
|
|
137
|
+
│ ├── _layout.tsx
|
|
138
|
+
│ └── $city.tsx
|
|
139
|
+
└── root.tsx
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| URL | Matched Route | Layout |
|
|
143
|
+
| -------------------------- | -------------------- | ----------------------------------- |
|
|
144
|
+
| `/` | `index.tsx` | `root.tsx` |
|
|
145
|
+
| `/login` | `_auth/login.tsx` | `root.tsx` > `_auth/_layout.tsx` |
|
|
146
|
+
| `/register` | `_auth/register.tsx` | `root.tsx` > `_auth/_layout.tsx` |
|
|
147
|
+
| `/concerts/salt-lake-city` | `concerts/$city.tsx` | `root.tsx` > `concerts/_layout.tsx` |
|
|
148
|
+
|
|
149
|
+
Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
|
|
150
|
+
|
|
151
|
+
Pathless routes can be used at any nesting level to create additional shared layouts.
|
|
152
|
+
|
|
153
|
+
<!-- prettier-ignore -->
|
|
154
|
+
```diff
|
|
155
|
+
app/
|
|
156
|
+
├── routes/
|
|
157
|
+
│ ├── _sales/
|
|
158
|
+
│ │ ├── _invoices/
|
|
159
|
+
│ │ │ ├── $invoiceId.tsx
|
|
160
|
+
│ │ │ ├── index.tsx
|
|
161
|
+
│ │ │ └── _layout.tsx
|
|
162
|
+
│ │ ├── overview.tsx
|
|
163
|
+
│ │ └── _layout.tsx
|
|
164
|
+
│ └── about.tsx
|
|
165
|
+
└── root.tsx
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| URL | Matched Route | Layout |
|
|
169
|
+
| ----------- | --------------------------------- | ----------------------------------------------------------- |
|
|
170
|
+
| `/` | `_sales/_invoices/index.tsx` | `root.tsx` > `_sales/_layout.tsx` > `_invoices/_layout.tsx` |
|
|
171
|
+
| `/about` | `about.tsx` | `root.tsx` |
|
|
172
|
+
| `/overview` | `_sales/overview.tsx` | `root.tsx` > `_sales/_layout.tsx` |
|
|
173
|
+
| `/102000` | `_sales/_invoices/$invoiceId.tsx` | `root.tsx` > `_sales/_layout.tsx` > `_invoices/_layout.tsx` |
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
> Note: Additional `_leading` underscores in a folder name have no effect so that `__sales/` is interpreted the same as `_sales/`. Files with a `_leading` underscore in the name are given no special treatment (`_about.tsx` matches the url `/_about`)
|
|
177
|
+
|
|
178
|
+
### Optional Segments
|
|
179
|
+
|
|
180
|
+
Wrapping a route segment in parentheses will make the segment optional.
|
|
181
|
+
|
|
182
|
+
<!-- prettier-ignore -->
|
|
183
|
+
```diff
|
|
184
|
+
app/
|
|
185
|
+
├── routes/
|
|
186
|
+
+ │ ├── ($lang).tsx
|
|
187
|
+
+ │ ├── ($lang).$productId.tsx
|
|
188
|
+
+ │ └── ($lang).categories.tsx
|
|
189
|
+
└── root.tsx
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| URL | Matched Route |
|
|
193
|
+
| -------------------------- | ------------------------ |
|
|
194
|
+
| `/` | `($lang).tsx` |
|
|
195
|
+
| `/categories` | `($lang).categories.tsx` |
|
|
196
|
+
| `/en/categories` | `($lang).categories.tsx` |
|
|
197
|
+
| `/fr/categories` | `($lang).categories.tsx` |
|
|
198
|
+
| `/american-flag-speedo` | `($lang).tsx` |
|
|
199
|
+
| `/en/american-flag-speedo` | `($lang).$productId.tsx` |
|
|
200
|
+
| `/fr/american-flag-speedo` | `($lang).$productId.tsx` |
|
|
201
|
+
|
|
202
|
+
You may wonder why `/american-flag-speedo` is matching the `($lang).tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, Remix cannot reliably determine if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang).tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
|
|
203
|
+
|
|
204
|
+
### Splat Routes
|
|
205
|
+
|
|
206
|
+
While [dynamic segments](#dynamic-segments) match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
|
|
207
|
+
|
|
208
|
+
<!-- prettier-ignore -->
|
|
209
|
+
```diff
|
|
210
|
+
app/
|
|
211
|
+
├── routes/
|
|
212
|
+
│ ├── index.tsx
|
|
213
|
+
+ │ ├── $.tsx
|
|
214
|
+
│ ├── about.tsx
|
|
215
|
+
+ │ └── files.$.tsx
|
|
216
|
+
└── root.tsx
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| URL | Matched Route |
|
|
220
|
+
| -------------------------------------------- | ------------- |
|
|
221
|
+
| `/` | `index.tsx` |
|
|
222
|
+
| `/beef/and/cheese` | `$.tsx` |
|
|
223
|
+
| `/files` | `files.$.tsx` |
|
|
224
|
+
| `/files/talks/remix-conf_old.pdf` | `files.$.tsx` |
|
|
225
|
+
| `/files/talks/remix-conf_final.pdf` | `files.$.tsx` |
|
|
226
|
+
| `/files/talks/remix-conf-FINAL-MAY_2022.pdf` | `files.$.tsx` |
|
|
227
|
+
|
|
228
|
+
### Escaping Special Characters
|
|
229
|
+
|
|
230
|
+
If you want one of the special characters Remix uses for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters.
|
|
231
|
+
|
|
232
|
+
| Filename | URL |
|
|
233
|
+
| ------------------------------ | ------------------ |
|
|
234
|
+
| `routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
|
|
235
|
+
| `routes/[sitemap.xml].tsx` | `/sitemap.xml` |
|
|
236
|
+
| `routes/weird-url.[index].tsx` | `/weird-url/index` |
|
|
237
|
+
| `routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
|
|
238
|
+
| `routes/[[so-weird]].tsx` | `/[so-weird]` |
|
|
239
|
+
|
|
240
|
+
## Folders for Organization
|
|
241
|
+
|
|
242
|
+
All files or folders with a `trailing_` underscore will be excluded from the file based routing. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
|
|
243
|
+
|
|
244
|
+
Consider these routes:
|
|
245
|
+
|
|
246
|
+
```diff
|
|
247
|
+
app/
|
|
248
|
+
├── routes/
|
|
249
|
+
│ ├── _landing/
|
|
250
|
+
│ │ ├── _layout.tsx
|
|
251
|
+
│ │ ├── about.tsx
|
|
252
|
+
│ │ └── index.tsx
|
|
253
|
+
│ ├── work/
|
|
254
|
+
│ │ ├── _layout.tsx
|
|
255
|
+
│ │ ├── projects.tsx
|
|
256
|
+
│ │ └── index.tsx
|
|
257
|
+
│ └── work.projects.$id.roadmap.tsx
|
|
258
|
+
└── root.tsx
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Some, or all of them can be folders holding additional helper files.
|
|
262
|
+
|
|
263
|
+
```diff
|
|
264
|
+
app/
|
|
265
|
+
├── routes/
|
|
266
|
+
│ ├── _landing/
|
|
267
|
+
│ │ ├── _layout.tsx
|
|
268
|
+
│ │ ├── about/
|
|
269
|
+
│ │ │ ├── helpers_
|
|
270
|
+
│ │ │ │ ├── employee-profile-card.tsx
|
|
271
|
+
│ │ │ │ ├── get-employee-data.server.tsx
|
|
272
|
+
│ │ │ │ └── team-photo.jpg
|
|
273
|
+
│ │ │ └── index.tsx
|
|
274
|
+
│ │ ├── footer_.tsx
|
|
275
|
+
│ │ ├── header_.tsx
|
|
276
|
+
│ │ ├── scroll-experience_.tsx
|
|
277
|
+
│ │ └── index.tsx
|
|
278
|
+
│ ├── work/
|
|
279
|
+
│ │ ├── _layout.tsx
|
|
280
|
+
│ │ ├── projects/
|
|
281
|
+
│ │ │ ├── get-projects.server_.tsx
|
|
282
|
+
│ │ │ ├── project-card_.tsx
|
|
283
|
+
│ │ │ ├── project-buttons_.tsx
|
|
284
|
+
│ │ │ └── index.tsx
|
|
285
|
+
│ │ ├── primary-nav_.tsx
|
|
286
|
+
│ │ ├── footer_.tsx
|
|
287
|
+
│ │ ├── stats_.tsx
|
|
288
|
+
│ │ └── index.tsx
|
|
289
|
+
│ └── work.projects.$id.roadmap/
|
|
290
|
+
│ ├── chart_.tsx
|
|
291
|
+
│ ├── update-timeline.server_.tsx
|
|
292
|
+
│ └── index.tsx
|
|
293
|
+
└── root.tsx
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
# these are the same route:
|
|
298
|
+
routes/work.tsx
|
|
299
|
+
routes/work/index.tsx
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
# Differences with Remix Routing v1, Remix Routing v2, and Remix Flat Routes
|
|
303
|
+
|
|
304
|
+
This routing convention aims to be more flexible than Remix Routing v1, more intuitive than Remix Routing v2, and simpler than
|
|
305
|
+
Remix Flat Routes. Ultimately this is a matter of choosing your trade-offs and matching personal preferences, but this is the
|
|
306
|
+
convention which makes most sense for us.
|
|
307
|
+
|
|
308
|
+
### Which files are treated as route files?
|
|
309
|
+
|
|
310
|
+
**Remix Routing v1:** All files in the `routes` directory becomes routes in the application.
|
|
311
|
+
|
|
312
|
+
**Remix Routing v2:** All *top-level* files in the `routes` directory become routes in the application, but folders with a `route.tsx`
|
|
313
|
+
file inside will also become routes in the application. Any files in a folder not named `route.tsx` will be ignored.
|
|
314
|
+
|
|
315
|
+
**Remix Flat Routes:** By default all files in the `routes` directory becomes routes in the application. If a folder is suffixed with
|
|
316
|
+
`+`, only the `index.tsx` file in that folder is treated as a route file.
|
|
317
|
+
|
|
318
|
+
**Pa Routes**: All files in the `routes` directory becomes routes in the application, unless it (or one or its parent folders) is suffixed with `_`.
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
### How do I define the layout for a set of routes?
|
|
322
|
+
|
|
323
|
+
**Remix Routing v1:** A file who's name matches the name of an adjacent folder serves as the layout for all files in that folder.
|
|
324
|
+
|
|
325
|
+
**Remix Routing v2:** All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx`. Sometimes you want
|
|
326
|
+
the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment.
|
|
327
|
+
|
|
328
|
+
**Remix Flat Routes:** All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx`. Sometimes you want
|
|
329
|
+
the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment.
|
|
330
|
+
`app/routes/concerts/_layout.tsx` is equivalent to `app/routes/concerts.tsx`.
|
|
331
|
+
|
|
332
|
+
**Pa Routes**: Files named `_layout` serve as a layout for all other routes in the same folder.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface Route {
|
|
2
|
+
path: string | undefined;
|
|
3
|
+
file: string;
|
|
4
|
+
index: boolean;
|
|
5
|
+
layout: boolean;
|
|
6
|
+
children: Route[];
|
|
7
|
+
id: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
interface Dir {
|
|
10
|
+
file: string;
|
|
11
|
+
contents: (string | Dir)[];
|
|
12
|
+
}
|
|
13
|
+
type ListFilesFunction = (dir: string) => Dir;
|
|
14
|
+
interface PaRoutesOptions {
|
|
15
|
+
listFiles?: ListFilesFunction;
|
|
16
|
+
}
|
|
17
|
+
interface Alias {
|
|
18
|
+
file: string;
|
|
19
|
+
alias: string;
|
|
20
|
+
children: string[];
|
|
21
|
+
}
|
|
22
|
+
declare function getRoutes(routeDir: string, options?: PaRoutesOptions): Route[];
|
|
23
|
+
declare function defaultListFiles(appDir: string, dirFilename: string): Dir;
|
|
24
|
+
declare function aliasing(routes: Route[], aliases: Alias[]): Route[];
|
|
25
|
+
declare const ReactRouterFileRoutes: {
|
|
26
|
+
getRoutes: typeof getRoutes;
|
|
27
|
+
defaultListFiles: typeof defaultListFiles;
|
|
28
|
+
aliasing: typeof aliasing;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { ReactRouterFileRoutes };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
function getRoutes(routeDir, options = {}) {
|
|
5
|
+
const listFiles = options.listFiles ?? ((routeDir2) => defaultListFiles("app", routeDir2));
|
|
6
|
+
const dirFiles = listFiles(routeDir);
|
|
7
|
+
const routes = getRoutesForDir(dirFiles, routeDir);
|
|
8
|
+
return routes;
|
|
9
|
+
}
|
|
10
|
+
function getRoutesForDir(dir, parentPath) {
|
|
11
|
+
const layout = dir.contents.find((c) => typeof c === "string" && path.parse(c).name === "_layout");
|
|
12
|
+
const children = dir.contents.filter((c) => c !== layout).filter((c) => {
|
|
13
|
+
const name = typeof c === "string" ? path.parse(c).name : path.parse(c.file).name;
|
|
14
|
+
return !name.endsWith("_") && !name.endsWith(".server");
|
|
15
|
+
}).flatMap((file) => {
|
|
16
|
+
if (typeof file === "string") {
|
|
17
|
+
const index = isIndexFile(file, dir.file);
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
path: index ? "" : getRoutePathForFile(path.parse(file).name, dir.file),
|
|
21
|
+
file,
|
|
22
|
+
index,
|
|
23
|
+
children: [],
|
|
24
|
+
layout: false,
|
|
25
|
+
id: void 0
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
} else {
|
|
29
|
+
return getRoutesForDir(file, dir.file);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (layout) {
|
|
33
|
+
const path2 = getRoutePathForFile(dir.file, parentPath);
|
|
34
|
+
return [{ path: path2 || void 0, file: layout, children, index: false, layout: true, id: void 0 }];
|
|
35
|
+
} else {
|
|
36
|
+
const dirPath = getRoutePathForFile(dir.file, parentPath);
|
|
37
|
+
return children.map((c) => {
|
|
38
|
+
const joined = joinPaths(dirPath, c.path);
|
|
39
|
+
const newPath = joined ? removeTrailingSlashes(joined) : joined;
|
|
40
|
+
return { ...c, path: newPath };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function joinPaths(...paths) {
|
|
45
|
+
const filtered = paths.filter((p) => p !== void 0);
|
|
46
|
+
const result = path.join(...filtered);
|
|
47
|
+
if (result === ".") {
|
|
48
|
+
return void 0;
|
|
49
|
+
} else {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getRoutePathForFile(filePath, dirPath) {
|
|
54
|
+
const filename = normalizeSlashes(path.relative(dirPath, filePath));
|
|
55
|
+
const segments = getRouteSegments(filename);
|
|
56
|
+
const mappedSegments = segments.map((segment) => {
|
|
57
|
+
if (segment.startsWith("_")) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
if (segment === "$") {
|
|
61
|
+
return "*";
|
|
62
|
+
}
|
|
63
|
+
if (segment.startsWith("$")) {
|
|
64
|
+
return `:${segment.slice(1)}`;
|
|
65
|
+
}
|
|
66
|
+
if (segment.startsWith("($")) {
|
|
67
|
+
return `:${segment.slice(2, segment.length - 1)}?`;
|
|
68
|
+
}
|
|
69
|
+
if (segment.startsWith("(")) {
|
|
70
|
+
return `${segment.slice(1, segment.length - 1)}?`;
|
|
71
|
+
}
|
|
72
|
+
return segment;
|
|
73
|
+
}).filter((s) => s !== null);
|
|
74
|
+
const routePath = mappedSegments.join("/");
|
|
75
|
+
return routePath;
|
|
76
|
+
}
|
|
77
|
+
function isIndexFile(filePath, dirPath) {
|
|
78
|
+
const relative = normalizeSlashes(path.relative(dirPath, filePath));
|
|
79
|
+
const isIndex = path.parse(relative).name === "index";
|
|
80
|
+
return isIndex;
|
|
81
|
+
}
|
|
82
|
+
function removeTrailingSlashes(p) {
|
|
83
|
+
return p === "/" ? p : p.replace(/\/$/, "");
|
|
84
|
+
}
|
|
85
|
+
function isPathSeparator(char) {
|
|
86
|
+
return /[\/\\.]/.test(char);
|
|
87
|
+
}
|
|
88
|
+
function getRouteSegments(name, paramPrefixChar = "$") {
|
|
89
|
+
let routeSegments = [];
|
|
90
|
+
let i = 0;
|
|
91
|
+
let routeSegment = "";
|
|
92
|
+
let state = "START";
|
|
93
|
+
let subState = "NORMAL";
|
|
94
|
+
let pushRouteSegment = (routeSegment2) => {
|
|
95
|
+
if (routeSegment2) {
|
|
96
|
+
routeSegments.push(routeSegment2);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
while (i < name.length) {
|
|
100
|
+
let char = name[i];
|
|
101
|
+
switch (state) {
|
|
102
|
+
case "START":
|
|
103
|
+
if (routeSegment.includes(paramPrefixChar) && !(routeSegment.startsWith(paramPrefixChar) || routeSegment.startsWith(`(${paramPrefixChar}`))) {
|
|
104
|
+
throw new Error(`Route params must start with prefix char ${paramPrefixChar}: ${routeSegment}`);
|
|
105
|
+
}
|
|
106
|
+
if (routeSegment.includes("(") && !routeSegment.startsWith("(") && !routeSegment.endsWith(")")) {
|
|
107
|
+
throw new Error(`Optional routes must start and end with parentheses: ${routeSegment}`);
|
|
108
|
+
}
|
|
109
|
+
pushRouteSegment(routeSegment);
|
|
110
|
+
routeSegment = "";
|
|
111
|
+
state = "PATH";
|
|
112
|
+
continue;
|
|
113
|
+
// restart without advancing index
|
|
114
|
+
case "PATH":
|
|
115
|
+
if (isPathSeparator(char) && subState === "NORMAL") {
|
|
116
|
+
state = "START";
|
|
117
|
+
break;
|
|
118
|
+
} else if (char === "[" && subState === "NORMAL") {
|
|
119
|
+
subState = "ESCAPE";
|
|
120
|
+
break;
|
|
121
|
+
} else if (char === "]" && subState === "ESCAPE") {
|
|
122
|
+
subState = "NORMAL";
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
routeSegment += char;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
pushRouteSegment(routeSegment);
|
|
131
|
+
return routeSegments;
|
|
132
|
+
}
|
|
133
|
+
function normalizeSlashes(file) {
|
|
134
|
+
return file.split(path.win32.sep).join("/");
|
|
135
|
+
}
|
|
136
|
+
function defaultListFiles(appDir, dirFilename) {
|
|
137
|
+
const contentsRaw = fs.readdirSync(path.join(appDir, dirFilename));
|
|
138
|
+
const contents = contentsRaw.map((filename) => {
|
|
139
|
+
const filePath = path.join(dirFilename, filename);
|
|
140
|
+
const stat = fs.lstatSync(path.join(appDir, dirFilename, filename));
|
|
141
|
+
if (stat.isDirectory()) {
|
|
142
|
+
return defaultListFiles(appDir, filePath);
|
|
143
|
+
} else if (stat.isFile()) {
|
|
144
|
+
return path.join(dirFilename, filename);
|
|
145
|
+
} else {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}).filter((f) => f !== null);
|
|
149
|
+
return { contents, file: dirFilename };
|
|
150
|
+
}
|
|
151
|
+
function aliasing(routes, aliases) {
|
|
152
|
+
function getAliasParentInstance(routes2, aliasFile, instance) {
|
|
153
|
+
routes2.forEach((route) => {
|
|
154
|
+
if (instance.route) return;
|
|
155
|
+
if (route.file === aliasFile) {
|
|
156
|
+
instance.route = route;
|
|
157
|
+
instance.parent = routes2;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (route.children.length > 0) getAliasParentInstance(route.children, aliasFile, instance);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
aliases.forEach((curr) => {
|
|
164
|
+
const { file, alias, children } = curr;
|
|
165
|
+
const instance = { route: void 0, parent: void 0 };
|
|
166
|
+
getAliasParentInstance(routes, file, instance);
|
|
167
|
+
if (!instance.route || !instance.parent) {
|
|
168
|
+
throw new Error(`File ${file} for ${alias} not found in routes`);
|
|
169
|
+
}
|
|
170
|
+
const selectedRoute = instance.route;
|
|
171
|
+
const parent = instance.parent;
|
|
172
|
+
parent.push({
|
|
173
|
+
file: selectedRoute.file,
|
|
174
|
+
index: selectedRoute.index,
|
|
175
|
+
children: selectedRoute.children.filter((child) => children?.length && child.path ? children.includes(child.path) : true).map((child) => {
|
|
176
|
+
return {
|
|
177
|
+
...child,
|
|
178
|
+
id: `${alias}/${child.path}`
|
|
179
|
+
};
|
|
180
|
+
}),
|
|
181
|
+
layout: selectedRoute.layout,
|
|
182
|
+
path: alias,
|
|
183
|
+
id: alias
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
return routes;
|
|
187
|
+
}
|
|
188
|
+
var ReactRouterFileRoutes = {
|
|
189
|
+
getRoutes,
|
|
190
|
+
defaultListFiles,
|
|
191
|
+
aliasing
|
|
192
|
+
};
|
|
193
|
+
export {
|
|
194
|
+
ReactRouterFileRoutes
|
|
195
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/react-router-file-routes",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"default": "./dist/index.js"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"version": "1.0.0-alpha.0",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "restricted"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.19.11"
|
|
20
|
+
},
|
|
21
|
+
"license": "UNLICENSED",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22.18.0"
|
|
24
|
+
},
|
|
25
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "beartest ./tests/**/*",
|
|
30
|
+
"format": "prettier --write src/ tests/",
|
|
31
|
+
"lint": "prettier --check src/ tests/"
|
|
32
|
+
}
|
|
33
|
+
}
|