@tanstack/router-generator
Version:
Modern and scalable routing for React applications
270 lines (248 loc) • 7.94 kB
text/typescript
import path, { join, resolve } from 'node:path'
import {
removeExt,
removeLeadingSlash,
removeTrailingSlash,
replaceBackslash,
routePathToVariable,
} from '../../utils'
import { getRouteNodes as getRouteNodesPhysical } from '../physical/getRouteNodes'
import { rootPathId } from '../physical/rootPathId'
import { virtualRootRouteSchema } from './config'
import { loadConfigFile } from './loadConfigFile'
import type {
VirtualRootRoute,
VirtualRouteNode,
} from '@tanstack/virtual-file-routes'
import type { GetRouteNodesResult, RouteNode } from '../../types'
import type { Config } from '../../config'
function ensureLeadingUnderScore(id: string) {
if (id.startsWith('_')) {
return id
}
return `_${id}`
}
function flattenTree(node: RouteNode): Array<RouteNode> {
const result = [node]
if (node.children) {
for (const child of node.children) {
result.push(...flattenTree(child))
}
}
delete node.children
return result
}
export async function getRouteNodes(
tsrConfig: Pick<
Config,
| 'routesDirectory'
| 'virtualRouteConfig'
| 'routeFileIgnorePrefix'
| 'disableLogging'
| 'indexToken'
| 'routeToken'
>,
root: string,
): Promise<GetRouteNodesResult> {
const fullDir = resolve(tsrConfig.routesDirectory)
if (tsrConfig.virtualRouteConfig === undefined) {
throw new Error(`virtualRouteConfig is undefined`)
}
let virtualRouteConfig: VirtualRootRoute
if (typeof tsrConfig.virtualRouteConfig === 'string') {
virtualRouteConfig = await getVirtualRouteConfigFromFileExport(
tsrConfig,
root,
)
} else {
virtualRouteConfig = tsrConfig.virtualRouteConfig
}
const { children, physicalDirectories } = await getRouteNodesRecursive(
tsrConfig,
root,
fullDir,
virtualRouteConfig.children,
)
const allNodes = flattenTree({
children,
filePath: virtualRouteConfig.file,
fullPath: replaceBackslash(join(fullDir, virtualRouteConfig.file)),
variableName: 'root',
routePath: `/${rootPathId}`,
_fsRouteType: '__root',
})
const rootRouteNode = allNodes[0]
const routeNodes = allNodes.slice(1)
return { rootRouteNode, routeNodes, physicalDirectories }
}
/**
* Get the virtual route config from a file export
*
* @example
* ```ts
* // routes.ts
* import { rootRoute } from '@tanstack/virtual-file-routes'
*
* export const routes = rootRoute({ ... })
* // or
* export default rootRoute({ ... })
* ```
*
*/
async function getVirtualRouteConfigFromFileExport(
tsrConfig: Pick<Config, 'virtualRouteConfig'>,
root: string,
): Promise<VirtualRootRoute> {
if (
tsrConfig.virtualRouteConfig === undefined ||
typeof tsrConfig.virtualRouteConfig !== 'string' ||
tsrConfig.virtualRouteConfig === ''
) {
throw new Error(`virtualRouteConfig is undefined or empty`)
}
const exports = await loadConfigFile(join(root, tsrConfig.virtualRouteConfig))
if (!('routes' in exports) && !('default' in exports)) {
throw new Error(
`routes not found in ${tsrConfig.virtualRouteConfig}. The routes export must be named like 'export const routes = ...' or done using 'export default ...'`,
)
}
const virtualRouteConfig =
'routes' in exports ? exports.routes : exports.default
return virtualRootRouteSchema.parse(virtualRouteConfig)
}
export async function getRouteNodesRecursive(
tsrConfig: Pick<
Config,
| 'routesDirectory'
| 'routeFileIgnorePrefix'
| 'disableLogging'
| 'indexToken'
| 'routeToken'
>,
root: string,
fullDir: string,
nodes?: Array<VirtualRouteNode>,
parent?: RouteNode,
): Promise<{ children: Array<RouteNode>; physicalDirectories: Array<string> }> {
if (nodes === undefined) {
return { children: [], physicalDirectories: [] }
}
const allPhysicalDirectories: Array<string> = []
const children = await Promise.all(
nodes.map(async (node) => {
if (node.type === 'physical') {
const { routeNodes, physicalDirectories } = await getRouteNodesPhysical(
{
...tsrConfig,
routesDirectory: resolve(fullDir, node.directory),
},
root,
)
allPhysicalDirectories.push(node.directory)
routeNodes.forEach((subtreeNode) => {
subtreeNode.variableName = routePathToVariable(
`${node.pathPrefix}/${removeExt(subtreeNode.filePath)}`,
)
subtreeNode.routePath = `${parent?.routePath ?? ''}${node.pathPrefix}${subtreeNode.routePath}`
subtreeNode.filePath = `${node.directory}/${subtreeNode.filePath}`
})
return routeNodes
}
function getFile(file: string) {
const filePath = file
const variableName = routePathToVariable(removeExt(filePath))
const fullPath = replaceBackslash(join(fullDir, filePath))
return { filePath, variableName, fullPath }
}
const parentRoutePath = removeTrailingSlash(parent?.routePath ?? '/')
switch (node.type) {
case 'index': {
const { filePath, variableName, fullPath } = getFile(node.file)
const routePath = `${parentRoutePath}/`
return {
filePath,
fullPath,
variableName,
routePath,
_fsRouteType: 'static',
} satisfies RouteNode
}
case 'route': {
const lastSegment = node.path
let routeNode: RouteNode
const routePath = `${parentRoutePath}/${removeLeadingSlash(lastSegment)}`
if (node.file) {
const { filePath, variableName, fullPath } = getFile(node.file)
routeNode = {
filePath,
fullPath,
variableName,
routePath,
_fsRouteType: 'static',
}
} else {
routeNode = {
filePath: '',
fullPath: '',
variableName: routePathToVariable(routePath),
routePath,
isVirtual: true,
_fsRouteType: 'static',
}
}
if (node.children !== undefined) {
const { children, physicalDirectories } =
await getRouteNodesRecursive(
tsrConfig,
root,
fullDir,
node.children,
routeNode,
)
routeNode.children = children
allPhysicalDirectories.push(...physicalDirectories)
// If the route has children, it should be a layout
routeNode._fsRouteType = 'layout'
}
return routeNode
}
case 'layout': {
const { filePath, variableName, fullPath } = getFile(node.file)
if (node.id !== undefined) {
node.id = ensureLeadingUnderScore(node.id)
} else {
const baseName = path.basename(filePath)
const fileNameWithoutExt = path.parse(baseName).name
node.id = ensureLeadingUnderScore(fileNameWithoutExt)
}
const lastSegment = node.id
const routePath = `${parentRoutePath}/${removeLeadingSlash(lastSegment)}`
const routeNode: RouteNode = {
fullPath,
filePath,
variableName,
routePath,
_fsRouteType: 'pathless_layout',
}
if (node.children !== undefined) {
const { children, physicalDirectories } =
await getRouteNodesRecursive(
tsrConfig,
root,
fullDir,
node.children,
routeNode,
)
routeNode.children = children
allPhysicalDirectories.push(...physicalDirectories)
}
return routeNode
}
}
}),
)
return {
children: children.flat(),
physicalDirectories: allPhysicalDirectories,
}
}