@react-router/fs-routes 7.0.0-pre.2 → 7.0.0-pre.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @react-router/fs-routes
2
2
 
3
+ ## 7.0.0-pre.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies:
8
+ - `@react-router/dev@7.0.0-pre.3`
9
+
3
10
  ## 7.0.0-pre.2
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @react-router/fs-routes
2
2
 
3
- File system routing conventions for [React Router.](https://github.com/remix-run/react-router)
3
+ File system routing conventions for [React Router](https://github.com/remix-run/react-router), for use within `routes.ts`.
4
4
 
5
5
  ```sh
6
6
  npm install @react-router/fs-routes
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { type RouteConfigEntry } from "@react-router/dev/routes";
1
+ import { RouteConfigEntry } from '@react-router/dev/routes';
2
+
2
3
  /**
3
4
  * Creates route config from the file system using a convention that matches
4
5
  * [Remix v2's route file
5
6
  * naming](https://remix.run/docs/en/v2/file-conventions/routes-files), for use
6
7
  * within `routes.ts`.
7
8
  */
8
- export declare function flatRoutes(options?: {
9
+ declare function flatRoutes(options?: {
9
10
  /**
10
11
  * An array of [minimatch](https://www.npmjs.com/package/minimatch) globs that match files to ignore.
11
12
  * Defaults to `[]`.
@@ -17,3 +18,5 @@ export declare function flatRoutes(options?: {
17
18
  */
18
19
  rootDirectory?: string;
19
20
  }): Promise<RouteConfigEntry[]>;
21
+
22
+ export { flatRoutes };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/fs-routes v7.0.0-pre.2
2
+ * @react-router/fs-routes v7.0.0-pre.3
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,39 +8,467 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- 'use strict';
11
+ "use strict";
12
+ var __create = Object.create;
13
+ var __defProp = Object.defineProperty;
14
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
15
+ var __getOwnPropNames = Object.getOwnPropertyNames;
16
+ var __getProtoOf = Object.getPrototypeOf;
17
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") {
24
+ for (let key of __getOwnPropNames(from))
25
+ if (!__hasOwnProp.call(to, key) && key !== except)
26
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
27
+ }
28
+ return to;
29
+ };
30
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
31
+ // If the importer is in node compatibility mode or this is not an ESM
32
+ // file that has been converted to a CommonJS file using a Babel-
33
+ // compatible transform (i.e. "__esModule" has not been set), then set
34
+ // "default" to the CommonJS "module.exports" for node compatibility.
35
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
36
+ mod
37
+ ));
38
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
12
39
 
13
- Object.defineProperty(exports, '__esModule', { value: true });
40
+ // index.ts
41
+ var react_router_fs_routes_exports = {};
42
+ __export(react_router_fs_routes_exports, {
43
+ flatRoutes: () => flatRoutes2
44
+ });
45
+ module.exports = __toCommonJS(react_router_fs_routes_exports);
46
+ var import_node_fs2 = __toESM(require("fs"));
47
+ var import_node_path3 = __toESM(require("path"));
48
+ var import_routes = require("@react-router/dev/routes");
14
49
 
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');
50
+ // manifest.ts
51
+ function routeManifestToRouteConfig(routeManifest, rootId = "root") {
52
+ let routeConfigById = {};
53
+ for (let id in routeManifest) {
54
+ let route = routeManifest[id];
55
+ routeConfigById[id] = {
56
+ id: route.id,
57
+ file: route.file,
58
+ path: route.path,
59
+ index: route.index,
60
+ caseSensitive: route.caseSensitive
61
+ };
62
+ }
63
+ let routeConfig = [];
64
+ for (let id in routeConfigById) {
65
+ let route = routeConfigById[id];
66
+ let parentId = routeManifest[route.id].parentId;
67
+ if (parentId === rootId) {
68
+ routeConfig.push(route);
69
+ } else {
70
+ let parentRoute = parentId && routeConfigById[parentId];
71
+ if (parentRoute) {
72
+ parentRoute.children = parentRoute.children || [];
73
+ parentRoute.children.push(route);
74
+ }
75
+ }
76
+ }
77
+ return routeConfig;
78
+ }
21
79
 
22
- function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
80
+ // flatRoutes.ts
81
+ var import_node_fs = __toESM(require("fs"));
82
+ var import_node_path2 = __toESM(require("path"));
83
+ var import_minimatch = require("minimatch");
23
84
 
24
- var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
25
- var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
85
+ // normalizeSlashes.ts
86
+ var import_node_path = __toESM(require("path"));
87
+ function normalizeSlashes(file) {
88
+ return file.split(import_node_path.default.win32.sep).join("/");
89
+ }
26
90
 
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;
91
+ // flatRoutes.ts
92
+ var routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"];
93
+ var paramPrefixChar = "$";
94
+ var escapeStart = "[";
95
+ var escapeEnd = "]";
96
+ var optionalStart = "(";
97
+ var optionalEnd = ")";
98
+ var PrefixLookupTrieEndSymbol = Symbol("PrefixLookupTrieEndSymbol");
99
+ var PrefixLookupTrie = class {
100
+ root = {
101
+ [PrefixLookupTrieEndSymbol]: false
102
+ };
103
+ add(value) {
104
+ if (!value) throw new Error("Cannot add empty string to PrefixLookupTrie");
105
+ let node = this.root;
106
+ for (let char of value) {
107
+ if (!node[char]) {
108
+ node[char] = {
109
+ [PrefixLookupTrieEndSymbol]: false
110
+ };
111
+ }
112
+ node = node[char];
113
+ }
114
+ node[PrefixLookupTrieEndSymbol] = true;
115
+ }
116
+ findAndRemove(prefix, filter) {
117
+ let node = this.root;
118
+ for (let char of prefix) {
119
+ if (!node[char]) return [];
120
+ node = node[char];
121
+ }
122
+ return this.#findAndRemoveRecursive([], node, prefix, filter);
123
+ }
124
+ #findAndRemoveRecursive(values, node, prefix, filter) {
125
+ for (let char of Object.keys(node)) {
126
+ this.#findAndRemoveRecursive(values, node[char], prefix + char, filter);
127
+ }
128
+ if (node[PrefixLookupTrieEndSymbol] && filter(prefix)) {
129
+ node[PrefixLookupTrieEndSymbol] = false;
130
+ values.push(prefix);
131
+ }
132
+ return values;
133
+ }
134
+ };
135
+ function flatRoutes(appDirectory, ignoredFilePatterns = [], prefix = "routes") {
136
+ let ignoredFileRegex = Array.from(/* @__PURE__ */ new Set(["**/.*", ...ignoredFilePatterns])).map((re) => (0, import_minimatch.makeRe)(re)).filter((re) => !!re);
137
+ let routesDir = import_node_path2.default.join(appDirectory, prefix);
138
+ let rootRoute = findFile(appDirectory, "root", routeModuleExts);
139
+ if (!rootRoute) {
140
+ throw new Error(
141
+ `Could not find a root route module in the app directory: ${appDirectory}`
142
+ );
143
+ }
144
+ if (!import_node_fs.default.existsSync(rootRoute)) {
145
+ throw new Error(
146
+ `Could not find the routes directory: ${routesDir}. Did you forget to create it?`
147
+ );
148
+ }
149
+ let entries = import_node_fs.default.readdirSync(routesDir, {
150
+ withFileTypes: true,
151
+ encoding: "utf-8"
152
+ });
153
+ let routes = [];
154
+ for (let entry of entries) {
155
+ let filepath = normalizeSlashes(import_node_path2.default.join(routesDir, entry.name));
156
+ let route = null;
157
+ if (entry.isDirectory()) {
158
+ route = findRouteModuleForFolder(
159
+ appDirectory,
160
+ filepath,
161
+ ignoredFileRegex
162
+ );
163
+ } else if (entry.isFile()) {
164
+ route = findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex);
165
+ }
166
+ if (route) routes.push(route);
167
+ }
168
+ let routeManifest = flatRoutesUniversal(appDirectory, routes, prefix);
169
+ return routeManifest;
170
+ }
171
+ function flatRoutesUniversal(appDirectory, routes, prefix = "routes") {
172
+ let urlConflicts = /* @__PURE__ */ new Map();
173
+ let routeManifest = {};
174
+ let prefixLookup = new PrefixLookupTrie();
175
+ let uniqueRoutes = /* @__PURE__ */ new Map();
176
+ let routeIdConflicts = /* @__PURE__ */ new Map();
177
+ let routeIds = /* @__PURE__ */ new Map();
178
+ for (let file of routes) {
179
+ let normalizedFile = normalizeSlashes(file);
180
+ let routeExt = import_node_path2.default.extname(normalizedFile);
181
+ let routeDir = import_node_path2.default.dirname(normalizedFile);
182
+ let normalizedApp = normalizeSlashes(appDirectory);
183
+ let routeId = routeDir === import_node_path2.default.posix.join(normalizedApp, prefix) ? import_node_path2.default.posix.relative(normalizedApp, normalizedFile).slice(0, -routeExt.length) : import_node_path2.default.posix.relative(normalizedApp, routeDir);
184
+ let conflict = routeIds.get(routeId);
185
+ if (conflict) {
186
+ let currentConflicts = routeIdConflicts.get(routeId);
187
+ if (!currentConflicts) {
188
+ currentConflicts = [import_node_path2.default.posix.relative(normalizedApp, conflict)];
189
+ }
190
+ currentConflicts.push(import_node_path2.default.posix.relative(normalizedApp, normalizedFile));
191
+ routeIdConflicts.set(routeId, currentConflicts);
192
+ continue;
193
+ }
194
+ routeIds.set(routeId, normalizedFile);
195
+ }
196
+ let sortedRouteIds = Array.from(routeIds).sort(
197
+ ([a], [b]) => b.length - a.length
198
+ );
199
+ for (let [routeId, file] of sortedRouteIds) {
200
+ let index = routeId.endsWith("_index");
201
+ let [segments, raw] = getRouteSegments(routeId.slice(prefix.length + 1));
202
+ let pathname = createRoutePath(segments, raw, index);
203
+ routeManifest[routeId] = {
204
+ file: file.slice(appDirectory.length + 1),
205
+ id: routeId,
206
+ path: pathname
207
+ };
208
+ if (index) routeManifest[routeId].index = true;
209
+ let childRouteIds = prefixLookup.findAndRemove(routeId, (value) => {
210
+ return [".", "/"].includes(value.slice(routeId.length).charAt(0));
211
+ });
212
+ prefixLookup.add(routeId);
213
+ if (childRouteIds.length > 0) {
214
+ for (let childRouteId of childRouteIds) {
215
+ routeManifest[childRouteId].parentId = routeId;
216
+ }
217
+ }
218
+ }
219
+ let parentChildrenMap = /* @__PURE__ */ new Map();
220
+ for (let [routeId] of sortedRouteIds) {
221
+ let config = routeManifest[routeId];
222
+ if (!config.parentId) continue;
223
+ let existingChildren = parentChildrenMap.get(config.parentId) || [];
224
+ existingChildren.push(config);
225
+ parentChildrenMap.set(config.parentId, existingChildren);
226
+ }
227
+ for (let [routeId] of sortedRouteIds) {
228
+ let config = routeManifest[routeId];
229
+ let originalPathname = config.path || "";
230
+ let pathname = config.path;
231
+ let parentConfig = config.parentId ? routeManifest[config.parentId] : null;
232
+ if (parentConfig?.path && pathname) {
233
+ pathname = pathname.slice(parentConfig.path.length).replace(/^\//, "").replace(/\/$/, "");
234
+ }
235
+ if (!config.parentId) config.parentId = "root";
236
+ config.path = pathname || void 0;
237
+ let lastRouteSegment = config.id.replace(new RegExp(`^${prefix}/`), "").split(".").pop();
238
+ let isPathlessLayoutRoute = lastRouteSegment && lastRouteSegment.startsWith("_") && lastRouteSegment !== "_index";
239
+ if (isPathlessLayoutRoute) {
240
+ continue;
241
+ }
242
+ let conflictRouteId = originalPathname + (config.index ? "?index" : "");
243
+ let conflict = uniqueRoutes.get(conflictRouteId);
244
+ uniqueRoutes.set(conflictRouteId, config);
245
+ if (conflict && (originalPathname || config.index)) {
246
+ let currentConflicts = urlConflicts.get(originalPathname);
247
+ if (!currentConflicts) currentConflicts = [conflict];
248
+ currentConflicts.push(config);
249
+ urlConflicts.set(originalPathname, currentConflicts);
250
+ continue;
251
+ }
252
+ }
253
+ if (routeIdConflicts.size > 0) {
254
+ for (let [routeId, files] of routeIdConflicts.entries()) {
255
+ console.error(getRouteIdConflictErrorMessage(routeId, files));
256
+ }
257
+ }
258
+ if (urlConflicts.size > 0) {
259
+ for (let [path4, routes2] of urlConflicts.entries()) {
260
+ for (let i = 1; i < routes2.length; i++) {
261
+ delete routeManifest[routes2[i].id];
262
+ }
263
+ let files = routes2.map((r) => r.file);
264
+ console.error(getRoutePathConflictErrorMessage(path4, files));
265
+ }
266
+ }
267
+ return routeManifest;
268
+ }
269
+ function findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex) {
270
+ let relativePath = normalizeSlashes(import_node_path2.default.relative(appDirectory, filepath));
271
+ let isIgnored = ignoredFileRegex.some((regex) => regex.test(relativePath));
272
+ if (isIgnored) return null;
273
+ return filepath;
274
+ }
275
+ function findRouteModuleForFolder(appDirectory, filepath, ignoredFileRegex) {
276
+ let relativePath = import_node_path2.default.relative(appDirectory, filepath);
277
+ let isIgnored = ignoredFileRegex.some((regex) => regex.test(relativePath));
278
+ if (isIgnored) return null;
279
+ let routeRouteModule = findFile(filepath, "route", routeModuleExts);
280
+ let routeIndexModule = findFile(filepath, "index", routeModuleExts);
281
+ if (routeRouteModule && routeIndexModule) {
282
+ let [segments, raw] = getRouteSegments(
283
+ import_node_path2.default.relative(appDirectory, filepath)
284
+ );
285
+ let routePath = createRoutePath(segments, raw, false);
286
+ console.error(
287
+ getRoutePathConflictErrorMessage(routePath || "/", [
288
+ routeRouteModule,
289
+ routeIndexModule
290
+ ])
291
+ );
292
+ }
293
+ return routeRouteModule || routeIndexModule || null;
294
+ }
295
+ function getRouteSegments(routeId) {
296
+ let routeSegments = [];
297
+ let rawRouteSegments = [];
298
+ let index = 0;
299
+ let routeSegment = "";
300
+ let rawRouteSegment = "";
301
+ let state = "NORMAL";
302
+ let pushRouteSegment = (segment, rawSegment) => {
303
+ if (!segment) return;
304
+ let notSupportedInRR = (segment2, char) => {
305
+ throw new Error(
306
+ `Route segment "${segment2}" for "${routeId}" cannot contain "${char}".
307
+ If this is something you need, upvote this proposal for React Router https://github.com/remix-run/react-router/discussions/9822.`
308
+ );
309
+ };
310
+ if (rawSegment.includes("*")) {
311
+ return notSupportedInRR(rawSegment, "*");
312
+ }
313
+ if (rawSegment.includes(":")) {
314
+ return notSupportedInRR(rawSegment, ":");
315
+ }
316
+ if (rawSegment.includes("/")) {
317
+ return notSupportedInRR(segment, "/");
318
+ }
319
+ routeSegments.push(segment);
320
+ rawRouteSegments.push(rawSegment);
321
+ };
322
+ while (index < routeId.length) {
323
+ let char = routeId[index];
324
+ index++;
325
+ switch (state) {
326
+ case "NORMAL": {
327
+ if (isSegmentSeparator(char)) {
328
+ pushRouteSegment(routeSegment, rawRouteSegment);
329
+ routeSegment = "";
330
+ rawRouteSegment = "";
331
+ state = "NORMAL";
332
+ break;
333
+ }
334
+ if (char === escapeStart) {
335
+ state = "ESCAPE";
336
+ rawRouteSegment += char;
337
+ break;
338
+ }
339
+ if (char === optionalStart) {
340
+ state = "OPTIONAL";
341
+ rawRouteSegment += char;
342
+ break;
343
+ }
344
+ if (!routeSegment && char === paramPrefixChar) {
345
+ if (index === routeId.length) {
346
+ routeSegment += "*";
347
+ rawRouteSegment += char;
348
+ } else {
349
+ routeSegment += ":";
350
+ rawRouteSegment += char;
351
+ }
352
+ break;
353
+ }
354
+ routeSegment += char;
355
+ rawRouteSegment += char;
356
+ break;
357
+ }
358
+ case "ESCAPE": {
359
+ if (char === escapeEnd) {
360
+ state = "NORMAL";
361
+ rawRouteSegment += char;
362
+ break;
363
+ }
364
+ routeSegment += char;
365
+ rawRouteSegment += char;
366
+ break;
367
+ }
368
+ case "OPTIONAL": {
369
+ if (char === optionalEnd) {
370
+ routeSegment += "?";
371
+ rawRouteSegment += char;
372
+ state = "NORMAL";
373
+ break;
374
+ }
375
+ if (char === escapeStart) {
376
+ state = "OPTIONAL_ESCAPE";
377
+ rawRouteSegment += char;
378
+ break;
379
+ }
380
+ if (!routeSegment && char === paramPrefixChar) {
381
+ if (index === routeId.length) {
382
+ routeSegment += "*";
383
+ rawRouteSegment += char;
384
+ } else {
385
+ routeSegment += ":";
386
+ rawRouteSegment += char;
387
+ }
388
+ break;
389
+ }
390
+ routeSegment += char;
391
+ rawRouteSegment += char;
392
+ break;
393
+ }
394
+ case "OPTIONAL_ESCAPE": {
395
+ if (char === escapeEnd) {
396
+ state = "OPTIONAL";
397
+ rawRouteSegment += char;
398
+ break;
399
+ }
400
+ routeSegment += char;
401
+ rawRouteSegment += char;
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ pushRouteSegment(routeSegment, rawRouteSegment);
407
+ return [routeSegments, rawRouteSegments];
408
+ }
409
+ function createRoutePath(routeSegments, rawRouteSegments, isIndex) {
410
+ let result = [];
411
+ if (isIndex) {
412
+ routeSegments = routeSegments.slice(0, -1);
413
+ }
414
+ for (let index = 0; index < routeSegments.length; index++) {
415
+ let segment = routeSegments[index];
416
+ let rawSegment = rawRouteSegments[index];
417
+ if (segment.startsWith("_") && rawSegment.startsWith("_")) {
418
+ continue;
419
+ }
420
+ if (segment.endsWith("_") && rawSegment.endsWith("_")) {
421
+ segment = segment.slice(0, -1);
422
+ }
423
+ result.push(segment);
424
+ }
425
+ return result.length ? result.join("/") : void 0;
426
+ }
427
+ function getRoutePathConflictErrorMessage(pathname, routes) {
428
+ let [taken, ...others] = routes;
429
+ if (!pathname.startsWith("/")) {
430
+ pathname = "/" + pathname;
431
+ }
432
+ return `\u26A0\uFE0F Route Path Collision: "${pathname}"
433
+
434
+ The following routes all define the same URL, only the first one will be used
435
+
436
+ \u{1F7E2} ${taken}
437
+ ` + others.map((route) => `\u2B55\uFE0F\uFE0F ${route}`).join("\n") + "\n";
438
+ }
439
+ function getRouteIdConflictErrorMessage(routeId, files) {
440
+ let [taken, ...others] = files;
441
+ return `\u26A0\uFE0F Route ID Collision: "${routeId}"
442
+
443
+ The following routes all define the same Route ID, only the first one will be used
444
+
445
+ \u{1F7E2} ${taken}
446
+ ` + others.map((route) => `\u2B55\uFE0F\uFE0F ${route}`).join("\n") + "\n";
447
+ }
448
+ function isSegmentSeparator(checkChar) {
449
+ if (!checkChar) return false;
450
+ return ["/", ".", import_node_path2.default.win32.sep].includes(checkChar);
451
+ }
452
+ function findFile(dir, basename, extensions) {
453
+ for (let ext of extensions) {
454
+ let name = basename + ext;
455
+ let file = import_node_path2.default.join(dir, name);
456
+ if (import_node_fs.default.existsSync(file)) return file;
457
+ }
458
+ return void 0;
459
+ }
460
+
461
+ // index.ts
462
+ async function flatRoutes2(options = {}) {
463
+ let { ignoredRouteFiles = [], rootDirectory: userRootDirectory = "routes" } = options;
464
+ let appDirectory = (0, import_routes.getAppDirectory)();
465
+ let rootDirectory = import_node_path3.default.resolve(appDirectory, userRootDirectory);
466
+ let relativeRootDirectory = import_node_path3.default.relative(appDirectory, rootDirectory);
467
+ let prefix = normalizeSlashes(relativeRootDirectory);
468
+ let routes = import_node_fs2.default.existsSync(rootDirectory) ? flatRoutes(appDirectory, ignoredRouteFiles, prefix) : {};
469
+ return routeManifestToRouteConfig(routes);
470
+ }
471
+ // Annotate the CommonJS export names for ESM import in node:
472
+ 0 && (module.exports = {
473
+ flatRoutes
474
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-router/fs-routes",
3
- "version": "7.0.0-pre.2",
4
- "description": "File system routing conventions for React Router",
3
+ "version": "7.0.0-pre.3",
4
+ "description": "File system routing conventions for React Router, for use within routes.ts",
5
5
  "bugs": {
6
6
  "url": "https://github.com/remix-run/react-router/issues"
7
7
  },
@@ -20,16 +20,31 @@
20
20
  },
21
21
  "./package.json": "./package.json"
22
22
  },
23
+ "wireit": {
24
+ "build": {
25
+ "command": "tsup",
26
+ "files": [
27
+ "*.ts",
28
+ "tsconfig.json",
29
+ "package.json"
30
+ ],
31
+ "output": [
32
+ "dist/**"
33
+ ]
34
+ }
35
+ },
23
36
  "dependencies": {
24
37
  "minimatch": "^9.0.0"
25
38
  },
26
39
  "devDependencies": {
40
+ "tsup": "^8.3.0",
27
41
  "typescript": "^5.1.6",
28
- "@react-router/dev": "7.0.0-pre.2"
42
+ "wireit": "0.14.9",
43
+ "@react-router/dev": "7.0.0-pre.3"
29
44
  },
30
45
  "peerDependencies": {
31
46
  "typescript": "^5.1.0",
32
- "@react-router/dev": "^7.0.0-pre.2"
47
+ "@react-router/dev": "^7.0.0-pre.3"
33
48
  },
34
49
  "peerDependenciesMeta": {
35
50
  "typescript": {
@@ -46,6 +61,6 @@
46
61
  "README.md"
47
62
  ],
48
63
  "scripts": {
49
- "tsc": "tsc"
64
+ "build": "wireit"
50
65
  }
51
66
  }
@@ -1,14 +0,0 @@
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;
@@ -1,435 +0,0 @@
1
- /**
2
- * @react-router/fs-routes v7.0.0-pre.2
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
-
79
- // Only read the routes directory
80
- let entries = fs__default["default"].readdirSync(routesDir, {
81
- withFileTypes: true,
82
- encoding: "utf-8"
83
- });
84
- let routes = [];
85
- for (let entry of entries) {
86
- let filepath = normalizeSlashes.normalizeSlashes(path__default["default"].join(routesDir, entry.name));
87
- let route = null;
88
- // If it's a directory, don't recurse into it, instead just look for a route module
89
- if (entry.isDirectory()) {
90
- route = findRouteModuleForFolder(appDirectory, filepath, ignoredFileRegex);
91
- } else if (entry.isFile()) {
92
- route = findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex);
93
- }
94
- if (route) routes.push(route);
95
- }
96
- let routeManifest = flatRoutesUniversal(appDirectory, routes, prefix);
97
- return routeManifest;
98
- }
99
- function flatRoutesUniversal(appDirectory, routes, prefix = "routes") {
100
- let urlConflicts = new Map();
101
- let routeManifest = {};
102
- let prefixLookup = new PrefixLookupTrie();
103
- let uniqueRoutes = new Map();
104
- let routeIdConflicts = new Map();
105
-
106
- // id -> file
107
- let routeIds = new Map();
108
- for (let file of routes) {
109
- let normalizedFile = normalizeSlashes.normalizeSlashes(file);
110
- let routeExt = path__default["default"].extname(normalizedFile);
111
- let routeDir = path__default["default"].dirname(normalizedFile);
112
- let normalizedApp = normalizeSlashes.normalizeSlashes(appDirectory);
113
- 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);
114
- let conflict = routeIds.get(routeId);
115
- if (conflict) {
116
- let currentConflicts = routeIdConflicts.get(routeId);
117
- if (!currentConflicts) {
118
- currentConflicts = [path__default["default"].posix.relative(normalizedApp, conflict)];
119
- }
120
- currentConflicts.push(path__default["default"].posix.relative(normalizedApp, normalizedFile));
121
- routeIdConflicts.set(routeId, currentConflicts);
122
- continue;
123
- }
124
- routeIds.set(routeId, normalizedFile);
125
- }
126
- let sortedRouteIds = Array.from(routeIds).sort(([a], [b]) => b.length - a.length);
127
- for (let [routeId, file] of sortedRouteIds) {
128
- let index = routeId.endsWith("_index");
129
- let [segments, raw] = getRouteSegments(routeId.slice(prefix.length + 1));
130
- let pathname = createRoutePath(segments, raw, index);
131
- routeManifest[routeId] = {
132
- file: file.slice(appDirectory.length + 1),
133
- id: routeId,
134
- path: pathname
135
- };
136
- if (index) routeManifest[routeId].index = true;
137
- let childRouteIds = prefixLookup.findAndRemove(routeId, value => {
138
- return [".", "/"].includes(value.slice(routeId.length).charAt(0));
139
- });
140
- prefixLookup.add(routeId);
141
- if (childRouteIds.length > 0) {
142
- for (let childRouteId of childRouteIds) {
143
- routeManifest[childRouteId].parentId = routeId;
144
- }
145
- }
146
- }
147
-
148
- // path creation
149
- let parentChildrenMap = new Map();
150
- for (let [routeId] of sortedRouteIds) {
151
- let config = routeManifest[routeId];
152
- if (!config.parentId) continue;
153
- let existingChildren = parentChildrenMap.get(config.parentId) || [];
154
- existingChildren.push(config);
155
- parentChildrenMap.set(config.parentId, existingChildren);
156
- }
157
- for (let [routeId] of sortedRouteIds) {
158
- let config = routeManifest[routeId];
159
- let originalPathname = config.path || "";
160
- let pathname = config.path;
161
- let parentConfig = config.parentId ? routeManifest[config.parentId] : null;
162
- if (parentConfig !== null && parentConfig !== void 0 && parentConfig.path && pathname) {
163
- pathname = pathname.slice(parentConfig.path.length).replace(/^\//, "").replace(/\/$/, "");
164
- }
165
- if (!config.parentId) config.parentId = "root";
166
- config.path = pathname || undefined;
167
-
168
- /**
169
- * We do not try to detect path collisions for pathless layout route
170
- * files because, by definition, they create the potential for route
171
- * collisions _at that level in the tree_.
172
- *
173
- * Consider example where a user may want multiple pathless layout routes
174
- * for different subfolders
175
- *
176
- * routes/
177
- * account.tsx
178
- * account._private.tsx
179
- * account._private.orders.tsx
180
- * account._private.profile.tsx
181
- * account._public.tsx
182
- * account._public.login.tsx
183
- * account._public.perks.tsx
184
- *
185
- * In order to support both a public and private layout for `/account/*`
186
- * URLs, we are creating a mutually exclusive set of URLs beneath 2
187
- * separate pathless layout routes. In this case, the route paths for
188
- * both account._public.tsx and account._private.tsx is the same
189
- * (/account), but we're again not expecting to match at that level.
190
- *
191
- * By only ignoring this check when the final portion of the filename is
192
- * pathless, we will still detect path collisions such as:
193
- *
194
- * routes/parent._pathless.foo.tsx
195
- * routes/parent._pathless2.foo.tsx
196
- *
197
- * and
198
- *
199
- * routes/parent._pathless/index.tsx
200
- * routes/parent._pathless2/index.tsx
201
- */
202
- let lastRouteSegment = config.id.replace(new RegExp(`^${prefix}/`), "").split(".").pop();
203
- let isPathlessLayoutRoute = lastRouteSegment && lastRouteSegment.startsWith("_") && lastRouteSegment !== "_index";
204
- if (isPathlessLayoutRoute) {
205
- continue;
206
- }
207
- let conflictRouteId = originalPathname + (config.index ? "?index" : "");
208
- let conflict = uniqueRoutes.get(conflictRouteId);
209
- uniqueRoutes.set(conflictRouteId, config);
210
- if (conflict && (originalPathname || config.index)) {
211
- let currentConflicts = urlConflicts.get(originalPathname);
212
- if (!currentConflicts) currentConflicts = [conflict];
213
- currentConflicts.push(config);
214
- urlConflicts.set(originalPathname, currentConflicts);
215
- continue;
216
- }
217
- }
218
- if (routeIdConflicts.size > 0) {
219
- for (let [routeId, files] of routeIdConflicts.entries()) {
220
- console.error(getRouteIdConflictErrorMessage(routeId, files));
221
- }
222
- }
223
-
224
- // report conflicts
225
- if (urlConflicts.size > 0) {
226
- for (let [path, routes] of urlConflicts.entries()) {
227
- // delete all but the first route from the manifest
228
- for (let i = 1; i < routes.length; i++) {
229
- delete routeManifest[routes[i].id];
230
- }
231
- let files = routes.map(r => r.file);
232
- console.error(getRoutePathConflictErrorMessage(path, files));
233
- }
234
- }
235
- return routeManifest;
236
- }
237
- function findRouteModuleForFile(appDirectory, filepath, ignoredFileRegex) {
238
- let relativePath = normalizeSlashes.normalizeSlashes(path__default["default"].relative(appDirectory, filepath));
239
- let isIgnored = ignoredFileRegex.some(regex => regex.test(relativePath));
240
- if (isIgnored) return null;
241
- return filepath;
242
- }
243
- function findRouteModuleForFolder(appDirectory, filepath, ignoredFileRegex) {
244
- let relativePath = path__default["default"].relative(appDirectory, filepath);
245
- let isIgnored = ignoredFileRegex.some(regex => regex.test(relativePath));
246
- if (isIgnored) return null;
247
- let routeRouteModule = findFile(filepath, "route", routeModuleExts);
248
- let routeIndexModule = findFile(filepath, "index", routeModuleExts);
249
-
250
- // if both a route and index module exist, throw a conflict error
251
- // preferring the route module over the index module
252
- if (routeRouteModule && routeIndexModule) {
253
- let [segments, raw] = getRouteSegments(path__default["default"].relative(appDirectory, filepath));
254
- let routePath = createRoutePath(segments, raw, false);
255
- console.error(getRoutePathConflictErrorMessage(routePath || "/", [routeRouteModule, routeIndexModule]));
256
- }
257
- return routeRouteModule || routeIndexModule || null;
258
- }
259
- function getRouteSegments(routeId) {
260
- let routeSegments = [];
261
- let rawRouteSegments = [];
262
- let index = 0;
263
- let routeSegment = "";
264
- let rawRouteSegment = "";
265
- let state = "NORMAL";
266
- let pushRouteSegment = (segment, rawSegment) => {
267
- if (!segment) return;
268
- let notSupportedInRR = (segment, char) => {
269
- 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.`);
270
- };
271
- if (rawSegment.includes("*")) {
272
- return notSupportedInRR(rawSegment, "*");
273
- }
274
- if (rawSegment.includes(":")) {
275
- return notSupportedInRR(rawSegment, ":");
276
- }
277
- if (rawSegment.includes("/")) {
278
- return notSupportedInRR(segment, "/");
279
- }
280
- routeSegments.push(segment);
281
- rawRouteSegments.push(rawSegment);
282
- };
283
- while (index < routeId.length) {
284
- let char = routeId[index];
285
- index++; //advance to next char
286
-
287
- switch (state) {
288
- case "NORMAL":
289
- {
290
- if (isSegmentSeparator(char)) {
291
- pushRouteSegment(routeSegment, rawRouteSegment);
292
- routeSegment = "";
293
- rawRouteSegment = "";
294
- state = "NORMAL";
295
- break;
296
- }
297
- if (char === escapeStart) {
298
- state = "ESCAPE";
299
- rawRouteSegment += char;
300
- break;
301
- }
302
- if (char === optionalStart) {
303
- state = "OPTIONAL";
304
- rawRouteSegment += char;
305
- break;
306
- }
307
- if (!routeSegment && char === paramPrefixChar) {
308
- if (index === routeId.length) {
309
- routeSegment += "*";
310
- rawRouteSegment += char;
311
- } else {
312
- routeSegment += ":";
313
- rawRouteSegment += char;
314
- }
315
- break;
316
- }
317
- routeSegment += char;
318
- rawRouteSegment += char;
319
- break;
320
- }
321
- case "ESCAPE":
322
- {
323
- if (char === escapeEnd) {
324
- state = "NORMAL";
325
- rawRouteSegment += char;
326
- break;
327
- }
328
- routeSegment += char;
329
- rawRouteSegment += char;
330
- break;
331
- }
332
- case "OPTIONAL":
333
- {
334
- if (char === optionalEnd) {
335
- routeSegment += "?";
336
- rawRouteSegment += char;
337
- state = "NORMAL";
338
- break;
339
- }
340
- if (char === escapeStart) {
341
- state = "OPTIONAL_ESCAPE";
342
- rawRouteSegment += char;
343
- break;
344
- }
345
- if (!routeSegment && char === paramPrefixChar) {
346
- if (index === routeId.length) {
347
- routeSegment += "*";
348
- rawRouteSegment += char;
349
- } else {
350
- routeSegment += ":";
351
- rawRouteSegment += char;
352
- }
353
- break;
354
- }
355
- routeSegment += char;
356
- rawRouteSegment += char;
357
- break;
358
- }
359
- case "OPTIONAL_ESCAPE":
360
- {
361
- if (char === escapeEnd) {
362
- state = "OPTIONAL";
363
- rawRouteSegment += char;
364
- break;
365
- }
366
- routeSegment += char;
367
- rawRouteSegment += char;
368
- break;
369
- }
370
- }
371
- }
372
-
373
- // process remaining segment
374
- pushRouteSegment(routeSegment, rawRouteSegment);
375
- return [routeSegments, rawRouteSegments];
376
- }
377
- function createRoutePath(routeSegments, rawRouteSegments, isIndex) {
378
- let result = [];
379
- if (isIndex) {
380
- routeSegments = routeSegments.slice(0, -1);
381
- }
382
- for (let index = 0; index < routeSegments.length; index++) {
383
- let segment = routeSegments[index];
384
- let rawSegment = rawRouteSegments[index];
385
-
386
- // skip pathless layout segments
387
- if (segment.startsWith("_") && rawSegment.startsWith("_")) {
388
- continue;
389
- }
390
-
391
- // remove trailing slash
392
- if (segment.endsWith("_") && rawSegment.endsWith("_")) {
393
- segment = segment.slice(0, -1);
394
- }
395
- result.push(segment);
396
- }
397
- return result.length ? result.join("/") : undefined;
398
- }
399
- function getRoutePathConflictErrorMessage(pathname, routes) {
400
- let [taken, ...others] = routes;
401
- if (!pathname.startsWith("/")) {
402
- pathname = "/" + pathname;
403
- }
404
- 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";
405
- }
406
- function getRouteIdConflictErrorMessage(routeId, files) {
407
- let [taken, ...others] = files;
408
- 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";
409
- }
410
- function isSegmentSeparator(checkChar) {
411
- if (!checkChar) return false;
412
- return ["/", ".", path__default["default"].win32.sep].includes(checkChar);
413
- }
414
- function findFile(dir, basename, extensions) {
415
- for (let ext of extensions) {
416
- let name = basename + ext;
417
- let file = path__default["default"].join(dir, name);
418
- if (fs__default["default"].existsSync(file)) return file;
419
- }
420
- return undefined;
421
- }
422
-
423
- exports.createRoutePath = createRoutePath;
424
- exports.escapeEnd = escapeEnd;
425
- exports.escapeStart = escapeStart;
426
- exports.flatRoutes = flatRoutes;
427
- exports.flatRoutesUniversal = flatRoutesUniversal;
428
- exports.getRouteIdConflictErrorMessage = getRouteIdConflictErrorMessage;
429
- exports.getRoutePathConflictErrorMessage = getRoutePathConflictErrorMessage;
430
- exports.getRouteSegments = getRouteSegments;
431
- exports.isSegmentSeparator = isSegmentSeparator;
432
- exports.optionalEnd = optionalEnd;
433
- exports.optionalStart = optionalStart;
434
- exports.paramPrefixChar = paramPrefixChar;
435
- exports.routeModuleExts = routeModuleExts;
@@ -1,13 +0,0 @@
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 DELETED
@@ -1,44 +0,0 @@
1
- /**
2
- * @react-router/fs-routes v7.0.0-pre.2
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;
@@ -1 +0,0 @@
1
- export declare function normalizeSlashes(file: string): string;
@@ -1,25 +0,0 @@
1
- /**
2
- * @react-router/fs-routes v7.0.0-pre.2
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;