mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-24 23:05:05 +00:00
- Created a new documentation site under `apps/docs` using Vite, React, and MDX. - Added essential files including `index.html`, `package.json`, and various configuration files. - Implemented a file-system based routing system with automatic route generation from markdown files. - Included support for syntax highlighting, responsive design, and TypeScript. - Established a structure for documentation content with initial deployment guides and storage provider information. Signed-off-by: Innei <tukon479@gmail.com>
259 lines
6.6 KiB
TypeScript
259 lines
6.6 KiB
TypeScript
import fs from 'node:fs/promises'
|
||
import path from 'node:path'
|
||
|
||
import { glob } from 'glob'
|
||
import type { Plugin } from 'vite'
|
||
|
||
interface RouteConfig {
|
||
path: string
|
||
component: string
|
||
title?: string
|
||
meta?: Record<string, unknown>
|
||
}
|
||
|
||
interface GenerateRoutesOptions {
|
||
contentsDir?: string
|
||
outputDir?: string
|
||
outputFile?: string
|
||
jsonFile?: string
|
||
indexFile?: string
|
||
}
|
||
|
||
const defaultOptions: Required<GenerateRoutesOptions> = {
|
||
contentsDir: 'contents',
|
||
outputDir: 'src',
|
||
outputFile: 'routes.ts',
|
||
jsonFile: 'routes.json',
|
||
indexFile: 'index',
|
||
}
|
||
|
||
/**
|
||
* 约定式路由生成器插件
|
||
*
|
||
* 根据 contents 目录下的文件自动生成路由配置
|
||
*
|
||
* 约定规则:
|
||
* - contents/index.md -> / (首页)
|
||
* - contents/guide.md -> /guide
|
||
* - contents/api/index.md -> /api (目录首页)
|
||
* - contents/api/auth.md -> /api/auth
|
||
* - contents/404.md -> /404 (404页面)
|
||
*/
|
||
export function routeGenerator(options: GenerateRoutesOptions = {}): Plugin {
|
||
const opts = { ...defaultOptions, ...options }
|
||
|
||
return {
|
||
name: 'route-generator',
|
||
enforce: 'pre',
|
||
|
||
async buildStart() {
|
||
await generateRoutes(opts)
|
||
},
|
||
|
||
async handleHotUpdate({ file, server }) {
|
||
// 监听 contents 目录文件变化,重新生成路由
|
||
if (file.includes(path.resolve(opts.contentsDir))) {
|
||
await generateRoutes(opts)
|
||
server.ws.send({
|
||
type: 'full-reload',
|
||
})
|
||
}
|
||
},
|
||
|
||
configureServer(server) {
|
||
// 开发模式下监听文件变化
|
||
server.middlewares.use('/__generate-routes', async (_req, res) => {
|
||
try {
|
||
await generateRoutes(opts)
|
||
res.end('Routes generated successfully')
|
||
} catch (error) {
|
||
res.statusCode = 500
|
||
res.end(`Error generating routes: ${error}`)
|
||
}
|
||
})
|
||
},
|
||
}
|
||
}
|
||
|
||
async function generateRoutes(options: Required<GenerateRoutesOptions>) {
|
||
const { contentsDir, outputDir, outputFile, jsonFile, indexFile } = options
|
||
|
||
try {
|
||
// 获取所有 markdown 文件
|
||
const pattern = path.join(contentsDir, '**/*.{md,mdx}')
|
||
const files = await glob(pattern, {
|
||
ignore: ['**/node_modules/**'],
|
||
absolute: false,
|
||
})
|
||
|
||
const routes: RouteConfig[] = []
|
||
|
||
for (const file of files) {
|
||
const route = await generateRouteFromFile(file, contentsDir, indexFile)
|
||
if (route) {
|
||
routes.push(route)
|
||
}
|
||
}
|
||
|
||
// 按路径排序,确保首页在最前面
|
||
routes.sort((a, b) => {
|
||
if (a.path === '/') return -1
|
||
if (b.path === '/') return 1
|
||
return a.path.localeCompare(b.path)
|
||
})
|
||
|
||
// 生成路由文件内容
|
||
const routeFileContent = generateRouteFileContent(routes)
|
||
|
||
// 确保输出目录存在
|
||
await fs.mkdir(outputDir, { recursive: true })
|
||
|
||
// 写入路由文件
|
||
const outputPath = path.join(outputDir, outputFile)
|
||
await fs.writeFile(outputPath, routeFileContent, 'utf-8')
|
||
|
||
// 生成 JSON 文件(不包含 component)
|
||
const routesJson = generateRoutesJson(routes)
|
||
const jsonPath = path.join(outputDir, jsonFile)
|
||
await fs.writeFile(jsonPath, routesJson, 'utf-8')
|
||
|
||
console.info(`✓ Generated ${routes.length} routes to ${outputPath}`)
|
||
console.info(`✓ Generated routes JSON to ${jsonPath}`)
|
||
} catch (error) {
|
||
console.error('Error generating routes:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
async function generateRouteFromFile(
|
||
file: string,
|
||
contentsDir: string,
|
||
indexFile: string,
|
||
): Promise<RouteConfig | null> {
|
||
try {
|
||
// 移除 contents 前缀和文件扩展名
|
||
let routePath = file.replace(new RegExp(`^${contentsDir}/`), '').replace(/\.(md|mdx)$/, '')
|
||
|
||
// 处理 index 文件
|
||
if (routePath === indexFile) {
|
||
routePath = '/'
|
||
} else if (routePath.endsWith(`/${indexFile}`)) {
|
||
const basePath = routePath.replace(`/${indexFile}`, '')
|
||
routePath = basePath ? `/${basePath}` : '/'
|
||
} else {
|
||
routePath = `/${routePath}`
|
||
}
|
||
|
||
// 生成组件导入路径,保留文件后缀
|
||
const componentPath = `../${file}`
|
||
|
||
// 读取文件内容获取元数据
|
||
const fileContent = await fs.readFile(file, 'utf-8')
|
||
const meta = extractFrontmatter(fileContent)
|
||
|
||
return {
|
||
path: routePath,
|
||
component: componentPath,
|
||
title: (typeof meta.title === 'string' ? meta.title : undefined) || generateTitleFromPath(routePath),
|
||
meta,
|
||
}
|
||
} catch (error) {
|
||
console.warn(`Warning: Could not process file ${file}:`, error)
|
||
return null
|
||
}
|
||
}
|
||
|
||
function extractFrontmatter(content: string): Record<string, unknown> {
|
||
const frontmatterRegex = /^---\n([\s\S]*?)\n---(?:\n|$)/
|
||
const match = content.match(frontmatterRegex)
|
||
|
||
if (!match) {
|
||
return {}
|
||
}
|
||
|
||
try {
|
||
// 简单的 YAML 解析(仅支持基本的 key: value 格式)
|
||
const yamlContent = match[1]
|
||
const meta: Record<string, unknown> = {}
|
||
|
||
yamlContent.split('\n').forEach((line) => {
|
||
const colonIndex = line.indexOf(':')
|
||
if (colonIndex > 0) {
|
||
const key = line.slice(0, colonIndex).trim()
|
||
const value = line
|
||
.slice(colonIndex + 1)
|
||
.trim()
|
||
.replaceAll(/^["']|["']$/g, '')
|
||
meta[key] = value
|
||
}
|
||
})
|
||
|
||
return meta
|
||
} catch (error) {
|
||
console.warn('Failed to parse frontmatter:', error)
|
||
return {}
|
||
}
|
||
}
|
||
|
||
function generateTitleFromPath(path: string): string {
|
||
if (path === '/') return 'Home'
|
||
|
||
return path
|
||
.split('/')
|
||
.filter(Boolean)
|
||
.map((segment) =>
|
||
segment
|
||
.split('-')
|
||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||
.join(' '),
|
||
)
|
||
.join(' - ')
|
||
}
|
||
|
||
function generateRouteFileContent(routes: RouteConfig[]): string {
|
||
const imports = routes.map((route, index) => `import Route${index} from '${route.component}'`).join('\n')
|
||
|
||
const routeObjects = routes
|
||
.map(
|
||
(route, index) => ` {
|
||
path: '${route.path}',
|
||
component: Route${index},
|
||
title: '${route.title}',
|
||
meta: ${JSON.stringify(route.meta, null, 4).replaceAll('\n', '\n ')}
|
||
}`,
|
||
)
|
||
.join(',\n')
|
||
|
||
return `// Auto-generated route configuration
|
||
// This file is automatically generated by the route-generator plugin
|
||
// Do not edit manually - your changes will be overwritten
|
||
|
||
${imports}
|
||
|
||
export interface RouteConfig {
|
||
path: string
|
||
component: React.ComponentType
|
||
title: string
|
||
meta: Record<string, unknown>
|
||
}
|
||
|
||
export const routes: RouteConfig[] = [
|
||
${routeObjects}
|
||
]
|
||
|
||
export default routes
|
||
`
|
||
}
|
||
|
||
function generateRoutesJson(routes: RouteConfig[]): string {
|
||
const routesData = routes.map((route) => ({
|
||
path: route.path,
|
||
title: route.title,
|
||
meta: route.meta,
|
||
}))
|
||
|
||
return JSON.stringify(routesData, null, 2)
|
||
}
|
||
|
||
export default routeGenerator
|