scan-api-diff.cjs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #!/usr/bin/env node
  2. /**
  3. * Ant Design v4 → v6 组件 API 差异扫描器
  4. *
  5. * 功能:扫描代码中使用的 antd v4 API,标记需要修改的地方
  6. * 使用方法:node scan-api-diff.js <目标目录路径>
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. // v4 → v6 需要修改的 API 模式
  11. const patterns = [
  12. // Modal, Drawer, Popover 等组件的 visible 改为 open
  13. {
  14. pattern: /visible\s*=\s*\{/g,
  15. suggestion: '❌ 使用了 visible 属性',
  16. fix: '将 visible 改为 open',
  17. severity: 'high',
  18. component: 'Modal/Drawer/Popover/Tooltip/Dropdown'
  19. },
  20. {
  21. pattern: /onVisibleChange\s*=\s*\{/g,
  22. suggestion: '❌ 使用了 onVisibleChange 属性',
  23. fix: '将 onVisibleChange 改为 onOpenChange',
  24. severity: 'high',
  25. component: 'Modal/Drawer/Popover/Tooltip/Dropdown'
  26. },
  27. // 静态方法调用(message, notification, Modal)
  28. {
  29. pattern: /message\.(success|error|info|warning|loading)\(/g,
  30. suggestion: '⚠️ 直接调用静态方法',
  31. fix: '改用 App.useApp() hook 或确保有 <App> 包裹',
  32. severity: 'medium',
  33. component: 'message'
  34. },
  35. {
  36. pattern: /notification\.(success|error|info|warning|open)\(/g,
  37. suggestion: '⚠️ 直接调用静态方法',
  38. fix: '改用 App.useApp() hook 或确保有 <App> 包裹',
  39. severity: 'medium',
  40. component: 'notification'
  41. },
  42. {
  43. pattern: /Modal\.(confirm|info|success|error|warning)\(/g,
  44. suggestion: '⚠️ 直接调用静态方法',
  45. fix: '改用 Modal.useModal() hook 或确保有 <App> 包裹',
  46. severity: 'medium',
  47. component: 'Modal'
  48. },
  49. // moment.js 使用
  50. {
  51. pattern: /import\s+.*moment.*from\s+['"]moment['"]/g,
  52. suggestion: '❌ 使用了 moment.js',
  53. fix: '替换为 dayjs',
  54. severity: 'high',
  55. component: 'DatePicker/TimePicker/Calendar'
  56. },
  57. {
  58. pattern: /moment\(/g,
  59. suggestion: '❌ 调用了 moment()',
  60. fix: '替换为 dayjs()',
  61. severity: 'high',
  62. component: 'DatePicker/TimePicker/Calendar'
  63. },
  64. // Form 相关(部分需要调整)
  65. {
  66. pattern: /getFieldDecorator/g,
  67. suggestion: '❌ 使用了旧版 Form API',
  68. fix: 'v4+ 已废弃,使用 Form.Item name 属性',
  69. severity: 'critical',
  70. component: 'Form'
  71. },
  72. // Dropdown overlay 改为 menu
  73. {
  74. pattern: /overlay\s*=\s*\{/g,
  75. suggestion: '⚠️ Dropdown 使用了 overlay',
  76. fix: '将 overlay 改为 menu (返回 Menu 组件)',
  77. severity: 'medium',
  78. component: 'Dropdown'
  79. },
  80. // PageHeader 已移除
  81. {
  82. pattern: /<PageHeader/g,
  83. suggestion: '❌ PageHeader 组件已移除',
  84. fix: '使用 @ant-design/pro-components 的 PageContainer 或自定义',
  85. severity: 'high',
  86. component: 'PageHeader'
  87. },
  88. // Comment 组件已移除
  89. {
  90. pattern: /<Comment/g,
  91. suggestion: '❌ Comment 组件已移除',
  92. fix: '使用 @ant-design/compatible 或自定义',
  93. severity: 'medium',
  94. component: 'Comment'
  95. },
  96. // BackTop 改为 FloatButton.BackTop
  97. {
  98. pattern: /<BackTop/g,
  99. suggestion: '⚠️ BackTop 组件 API 有变更',
  100. fix: '使用 FloatButton.BackTop',
  101. severity: 'low',
  102. component: 'BackTop'
  103. },
  104. // Less 变量
  105. {
  106. pattern: /@import\s+['"]~antd\/.*\.less['"]/g,
  107. suggestion: '⚠️ 引入了 antd less 文件',
  108. fix: '改用 ConfigProvider theme 配置',
  109. severity: 'medium',
  110. component: 'Theme'
  111. },
  112. {
  113. pattern: /@primary-color|@link-color|@success-color|@warning-color|@error-color/g,
  114. suggestion: '⚠️ 使用了 less 变量',
  115. fix: '改用 Design Token',
  116. severity: 'medium',
  117. component: 'Theme'
  118. },
  119. ];
  120. // 扫描结果统计
  121. const stats = {
  122. totalFiles: 0,
  123. scannedFiles: 0,
  124. filesWithIssues: 0,
  125. issues: {
  126. critical: 0,
  127. high: 0,
  128. medium: 0,
  129. low: 0
  130. }
  131. };
  132. // 问题汇总
  133. const issuesByFile = new Map();
  134. // 递归扫描目录
  135. function scanDirectory(dir, baseDir) {
  136. const files = fs.readdirSync(dir);
  137. files.forEach(file => {
  138. const filePath = path.join(dir, file);
  139. const stat = fs.statSync(filePath);
  140. if (stat.isDirectory()) {
  141. // 跳过 node_modules 等目录
  142. if (!['node_modules', '.git', 'build', 'dist', '.next'].includes(file)) {
  143. scanDirectory(filePath, baseDir);
  144. }
  145. } else if (stat.isFile()) {
  146. // 只扫描 tsx, ts, jsx, js 文件
  147. if (/\.(tsx?|jsx?)$/.test(file)) {
  148. stats.totalFiles++;
  149. scanFile(filePath, baseDir);
  150. }
  151. }
  152. });
  153. }
  154. // 扫描单个文件
  155. function scanFile(filePath, baseDir) {
  156. const content = fs.readFileSync(filePath, 'utf-8');
  157. const relativePath = path.relative(baseDir, filePath);
  158. stats.scannedFiles++;
  159. const fileIssues = [];
  160. patterns.forEach(({ pattern, suggestion, fix, severity, component }) => {
  161. const matches = content.matchAll(pattern);
  162. for (const match of matches) {
  163. // 计算行号
  164. const lines = content.substring(0, match.index).split('\n');
  165. const lineNumber = lines.length;
  166. const columnNumber = lines[lines.length - 1].length + 1;
  167. fileIssues.push({
  168. line: lineNumber,
  169. column: columnNumber,
  170. code: match[0],
  171. suggestion,
  172. fix,
  173. severity,
  174. component
  175. });
  176. stats.issues[severity]++;
  177. }
  178. });
  179. if (fileIssues.length > 0) {
  180. stats.filesWithIssues++;
  181. issuesByFile.set(relativePath, fileIssues);
  182. }
  183. }
  184. // 生成报告
  185. function generateReport() {
  186. console.log('\n' + '='.repeat(80));
  187. console.log('📊 Ant Design v4 → v6 API 差异扫描报告');
  188. console.log('='.repeat(80) + '\n');
  189. console.log('📁 扫描统计:');
  190. console.log(` 总文件数: ${stats.totalFiles}`);
  191. console.log(` 已扫描: ${stats.scannedFiles}`);
  192. console.log(` 有问题的文件: ${stats.filesWithIssues}\n`);
  193. console.log('🚨 问题统计:');
  194. console.log(` 🔴 Critical: ${stats.issues.critical} 个`);
  195. console.log(` 🟠 High: ${stats.issues.high} 个`);
  196. console.log(` 🟡 Medium: ${stats.issues.medium} 个`);
  197. console.log(` 🟢 Low: ${stats.issues.low} 个\n`);
  198. console.log('='.repeat(80) + '\n');
  199. if (issuesByFile.size === 0) {
  200. console.log('✅ 未发现明显的兼容性问题!\n');
  201. return;
  202. }
  203. console.log('📋 详细问题列表:\n');
  204. // 按严重程度排序
  205. const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
  206. const sortedFiles = Array.from(issuesByFile.entries()).sort((a, b) => {
  207. const minSeverityA = Math.min(...a[1].map(i => severityOrder[i.severity]));
  208. const minSeverityB = Math.min(...b[1].map(i => severityOrder[i.severity]));
  209. return minSeverityA - minSeverityB;
  210. });
  211. sortedFiles.forEach(([file, issues]) => {
  212. const severityIcon = {
  213. critical: '🔴',
  214. high: '🟠',
  215. medium: '🟡',
  216. low: '🟢'
  217. };
  218. console.log(`📄 ${file}`);
  219. console.log(' ' + '-'.repeat(76));
  220. issues.forEach(issue => {
  221. console.log(` ${severityIcon[issue.severity]} Line ${issue.line}:${issue.column} [${issue.component}]`);
  222. console.log(` ${issue.suggestion}`);
  223. console.log(` 代码: ${issue.code}`);
  224. console.log(` 💡 修复建议: ${issue.fix}`);
  225. console.log('');
  226. });
  227. });
  228. console.log('='.repeat(80) + '\n');
  229. // 按组件分组统计
  230. console.log('📊 按组件分类统计:\n');
  231. const componentStats = new Map();
  232. issuesByFile.forEach(issues => {
  233. issues.forEach(issue => {
  234. const count = componentStats.get(issue.component) || 0;
  235. componentStats.set(issue.component, count + 1);
  236. });
  237. });
  238. const sortedComponents = Array.from(componentStats.entries())
  239. .sort((a, b) => b[1] - a[1]);
  240. sortedComponents.forEach(([component, count]) => {
  241. console.log(` ${component}: ${count} 个问题`);
  242. });
  243. console.log('\n' + '='.repeat(80));
  244. console.log('💡 建议:');
  245. console.log(' 1. 优先处理 🔴 Critical 和 🟠 High 级别的问题');
  246. console.log(' 2. 使用 import-replace.js 脚本批量替换简单的 API');
  247. console.log(' 3. 参考 migration-checklist.md 了解详细迁移步骤');
  248. console.log('='.repeat(80) + '\n');
  249. }
  250. // 导出为 JSON 格式(可选)
  251. function exportJson(outputPath) {
  252. const result = {
  253. timestamp: new Date().toISOString(),
  254. stats,
  255. issues: Object.fromEntries(issuesByFile)
  256. };
  257. fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
  258. console.log(`\n📝 详细报告已导出到: ${outputPath}\n`);
  259. }
  260. // 主函数
  261. function main() {
  262. const args = process.argv.slice(2);
  263. if (args.length === 0) {
  264. console.log('用法: node scan-api-diff.js <目标目录路径> [--json=输出文件.json]');
  265. console.log('示例: node scan-api-diff.js ../old-project/src --json=report.json');
  266. process.exit(1);
  267. }
  268. const targetDir = args[0];
  269. const jsonOutput = args.find(arg => arg.startsWith('--json='))?.split('=')[1];
  270. if (!fs.existsSync(targetDir)) {
  271. console.error(`❌ 目录不存在: ${targetDir}`);
  272. process.exit(1);
  273. }
  274. console.log(`\n🔍 开始扫描目录: ${targetDir}\n`);
  275. scanDirectory(targetDir, targetDir);
  276. generateReport();
  277. if (jsonOutput) {
  278. exportJson(jsonOutput);
  279. }
  280. }
  281. main();