Files
tuono/packages/fs-router-vite-plugin/src/generator.ts
T

260 lines
7.2 KiB
TypeScript
Raw Normal View History

import * as fsp from 'fs/promises'
import path from 'path'
2024-12-01 10:07:35 +01:00
import { format } from 'prettier'
2024-07-23 19:13:18 +02:00
import { buildRouteConfig } from './build-route-config'
import { hasParentRoute } from './has-parent-route'
import {
cleanPath,
determineNodePath,
multiSortBy,
replaceBackslash,
removeExt,
routePathToVariable,
removeGroups,
2024-07-23 19:13:18 +02:00
removeLastSlash,
removeUnderscores,
} from './utils'
2024-07-08 21:39:00 +02:00
import type { Config, RouteNode } from './types'
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
2024-07-23 19:13:18 +02:00
import { sortRouteNodes } from './sort-route-nodes'
2024-11-04 19:25:46 +01:00
import isDefaultExported from './utils/is-default-exported'
let latestTask = 0
const defaultConfig: Config = {
folderName: ROUTES_FOLDER,
generatedRouteTree: GENERATED_ROUTE_TREE,
}
let isFirst = false
let skipMessage = false
2024-05-11 15:27:51 +02:00
async function getRouteNodes(
config = defaultConfig,
): Promise<{ routeNodes: Array<RouteNode>; rustHandlersNodes: Array<string> }> {
const routeNodes: Array<RouteNode> = []
const rustHandlersNodes: Array<string> = []
async function recurse(dir: string): Promise<void> {
const fullDir = path.resolve(config.folderName, dir)
const dirList = await fsp.readdir(fullDir, { withFileTypes: true })
await Promise.all(
dirList.map(async (dirent) => {
const fullPath = path.join(fullDir, dirent.name)
const relativePath = path.join(dir, dirent.name)
if (dirent.isDirectory()) {
await recurse(relativePath)
2024-07-22 07:59:43 +02:00
} else if (fullPath.match(/\.(tsx|ts|jsx|js|mdx)$/)) {
2024-11-04 19:25:46 +01:00
// Check that the route is correctly default exported
if (
fullPath.match(/\.(tsx|ts|jsx|js)$/) &&
!isDefaultExported((await fsp.readFile(fullPath)).toString())
) {
return
}
const filePath = replaceBackslash(path.join(dir, dirent.name))
const filePathNoExt = removeExt(filePath)
2024-12-10 19:30:35 +01:00
let routePath = cleanPath(`/${filePathNoExt}`) || ''
const variableName = routePathToVariable(routePath)
// Remove the index from the route path and
// if the route path is empty, use `/'
if (routePath === 'index') {
routePath = '/'
}
routePath = routePath.replace(/\/index$/, '/') || '/'
routeNodes.push({
filePath,
fullPath,
routePath,
variableName,
})
2024-05-11 15:27:51 +02:00
} else if (fullPath.match(/\.(rs)$/)) {
const filePath = replaceBackslash(path.join(dir, dirent.name))
const filePathNoExt = removeExt(filePath)
let routePath =
cleanPath(`/${filePathNoExt.split('.').join('/')}`) || ''
if (routePath === 'index') {
routePath = '/'
}
routePath = routePath.replace(/\/index$/, '/') || '/'
rustHandlersNodes.push(routePath)
}
}),
)
}
await recurse('./')
2024-05-11 15:27:51 +02:00
return { routeNodes, rustHandlersNodes }
}
export async function routeGenerator(config = defaultConfig): Promise<void> {
if (!isFirst) {
isFirst = true
} else if (skipMessage) {
skipMessage = false
}
const checkLatest = (): boolean => {
if (latestTask !== taskId) {
skipMessage = true
return false
}
return true
}
const taskId = latestTask + 1
latestTask = taskId
2024-05-11 15:27:51 +02:00
const { routeNodes: beforeRouteNodes, rustHandlersNodes } =
await getRouteNodes(config)
2024-07-23 19:13:18 +02:00
const preRouteNodes = sortRouteNodes(beforeRouteNodes)
const routeNodes: Array<RouteNode> = []
2024-07-23 19:13:18 +02:00
// Loop over the flat list of routeNodes and
// build up a tree based on the routeNodes' routePath
2024-12-01 10:07:35 +01:00
const handleNode = (node: RouteNode): void => {
const parentRoute = hasParentRoute(routeNodes, node, node.routePath)
if (parentRoute) node.parent = parentRoute
node.path = determineNodePath(node)
2024-07-23 19:13:18 +02:00
node.cleanedPath = removeLastSlash(
2024-12-10 19:30:35 +01:00
removeGroups(removeUnderscores(node.path) ?? ''),
)
if (node.parent) {
node.parent.children = node.parent.children ?? []
node.parent.children.push(node)
}
routeNodes.push(node)
}
for (const node of preRouteNodes) {
2024-12-01 10:07:35 +01:00
handleNode(node)
}
const routeConfigChildrenText = buildRouteConfig(routeNodes)
const sortedRouteNodes = multiSortBy(routeNodes, [
(d): number => (d.routePath.includes(`/${ROOT_PATH_ID}`) ? -1 : 1),
(d): number => d.routePath.split('/').length,
(d): number => (d.routePath.endsWith("index'") ? -1 : 1),
(d): any => d,
])
2024-05-11 15:08:49 +02:00
const imports = [
...sortedRouteNodes.map((node) => {
2024-07-22 07:59:43 +02:00
const extension = node.filePath.endsWith('mdx') ? '.mdx' : ''
return `const ${
node.variableName
2024-06-21 18:52:58 +02:00
}Import = dynamic(() => import('./${replaceBackslash(
2024-05-11 15:08:49 +02:00
removeExt(
path.relative(
path.dirname(config.generatedRouteTree),
path.resolve(config.folderName, node.filePath),
),
false,
),
2024-07-22 07:59:43 +02:00
)}${extension}'))`
2024-05-11 15:08:49 +02:00
}),
].join('\n')
const createRoutes = [
...sortedRouteNodes.map((node) => {
2024-07-23 19:13:18 +02:00
const isRoot = node.routePath.endsWith(ROOT_PATH_ID)
const rootDeclaration = isRoot ? ', isRoot: true' : ''
return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import${rootDeclaration} })`
2024-05-11 15:08:49 +02:00
}),
].join('\n')
2024-05-11 15:27:51 +02:00
const createRouteUpdates = [
sortedRouteNodes
.map((node) => {
return [
2024-05-11 15:08:49 +02:00
`const ${node.variableName}Route = ${node.variableName}.update({
${[
2024-07-23 19:13:18 +02:00
!node.path?.endsWith(ROOT_PATH_ID) && `path: '${node.cleanedPath}'`,
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
2024-05-11 15:27:51 +02:00
rustHandlersNodes.includes(node.path || '')
? 'hasHandler: true'
: '',
]
.filter(Boolean)
.join(',')}
})`,
].join('')
})
.join('\n\n'),
2024-05-11 15:27:51 +02:00
]
const routeImports = [
'// This file is auto-generated by Tuono',
2024-06-21 18:52:58 +02:00
"import { createRoute, dynamic } from 'tuono'",
2024-05-11 15:27:51 +02:00
[
`import RootImport from './${replaceBackslash(
path.relative(
path.dirname(config.generatedRouteTree),
path.resolve(config.folderName, ROOT_PATH_ID),
),
)}'`,
].join('\n'),
imports,
'const rootRoute = createRoute({ isRoot: true, component: RootImport });',
createRoutes,
'// Create/Update Routes',
createRouteUpdates,
'// Create and export the route tree',
`export const routeTree = rootRoute.addChildren([${routeConfigChildrenText}])`,
]
.filter(Boolean)
.join('\n\n')
const routeConfigFileContent = await format(routeImports, {
semi: false,
singleQuote: true,
parser: 'typescript',
})
const routeTreeContent = await fsp
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
2024-12-01 10:07:35 +01:00
.catch((e) => {
const err = e as Error & { code?: string }
if (err.code === 'ENOENT') {
return undefined
}
throw err
})
if (!checkLatest()) return
if (routeTreeContent !== routeConfigFileContent) {
await fsp.mkdir(path.dirname(path.resolve(config.generatedRouteTree)), {
recursive: true,
})
if (!checkLatest()) return
await fsp.writeFile(
path.resolve(config.generatedRouteTree),
routeConfigFileContent,
)
}
}