@react-router/fs-routes 0.0.0-experimental-a26b992a1
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/LICENSE.md +23 -0
- package/README.md +7 -0
- package/dist/flatRoutes.d.ts +14 -0
- package/dist/flatRoutes.js +425 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +46 -0
- package/dist/manifest.d.ts +13 -0
- package/dist/manifest.js +44 -0
- package/dist/normalizeSlashes.d.ts +1 -0
- package/dist/normalizeSlashes.js +25 -0
- package/package.json +51 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) React Training LLC 2015-2019
|
|
4
|
+
Copyright (c) Remix Software Inc. 2020-2021
|
|
5
|
+
Copyright (c) Shopify Inc. 2022-2023
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RouteManifest } from "./manifest";
|
|
2
|
+
export declare const routeModuleExts: string[];
|
|
3
|
+
export declare let paramPrefixChar: "$";
|
|
4
|
+
export declare let escapeStart: "[";
|
|
5
|
+
export declare let escapeEnd: "]";
|
|
6
|
+
export declare let optionalStart: "(";
|
|
7
|
+
export declare let optionalEnd: ")";
|
|
8
|
+
export declare function flatRoutes(appDirectory: string, ignoredFilePatterns?: string[], prefix?: string): RouteManifest;
|
|
9
|
+
export declare function flatRoutesUniversal(appDirectory: string, routes: string[], prefix?: string): RouteManifest;
|
|
10
|
+
export declare function getRouteSegments(routeId: string): [string[], string[]];
|
|
11
|
+
export declare function createRoutePath(routeSegments: string[], rawRouteSegments: string[], isIndex?: boolean): string | undefined;
|
|
12
|
+
export declare function getRoutePathConflictErrorMessage(pathname: string, routes: string[]): string;
|
|
13
|
+
export declare function getRouteIdConflictErrorMessage(routeId: string, files: string[]): string;
|
|
14
|
+
export declare function isSegmentSeparator(checkChar: string | undefined): boolean;
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @react-router/fs-routes v0.0.0-experimental-a26b992a1
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Remix Software Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE.md file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
14
|
+
|
|
15
|
+
var fs = require('node:fs');
|
|
16
|
+
var path = require('node:path');
|
|
17
|
+
var minimatch = require('minimatch');
|
|
18
|
+
var normalizeSlashes = require('./normalizeSlashes.js');
|
|
19
|
+
|
|
20
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
21
|
+
|
|
22
|
+
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
23
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
24
|
+
|
|
25
|
+
const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
|
|
26
|
+
let paramPrefixChar = "$";
|
|
27
|
+
let escapeStart = "[";
|
|
28
|
+
let escapeEnd = "]";
|
|
29
|
+
let optionalStart = "(";
|
|
30
|
+
let optionalEnd = ")";
|
|
31
|
+
const PrefixLookupTrieEndSymbol = Symbol("PrefixLookupTrieEndSymbol");
|
|
32
|
+
class PrefixLookupTrie {
|
|
33
|
+
root = {
|
|
34
|
+
[PrefixLookupTrieEndSymbol]: false
|
|
35
|
+
};
|
|
36
|
+
add(value) {
|
|
37
|
+
if (!value) throw new Error("Cannot add empty string to PrefixLookupTrie");
|
|
38
|
+
let node = this.root;
|
|
39
|
+
for (let char of value) {
|
|
40
|
+
if (!node[char]) {
|
|
41
|
+
node[char] = {
|
|
42
|
+
[PrefixLookupTrieEndSymbol]: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
node = node[char];
|
|
46
|
+
}
|
|
47
|
+
node[PrefixLookupTrieEndSymbol] = true;
|
|
48
|
+
}
|
|
49
|
+
findAndRemove(prefix, filter) {
|
|
50
|
+
let node = this.root;
|
|
51
|
+
for (let char of prefix) {
|
|
52
|
+
if (!node[char]) return [];
|
|
53
|
+
node = node[char];
|
|
54
|
+
}
|
|
55
|
+
return this.#findAndRemoveRecursive([], node, prefix, filter);
|
|
56
|
+
}
|
|
57
|
+
#findAndRemoveRecursive(values, node, prefix, filter) {
|
|
58
|
+
for (let char of Object.keys(node)) {
|
|
59
|
+
this.#findAndRemoveRecursive(values, node[char], prefix + char, filter);
|
|
60
|
+
}
|
|
61
|
+
if (node[PrefixLookupTrieEndSymbol] && filter(prefix)) {
|
|
62
|
+
node[PrefixLookupTrieEndSymbol] = false;
|
|
63
|
+
values.push(prefix);
|
|
64
|
+
}
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function flatRoutes(appDirectory, ignoredFilePatterns = [], prefix = "routes") {
|
|
69
|
+
let ignoredFileRegex = Array.from(new Set(["**/.*", ...ignoredFilePatterns])).map(re => minimatch.makeRe(re)).filter(re => !!re);
|
|
70
|
+
let routesDir = path__default["default"].join(appDirectory, prefix);
|
|
71
|
+
let rootRoute = findFile(appDirectory, "root", routeModuleExts);
|
|
72
|
+
if (!rootRoute) {
|
|
73
|
+
throw new Error(`Could not find a root route module in the app directory: ${appDirectory}`);
|
|
74
|
+
}
|
|
75
|
+
if (!fs__default["default"].existsSync(rootRoute)) {
|
|
76
|
+
throw new Error(`Could not find the routes directory: ${routesDir}. Did you forget to create it?`);
|
|
77
|
+
}
|
|
78
|
+
// Only read the routes directory
|
|
79
|
+
let entries = fs__default["default"].readdirSync(routesDir, {
|
|
80
|
+
withFileTypes: true,
|
|
81
|
+
encoding: "utf-8"
|
|
82
|
+
});
|
|
83
|
+
let routes = [];
|
|
84
|
+
for (let entry of entries) {
|
|
85
|
+
let filepath = normalizeSlashes.normalizeSlashes(path__default["default"].join(routesDir, entry.name));
|
|
86
|
+
let route = null;
|
|
87
|
+
// If it's a directory, don't recurse into it, instead just look for a route module
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
route = findRouteModuleForFolder(appDirectory, filepath, ignoredFileRegex);
|
|
90
|
+
} else if (entry.isFile()) {
|
|
91
|
+
route = findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex);
|
|
92
|
+
}
|
|
93
|
+
if (route) routes.push(route);
|
|
94
|
+
}
|
|
95
|
+
let routeManifest = flatRoutesUniversal(appDirectory, routes, prefix);
|
|
96
|
+
return routeManifest;
|
|
97
|
+
}
|
|
98
|
+
function flatRoutesUniversal(appDirectory, routes, prefix = "routes") {
|
|
99
|
+
let urlConflicts = new Map();
|
|
100
|
+
let routeManifest = {};
|
|
101
|
+
let prefixLookup = new PrefixLookupTrie();
|
|
102
|
+
let uniqueRoutes = new Map();
|
|
103
|
+
let routeIdConflicts = new Map();
|
|
104
|
+
// id -> file
|
|
105
|
+
let routeIds = new Map();
|
|
106
|
+
for (let file of routes) {
|
|
107
|
+
let normalizedFile = normalizeSlashes.normalizeSlashes(file);
|
|
108
|
+
let routeExt = path__default["default"].extname(normalizedFile);
|
|
109
|
+
let routeDir = path__default["default"].dirname(normalizedFile);
|
|
110
|
+
let normalizedApp = normalizeSlashes.normalizeSlashes(appDirectory);
|
|
111
|
+
let routeId = routeDir === path__default["default"].posix.join(normalizedApp, prefix) ? path__default["default"].posix.relative(normalizedApp, normalizedFile).slice(0, -routeExt.length) : path__default["default"].posix.relative(normalizedApp, routeDir);
|
|
112
|
+
let conflict = routeIds.get(routeId);
|
|
113
|
+
if (conflict) {
|
|
114
|
+
let currentConflicts = routeIdConflicts.get(routeId);
|
|
115
|
+
if (!currentConflicts) {
|
|
116
|
+
currentConflicts = [path__default["default"].posix.relative(normalizedApp, conflict)];
|
|
117
|
+
}
|
|
118
|
+
currentConflicts.push(path__default["default"].posix.relative(normalizedApp, normalizedFile));
|
|
119
|
+
routeIdConflicts.set(routeId, currentConflicts);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
routeIds.set(routeId, normalizedFile);
|
|
123
|
+
}
|
|
124
|
+
let sortedRouteIds = Array.from(routeIds).sort(([a], [b]) => b.length - a.length);
|
|
125
|
+
for (let [routeId, file] of sortedRouteIds) {
|
|
126
|
+
let index = routeId.endsWith("_index");
|
|
127
|
+
let [segments, raw] = getRouteSegments(routeId.slice(prefix.length + 1));
|
|
128
|
+
let pathname = createRoutePath(segments, raw, index);
|
|
129
|
+
routeManifest[routeId] = {
|
|
130
|
+
file: file.slice(appDirectory.length + 1),
|
|
131
|
+
id: routeId,
|
|
132
|
+
path: pathname
|
|
133
|
+
};
|
|
134
|
+
if (index) routeManifest[routeId].index = true;
|
|
135
|
+
let childRouteIds = prefixLookup.findAndRemove(routeId, value => {
|
|
136
|
+
return [".", "/"].includes(value.slice(routeId.length).charAt(0));
|
|
137
|
+
});
|
|
138
|
+
prefixLookup.add(routeId);
|
|
139
|
+
if (childRouteIds.length > 0) {
|
|
140
|
+
for (let childRouteId of childRouteIds) {
|
|
141
|
+
routeManifest[childRouteId].parentId = routeId;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// path creation
|
|
146
|
+
let parentChildrenMap = new Map();
|
|
147
|
+
for (let [routeId] of sortedRouteIds) {
|
|
148
|
+
let config = routeManifest[routeId];
|
|
149
|
+
if (!config.parentId) continue;
|
|
150
|
+
let existingChildren = parentChildrenMap.get(config.parentId) || [];
|
|
151
|
+
existingChildren.push(config);
|
|
152
|
+
parentChildrenMap.set(config.parentId, existingChildren);
|
|
153
|
+
}
|
|
154
|
+
for (let [routeId] of sortedRouteIds) {
|
|
155
|
+
let config = routeManifest[routeId];
|
|
156
|
+
let originalPathname = config.path || "";
|
|
157
|
+
let pathname = config.path;
|
|
158
|
+
let parentConfig = config.parentId ? routeManifest[config.parentId] : null;
|
|
159
|
+
if (parentConfig?.path && pathname) {
|
|
160
|
+
pathname = pathname.slice(parentConfig.path.length).replace(/^\//, "").replace(/\/$/, "");
|
|
161
|
+
}
|
|
162
|
+
if (!config.parentId) config.parentId = "root";
|
|
163
|
+
config.path = pathname || undefined;
|
|
164
|
+
/**
|
|
165
|
+
* We do not try to detect path collisions for pathless layout route
|
|
166
|
+
* files because, by definition, they create the potential for route
|
|
167
|
+
* collisions _at that level in the tree_.
|
|
168
|
+
*
|
|
169
|
+
* Consider example where a user may want multiple pathless layout routes
|
|
170
|
+
* for different subfolders
|
|
171
|
+
*
|
|
172
|
+
* routes/
|
|
173
|
+
* account.tsx
|
|
174
|
+
* account._private.tsx
|
|
175
|
+
* account._private.orders.tsx
|
|
176
|
+
* account._private.profile.tsx
|
|
177
|
+
* account._public.tsx
|
|
178
|
+
* account._public.login.tsx
|
|
179
|
+
* account._public.perks.tsx
|
|
180
|
+
*
|
|
181
|
+
* In order to support both a public and private layout for `/account/*`
|
|
182
|
+
* URLs, we are creating a mutually exclusive set of URLs beneath 2
|
|
183
|
+
* separate pathless layout routes. In this case, the route paths for
|
|
184
|
+
* both account._public.tsx and account._private.tsx is the same
|
|
185
|
+
* (/account), but we're again not expecting to match at that level.
|
|
186
|
+
*
|
|
187
|
+
* By only ignoring this check when the final portion of the filename is
|
|
188
|
+
* pathless, we will still detect path collisions such as:
|
|
189
|
+
*
|
|
190
|
+
* routes/parent._pathless.foo.tsx
|
|
191
|
+
* routes/parent._pathless2.foo.tsx
|
|
192
|
+
*
|
|
193
|
+
* and
|
|
194
|
+
*
|
|
195
|
+
* routes/parent._pathless/index.tsx
|
|
196
|
+
* routes/parent._pathless2/index.tsx
|
|
197
|
+
*/
|
|
198
|
+
let lastRouteSegment = config.id.replace(new RegExp(`^${prefix}/`), "").split(".").pop();
|
|
199
|
+
let isPathlessLayoutRoute = lastRouteSegment && lastRouteSegment.startsWith("_") && lastRouteSegment !== "_index";
|
|
200
|
+
if (isPathlessLayoutRoute) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
let conflictRouteId = originalPathname + (config.index ? "?index" : "");
|
|
204
|
+
let conflict = uniqueRoutes.get(conflictRouteId);
|
|
205
|
+
uniqueRoutes.set(conflictRouteId, config);
|
|
206
|
+
if (conflict && (originalPathname || config.index)) {
|
|
207
|
+
let currentConflicts = urlConflicts.get(originalPathname);
|
|
208
|
+
if (!currentConflicts) currentConflicts = [conflict];
|
|
209
|
+
currentConflicts.push(config);
|
|
210
|
+
urlConflicts.set(originalPathname, currentConflicts);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (routeIdConflicts.size > 0) {
|
|
215
|
+
for (let [routeId, files] of routeIdConflicts.entries()) {
|
|
216
|
+
console.error(getRouteIdConflictErrorMessage(routeId, files));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// report conflicts
|
|
220
|
+
if (urlConflicts.size > 0) {
|
|
221
|
+
for (let [path, routes] of urlConflicts.entries()) {
|
|
222
|
+
// delete all but the first route from the manifest
|
|
223
|
+
for (let i = 1; i < routes.length; i++) {
|
|
224
|
+
delete routeManifest[routes[i].id];
|
|
225
|
+
}
|
|
226
|
+
let files = routes.map(r => r.file);
|
|
227
|
+
console.error(getRoutePathConflictErrorMessage(path, files));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return routeManifest;
|
|
231
|
+
}
|
|
232
|
+
function findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex) {
|
|
233
|
+
let relativePath = normalizeSlashes.normalizeSlashes(path__default["default"].relative(appDirectory, filepath));
|
|
234
|
+
let isIgnored = ignoredFileRegex.some(regex => regex.test(relativePath));
|
|
235
|
+
if (isIgnored) return null;
|
|
236
|
+
return filepath;
|
|
237
|
+
}
|
|
238
|
+
function findRouteModuleForFolder(appDirectory, filepath, ignoredFileRegex) {
|
|
239
|
+
let relativePath = path__default["default"].relative(appDirectory, filepath);
|
|
240
|
+
let isIgnored = ignoredFileRegex.some(regex => regex.test(relativePath));
|
|
241
|
+
if (isIgnored) return null;
|
|
242
|
+
let routeRouteModule = findFile(filepath, "route", routeModuleExts);
|
|
243
|
+
let routeIndexModule = findFile(filepath, "index", routeModuleExts);
|
|
244
|
+
// if both a route and index module exist, throw a conflict error
|
|
245
|
+
// preferring the route module over the index module
|
|
246
|
+
if (routeRouteModule && routeIndexModule) {
|
|
247
|
+
let [segments, raw] = getRouteSegments(path__default["default"].relative(appDirectory, filepath));
|
|
248
|
+
let routePath = createRoutePath(segments, raw, false);
|
|
249
|
+
console.error(getRoutePathConflictErrorMessage(routePath || "/", [routeRouteModule, routeIndexModule]));
|
|
250
|
+
}
|
|
251
|
+
return routeRouteModule || routeIndexModule || null;
|
|
252
|
+
}
|
|
253
|
+
function getRouteSegments(routeId) {
|
|
254
|
+
let routeSegments = [];
|
|
255
|
+
let rawRouteSegments = [];
|
|
256
|
+
let index = 0;
|
|
257
|
+
let routeSegment = "";
|
|
258
|
+
let rawRouteSegment = "";
|
|
259
|
+
let state = "NORMAL";
|
|
260
|
+
let pushRouteSegment = (segment, rawSegment) => {
|
|
261
|
+
if (!segment) return;
|
|
262
|
+
let notSupportedInRR = (segment, char) => {
|
|
263
|
+
throw new Error(`Route segment "${segment}" for "${routeId}" cannot contain "${char}".\n` + `If this is something you need, upvote this proposal for React Router https://github.com/remix-run/react-router/discussions/9822.`);
|
|
264
|
+
};
|
|
265
|
+
if (rawSegment.includes("*")) {
|
|
266
|
+
return notSupportedInRR(rawSegment, "*");
|
|
267
|
+
}
|
|
268
|
+
if (rawSegment.includes(":")) {
|
|
269
|
+
return notSupportedInRR(rawSegment, ":");
|
|
270
|
+
}
|
|
271
|
+
if (rawSegment.includes("/")) {
|
|
272
|
+
return notSupportedInRR(segment, "/");
|
|
273
|
+
}
|
|
274
|
+
routeSegments.push(segment);
|
|
275
|
+
rawRouteSegments.push(rawSegment);
|
|
276
|
+
};
|
|
277
|
+
while (index < routeId.length) {
|
|
278
|
+
let char = routeId[index];
|
|
279
|
+
index++; //advance to next char
|
|
280
|
+
switch (state) {
|
|
281
|
+
case "NORMAL":
|
|
282
|
+
{
|
|
283
|
+
if (isSegmentSeparator(char)) {
|
|
284
|
+
pushRouteSegment(routeSegment, rawRouteSegment);
|
|
285
|
+
routeSegment = "";
|
|
286
|
+
rawRouteSegment = "";
|
|
287
|
+
state = "NORMAL";
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (char === escapeStart) {
|
|
291
|
+
state = "ESCAPE";
|
|
292
|
+
rawRouteSegment += char;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
if (char === optionalStart) {
|
|
296
|
+
state = "OPTIONAL";
|
|
297
|
+
rawRouteSegment += char;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (!routeSegment && char === paramPrefixChar) {
|
|
301
|
+
if (index === routeId.length) {
|
|
302
|
+
routeSegment += "*";
|
|
303
|
+
rawRouteSegment += char;
|
|
304
|
+
} else {
|
|
305
|
+
routeSegment += ":";
|
|
306
|
+
rawRouteSegment += char;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
routeSegment += char;
|
|
311
|
+
rawRouteSegment += char;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "ESCAPE":
|
|
315
|
+
{
|
|
316
|
+
if (char === escapeEnd) {
|
|
317
|
+
state = "NORMAL";
|
|
318
|
+
rawRouteSegment += char;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
routeSegment += char;
|
|
322
|
+
rawRouteSegment += char;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case "OPTIONAL":
|
|
326
|
+
{
|
|
327
|
+
if (char === optionalEnd) {
|
|
328
|
+
routeSegment += "?";
|
|
329
|
+
rawRouteSegment += char;
|
|
330
|
+
state = "NORMAL";
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (char === escapeStart) {
|
|
334
|
+
state = "OPTIONAL_ESCAPE";
|
|
335
|
+
rawRouteSegment += char;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (!routeSegment && char === paramPrefixChar) {
|
|
339
|
+
if (index === routeId.length) {
|
|
340
|
+
routeSegment += "*";
|
|
341
|
+
rawRouteSegment += char;
|
|
342
|
+
} else {
|
|
343
|
+
routeSegment += ":";
|
|
344
|
+
rawRouteSegment += char;
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
routeSegment += char;
|
|
349
|
+
rawRouteSegment += char;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "OPTIONAL_ESCAPE":
|
|
353
|
+
{
|
|
354
|
+
if (char === escapeEnd) {
|
|
355
|
+
state = "OPTIONAL";
|
|
356
|
+
rawRouteSegment += char;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
routeSegment += char;
|
|
360
|
+
rawRouteSegment += char;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// process remaining segment
|
|
366
|
+
pushRouteSegment(routeSegment, rawRouteSegment);
|
|
367
|
+
return [routeSegments, rawRouteSegments];
|
|
368
|
+
}
|
|
369
|
+
function createRoutePath(routeSegments, rawRouteSegments, isIndex) {
|
|
370
|
+
let result = [];
|
|
371
|
+
if (isIndex) {
|
|
372
|
+
routeSegments = routeSegments.slice(0, -1);
|
|
373
|
+
}
|
|
374
|
+
for (let index = 0; index < routeSegments.length; index++) {
|
|
375
|
+
let segment = routeSegments[index];
|
|
376
|
+
let rawSegment = rawRouteSegments[index];
|
|
377
|
+
// skip pathless layout segments
|
|
378
|
+
if (segment.startsWith("_") && rawSegment.startsWith("_")) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// remove trailing slash
|
|
382
|
+
if (segment.endsWith("_") && rawSegment.endsWith("_")) {
|
|
383
|
+
segment = segment.slice(0, -1);
|
|
384
|
+
}
|
|
385
|
+
result.push(segment);
|
|
386
|
+
}
|
|
387
|
+
return result.length ? result.join("/") : undefined;
|
|
388
|
+
}
|
|
389
|
+
function getRoutePathConflictErrorMessage(pathname, routes) {
|
|
390
|
+
let [taken, ...others] = routes;
|
|
391
|
+
if (!pathname.startsWith("/")) {
|
|
392
|
+
pathname = "/" + pathname;
|
|
393
|
+
}
|
|
394
|
+
return `⚠️ Route Path Collision: "${pathname}"\n\n` + `The following routes all define the same URL, only the first one will be used\n\n` + `🟢 ${taken}\n` + others.map(route => `⭕️️ ${route}`).join("\n") + "\n";
|
|
395
|
+
}
|
|
396
|
+
function getRouteIdConflictErrorMessage(routeId, files) {
|
|
397
|
+
let [taken, ...others] = files;
|
|
398
|
+
return `⚠️ Route ID Collision: "${routeId}"\n\n` + `The following routes all define the same Route ID, only the first one will be used\n\n` + `🟢 ${taken}\n` + others.map(route => `⭕️️ ${route}`).join("\n") + "\n";
|
|
399
|
+
}
|
|
400
|
+
function isSegmentSeparator(checkChar) {
|
|
401
|
+
if (!checkChar) return false;
|
|
402
|
+
return ["/", ".", path__default["default"].win32.sep].includes(checkChar);
|
|
403
|
+
}
|
|
404
|
+
function findFile(dir, basename, extensions) {
|
|
405
|
+
for (let ext of extensions) {
|
|
406
|
+
let name = basename + ext;
|
|
407
|
+
let file = path__default["default"].join(dir, name);
|
|
408
|
+
if (fs__default["default"].existsSync(file)) return file;
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
exports.createRoutePath = createRoutePath;
|
|
414
|
+
exports.escapeEnd = escapeEnd;
|
|
415
|
+
exports.escapeStart = escapeStart;
|
|
416
|
+
exports.flatRoutes = flatRoutes;
|
|
417
|
+
exports.flatRoutesUniversal = flatRoutesUniversal;
|
|
418
|
+
exports.getRouteIdConflictErrorMessage = getRouteIdConflictErrorMessage;
|
|
419
|
+
exports.getRoutePathConflictErrorMessage = getRoutePathConflictErrorMessage;
|
|
420
|
+
exports.getRouteSegments = getRouteSegments;
|
|
421
|
+
exports.isSegmentSeparator = isSegmentSeparator;
|
|
422
|
+
exports.optionalEnd = optionalEnd;
|
|
423
|
+
exports.optionalStart = optionalStart;
|
|
424
|
+
exports.paramPrefixChar = paramPrefixChar;
|
|
425
|
+
exports.routeModuleExts = routeModuleExts;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type RouteConfigEntry } from "@react-router/dev/routes";
|
|
2
|
+
/**
|
|
3
|
+
* Creates route config from the file system using a convention that matches
|
|
4
|
+
* [Remix v2's route file
|
|
5
|
+
* naming](https://remix.run/docs/en/v2/file-conventions/routes-files), for use
|
|
6
|
+
* within `routes.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function flatRoutes(options?: {
|
|
9
|
+
/**
|
|
10
|
+
* An array of [minimatch](https://www.npmjs.com/package/minimatch) globs that match files to ignore.
|
|
11
|
+
* Defaults to `[]`.
|
|
12
|
+
*/
|
|
13
|
+
ignoredRouteFiles?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* The directory containing file system routes, relative to the app directory.
|
|
16
|
+
* Defaults to `"./routes"`.
|
|
17
|
+
*/
|
|
18
|
+
rootDirectory?: string;
|
|
19
|
+
}): Promise<RouteConfigEntry[]>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @react-router/fs-routes v0.0.0-experimental-a26b992a1
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Remix Software Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE.md file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
14
|
+
|
|
15
|
+
var fs = require('node:fs');
|
|
16
|
+
var path = require('node:path');
|
|
17
|
+
var routes = require('@react-router/dev/routes');
|
|
18
|
+
var manifest = require('./manifest.js');
|
|
19
|
+
var flatRoutes$1 = require('./flatRoutes.js');
|
|
20
|
+
var normalizeSlashes = require('./normalizeSlashes.js');
|
|
21
|
+
|
|
22
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
23
|
+
|
|
24
|
+
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
25
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates route config from the file system using a convention that matches
|
|
29
|
+
* [Remix v2's route file
|
|
30
|
+
* naming](https://remix.run/docs/en/v2/file-conventions/routes-files), for use
|
|
31
|
+
* within `routes.ts`.
|
|
32
|
+
*/
|
|
33
|
+
async function flatRoutes(options = {}) {
|
|
34
|
+
let {
|
|
35
|
+
ignoredRouteFiles = [],
|
|
36
|
+
rootDirectory: userRootDirectory = "routes"
|
|
37
|
+
} = options;
|
|
38
|
+
let appDirectory = routes.getAppDirectory();
|
|
39
|
+
let rootDirectory = path__default["default"].resolve(appDirectory, userRootDirectory);
|
|
40
|
+
let relativeRootDirectory = path__default["default"].relative(appDirectory, rootDirectory);
|
|
41
|
+
let prefix = normalizeSlashes.normalizeSlashes(relativeRootDirectory);
|
|
42
|
+
let routes$1 = fs__default["default"].existsSync(rootDirectory) ? flatRoutes$1.flatRoutes(appDirectory, ignoredRouteFiles, prefix) : {};
|
|
43
|
+
return manifest.routeManifestToRouteConfig(routes$1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
exports.flatRoutes = flatRoutes;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RouteConfigEntry } from "@react-router/dev/routes";
|
|
2
|
+
export interface RouteManifestEntry {
|
|
3
|
+
path?: string;
|
|
4
|
+
index?: boolean;
|
|
5
|
+
caseSensitive?: boolean;
|
|
6
|
+
id: string;
|
|
7
|
+
parentId?: string;
|
|
8
|
+
file: string;
|
|
9
|
+
}
|
|
10
|
+
export interface RouteManifest {
|
|
11
|
+
[routeId: string]: RouteManifestEntry;
|
|
12
|
+
}
|
|
13
|
+
export declare function routeManifestToRouteConfig(routeManifest: RouteManifest, rootId?: string): RouteConfigEntry[];
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @react-router/fs-routes v0.0.0-experimental-a26b992a1
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Remix Software Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE.md file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
14
|
+
|
|
15
|
+
function routeManifestToRouteConfig(routeManifest, rootId = "root") {
|
|
16
|
+
let routeConfigById = {};
|
|
17
|
+
for (let id in routeManifest) {
|
|
18
|
+
let route = routeManifest[id];
|
|
19
|
+
routeConfigById[id] = {
|
|
20
|
+
id: route.id,
|
|
21
|
+
file: route.file,
|
|
22
|
+
path: route.path,
|
|
23
|
+
index: route.index,
|
|
24
|
+
caseSensitive: route.caseSensitive
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let routeConfig = [];
|
|
28
|
+
for (let id in routeConfigById) {
|
|
29
|
+
let route = routeConfigById[id];
|
|
30
|
+
let parentId = routeManifest[route.id].parentId;
|
|
31
|
+
if (parentId === rootId) {
|
|
32
|
+
routeConfig.push(route);
|
|
33
|
+
} else {
|
|
34
|
+
let parentRoute = parentId && routeConfigById[parentId];
|
|
35
|
+
if (parentRoute) {
|
|
36
|
+
parentRoute.children = parentRoute.children || [];
|
|
37
|
+
parentRoute.children.push(route);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return routeConfig;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
exports.routeManifestToRouteConfig = routeManifestToRouteConfig;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeSlashes(file: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @react-router/fs-routes v0.0.0-experimental-a26b992a1
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Remix Software Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE.md file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
14
|
+
|
|
15
|
+
var path = require('node:path');
|
|
16
|
+
|
|
17
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
18
|
+
|
|
19
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
20
|
+
|
|
21
|
+
function normalizeSlashes(file) {
|
|
22
|
+
return file.split(path__default["default"].win32.sep).join("/");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.normalizeSlashes = normalizeSlashes;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-router/fs-routes",
|
|
3
|
+
"version": "0.0.0-experimental-a26b992a1",
|
|
4
|
+
"description": "File system routing conventions for React Router",
|
|
5
|
+
"bugs": {
|
|
6
|
+
"url": "https://github.com/remix-run/react-router/issues"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/remix-run/react-router",
|
|
11
|
+
"directory": "packages/react-router-fs-routes"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"typings": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"minimatch": "^9.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.1.6",
|
|
28
|
+
"@react-router/dev": "0.0.0-experimental-a26b992a1"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"typescript": "^5.1.0",
|
|
32
|
+
"@react-router/dev": "^0.0.0-experimental-a26b992a1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"typescript": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist/",
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"LICENSE.md",
|
|
46
|
+
"README.md"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"tsc": "tsc"
|
|
50
|
+
}
|
|
51
|
+
}
|