
Component Docs Batcher
- 13 installs
- 1 repo stars
- Updated July 28, 2026
- evanfang0054/cc-system-creator-scripts
Helps with ai & agent building tasks.
About
component-docs-batcher is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
- component-docs-batcher
- AI & Agent Building
- AI-coding skill
Component Docs Batcher by the numbers
- 13 all-time installs (skills.sh)
- Ranked #11,409 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/evanfang0054/cc-system-creator-scripts --skill component-docs-batcherAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 28, 2026 |
| Repository | evanfang0054/cc-system-creator-scripts ↗ |
What it does
Helps with ai & agent building tasks.
Files
Component Docs Batcher
概述
Component Docs Batcher 是一个自动化批量生成和维护前端组件库文档的工具。专为 Monorepo 架构的组件库设计,通过智能扫描主要组件文件,对比文档更新状态,生成规范化的组件文档,确保文档与代码保持同步。
核心功能
- 智能组件识别: 自动识别组件目录中的主要组件文件(如 button/button.tsx),跳过 index.ts、store、hooks 等辅助文件
- Git时间对比: 通过对比组件文件和文档的git最后提交时间,确定文档状态(比文件系统时间更准确)
- 任务清单生成: 自动生成结构化的todos.md任务清单
- 批量文档生成: 按照规范批量生成完整的组件文档
- 组件库优化: 针对 Monorepo 组件库结构优化,文档位于组件目录下的 index.zh-CN.md
工作流程
步骤1: 扫描组件
使用 scan-components.ts 脚本扫描组件库,识别需要处理的组件:
# 扫描指定组件库目录
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/packages/atom-ui-mobile/src/components
# 输出JSON格式(用于程序化处理)
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/components --json组件识别规则:
- 扫描组件目录下的一级子目录(如
button/,alert/) - 识别主要组件文件:与目录名同名的
.tsx或.jsx文件(如button/button.tsx) - 自动跳过辅助文件:
index.ts,*.store.ts,use*.ts等 - 自动跳过辅助目录:
styles/,__tests__/等
扫描结果分类:
- ❌ 文档缺失: 组件目录下没有
index.zh-CN.md文档 - ⚠️ 文档过时: 组件文件的git最后提交时间晚于文档
- ✅ 文档最新: 组件和文档的git最后提交时间一致(不输出)
时间对比说明:
- 使用
git log -1 --format=%ct获取文件的最后提交时间 - 如果文件没有git历史(新文件),则使用文件系统时间作为备选
- git时间比文件系统修改时间更准确,不受checkout、pull等操作影响
步骤2: 生成任务清单
使用 generate-todos.ts 生成详细的任务清单:
# 在当前目录生成todos.md
npx ts-node .claude/skills/component-docs-batcher/scripts/generate-todos.ts
# 在指定目录生成
npx ts-node .claude/skills/component-docs-batcher/scripts/generate-todos.ts /path/to/components /output/path/todos.md生成的 todos.md 包含:
- 📊 统计信息(新增/更新数量)
- ✅ 任务列表(每个组件的详细任务)
- 📝 使用说明
- 📚 文档规范参考
步骤3: 分析组件结构
对于每个需要处理的组件,使用 analyze-component.ts 分析组件结构:
# 分析单个组件
npx ts-node .claude/skills/component-docs-batcher/scripts/analyze-component.ts /path/to/Component.tsx
# 输出JSON格式
npx ts-node .claude/skills/component-docs-batcher/scripts/analyze-component.ts /path/to/Component.tsx --json分析内容包括:
- 组件类型(component/hook/util/type)
- Props接口(属性名、类型、默认值、必填性)
- TypeScript类型定义
- 导入依赖
- 代码示例(从注释提取)
步骤4: 批量生成文档
根据组件分析结果,为每个组件生成符合规范的文档:
1. 读取组件源码: 使用 Read 工具读取完整的组件文件 2. 理解组件逻辑: 不要依赖代码注释,独立理解组件实现 3. 提取关键信息:
- Props接口及其所有属性
- TypeScript类型定义
- 组件的使用场景和交互逻辑
- 依赖的第三方组件API(如 Ant Design Mobile)
4. 确定正确的导出名称: ⚠️ 重要! 在生成文档前,必须检查 /packages/atom-ui-mobile/src/index.ts 确认组件的实际导出名称
- 例如:
qr-code组件导出为QrCode(不是QRCode) - 例如:
message组件导出为小写的message - 例如:
time-select直接导出为TimeSelect(没有default as)
5. 生成文档: 按照 文档格式规范 生成完整文档 6. 质量验证: 对照质量检查清单验证文档完整性
重要规则:
- 文档路径:组件目录下的
index.zh-CN.md(如button/index.zh-CN.md) - 独立理解组件,不复制代码注释
- 完整列出所有API,特别是继承的第三方组件API(如 Ant Design Mobile Button 的所有属性)
- 提供真实可用的代码示例
- 导出名称必须与 `src/index.ts` 中的定义完全一致(包括大小写)
- 引用方式示例:
import { Button } from '@evanfang/atom-ui-mobile';
步骤5: 更新任务状态
完成每个组件文档后,在 todos.md 中标记任务为已完成:
1. [x] 读取组件源码
2. [x] 分析组件结构
3. [x] 创建/更新文档
4. [x] 验证文档格式
5. [x] 检查文档完整性所有任务完成后,删除 todos.md 文件。
文档格式规范
所有组件文档必须遵循标准格式。详见 文档格式规范。
关键要求:
- 完整的API表格(属性名、类型、说明、默认值)
- TypeScript类型定义表格
- 真实可用的代码示例
- 主题变量列表或"无"的明确说明
使用场景
场景1: 定期批量更新
在项目迭代过程中,定期执行批量文档更新:
# 1. 扫描组件库
cd /Users/arwen/Desktop/Arwen/Demo/cc-system
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts \
/Users/arwen/Desktop/Arwen/evanfang/dp-design/packages/atom-ui-mobile/src/components
# 2. 生成任务清单
npx ts-node .claude/skills/component-docs-batcher/scripts/generate-todos.ts \
/Users/arwen/Desktop/Arwen/evanfang/dp-design/packages/atom-ui-mobile/src/components
# 3. 按照生成的todos.md批量处理每个组件
# 4. 完成后删除todos.md场景2: 单个组件维护
针对特定组件快速更新文档:
# 1. 分析组件结构
npx ts-node .claude/skills/component-docs-batcher/scripts/analyze-component.ts \
/Users/arwen/Desktop/Arwen/evanfang/dp-design/packages/atom-ui-mobile/src/components/button/button.tsx
# 2. 根据分析结果生成/更新文档
# 文档位置: button/index.zh-CN.md场景3: 新组件文档初始化
为新创建的组件快速生成文档:
# 1. 检查组件是否需要文档
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts \
/Users/arwen/Desktop/Arwen/evanfang/dp-design/packages/atom-ui-mobile/src/components
# 2. 新组件会在"文档缺失"列表中显示
# 3. 为新组件生成 index.zh-CN.md 文档资源
scripts/
可执行的TypeScript脚本,提供扫描、分析和文档生成功能。
scan-components.ts: 组件文件扫描器
- 扫描指定目录下的组件文件
- 对比组件和文档的修改时间
- 输出需要更新或新增的组件列表
- 支持JSON格式输出
generate-todos.ts: 任务清单生成器
- 基于扫描结果生成todos.md
- 包含详细的任务步骤和统计信息
- 提供文档规范引用
analyze-component.ts: 组件结构分析器
- 分析组件源代码结构
- 提取Props、类型、依赖等信息
- 输出结构化的组件分析结果
references/
文档和参考资料,指导文档生成过程。
docs-format.md: 组件文档格式规范
- 标准文档模板
- 编写规则和质量检查清单
- 不同组件类型的特殊要求
组件文档格式规范
本文档定义了所有组件文档必须遵循的标准格式。
⚠️ 重要:确定正确的导出名称
在编写文档前,必须先检查 /packages/atom-ui-mobile/src/index.ts 文件,确认组件的实际导出名称。导出名称可能与组件目录名不同!
常见导出名称示例:
qr-code目录 → 导出为QrCode(不是QRCode)message目录 → 导出为小写messagetime-select目录 → 导出为TimeSelectresult-page→ResultPagesearch-bar→SearchBar
引用方式必须使用实际导出名称:
// ✅ 正确 - 使用 index.ts 中定义的导出名称
import { QrCode, message, TimeSelect } from '@evanfang/atom-ui-mobile';
// ❌ 错误 - 不要猜测组件名称
import { QRCode, Message, TimeSelectPanel } from '@evanfang/atom-ui-mobile';文档命名规则
- 位置: 组件目录下
- 命名:
index.zh-CN.md - 示例:
components/button/index.zh-CN.md
标准文档模板
# 技术文档
## 引用方式
**注意:组件名称必须与 src/index.ts 中的导出名称一致**
import { ComponentName } from '@evanfang/atom-ui-mobile';
## 功能类型
[工具/组件]
## 功能名称
[组件/工具名称]
## 功能描述
[组件/工具功能描述,包含适用场景和核心特性]
## 何时使用
[说明组件/工具在什么情况下使用]
## 使用示例
[给出组件/工具的使用示例代码]
## API
[仅包含组件/工具 API,包含属性名、类型、说明、默认值,详细列出继承的第三方组件的所有属性。若存在多个组件/功能,需分开列出各组件/功能的 API]
| 属性名 | 类型 | 说明 | 默认值 |
|-------|------|-----|-------|
| {prop1} | {type1} | {description1} | {default1} |
| {prop2} | {type2} | {description2} | {default2} |
## 类型描述
[仅包含组件/工具类型描述,包含类型名、类型详情、必填、默认值、说明,不要重复列出类型内部的字段]
| 类型名 | 类型详情 | 必填 | 默认值 | 说明 |
|-------|------|-----|-------|------|
| {type1} | {typeDetail1} | {required1} | {default1} | {description1} |
| {type2} | {typeDetail2} | {required2} | {default2} | {description2} |
## 主题变量(可选)
若代码中无需要暴露的自定义主题变量,需在此说明。
| Token 名称 | 描述 | 类型 | 默认值 |
| ---------------- | -------- | ---------------- | ------ |
| {name1} | {description1} | (type1) | {default1} |
| {name2} | {description2} | (type2) | {default2} |编写规则
1. 独立理解组件: 不要直接复制代码注释,需独立分析组件实现逻辑 2. 完整的API文档: 必须列出所有Props属性及其类型、说明、默认值 3. 继承的API: 如果组件继承自第三方组件库(如Ant Design),必须完整列出所有继承的API,不能简化 4. 类型定义: 清晰描述所有自定义TypeScript类型 5. 主题变量: 如果组件支持主题定制,列出所有CSS变量;如果没有,明确说明"无" 6. 实用示例: 提供真实可用的代码示例,展示主要使用场景
不同组件类型的特殊要求
React组件
- 重点描述Props接口
- 列出所有事件处理器(onXxx)
- 说明组件的状态管理(如有)
- 提供完整的交互示例
Hooks
- 说明Hook的用途和返回值
- 详细描述参数类型和含义
- 提供使用场景示例
- 注意副作用和依赖项
工具函数
- 说明函数的输入输出
- 列出所有参数及其类型
- 提供使用示例和边界情况处理
- 说明算法复杂度(如相关)
类型定义
- 描述类型的用途
- 列出所有属性和可选性
- 提供类型使用示例
质量检查清单
- [ ] 文档路径与组件路径一致
- [ ] 引用方式正确
- [ ] 功能描述清晰完整
- [ ] API列表完整无遗漏
- [ ] 类型定义准确
- [ ] 使用示例可运行
- [ ] 主题变量明确(存在或说明无)
- [ ] 格式符合Markdown规范
/**
* 组件分析器
* 分析组件源代码,提取Props、类型、导入等信息
*/
import * as fs from 'fs';
import * as path from 'path';
interface PropInfo {
name: string;
type: string;
description?: string;
defaultValue?: string;
required?: boolean;
}
interface TypeInfo {
name: string;
properties: PropInfo[];
description?: string;
}
interface ComponentAnalysis {
name: string;
type: 'component' | 'hook' | 'util' | 'type';
description?: string;
imports: string[];
props: PropInfo[];
types: TypeInfo[];
examples: string[];
dependencies: string[];
}
/**
* 判断组件类型
*/
function detectComponentType(filePath: string, content: string): ComponentAnalysis['type'] {
const fileName = path.basename(filePath);
// Hook检测
if (/^use[A-Z]/.test(fileName) || content.includes('use') && content.includes('useState')) {
return 'hook';
}
// 工具函数检测
if (content.includes('export function') && !content.includes('React')) {
return 'util';
}
// 类型定义检测
if (fileName.includes('.d.ts') || content.includes('export interface') || content.includes('export type')) {
return 'type';
}
// 默认为组件
return 'component';
}
/**
* 提取导入语句
*/
function extractImports(content: string): string[] {
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
const imports: string[] = [];
let match;
while ((match = importRegex.exec(content)) !== null) {
if (!imports.includes(match[1])) {
imports.push(match[1]);
}
}
return imports;
}
/**
* 提取Props接口
*/
function extractPropsInterface(content: string): PropInfo[] {
const props: PropInfo[] = [];
// 查找Props接口定义
const interfaceRegex = /(?:interface|type)\s+(\w*Props?\w*)\s*(?:extends\s+(\w+))?\s*\{([^}]+)\}/g;
let match;
while ((match = interfaceRegex.exec(content)) !== null) {
const interfaceName = match[1];
const interfaceBody = match[3];
// 提取接口属性
const propertyRegex = /(\w+)\s*(\?)?:\s*([^;,=\n]+)(?:\s*=\s*([^;,]+))?(?:[;,])/g;
let propMatch;
while ((propMatch = propertyRegex.exec(interfaceBody)) !== null) {
props.push({
name: propMatch[1],
type: propMatch[3].trim(),
required: !propMatch[2], // 有?表示可选
defaultValue: propMatch[4]?.trim()
});
}
}
return props;
}
/**
* 提取类型定义
*/
function extractTypes(content: string): TypeInfo[] {
const types: TypeInfo[] = [];
// 查找所有接口和类型定义
const typeRegex = /(?:export\s+)?(?:interface|type)\s+(\w+)\s*(?:extends\s+(\w+))?\s*\{([^}]+)\}/g;
let match;
while ((match = typeRegex.exec(content)) !== null) {
const typeName = match[1];
const typeBody = match[3];
// 跳过Props类型(已在props中处理)
if (typeName.toLowerCase().includes('props')) {
continue;
}
const properties: PropInfo[] = [];
const propertyRegex = /(\w+)\s*(\?)?:\s*([^;,=\n]+)(?:\s*=\s*([^;,]+))?(?:[;,])/g;
let propMatch;
while ((propMatch = propertyRegex.exec(typeBody)) !== null) {
properties.push({
name: propMatch[1],
type: propMatch[3].trim(),
required: !propMatch[2]
});
}
types.push({
name: typeName,
properties
});
}
return types;
}
/**
* 提取代码示例(从注释中)
*/
function extractExamples(content: string): string[] {
const examples: string[] = [];
// 查找 @example 注释
const exampleRegex = /@example\s+([\s\S]*?)(?=@\w+|\*\/)/g;
let match;
while ((match = exampleRegex.exec(content)) !== null) {
examples.push(match[1].trim());
}
return examples;
}
/**
* 提取组件描述(从注释中)
*/
function extractDescription(content: string): string | undefined {
// 查找文件顶部的描述注释
const descRegex = /\/\*\*[\s\S]*?\* @(?:description|brief)\s+([^\n]+)[\s\S]*?\*\//;
const match = descRegex.exec(content);
return match?.[1]?.trim();
}
/**
* 分析组件文件
*/
function analyzeComponent(filePath: string): ComponentAnalysis {
const content = fs.readFileSync(filePath, 'utf-8');
const fileName = path.basename(filePath, path.extname(filePath));
const componentType = detectComponentType(filePath, content);
const imports = extractImports(content);
const props = extractPropsInterface(content);
const types = extractTypes(content);
const examples = extractExamples(content);
const description = extractDescription(content);
// 提取外部依赖(从import中过滤相对路径)
const dependencies = imports.filter(imp => !imp.startsWith('.'));
return {
name: fileName,
type: componentType,
description,
imports,
props,
types,
examples,
dependencies
};
}
/**
* 打印分析结果
*/
function printAnalysis(analysis: ComponentAnalysis): void {
console.log(`\n📦 组件分析结果:`);
console.log(` 名称: ${analysis.name}`);
console.log(` 类型: ${analysis.type}`);
if (analysis.description) {
console.log(` 描述: ${analysis.description}`);
}
console.log(`\n📥 导入依赖 (${analysis.imports.length}):`);
analysis.imports.forEach(imp => console.log(` - ${imp}`));
console.log(`\n📋 Props (${analysis.props.length}):`);
analysis.props.forEach(prop => {
console.log(` - ${prop.name}${prop.required ? '' : '?'}: ${prop.type}`);
if (prop.defaultValue) {
console.log(` 默认值: ${prop.defaultValue}`);
}
});
if (analysis.types.length > 0) {
console.log(`\n📝 类型定义 (${analysis.types.length}):`);
analysis.types.forEach(type => {
console.log(` - ${type.name} (${type.properties.length} 个属性)`);
});
}
if (analysis.examples.length > 0) {
console.log(`\n💡 示例代码:`);
analysis.examples.forEach((ex, i) => {
console.log(` 示例 ${i + 1}:`);
console.log(` ${ex.split('\n').join('\n ')}`);
});
}
}
// CLI 接口
if (require.main === module) {
const args = process.argv.slice(2);
const filePath = args[0];
if (!filePath) {
console.error('❌ 请提供组件文件路径');
console.log('用法: npx ts-node analyze-component.ts <component-file-path>');
process.exit(1);
}
if (!fs.existsSync(filePath)) {
console.error(`❌ 文件不存在: ${filePath}`);
process.exit(1);
}
try {
console.log(`🔍 分析组件: ${filePath}\n`);
const analysis = analyzeComponent(filePath);
printAnalysis(analysis);
// 输出JSON格式
if (args.includes('--json')) {
console.log('\n📄 JSON 输出:');
console.log(JSON.stringify(analysis, null, 2));
}
} catch (error) {
console.error('❌ 分析失败:', error);
process.exit(1);
}
}
export { analyzeComponent, ComponentAnalysis, PropInfo, TypeInfo };
/**
* TODO生成器
* 根据组件扫描结果生成todos.md任务清单
*/
import * as fs from 'fs';
import * as path from 'path';
import { scanComponents, ComponentInfo } from './scan-components';
interface TodoItem {
number: number;
componentPath: string;
docPath: string;
status: 'missing' | 'outdated';
completed: boolean;
}
/**
* 生成todos.md内容
*/
function generateTodosMarkdown(components: ComponentInfo[]): string {
const date = new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
const missing = components.filter(c => c.status === 'missing');
const outdated = components.filter(c => c.status === 'outdated');
let markdown = `# 组件文档更新任务清单
> 生成时间: ${date}
> 总计: ${components.length} 个任务(${missing.length} 个新增,${outdated.length} 个更新)
> 使用git提交历史进行时间对比
---
## 📋 统计信息
- **需要新增文档**: ${missing.length} 个组件
- **需要更新文档**: ${outdated.length} 个组件
- **总任务数**: ${components.length} 个
---
## ✅ 任务列表
`;
// 生成任务列表
components.forEach((comp, index) => {
const number = index + 1;
const isMissing = comp.status === 'missing';
const statusIcon = isMissing ? '🆕' : '🔄';
const statusText = isMissing ? '新增文档' : '更新文档';
const relativePath = path.relative(process.cwd(), comp.componentPath);
markdown += `### ${number}. ${statusIcon} ${statusText}: ${path.basename(comp.componentPath)}
\`\`\`
组件路径: ${relativePath}
文档路径: ${path.relative(process.cwd(), comp.docPath)}
状态: ${isMissing ? '文档缺失' : '文档过时'}
组件git提交时间: ${comp.componentMtime.toLocaleString('zh-CN')}
${isMissing ? '' : `文档git提交时间: ${comp.docMtime?.toLocaleString('zh-CN')}`}
\`\`\`
#### 操作步骤
1. [ ] 读取组件源码: \`${relativePath}\`
2. [ ] 分析组件结构(Props、类型、依赖等)
3. [ ] ${isMissing ? '创建新文档' : '更新现有文档'}: \`${path.relative(process.cwd(), comp.docPath)}\`
4. [ ] 验证文档格式是否符合规范
5. [ ] 检查文档完整性(API、类型、示例等)
---
`;
});
// 添加使用说明
markdown += `## 📝 使用说明
1. 按照任务列表顺序处理每个组件
2. 完成一个任务后,在对应的 [ ] 中添加 x 标记为已完成
3. 所有任务完成后,删除此文件
## 📚 文档规范参考
所有组件文档必须遵循以下格式规范:
- **引用方式**: import 方式
- **功能类型**: 工具/组件
- **功能名称**: 组件/工具名称
- **功能描述**: 详细描述和适用场景
- **何时使用**: 使用场景说明
- **使用示例**: 代码示例
- **API**: 完整的API表格(属性名、类型、说明、默认值)
- **类型描述**: TypeScript类型定义表格
- **主题变量**: CSS主题变量(如有)
`;
return markdown;
}
/**
* 写入todos.md文件
*/
function writeTodosFile(content: string, outputPath: string = 'todos.md'): void {
fs.writeFileSync(outputPath, content, 'utf-8');
console.log(`✅ 已生成任务清单: ${outputPath}`);
}
// CLI 接口
if (require.main === module) {
const args = process.argv.slice(2);
const rootDir = args[0] || process.cwd();
const outputPath = args[1] || path.join(rootDir, 'todos.md');
console.log(`🔍 扫描组件中...`);
try {
const components = scanComponents({ rootDir });
if (components.length === 0) {
console.log(`✅ 所有组件文档都是最新的,无需更新!`);
process.exit(0);
}
console.log(`📝 生成任务清单中...\n`);
const content = generateTodosMarkdown(components);
writeTodosFile(content, outputPath);
console.log(`\n🎉 任务清单生成完成!`);
console.log(` - 文件位置: ${outputPath}`);
console.log(` - 任务数量: ${components.length}`);
console.log(`\n💡 下一步: 打开 todos.md 查看详细任务列表\n`);
} catch (error) {
console.error('❌ 生成失败:', error);
process.exit(1);
}
}
export { generateTodosMarkdown, writeTodosFile, TodoItem };
/**
* 组件文档扫描器
* 扫描项目中的组件文件,对比文档更新时间,识别需要更新或新增文档的组件
* 使用git提交历史获取准确的文件修改时间
*/
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
interface ComponentInfo {
componentPath: string;
docPath: string;
componentMtime: Date;
docMtime: Date | null;
status: 'missing' | 'outdated' | 'current';
}
interface ScanOptions {
rootDir: string;
extensions?: string[];
docFileName?: string; // 文档文件名(默认为 index.zh-CN.md)
excludeDirs?: string[];
excludeFiles?: string[]; // 排除的文件模式
}
/**
* 判断文件是否应该被排除
*/
function shouldExcludeFile(fileName: string, excludePatterns: string[]): boolean {
return excludePatterns.some(pattern => {
// 支持简单的通配符匹配
const regex = new RegExp(pattern.replace('*', '.*'));
return regex.test(fileName);
});
}
/**
* 获取文件的git最后提交时间
* @param filePath 文件路径
* @returns 最后提交时间的Date对象,如果文件不在git历史中则返回null
*/
function getGitLastModifiedTime(filePath: string): Date | null {
try {
// 从文件所在目录开始向上查找git仓库根目录
let currentDir = path.dirname(filePath);
let gitRoot: string | null = null;
// 向上查找.git目录,最多查找10层
for (let i = 0; i < 10; i++) {
const gitDir = path.join(currentDir, '.git');
if (fs.existsSync(gitDir)) {
gitRoot = currentDir;
break;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
// 已到达根目录
break;
}
currentDir = parentDir;
}
if (!gitRoot) {
// 文件不在git仓库中
return null;
}
const gitDirPath = path.join(gitRoot, '.git');
// 检查文件是否被git跟踪(使用绝对路径)
try {
execSync(
`git --git-dir="${gitDirPath}" --work-tree="${gitRoot}" ls-files --error-unmatch "${filePath}"`,
{
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}
);
} catch {
// 文件不在git跟踪中(可能是新文件)
return null;
}
// 获取文件的最后提交时间戳(Unix时间戳,秒)
const timestamp = execSync(
`git --git-dir="${gitDirPath}" --work-tree="${gitRoot}" log -1 --format=%ct -- "${filePath}"`,
{
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore']
}
).trim();
if (!timestamp) {
return null;
}
// 转换为Date对象(秒转毫秒)
return new Date(parseInt(timestamp) * 1000);
} catch (error) {
// 获取git时间失败,返回null
return null;
}
}
/**
* 扫描目录下的组件文件
*/
function scanComponents(options: ScanOptions): ComponentInfo[] {
const {
rootDir,
extensions = ['.tsx', '.jsx'], // 默认只扫描组件文件,不包括 .ts/.js
docFileName = 'index.zh-CN.md',
excludeDirs = ['node_modules', 'dist', 'build', '.git', 'styles'],
excludeFiles = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', '*.store.ts', '*.store.js', 'use*.ts', 'use*.js']
} = options;
const components: ComponentInfo[] = [];
/**
* 扫描组件目录(一级子目录)
*/
function scanComponentDirs(rootDir: string) {
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
for (const entry of entries) {
const componentDir = path.join(rootDir, entry.name);
// 只处理一级子目录
if (!entry.isDirectory()) {
continue;
}
// 跳过排除的目录
if (excludeDirs.includes(entry.name)) {
continue;
}
// 在组件目录中查找主要的组件文件
const files = fs.readdirSync(componentDir);
const mainComponentFile = files.find(file => {
// 匹配与目录名同名的 .tsx 或 .jsx 文件
const baseName = path.basename(file, path.extname(file));
const ext = path.extname(file);
const isMainComponent = baseName.toLowerCase() === entry.name.toLowerCase() && extensions.includes(ext);
const notExcluded = !shouldExcludeFile(file, excludeFiles);
return isMainComponent && notExcluded;
});
if (mainComponentFile) {
const componentPath = path.join(componentDir, mainComponentFile);
const docPath = path.join(componentDir, docFileName);
// 使用git获取组件文件修改时间
const componentMtime = getGitLastModifiedTime(componentPath);
// 如果组件文件没有git历史(新文件),使用文件系统时间作为备选
const componentTime = componentMtime || fs.statSync(componentPath).mtime;
// 检查文档是否存在
let docMtime: Date | null = null;
let status: ComponentInfo['status'] = 'missing';
if (fs.existsSync(docPath)) {
// 使用git获取文档修改时间
const gitDocTime = getGitLastModifiedTime(docPath);
// 如果文档没有git历史(新文件),使用文件系统时间作为备选
docMtime = gitDocTime || fs.statSync(docPath).mtime;
// 比较时间:组件比文档新则需要更新
if (componentTime > docMtime) {
status = 'outdated';
} else {
status = 'current';
}
}
// 只记录需要更新或新增的组件
if (status !== 'current') {
components.push({
componentPath,
docPath,
componentMtime: componentTime,
docMtime,
status
});
}
}
}
}
scanComponentDirs(rootDir);
return components;
}
/**
* 打印扫描结果
*/
function printResults(components: ComponentInfo[]) {
const missing = components.filter(c => c.status === 'missing').length;
const outdated = components.filter(c => c.status === 'outdated').length;
console.log(`\n📊 扫描结果:`);
console.log(` - 需要新增文档: ${missing} 个组件`);
console.log(` - 需要更新文档: ${outdated} 个组件`);
console.log(` - 总计: ${components.length} 个组件`);
console.log(` ℹ️ 使用git提交历史进行时间对比\n`);
if (components.length > 0) {
console.log(`📝 组件列表:`);
components.forEach((comp, index) => {
const statusIcon = comp.status === 'missing' ? '❌' : '⚠️';
const statusText = comp.status === 'missing' ? '缺失' : '过时';
const componentName = path.basename(path.dirname(comp.componentPath));
console.log(` ${index + 1}. ${statusIcon} ${componentName}`);
console.log(` 组件文件: ${path.basename(comp.componentPath)}`);
console.log(` 状态: ${statusText}`);
console.log(` 组件git提交时间: ${comp.componentMtime.toLocaleString('zh-CN')}`);
if (comp.docMtime) {
console.log(` 文档git提交时间: ${comp.docMtime.toLocaleString('zh-CN')}`);
}
console.log('');
});
}
}
// CLI 接口
if (require.main === module) {
const args = process.argv.slice(2);
const rootDir = args[0] || process.cwd();
console.log(`🔍 开始扫描组件...`);
console.log(` 根目录: ${rootDir}\n`);
try {
const components = scanComponents({ rootDir });
printResults(components);
// 输出JSON格式供后续处理
if (args.includes('--json')) {
console.log('\n📄 JSON 输出:');
console.log(JSON.stringify(components, null, 2));
}
} catch (error) {
console.error('❌ 扫描失败:', error);
process.exit(1);
}
}
export { scanComponents, ComponentInfo, ScanOptions };
Component Docs Batcher
批量生成和维护前端组件库文档的自动化工具。
核心功能
- 智能组件识别: 识别主要组件文件(如button/button.tsx),跳过index.ts、store、hooks等辅助文件
- Git时间对比: 使用
git log对比组件和文档的最后提交时间(比文件系统时间更准确) - 任务清单生成: 自动生成结构化的todos.md
- 批量文档生成: 按规范批量生成完整组件文档(index.zh-CN.md)
工作流程
1. 扫描组件
# 扫描组件库
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/components
# 输出JSON格式
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/components --json识别规则: 扫描一级子目录,识别同名.tsx/.jsx文件,跳过index.ts/.store.ts/use.ts等辅助文件和styles/__tests__等目录
扫描结果:
- ❌ 文档缺失:组件目录下没有index.zh-CN.md
- ⚠️ 文档过时:组件文件的git时间晚于文档
- ✅ 文档最新:不输出
2. 生成任务清单
npx ts-node .claude/skills/component-docs-batcher/scripts/generate-todos.ts /path/to/components /output/todos.md生成包含统计信息、任务列表、使用说明、文档规范引用的todos.md
3. 分析组件结构
npx ts-node .claude/skills/component-docs-batcher/scripts/analyze-component.ts /path/to/Component.tsx --json分析内容:组件类型、Props接口、TypeScript类型定义、导入依赖、代码示例
4. 批量生成文档
1. 读取组件源码(使用Read工具) 2. 理解组件逻辑(不依赖代码注释,独立理解) 3. 提取关键信息:Props接口、TypeScript类型、使用场景、第三方组件API 4. 确定导出名称:⚠️ 必须检查/packages/atom-ui-mobile/src/index.ts确认实际导出名称
- 例:
qr-code→QrCode(不是QRCode) - 例:
message→ 小写message - 例:
time-select→TimeSelect(无default as)
5. 按照文档格式规范生成完整文档 6. 质量验证
重要规则:
- 文档路径:组件目录下的
index.zh-CN.md - 独立理解组件,不复制代码注释
- 完整列出所有API(特别是继承的第三方组件API)
- 提供真实可用的代码示例
- 导出名称必须与
src/index.ts完全一致 - 引用示例:
import { Button } from '@evanfang/atom-ui-mobile';
5. 更新任务状态
在todos.md中标记任务完成,完成后删除todos.md
使用场景
定期批量更新
# 1. 扫描组件库
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/components
# 2. 生成任务清单
npx ts-node .claude/skills/component-docs-batcher/scripts/generate-todos.ts /path/to/components
# 3. 按todos.md批量处理,完成后删除todos.md单个组件维护
# 1. 分析组件
npx ts-node .claude/skills/component-docs-batcher/scripts/analyze-component.ts /path/to/Component.tsx
# 2. 生成/更新文档(位置:组件目录/index.zh-CN.md)新组件文档初始化
# 扫描组件库,新组件会在"文档缺失"列表中显示
npx ts-node .claude/skills/component-docs-batcher/scripts/scan-components.ts /path/to/components文档规范
详见文档格式规范
关键要求: 完整的API表格(属性名、类型、说明、默认值)、TypeScript类型定义表格、真实可用的代码示例、主题变量列表或"无"的明确说明
资源
scripts/
- scan-components.ts: 扫描组件文件,对比修改时间,输出需更新/新增的组件列表(支持JSON)
- generate-todos.ts: 生成todos.md任务清单(含统计、步骤、规范引用)
- analyze-component.ts: 分析组件结构,提取Props、类型、依赖等
references/
- docs-format.md: 标准文档模板、编写规则、质量检查清单