UNPKG

@tanstack/router-generator

Version:

Modern and scalable routing for React applications

309 lines (267 loc) 9.86 kB
import path from 'node:path' import * as fsp from 'node:fs/promises' import { determineInitialRoutePath, removeExt, replaceBackslash, routePathToVariable, } from '../../utils' import { getRouteNodes as getRouteNodesVirtual } from '../virtual/getRouteNodes' import { loadConfigFile } from '../virtual/loadConfigFile' import { logging } from '../../logger' import { rootPathId } from './rootPathId' import type { VirtualRootRoute, VirtualRouteSubtreeConfig, } from '@tanstack/virtual-file-routes' import type { FsRouteType, GetRouteNodesResult, RouteNode } from '../../types' import type { Config } from '../../config' const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/ const virtualConfigFileRegExp = /__virtual\.[mc]?[jt]s$/ export function isVirtualConfigFile(fileName: string): boolean { return virtualConfigFileRegExp.test(fileName) } export async function getRouteNodes( config: Pick< Config, | 'routesDirectory' | 'routeFilePrefix' | 'routeFileIgnorePrefix' | 'routeFileIgnorePattern' | 'disableLogging' | 'routeToken' | 'indexToken' >, root: string, ): Promise<GetRouteNodesResult> { const { routeFilePrefix, routeFileIgnorePrefix, routeFileIgnorePattern } = config const logger = logging({ disabled: config.disableLogging }) const routeFileIgnoreRegExp = new RegExp(routeFileIgnorePattern ?? '', 'g') const routeNodes: Array<RouteNode> = [] const allPhysicalDirectories: Array<string> = [] async function recurse(dir: string) { const fullDir = path.resolve(config.routesDirectory, dir) let dirList = await fsp.readdir(fullDir, { withFileTypes: true }) dirList = dirList.filter((d) => { if ( d.name.startsWith('.') || (routeFileIgnorePrefix && d.name.startsWith(routeFileIgnorePrefix)) ) { return false } if (routeFilePrefix) { return d.name.startsWith(routeFilePrefix) } if (routeFileIgnorePattern) { return !d.name.match(routeFileIgnoreRegExp) } return true }) const virtualConfigFile = dirList.find((dirent) => { return dirent.isFile() && isVirtualConfigFile(dirent.name) }) if (virtualConfigFile !== undefined) { const virtualRouteConfigExport = await loadConfigFile( path.resolve(fullDir, virtualConfigFile.name), ) let virtualRouteSubtreeConfig: VirtualRouteSubtreeConfig if (typeof virtualRouteConfigExport.default === 'function') { virtualRouteSubtreeConfig = await virtualRouteConfigExport.default() } else { virtualRouteSubtreeConfig = virtualRouteConfigExport.default } const dummyRoot: VirtualRootRoute = { type: 'root', file: '', children: virtualRouteSubtreeConfig, } const { routeNodes: virtualRouteNodes, physicalDirectories } = await getRouteNodesVirtual( { ...config, routesDirectory: fullDir, virtualRouteConfig: dummyRoot, }, root, ) allPhysicalDirectories.push(...physicalDirectories) virtualRouteNodes.forEach((node) => { const filePath = replaceBackslash(path.join(dir, node.filePath)) const routePath = `/${dir}${node.routePath}` node.variableName = routePathToVariable( `${dir}/${removeExt(node.filePath)}`, ) node.routePath = routePath node.filePath = filePath }) routeNodes.push(...virtualRouteNodes) return } await Promise.all( dirList.map(async (dirent) => { const fullPath = replaceBackslash(path.join(fullDir, dirent.name)) const relativePath = path.posix.join(dir, dirent.name) if (dirent.isDirectory()) { await recurse(relativePath) } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) { const filePath = replaceBackslash(path.join(dir, dirent.name)) const filePathNoExt = removeExt(filePath) let routePath = determineInitialRoutePath(filePathNoExt) if (routeFilePrefix) { routePath = routePath.replaceAll(routeFilePrefix, '') } if (disallowedRouteGroupConfiguration.test(dirent.name)) { const errorMessage = `A route configuration for a route group was found at \`${filePath}\`. This is not supported. Did you mean to use a layout/pathless route instead?` logger.error(`ERROR: ${errorMessage}`) throw new Error(errorMessage) } const meta = getRouteMeta(routePath, config) const variableName = meta.variableName let routeType: FsRouteType = meta.fsRouteType if (routeType === 'lazy') { routePath = routePath.replace(/\/lazy$/, '') } // this check needs to happen after the lazy route has been cleaned up // since the routePath is used to determine if a route is pathless if (isValidPathlessLayoutRoute(routePath, routeType, config)) { routeType = 'pathless_layout' } ;( [ ['component', 'component'], ['errorComponent', 'errorComponent'], ['pendingComponent', 'pendingComponent'], ['loader', 'loader'], ] satisfies Array<[FsRouteType, string]> ).forEach(([matcher, type]) => { if (routeType === matcher) { logger.warn( `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`, ) } }) routePath = routePath.replace( new RegExp( `/(component|errorComponent|pendingComponent|loader|${config.routeToken}|lazy)$`, ), '', ) if (routePath === config.indexToken) { routePath = '/' } routePath = routePath.replace(new RegExp(`/${config.indexToken}$`), '/') || '/' routeNodes.push({ filePath, fullPath, routePath, variableName, _fsRouteType: routeType, }) } }), ) return routeNodes } await recurse('./') const rootRouteNode = routeNodes.find((d) => d.routePath === `/${rootPathId}`) if (rootRouteNode) { rootRouteNode._fsRouteType = '__root' rootRouteNode.variableName = 'root' } return { rootRouteNode, routeNodes, physicalDirectories: allPhysicalDirectories, } } /** * Determines the metadata for a given route path based on the provided configuration. * * @param routePath - The determined initial routePath. * @param config - The user configuration object. * @returns An object containing the type of the route and the variable name derived from the route path. */ export function getRouteMeta( routePath: string, config: Pick<Config, 'routeToken' | 'indexToken'>, ): { // `__root` is can be more easily determined by filtering down to routePath === /${rootPathId} // `pathless` is needs to determined after `lazy` has been cleaned up from the routePath fsRouteType: Extract< FsRouteType, | 'static' | 'layout' | 'api' | 'lazy' | 'loader' | 'component' | 'pendingComponent' | 'errorComponent' > variableName: string } { let fsRouteType: FsRouteType = 'static' if (routePath.endsWith(`/${config.routeToken}`)) { // layout routes, i.e `/foo/route.tsx` or `/foo/_layout/route.tsx` fsRouteType = 'layout' } else if (routePath.endsWith('/lazy')) { // lazy routes, i.e. `/foo.lazy.tsx` fsRouteType = 'lazy' } else if (routePath.endsWith('/loader')) { // loader routes, i.e. `/foo.loader.tsx` fsRouteType = 'loader' } else if (routePath.endsWith('/component')) { // component routes, i.e. `/foo.component.tsx` fsRouteType = 'component' } else if (routePath.endsWith('/pendingComponent')) { // pending component routes, i.e. `/foo.pendingComponent.tsx` fsRouteType = 'pendingComponent' } else if (routePath.endsWith('/errorComponent')) { // error component routes, i.e. `/foo.errorComponent.tsx` fsRouteType = 'errorComponent' } const variableName = routePathToVariable(routePath) return { fsRouteType, variableName } } /** * Used to validate if a route is a pathless layout route * @param normalizedRoutePath Normalized route path, i.e `/foo/_layout/route.tsx` and `/foo._layout.route.tsx` to `/foo/_layout/route` * @param config The `router-generator` configuration object * @returns Boolean indicating if the route is a pathless layout route */ function isValidPathlessLayoutRoute( normalizedRoutePath: string, routeType: FsRouteType, config: Pick<Config, 'routeToken' | 'indexToken'>, ): boolean { if (routeType === 'lazy') { return false } const segments = normalizedRoutePath.split('/').filter(Boolean) if (segments.length === 0) { return false } const lastRouteSegment = segments[segments.length - 1]! const secondToLastRouteSegment = segments[segments.length - 2] // If segment === __root, then exit as false if (lastRouteSegment === rootPathId) { return false } // If segment === config.routeToken and secondToLastSegment is a string that starts with _, then exit as true // Since the route is actually a configuration route for a layout/pathless route // i.e. /foo/_layout/route.tsx === /foo/_layout.tsx if ( lastRouteSegment === config.routeToken && typeof secondToLastRouteSegment === 'string' ) { return secondToLastRouteSegment.startsWith('_') } // Segment starts with _ return ( lastRouteSegment !== config.indexToken && lastRouteSegment !== config.routeToken && lastRouteSegment.startsWith('_') ) }