UNPKG

antd-doc-gen

Version:

CLI tool to generate docs from any Ant Design style component library

508 lines (438 loc) 15.9 kB
const fs = require("fs"); const path = require("path"); const glob = require("glob"); const readline = require("readline"); const chalk = require("chalk"); // 创建readline接口用于用户交互 let rl; function createReadlineInterface() { if (!rl) { rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); } return rl; } // 检查目录是否存在并处理 function checkOutputDirExists(outputDir, autoConfirm = false) { return new Promise((resolve) => { if (fs.existsSync(outputDir)) { if (autoConfirm) { console.log(chalk.yellow(`目录 ${outputDir} 已存在,将自动覆盖...`)); fs.rmSync(outputDir, { recursive: true, force: true }); ensureDirectoryExists(outputDir); resolve(true); return; } const rl = createReadlineInterface(); rl.question( chalk.yellow(`目录 ${outputDir} 已存在,是否覆盖? (y/n): `), (answer) => { if (answer.toLowerCase() === "y") { console.log(chalk.yellow(`将覆盖 ${outputDir} 中的文件`)); fs.rmSync(outputDir, { recursive: true, force: true }); ensureDirectoryExists(outputDir); resolve(true); } else { console.log(chalk.blue("操作已取消")); resolve(false); } } ); } else { ensureDirectoryExists(outputDir); resolve(true); } }); } /** * 使用 fs 模块查找文件 * @param {string} basePath - 基础路径 * @param {string} componentDir - 组件目录路径(相对于basePath) * @param {string} docPattern - 文档文件名模式(例如 "index.zh-CN.md") * @param {Function} filterFn - 过滤函数,用于过滤组件目录 * @returns {string[]} - 找到的文档文件路径数组 */ function findDocsByFs(basePath, componentDir, docPattern, filterFn = null) { try { const fullComponentDir = path.join(basePath, componentDir); if (!fs.existsSync(fullComponentDir)) { console.log(chalk.red(`组件目录不存在: ${fullComponentDir}`)); return []; } // 获取组件目录下的所有子目录 const componentDirs = fs.readdirSync(fullComponentDir).filter((dir) => { const dirPath = path.join(fullComponentDir, dir); return fs.statSync(dirPath).isDirectory() && (!filterFn || filterFn(dir)); }); // 查找每个组件目录下的文档文件 const docs = componentDirs .map((dir) => { const docPath = path.join(componentDir, dir, docPattern); return fs.existsSync(path.join(basePath, docPath)) ? docPath : null; }) .filter(Boolean); console.log(chalk.yellow(`使用 fs 模块找到的文件数量: ${docs.length}`)); if (docs.length > 0) { console.log(chalk.yellow(`示例文件: ${docs.slice(0, 5).join(", ")}`)); } return docs; } catch (error) { console.error(chalk.red(`查找文件时出错: ${error.message}`)); return []; } } // 查找所有组件目录下的中文文档 function findComponentDocs(basePath, repoUrl = "") { // 规范化仓库地址,去掉可能的 .git 后缀 const normalizedRepoUrl = repoUrl.replace(/\.git$/, ""); // 默认过滤函数,排除 overview 和 _util 目录 const defaultFilter = (dir) => dir !== "overview" && dir !== "_util"; // 根据仓库地址确定查找路径和文件模式 if (normalizedRepoUrl.includes("/ant-design/ant-design-mini")) { // 3. ant-design-mini console.log(chalk.blue(`使用 ant-design-mini 模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); return findDocsByFs(basePath, "src", "index.md", defaultFilter); } else if (normalizedRepoUrl.includes("/ant-design/ant-design-mobile")) { // 2. ant-design-mobile console.log(chalk.blue(`使用 ant-design-mobile 模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); return findDocsByFs( basePath, "src/components", "index.zh.md", defaultFilter ); } else if (normalizedRepoUrl.includes("/ant-design/ant-design-web3")) { // 4. ant-design-web3 console.log(chalk.blue(`使用 ant-design-web3 模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); return findDocsByFs( basePath, "packages/web3/src", "index.zh-CN.md", defaultFilter ); } else if (normalizedRepoUrl.includes("/ant-design/x")) { // 5. ant-design/x console.log(chalk.blue(`使用 ant-design/x 模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); return findDocsByFs( basePath, "components", "index.zh-CN.md", defaultFilter ); } else if (normalizedRepoUrl.includes("/ant-design/ant-design")) { // 1. ant-design 主库 console.log(chalk.blue(`使用 ant-design 主库模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); return findDocsByFs( basePath, "components", "index.zh-CN.md", defaultFilter ); } else { // 默认情况,尝试多种常见路径 console.log(chalk.blue(`使用默认模式查找文档`)); console.log(chalk.yellow(`basePath: ${basePath}`)); // 尝试多种常见路径,按优先级顺序 const searchPaths = [ { dir: "components", pattern: "index.zh-CN.md" }, { dir: "components", pattern: "index.zh.md" }, { dir: "components", pattern: "index.md" }, { dir: "src/components", pattern: "index.zh-CN.md" }, { dir: "src/components", pattern: "index.zh.md" }, { dir: "src/components", pattern: "index.md" }, { dir: "src", pattern: "index.md" }, ]; // 依次尝试每种路径 for (const { dir, pattern } of searchPaths) { const docs = findDocsByFs(basePath, dir, pattern, defaultFilter); if (docs.length > 0) { return docs; } } // 如果所有路径都没找到文档,尝试使用 glob 作为后备方案 console.log( chalk.yellow(`使用 fs 模块未找到文档,尝试使用 glob 模式作为后备方案`) ); const patterns = [ "components/*/index.zh-CN.md", "components/*/index.zh.md", "components/*/index.md", "src/components/*/index.zh-CN.md", "src/components/*/index.zh.md", "src/components/*/index.md", "src/*/index.md", ]; let allDocs = []; patterns.forEach((pattern) => { const docs = glob.sync(pattern, { cwd: basePath }); docs.forEach((doc) => { if (!allDocs.includes(doc)) { allDocs.push(doc); } }); }); console.log(chalk.yellow(`使用 glob 找到的文件数量: ${allDocs.length}`)); if (allDocs.length > 0) { console.log(chalk.yellow(`示例文件: ${allDocs.slice(0, 5).join(", ")}`)); } // 过滤掉 overview 和 _util 文件夹下的文档 return allDocs.filter((docPath) => { const pathParts = docPath.split(path.sep); const componentIndex = pathParts.findIndex( (part) => part === "components" || part === "src" ); if (componentIndex !== -1 && componentIndex + 1 < pathParts.length) { const componentName = pathParts[componentIndex + 1]; return componentName !== "overview" && componentName !== "_util"; } return true; }); } } // 解析文档中的demo引用 function parseDocContent(content) { // 修改正则表达式以匹配任意路径的 demo const regex = /<code\s+src="([^"]+)"[^>]*>(.*?)<\/code>/g; const matches = []; let match; while ((match = regex.exec(content)) !== null) { matches.push({ full: match[0], demoPath: match[1], // 直接使用完整的路径 description: match[2], }); } return matches; } // 读取文件内容 function readFile(filePath, silent = false) { try { return fs.readFileSync(filePath, "utf8"); } catch (error) { if (!silent) { console.error(chalk.red(`Error reading file ${filePath}:`), error); } return null; } } // 确保目录存在 function ensureDirectoryExists(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } // 格式化组件名称,例如 input -> Input, auto-complete -> AutoComplete function formatComponentName(name) { return name .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(""); } // 处理demo路径,自动补充后缀 function normalizeDemoPath(demoPath) { // 如果路径已经有扩展名,直接返回 if (path.extname(demoPath)) { return demoPath; } // 如果没有扩展名,返回带 .tsx 后缀的路径,实际文件检查在 mergeDemoFiles 中进行 return `${demoPath}.tsx`; } // 整合demo文件和md文件 function mergeDemoFiles(docFilePath, basePath, outputDir, repoUrl = "") { const docDir = path.dirname(docFilePath); const docContent = readFile(path.join(basePath, docFilePath)); if (!docContent) return; const demoRefs = parseDocContent(docContent); let updatedContent = docContent; // 如果有 demo 引用,则处理它们 if (demoRefs.length > 0) { for (const ref of demoRefs) { // 先尝试 .tsx 后缀 let normalizedDemoPath = normalizeDemoPath(ref.demoPath); let tsxPath = path.join(basePath, docDir, normalizedDemoPath); let tsxContent = readFile(tsxPath, true); // 静默读取,不报错 // 如果找不到 .tsx 文件,尝试 .ts 后缀 if (!tsxContent && !path.extname(ref.demoPath)) { normalizedDemoPath = `${ref.demoPath}.ts`; tsxPath = path.join(basePath, docDir, normalizedDemoPath); tsxContent = readFile(tsxPath, true); } // 如果两种后缀都找不到,报错并继续处理下一个 if (!tsxContent) { console.warn( chalk.yellow( `Could not read file for demo: ${ref.demoPath} (tried both .tsx and .ts)` ) ); continue; } // 查找对应的 md 文件 const mdPath = tsxPath.replace(/\.(tsx|ts)$/, ".md"); const mdContent = readFile(mdPath, true); // 创建整合后的内容 let mergedContent = `## ${ref.description}\n\n`; // 如果有md文件就添加内容 if (mdContent) { mergedContent += `${mdContent}\n\n`; } mergedContent += "```tsx\n"; mergedContent += tsxContent; mergedContent += "\n```\n"; // 替换原来的引用 updatedContent = updatedContent.replace(ref.full, mergedContent); } } // 不再显示没有 demo 引用的警告,直接使用原始文档内容 // 计算新的输出路径 let componentName; let outputFilePath; // 根据不同仓库类型提取组件名 if (repoUrl.includes("/ant-design/ant-design-mini")) { // 对于 ant-design-mini,组件名就是 src 下的一级目录名 const pathParts = docFilePath.split(path.sep); const srcIndex = pathParts.indexOf("src"); if (srcIndex !== -1 && srcIndex + 1 < pathParts.length) { componentName = formatComponentName(pathParts[srcIndex + 1]); outputFilePath = path.join( outputDir, "components", `${componentName}.md` ); } } else if (repoUrl.includes("/ant-design/ant-design-web3")) { // 对于 ant-design-web3,组件名在 packages/web3/src/ 下的一级目录 const pathParts = docFilePath.split(path.sep); const srcIndex = pathParts.indexOf("src"); if (srcIndex !== -1 && srcIndex + 1 < pathParts.length) { componentName = formatComponentName(pathParts[srcIndex + 1]); outputFilePath = path.join( outputDir, "components", `${componentName}.md` ); } } else { // 通用逻辑,尝试从路径中提取组件名 const pathParts = docFilePath.split(path.sep); // 尝试找到 components 目录 const componentIndex = pathParts.indexOf("components"); if (componentIndex !== -1 && componentIndex + 1 < pathParts.length) { componentName = formatComponentName(pathParts[componentIndex + 1]); } else { // 如果没有 components 目录,尝试从路径中提取组件名 // 假设组件名是文档路径的倒数第二个部分 componentName = formatComponentName(pathParts[pathParts.length - 2]); } if (componentName) { outputFilePath = path.join( outputDir, "components", `${componentName}.md` ); } } if (outputFilePath) { const outputDirPath = path.dirname(outputFilePath); // 确保输出目录存在 ensureDirectoryExists(outputDirPath); // 写入到新文件 try { fs.writeFileSync(outputFilePath, updatedContent); console.log(chalk.green(`Successfully generated ${outputFilePath}`)); } catch (error) { console.error(chalk.red(`Error writing to ${outputFilePath}:`), error); } } else { console.warn( chalk.yellow(`Could not determine component name for ${docFilePath}`) ); } } // 生成组件导航索引文件 function generateIndexFile(docPaths, outputDir) { const componentsDir = path.join(outputDir, "components"); // 检查组件目录是否存在 if (!fs.existsSync(componentsDir)) { console.log(chalk.yellow(`组件目录不存在: ${componentsDir}`)); return; } // 读取组件目录中的所有 md 文件 const componentFiles = fs .readdirSync(componentsDir) .filter((file) => file.endsWith(".md")) .map((file) => ({ name: path.basename(file, ".md"), path: `./components/${file}`, })); if (componentFiles.length === 0) { console.log(chalk.yellow(`组件目录中没有找到 Markdown 文件`)); return; } // 按字母顺序排序组件 componentFiles.sort((a, b) => a.name.localeCompare(b.name)); // 生成索引内容 let indexContent = `# 组件文档索引\n\n`; componentFiles.forEach((component) => { indexContent += `- [${component.name}](${component.path})\n`; }); // 写入索引文件 const indexPath = path.join(outputDir, "index.md"); fs.writeFileSync(indexPath, indexContent); console.log(chalk.green(`Generated index file at ${indexPath}`)); } // 导出主函数 async function mergeDocFiles(options) { const { projectPath, outputPath, autoConfirm = false, repoUrl = "", docPaths = undefined, } = options; // 检查输出目录 const shouldContinue = await checkOutputDirExists(outputPath, autoConfirm); if (!shouldContinue) { if (rl) rl.close(); return false; } // 如果指定了文档路径,直接使用;否则查找所有组件文档 const finalDocPaths = docPaths || findComponentDocs(projectPath, repoUrl); if (!finalDocPaths || finalDocPaths.length === 0) { console.log( chalk.red("未找到任何组件文档,请确认项目路径或指定的文档路径是否正确。") ); if (rl) rl.close(); return false; } // 处理文档路径,确保是相对于项目路径的 const processedDocPaths = finalDocPaths.map((docPath) => { if (path.isAbsolute(docPath)) { // 如果是绝对路径,转换为相对于项目路径的路径 const relativePath = path.relative(projectPath, docPath); return relativePath; } return docPath; }); console.log(chalk.blue(`找到 ${processedDocPaths.length} 个组件文档文件。`)); for (const docPath of processedDocPaths) { console.log(chalk.blue(`Processing ${docPath}...`)); mergeDemoFiles(docPath, projectPath, outputPath, repoUrl); } // 生成索引文件 generateIndexFile(processedDocPaths, outputPath); console.log( chalk.green( `Finished processing all files. Merged docs are in ${outputPath}` ) ); if (rl) rl.close(); return true; } module.exports = { mergeDocFiles };