remix-flat-routes
Advanced tools
Comparing version 0.4.8 to 0.5.0
# CHANGELOG | ||
## v0.5.0 | ||
- π¨ Update flatRoutes with new features | ||
- Uses same function as Remix core | ||
- Allows to maintain extended flat-routes function | ||
- Customizations passed in `options` | ||
- Add support for "hybrid" routes | ||
- Add support for extended route filenames | ||
- Add support for multiple route folders | ||
- Add support for custom param prefix character | ||
- Add support for custom base path | ||
## v0.4.7 | ||
@@ -4,0 +16,0 @@ |
@@ -0,5 +1,8 @@ | ||
import type { DefineRouteFunction, RouteManifest } from '@remix-run/dev/config/routes'; | ||
declare type RouteInfo = { | ||
id: string; | ||
path: string; | ||
file: string; | ||
name: string; | ||
segments: string[]; | ||
parentId?: string; | ||
@@ -9,5 +12,2 @@ index?: boolean; | ||
}; | ||
interface RouteManifest { | ||
[key: string]: RouteInfo; | ||
} | ||
declare type DefineRouteOptions = { | ||
@@ -20,5 +20,6 @@ caseSensitive?: boolean; | ||
}; | ||
declare type DefineRouteFunction = (path: string | undefined, file: string, optionsOrChildren?: DefineRouteOptions | DefineRouteChildren, children?: DefineRouteChildren) => void; | ||
export declare type VisitFilesFunction = (dir: string, visitor: (file: string) => void, baseDir?: string) => void; | ||
declare type FlatRoutesOptions = { | ||
export declare type FlatRoutesOptions = { | ||
routeDir?: string | string[]; | ||
defineRoutes?: DefineRoutesFunction; | ||
basePath?: string; | ||
@@ -28,7 +29,22 @@ visitFiles?: VisitFilesFunction; | ||
ignoredRouteFiles?: string[]; | ||
routeRegex?: RegExp; | ||
}; | ||
export declare type DefineRoutesFunction = (callback: (route: DefineRouteFunction) => void) => any; | ||
export default function flatRoutes(baseDir: string, defineRoutes: DefineRoutesFunction, options?: FlatRoutesOptions): RouteManifest; | ||
export declare function getRouteInfo(baseDir: string, routeFile: string, basePath?: string, paramsPrefixChar?: string): RouteInfo | null; | ||
export type { DefineRouteFunction, DefineRouteOptions, DefineRouteChildren, RouteManifest, RouteInfo, }; | ||
export { flatRoutes }; | ||
export type { DefineRouteFunction, DefineRouteOptions, DefineRouteChildren, RouteManifest, RouteInfo, }; | ||
export default function flatRoutes(routeDir: string | string[], defineRoutes: DefineRoutesFunction, options?: FlatRoutesOptions): RouteManifest; | ||
export declare function isRouteModuleFile(filename: string, routeRegex: RegExp): boolean; | ||
export declare function isIndexRoute(routeId: string): boolean; | ||
export declare function getRouteInfo(routeDir: string, file: string, options: FlatRoutesOptions): { | ||
id: string; | ||
path: string; | ||
file: string; | ||
name: string; | ||
segments: string[]; | ||
index: boolean; | ||
}; | ||
export declare function createRoutePath(routeSegments: string[], index: boolean, options: FlatRoutesOptions): string | undefined; | ||
export declare function getRouteSegments(name: string, paramPrefixChar?: string): string[]; | ||
export declare function defaultVisitFiles(dir: string, visitor: (file: string) => void, baseDir?: string): void; | ||
export declare function createRouteId(file: string): string; | ||
export declare function normalizeSlashes(file: string): string; |
@@ -29,56 +29,26 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.flatRoutes = exports.getRouteInfo = void 0; | ||
exports.normalizeSlashes = exports.createRouteId = exports.defaultVisitFiles = exports.getRouteSegments = exports.createRoutePath = exports.getRouteInfo = exports.isIndexRoute = exports.isRouteModuleFile = exports.flatRoutes = void 0; | ||
const fs = __importStar(require("fs")); | ||
const minimatch_1 = __importDefault(require("minimatch")); | ||
const path = __importStar(require("path")); | ||
const util_1 = require("./util"); | ||
function flatRoutes(baseDir, defineRoutes, options = {}) { | ||
const defaultOptions = { | ||
routeDir: 'routes', | ||
basePath: '/', | ||
paramPrefixChar: '$', | ||
routeRegex: /\/((index|route|layout|page)|(_[a-zA-Z0-9_$.-]+)|([a-zA-Z0-9_$.\[\]-]+\.route))\.(ts|tsx|js|jsx|md|mdx)$/, | ||
}; | ||
const defaultDefineRoutes = undefined; | ||
function flatRoutes(routeDir, defineRoutes, options = defaultOptions) { | ||
var _a; | ||
const routeMap = new Map(); | ||
const parentMap = new Map(); | ||
const visitor = (options === null || options === void 0 ? void 0 : options.visitFiles) || util_1.visitFiles; | ||
const ignoredFilePatterns = (_a = options === null || options === void 0 ? void 0 : options.ignoredRouteFiles) !== null && _a !== void 0 ? _a : []; | ||
// initialize root route | ||
routeMap.set('root', { | ||
path: '', | ||
file: 'root.tsx', | ||
name: 'root', | ||
parentId: '', | ||
index: false, | ||
const routes = _flatRoutes('app', (_a = options.ignoredRouteFiles) !== null && _a !== void 0 ? _a : [], { | ||
...options, | ||
routeDir, | ||
defineRoutes, | ||
}); | ||
let routes = defineRoutes(route => { | ||
visitor(`app/${baseDir}`, routeFile => { | ||
let file = `app/${baseDir}/${routeFile}`; | ||
let absoluteFile = path.resolve(file); | ||
if (ignoredFilePatterns.some(pattern => (0, minimatch_1.default)(absoluteFile, pattern, { dot: true }))) { | ||
return; | ||
} | ||
const routeInfo = getRouteInfo(baseDir, routeFile, options.basePath, options.paramPrefixChar); | ||
if (!routeInfo) | ||
return; | ||
routeMap.set(routeInfo.name, routeInfo); | ||
}); | ||
// setup parent map | ||
for (let [name, route] of routeMap) { | ||
if (name === 'root') | ||
continue; | ||
let parentRoute = getParentRoute(routeMap, name); | ||
if (parentRoute) { | ||
let parent = parentMap.get(parentRoute); | ||
if (!parent) { | ||
parent = { | ||
routeInfo: routeMap.get(parentRoute), | ||
children: [], | ||
}; | ||
parentMap.set(parentRoute, parent); | ||
} | ||
parent.children.push(route); | ||
} | ||
// update undefined parentIds to 'root' | ||
Object.values(routes).forEach(route => { | ||
if (route.parentId === undefined) { | ||
route.parentId = 'root'; | ||
} | ||
// start with root | ||
getRoutes(parentMap, 'root', route); | ||
}); | ||
// don't return root since remix already provides it | ||
if (routes) { | ||
delete routes.root; | ||
} | ||
// HACK: Update the route ids for index routes to work around | ||
@@ -112,98 +82,282 @@ // a bug in Remix as of v1.7.5. Need this until PR #4560 is merged. | ||
} | ||
function isIgnoredRouteFile(file, ignoredRouteFiles) { | ||
return ignoredRouteFiles.some(ignoredFile => file.endsWith(ignoredFile)); | ||
} | ||
function getParentRoute(routeMap, name) { | ||
var parentName = name.substring(0, name.lastIndexOf('.')); | ||
if (parentName === '') { | ||
return 'root'; | ||
// this function uses the same signature as the one used in core remix | ||
// this way we can continue to enhance this package and still maintain | ||
// compatibility with remix | ||
function _flatRoutes(appDir, ignoredFilePatternsOrOptions, options) { | ||
var _a, _b, _c, _d; | ||
// get options | ||
let ignoredFilePatterns = []; | ||
if (ignoredFilePatternsOrOptions && | ||
!Array.isArray(ignoredFilePatternsOrOptions)) { | ||
options = ignoredFilePatternsOrOptions; | ||
} | ||
if (routeMap.has(parentName)) { | ||
return parentName; | ||
else { | ||
ignoredFilePatterns = ignoredFilePatternsOrOptions !== null && ignoredFilePatternsOrOptions !== void 0 ? ignoredFilePatternsOrOptions : []; | ||
} | ||
return getParentRoute(routeMap, parentName); | ||
} | ||
function getRoutes(parentMap, parent, route) { | ||
let parentRoute = parentMap.get(parent); | ||
if (parentRoute && parentRoute.children) { | ||
const routeOptions = { | ||
caseSensitive: false, | ||
index: parentRoute.routeInfo.index, | ||
}; | ||
const routeChildren = () => { | ||
for (let child of parentRoute.children) { | ||
getRoutes(parentMap, child.name, route); | ||
let path = child.path.substring(parentRoute.routeInfo.path.length); | ||
if (path.startsWith('/')) | ||
path = path.substring(1); | ||
route(path, child.file, { index: child.index }); | ||
} | ||
}; | ||
route(parentRoute.routeInfo.path, parentRoute.routeInfo.file, routeOptions, routeChildren); | ||
if (!options) { | ||
options = defaultOptions; | ||
} | ||
} | ||
function getRouteInfo(baseDir, routeFile, basePath, paramsPrefixChar) { | ||
let url = basePath !== null && basePath !== void 0 ? basePath : ''; | ||
if (url.startsWith('/')) { | ||
url = url.substring(1); | ||
let routeMap = new Map(); | ||
let nameMap = new Map(); | ||
let routeDirs = Array.isArray(options.routeDir) | ||
? options.routeDir | ||
: [(_a = options.routeDir) !== null && _a !== void 0 ? _a : 'routes']; | ||
let defineRoutes = (_b = options.defineRoutes) !== null && _b !== void 0 ? _b : defaultDefineRoutes; | ||
if (!defineRoutes) { | ||
throw new Error('You must provide a defineRoutes function'); | ||
} | ||
// get extension | ||
let ext = path.extname(routeFile); | ||
// only process valid route files | ||
if (!['.js', '.jsx', '.ts', '.tsx', '.md', '.mdx'].includes(ext)) { | ||
return null; | ||
let visitFiles = (_c = options.visitFiles) !== null && _c !== void 0 ? _c : defaultVisitFiles; | ||
let routeRegex = (_d = options.routeRegex) !== null && _d !== void 0 ? _d : defaultOptions.routeRegex; | ||
for (let routeDir of routeDirs) { | ||
visitFiles(path.join(appDir, routeDir), file => { | ||
if (ignoredFilePatterns && | ||
ignoredFilePatterns.some(pattern => (0, minimatch_1.default)(file, pattern, { dot: true }))) { | ||
return; | ||
} | ||
if (isRouteModuleFile(file, routeRegex)) { | ||
let routeInfo = getRouteInfo(routeDir, file, options); | ||
routeMap.set(routeInfo.id, routeInfo); | ||
nameMap.set(routeInfo.name, routeInfo); | ||
return; | ||
} | ||
}); | ||
} | ||
// remove extension from name and normalize path separators | ||
let name = routeFile | ||
.substring(0, routeFile.length - ext.length) | ||
.replace(path.win32.sep, '/'); | ||
if (name.includes('/')) { | ||
// route flat-folder so only process index/layout routes | ||
if (['/index', '/_index', '/_layout', '/_route', '.route'].every(suffix => !name.endsWith(suffix))) { | ||
// ignore non-index routes | ||
return null; | ||
// update parentIds for all routes | ||
Array.from(routeMap.values()).forEach(routeInfo => { | ||
let parentId = findParentRouteId(routeInfo, nameMap); | ||
routeInfo.parentId = parentId; | ||
}); | ||
let uniqueRoutes = new Map(); | ||
// Then, recurse through all routes using the public defineRoutes() API | ||
function defineNestedRoutes(defineRoute, parentId) { | ||
var _a, _b, _c; | ||
let childRoutes = Array.from(routeMap.values()).filter(routeInfo => routeInfo.parentId === parentId); | ||
let parentRoute = parentId ? routeMap.get(parentId) : undefined; | ||
let parentRoutePath = (_a = parentRoute === null || parentRoute === void 0 ? void 0 : parentRoute.path) !== null && _a !== void 0 ? _a : '/'; | ||
for (let childRoute of childRoutes) { | ||
let routePath = (_c = (_b = childRoute === null || childRoute === void 0 ? void 0 : childRoute.path) === null || _b === void 0 ? void 0 : _b.slice(parentRoutePath.length)) !== null && _c !== void 0 ? _c : ''; | ||
// remove leading slash | ||
if (routePath.startsWith('/')) { | ||
routePath = routePath.slice(1); | ||
} | ||
let index = childRoute.index; | ||
let fullPath = childRoute.path; | ||
let uniqueRouteId = (fullPath || '') + (index ? '?index' : ''); | ||
if (uniqueRouteId) { | ||
if (uniqueRoutes.has(uniqueRouteId)) { | ||
throw new Error(`Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify(childRoute.id)} conflicts with route ${JSON.stringify(uniqueRoutes.get(uniqueRouteId))}`); | ||
} | ||
else { | ||
uniqueRoutes.set(uniqueRouteId, childRoute.id); | ||
} | ||
} | ||
if (index) { | ||
let invalidChildRoutes = Object.values(routeMap).filter(routeInfo => routeInfo.parentId === childRoute.id); | ||
if (invalidChildRoutes.length > 0) { | ||
throw new Error(`Child routes are not allowed in index routes. Please remove child routes of ${childRoute.id}`); | ||
} | ||
defineRoute(routePath, routeMap.get(childRoute.id).file, { | ||
index: true, | ||
}); | ||
} | ||
else { | ||
defineRoute(routePath, routeMap.get(childRoute.id).file, () => { | ||
defineNestedRoutes(defineRoute, childRoute.id); | ||
}); | ||
} | ||
} | ||
if (name.endsWith('.route')) { | ||
// convert docs/readme.route to docs.readme/_index | ||
name = name.replace(/[\/\\]/g, '.').replace(/\.route$/, '/_index'); | ||
} | ||
name = path.dirname(name); | ||
} | ||
let routeSegments = (0, util_1.getRouteSegments)(name); | ||
for (let i = 0; i < routeSegments.length; i++) { | ||
let routeSegment = routeSegments[i]; | ||
url = appendPathSegment(url, routeSegment, paramsPrefixChar); | ||
let routes = defineRoutes(defineNestedRoutes); | ||
return routes; | ||
} | ||
const routeModuleExts = ['.js', '.jsx', '.ts', '.tsx', '.md', '.mdx']; | ||
const serverRegex = /\.server\.(ts|tsx|js|jsx|md|mdx)$/; | ||
const indexRouteRegex = /((^|[.])(index|_index))(\/[^\/]+)?$|(\/_?index\/)/; | ||
function isRouteModuleFile(filename, routeRegex) { | ||
// flat files only need correct extension | ||
let isFlatFile = !filename.includes(path.sep); | ||
if (isFlatFile) { | ||
return routeModuleExts.includes(path.extname(filename)); | ||
} | ||
return { | ||
path: url, | ||
file: path.join(baseDir, routeFile), | ||
name, | ||
//parent: parent will be calculated after all routes are defined, | ||
index: routeSegments.at(-1) === 'index' || routeSegments.at(-1) === '_index', | ||
let isRoute = routeRegex.test(filename); | ||
if (isRoute) { | ||
// check to see if it ends in .server.tsx because you may have | ||
// a _route.tsx and and _route.server.tsx and only the _route.tsx | ||
// file should be considered a route | ||
let isServer = serverRegex.test(filename); | ||
return !isServer; | ||
} | ||
return false; | ||
} | ||
exports.isRouteModuleFile = isRouteModuleFile; | ||
function isIndexRoute(routeId) { | ||
return indexRouteRegex.test(routeId); | ||
} | ||
exports.isIndexRoute = isIndexRoute; | ||
function getRouteInfo(routeDir, file, options) { | ||
let filePath = path.join(routeDir, file); | ||
let routeId = createRouteId(filePath); | ||
let routeIdWithoutRoutes = routeId.slice(routeDir.length + 1); | ||
let index = isIndexRoute(routeIdWithoutRoutes); | ||
let routeSegments = getRouteSegments(routeIdWithoutRoutes, options.paramPrefixChar); | ||
let routePath = createRoutePath(routeSegments, index, options); | ||
let routeInfo = { | ||
id: routeId, | ||
path: routePath, | ||
file: filePath, | ||
name: routeSegments.join('/'), | ||
segments: routeSegments, | ||
index, | ||
}; | ||
return routeInfo; | ||
} | ||
exports.getRouteInfo = getRouteInfo; | ||
function appendPathSegment(url, segment, paramsPrefixChar = '$') { | ||
if (segment) { | ||
if (['index', '_index'].some(name => segment === name)) { | ||
// index routes don't affect the the path | ||
return url; | ||
} | ||
// create full path starting with / | ||
function createRoutePath(routeSegments, index, options) { | ||
var _a, _b; | ||
let result = ''; | ||
let basePath = (_a = options.basePath) !== null && _a !== void 0 ? _a : '/'; | ||
let paramPrefixChar = (_b = options.paramPrefixChar) !== null && _b !== void 0 ? _b : '$'; | ||
if (index) { | ||
// replace index with blank | ||
routeSegments[routeSegments.length - 1] = ''; | ||
} | ||
for (let i = 0; i < routeSegments.length; i++) { | ||
let segment = routeSegments[i]; | ||
// skip pathless layout segments | ||
if (segment.startsWith('_')) { | ||
// handle pathless route (not included in url) | ||
return url; | ||
continue; | ||
} | ||
// remove trailing slash | ||
if (segment.endsWith('_')) { | ||
// handle parent override | ||
segment = segment.substring(0, segment.length - 1); | ||
segment = segment.slice(0, -1); | ||
} | ||
if (segment.startsWith(paramsPrefixChar)) { | ||
// handle params | ||
segment = segment === paramsPrefixChar ? '*' : `:${segment.substring(1)}`; | ||
// handle param segments: $ => *, $id => :id | ||
if (segment.startsWith(paramPrefixChar)) { | ||
if (segment === paramPrefixChar) { | ||
result += `/*`; | ||
} | ||
else { | ||
result += `/:${segment.slice(1)}`; | ||
} | ||
// handle optional segments: (segment) => segment? | ||
} | ||
if (url) | ||
url += '/'; | ||
url += segment; | ||
else if (segment.startsWith('(')) { | ||
result += `/${segment.slice(1, segment.length - 1)}?`; | ||
} | ||
else { | ||
result += `/${segment}`; | ||
} | ||
} | ||
return url; | ||
if (basePath !== '/') { | ||
result = basePath + result; | ||
} | ||
return result || undefined; | ||
} | ||
exports.createRoutePath = createRoutePath; | ||
function findParentRouteId(routeInfo, nameMap) { | ||
let parentName = routeInfo.segments.slice(0, -1).join('/'); | ||
while (parentName) { | ||
if (nameMap.has(parentName)) { | ||
return nameMap.get(parentName).id; | ||
} | ||
parentName = parentName.substring(0, parentName.lastIndexOf('/')); | ||
} | ||
return undefined; | ||
} | ||
function getRouteSegments(name, paramPrefixChar = '$') { | ||
let routeSegments = []; | ||
let index = 0; | ||
let routeSegment = ''; | ||
let state = 'START'; | ||
let subState = 'NORMAL'; | ||
// do not remove segments ending in .route | ||
// since these would be part of the route directory name | ||
// docs/readme.route.tsx => docs/readme | ||
if (!name.endsWith('.route')) { | ||
// remove last segment since this should just be the | ||
// route filename and we only want the directory name | ||
// docs/_layout.tsx => docs | ||
let last = name.lastIndexOf('/'); | ||
if (last >= 0) { | ||
name = name.substring(0, last); | ||
} | ||
} | ||
let pushRouteSegment = (routeSegment) => { | ||
if (routeSegment) { | ||
routeSegments.push(routeSegment); | ||
} | ||
}; | ||
while (index < name.length) { | ||
let char = name[index]; | ||
switch (state) { | ||
case 'START': | ||
// process existing segment | ||
if (routeSegment.includes(paramPrefixChar) && | ||
!routeSegment.startsWith(paramPrefixChar)) { | ||
throw new Error(`Route params must start with prefix char ${paramPrefixChar}: ${routeSegment}`); | ||
} | ||
if (routeSegment.includes('(') && | ||
!routeSegment.startsWith('(') && | ||
!routeSegment.endsWith(')')) { | ||
throw new Error(`Optional routes must start and end with parentheses: ${routeSegment}`); | ||
} | ||
pushRouteSegment(routeSegment); | ||
routeSegment = ''; | ||
state = 'PATH'; | ||
continue; // restart without advancing index | ||
case 'PATH': | ||
if (isPathSeparator(char) && subState === 'NORMAL') { | ||
state = 'START'; | ||
break; | ||
} | ||
else if (char === '[') { | ||
subState = 'ESCAPE'; | ||
break; | ||
} | ||
else if (char === ']') { | ||
subState = 'NORMAL'; | ||
break; | ||
} | ||
routeSegment += char; | ||
break; | ||
} | ||
index++; // advance to next character | ||
} | ||
// process remaining segment | ||
pushRouteSegment(routeSegment); | ||
// strip trailing .route segment | ||
if (routeSegments.at(-1) === 'route') { | ||
routeSegments = routeSegments.slice(0, -1); | ||
} | ||
return routeSegments; | ||
} | ||
exports.getRouteSegments = getRouteSegments; | ||
const pathSeparatorRegex = /[/\\.]/; | ||
function isPathSeparator(char) { | ||
return pathSeparatorRegex.test(char); | ||
} | ||
function defaultVisitFiles(dir, visitor, baseDir = dir) { | ||
for (let filename of fs.readdirSync(dir)) { | ||
let file = path.resolve(dir, filename); | ||
let stat = fs.lstatSync(file); | ||
if (stat.isDirectory()) { | ||
defaultVisitFiles(file, visitor, baseDir); | ||
} | ||
else if (stat.isFile()) { | ||
visitor(path.relative(baseDir, file)); | ||
} | ||
} | ||
} | ||
exports.defaultVisitFiles = defaultVisitFiles; | ||
function createRouteId(file) { | ||
return normalizeSlashes(stripFileExtension(file)); | ||
} | ||
exports.createRouteId = createRouteId; | ||
function normalizeSlashes(file) { | ||
return file.split(path.win32.sep).join('/'); | ||
} | ||
exports.normalizeSlashes = normalizeSlashes; | ||
function stripFileExtension(file) { | ||
return file.replace(/\.[a-z0-9]+$/i, ''); | ||
} |
@@ -29,6 +29,6 @@ "use strict"; | ||
const path = __importStar(require("path")); | ||
const util_1 = require("./util"); | ||
const index_1 = require("./index"); | ||
function migrate(sourceDir, targetDir, options = { convention: 'flat-files' }) { | ||
var visitor = options.convention === 'flat-files' ? flatFiles : flatFolders; | ||
(0, util_1.visitFiles)(sourceDir, visitor(sourceDir, targetDir)); | ||
(0, index_1.defaultVisitFiles)(sourceDir, visitor(sourceDir, targetDir)); | ||
} | ||
@@ -69,3 +69,3 @@ exports.migrate = migrate; | ||
.map(pathSegment => { | ||
const routeSegments = (0, util_1.getRouteSegments)(pathSegment); | ||
const routeSegments = (0, index_1.getRouteSegments)(pathSegment); | ||
return getFlatRoute(routeSegments); | ||
@@ -72,0 +72,0 @@ }) |
{ | ||
"name": "remix-flat-routes", | ||
"version": "0.4.8", | ||
"version": "0.5.0", | ||
"description": "Package for generating routes using flat convention", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
116
README.md
@@ -11,2 +11,118 @@ # Remix Flat Routes | ||
## β¨π New in v0.5.0 | ||
### Integration with Remix Core | ||
Remix flat routes will be a core feature in a future version of Remix. This will be enabled using a config option. | ||
I plan to continue to maintain this package in the future to enable enhancements that will not be in the Remix core version. To simplify maintenance, I expose all enhancements via the `options` parameter. | ||
### Hybrid Routes | ||
You can now use nested folders for your route names, yet still keep the colocation feature of flat routes. | ||
If you have a large app, its not uncommon to have routes nested many levels deep. With default flat routes, the folder name is the entire route path: `some.really.long.route.edit/index.tsx` | ||
Often you may have several parent layouts like `_public` or `admin`. Instead of having to repeat the name in every route, you can create top-level folders, then nest your routes under them. This way you can still take advantage of flat folders with colocation. | ||
**Before** | ||
```shell | ||
β― tree app/routes-folders | ||
app/routes-folders | ||
βββ _index | ||
β βββ page.tsx | ||
βββ _public | ||
β βββ _layout.tsx | ||
βββ _public.about | ||
β βββ index.tsx | ||
βββ _public.contact[.jpg] | ||
β βββ index.tsx | ||
βββ test.$ | ||
β βββ _route.server.tsx | ||
β βββ _route.tsx | ||
βββ users | ||
β βββ _layout.tsx | ||
β βββ users.css | ||
βββ users.$userId | ||
β βββ _route.tsx | ||
β βββ avatar.png | ||
βββ users.$userId_.edit | ||
β βββ _route.tsx | ||
βββ users._index | ||
βββ index.tsx | ||
``` | ||
**After** | ||
```shell | ||
β― tree app/routes-hybrid | ||
app/routes-hybrid | ||
βββ _index | ||
β βββ index.tsx | ||
βββ _public | ||
β βββ _layout.tsx | ||
β βββ about | ||
β β βββ _route.tsx | ||
β βββ contact[.jpg] | ||
β βββ _route.tsx | ||
βββ test.$ | ||
β βββ _route.tsx | ||
βββ users | ||
βββ $userId | ||
β βββ _route.tsx | ||
β βββ avatar.png | ||
βββ $userId_.edit | ||
β βββ _route.tsx | ||
βββ _index | ||
β βββ index.tsx | ||
βββ _layout.tsx | ||
βββ users.css | ||
``` | ||
### Extended Route Filenames | ||
In addition to the standard `index | route | page | layout` names, any file that has a `_` prefix will be treated as the route file. This will make it easier to find a specific route instead of looking through a bunch of `index.tsx` files. This was inspired by [SolidStart](https://start.solidjs.com/core-concepts/routing) "Renaming Index" feature. | ||
So instead of | ||
``` | ||
_public.about/index.tsx | ||
_public.contact/index.tsx | ||
_public.privacy/index.tsx | ||
``` | ||
You can name them | ||
``` | ||
_public.about/_about.tsx | ||
_public.contact/_contact.tsx | ||
_public.privacy/_privacy.tsx | ||
``` | ||
### Multiple Route Folders | ||
You can now pass in additional route folders besides the default `routes` folder. These routes will be merged into a single namespace, so you can have routes in one folder that will use shared routes from another. | ||
### Custom Param Prefix | ||
You can override the default param prefix of `$`. Some shells use the `$` prefix for variables, and this can be an issue due to shell expansion. Use any character that is a valid filename, for example: `^` | ||
``` | ||
users.^userId.tsx => users/:userId | ||
test.^.tsx => test/* | ||
``` | ||
### Custom Base Path | ||
You can override the default base path of `/`. This will prepend your base path to the root path. | ||
### Optional Route Segments | ||
React Router will introduce a new feature for optional route segments. To use optional segments in flat routes, simply wrap your route name in `()`. | ||
``` | ||
parent.(optional).tsx => parent/optional? | ||
``` | ||
## π Installation | ||
@@ -13,0 +129,0 @@ |
46542
572
422
10