**What** - #9338 had a regression which caused the import path in some virtual modules to be invalid on Windows. - This PR fixes the issue so we now again create the correct import paths, and adds tests to prevent this from slipping in again.
164 lines
3.6 KiB
TypeScript
164 lines
3.6 KiB
TypeScript
import fs from "fs/promises"
|
|
import { outdent } from "outdent"
|
|
import { isIdentifier, isObjectProperty, parse, traverse } from "../babel"
|
|
import { logger } from "../logger"
|
|
import {
|
|
crawl,
|
|
getConfigObjectProperties,
|
|
getParserOptions,
|
|
normalizePath,
|
|
} from "../utils"
|
|
import { getRoute } from "./helpers"
|
|
|
|
type MenuItem = {
|
|
icon?: string
|
|
label: string
|
|
path: string
|
|
}
|
|
|
|
type MenuItemResult = {
|
|
import: string
|
|
menuItem: MenuItem
|
|
}
|
|
|
|
export async function generateMenuItems(sources: Set<string>) {
|
|
const files = await getFilesFromSources(sources)
|
|
const results = await getMenuItemResults(files)
|
|
|
|
const imports = results.map((result) => result.import).flat()
|
|
const code = generateCode(results)
|
|
|
|
return {
|
|
imports,
|
|
code,
|
|
}
|
|
}
|
|
|
|
function generateCode(results: MenuItemResult[]): string {
|
|
return outdent`
|
|
menuItems: [
|
|
${results
|
|
.map((result) => formatMenuItem(result.menuItem))
|
|
.join(",\n")}
|
|
]
|
|
}
|
|
`
|
|
}
|
|
|
|
function formatMenuItem(route: MenuItem): string {
|
|
return `{
|
|
label: ${route.label},
|
|
icon: ${route.icon ? route.icon : "undefined"},
|
|
path: "${route.path}",
|
|
}`
|
|
}
|
|
|
|
async function getFilesFromSources(sources: Set<string>): Promise<string[]> {
|
|
const files = (
|
|
await Promise.all(
|
|
Array.from(sources).map(async (source) =>
|
|
crawl(`${source}/routes`, "page", { min: 1 })
|
|
)
|
|
)
|
|
).flat()
|
|
return files
|
|
}
|
|
|
|
async function getMenuItemResults(files: string[]): Promise<MenuItemResult[]> {
|
|
const results = await Promise.all(files.map(parseFile))
|
|
return results.filter((item): item is MenuItemResult => item !== null)
|
|
}
|
|
|
|
async function parseFile(
|
|
file: string,
|
|
index: number
|
|
): Promise<MenuItemResult | null> {
|
|
const config = await getRouteConfig(file)
|
|
|
|
if (!config) {
|
|
return null
|
|
}
|
|
|
|
if (!config.label) {
|
|
logger.warn(`Config is missing a label.`, {
|
|
file,
|
|
})
|
|
}
|
|
|
|
const import_ = generateImport(file, index)
|
|
const menuItem = generateMenuItem(config, file, index)
|
|
|
|
return {
|
|
import: import_,
|
|
menuItem,
|
|
}
|
|
}
|
|
|
|
function generateImport(file: string, index: number): string {
|
|
const path = normalizePath(file)
|
|
return `import { config as ${generateRouteConfigName(index)} } from "${path}"`
|
|
}
|
|
|
|
function generateMenuItem(
|
|
config: { label: boolean; icon: boolean },
|
|
file: string,
|
|
index: number
|
|
): MenuItem {
|
|
const configName = generateRouteConfigName(index)
|
|
const routePath = getRoute(file)
|
|
|
|
return {
|
|
label: `${configName}.label`,
|
|
icon: config.icon ? `${configName}.icon` : undefined,
|
|
path: routePath,
|
|
}
|
|
}
|
|
|
|
async function getRouteConfig(
|
|
file: string
|
|
): Promise<{ label: boolean; icon: boolean } | null> {
|
|
const code = await fs.readFile(file, "utf-8")
|
|
const ast = parse(code, getParserOptions(file))
|
|
|
|
let config: { label: boolean; icon: boolean } | null = null
|
|
|
|
try {
|
|
traverse(ast, {
|
|
ExportNamedDeclaration(path) {
|
|
const properties = getConfigObjectProperties(path)
|
|
|
|
if (!properties) {
|
|
return
|
|
}
|
|
|
|
const hasLabel = properties.some(
|
|
(prop) =>
|
|
isObjectProperty(prop) && isIdentifier(prop.key, { name: "label" })
|
|
)
|
|
|
|
if (!hasLabel) {
|
|
return
|
|
}
|
|
|
|
const hasIcon = properties.some(
|
|
(prop) =>
|
|
isObjectProperty(prop) && isIdentifier(prop.key, { name: "icon" })
|
|
)
|
|
|
|
config = { label: hasLabel, icon: hasIcon }
|
|
},
|
|
})
|
|
} catch (e) {
|
|
logger.error(`An error occurred while traversing the file.`, {
|
|
file,
|
|
error: e,
|
|
})
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
function generateRouteConfigName(index: number): string {
|
|
return `RouteConfig${index}`
|
|
}
|