2024-05-01 19:30:45 +02:00
|
|
|
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'
|
2024-05-01 19:30:45 +02:00
|
|
|
import {
|
|
|
|
|
cleanPath,
|
|
|
|
|
determineNodePath,
|
|
|
|
|
multiSortBy,
|
|
|
|
|
replaceBackslash,
|
|
|
|
|
removeExt,
|
|
|
|
|
routePathToVariable,
|
|
|
|
|
removeGroups,
|
2024-07-23 19:13:18 +02:00
|
|
|
removeLastSlash,
|
2024-05-01 19:30:45 +02:00
|
|
|
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-05-01 19:30:45 +02:00
|
|
|
|
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'
|
2024-05-01 19:30:45 +02:00
|
|
|
|
|
|
|
|
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,
|
2024-12-07 11:58:58 +01:00
|
|
|
): Promise<{ routeNodes: Array<RouteNode>; rustHandlersNodes: Array<string> }> {
|
|
|
|
|
const routeNodes: Array<RouteNode> = []
|
|
|
|
|
const rustHandlersNodes: Array<string> = []
|
2024-05-01 19:30:45 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2024-05-01 19:30:45 +02:00
|
|
|
const filePath = replaceBackslash(path.join(dir, dirent.name))
|
|
|
|
|
const filePathNoExt = removeExt(filePath)
|
2024-12-10 19:30:35 +01:00
|
|
|
let routePath = cleanPath(`/${filePathNoExt}`) || ''
|
2024-05-01 19:30:45 +02:00
|
|
|
|
|
|
|
|
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)
|
2024-05-01 19:30:45 +02:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await recurse('./')
|
|
|
|
|
|
2024-05-11 15:27:51 +02:00
|
|
|
return { routeNodes, rustHandlersNodes }
|
2024-05-01 19:30:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-05-01 19:30:45 +02:00
|
|
|
|
2024-07-23 19:13:18 +02:00
|
|
|
const preRouteNodes = sortRouteNodes(beforeRouteNodes)
|
2024-05-01 19:30:45 +02:00
|
|
|
|
2024-12-07 11:58:58 +01:00
|
|
|
const routeNodes: Array<RouteNode> = []
|
2024-05-01 19:30:45 +02:00
|
|
|
|
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 => {
|
2024-05-01 19:30:45 +02:00
|
|
|
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) ?? ''),
|
2024-05-01 19:30:45 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
2024-05-01 19:30:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
2024-12-22 12:08:21 +01:00
|
|
|
(d): RouteNode => d,
|
2024-05-01 19:30:45 +02:00
|
|
|
])
|
|
|
|
|
|
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' : ''
|
2024-06-20 19:40:14 +02:00
|
|
|
return `const ${
|
2024-12-22 12:08:21 +01:00
|
|
|
node.variableName as string
|
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' : ''
|
2024-12-22 12:08:21 +01:00
|
|
|
const variableName = node.variableName as string
|
2024-07-23 19:13:18 +02:00
|
|
|
|
2024-12-22 12:08:21 +01:00
|
|
|
return `const ${variableName} = createRoute({ component: ${variableName}Import${rootDeclaration} })`
|
2024-05-11 15:08:49 +02:00
|
|
|
}),
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
2024-05-11 15:27:51 +02:00
|
|
|
const createRouteUpdates = [
|
2024-05-01 19:30:45 +02:00
|
|
|
sortedRouteNodes
|
|
|
|
|
.map((node) => {
|
2024-12-22 12:08:21 +01:00
|
|
|
const variableName = node.variableName as string
|
|
|
|
|
const cleanedPath = node.cleanedPath as string
|
2024-05-01 19:30:45 +02:00
|
|
|
return [
|
2024-12-22 12:08:21 +01:00
|
|
|
`const ${variableName}Route = ${variableName}.update({
|
2024-05-01 19:30:45 +02:00
|
|
|
${[
|
2024-12-22 12:08:21 +01:00
|
|
|
!node.path?.endsWith(ROOT_PATH_ID) && `path: '${cleanedPath}'`,
|
2024-05-01 19:30:45 +02:00
|
|
|
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
|
2024-05-11 15:27:51 +02:00
|
|
|
rustHandlersNodes.includes(node.path || '')
|
|
|
|
|
? 'hasHandler: true'
|
|
|
|
|
: '',
|
2024-05-01 19:30:45 +02:00
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(',')}
|
2024-06-20 19:40:14 +02:00
|
|
|
})`,
|
2024-05-01 19:30:45 +02:00
|
|
|
].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,
|
2024-05-01 19:30:45 +02:00
|
|
|
'// 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-22 12:08:21 +01:00
|
|
|
.catch((e: unknown) => {
|
2024-12-01 10:07:35 +01:00
|
|
|
const err = e as Error & { code?: string }
|
2024-05-01 19:30:45 +02:00
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|