import-replace.cjs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #!/usr/bin/env node
  2. /**
  3. * Ant Design v4 → v6 Import 语句批量替换工具
  4. *
  5. * 功能:自动替换代码中的 antd API 调用
  6. * 使用方法:node import-replace.js <目标目录路径> [--dry-run] [--backup]
  7. *
  8. * 参数说明:
  9. * --dry-run : 只显示将要修改的内容,不实际修改文件
  10. * --backup : 修改前备份原文件(.bak)
  11. */
  12. const fs = require('fs');
  13. const path = require('path');
  14. // 替换规则配置
  15. const replacements = [
  16. // 1. visible → open
  17. {
  18. name: 'visible to open (props)',
  19. pattern: /(\s+)visible(\s*=\s*\{)/g,
  20. replacement: '$1open$2',
  21. filePattern: /\.(tsx?|jsx?)$/,
  22. description: 'Modal/Drawer/Popover 等组件的 visible 改为 open'
  23. },
  24. {
  25. name: 'onVisibleChange to onOpenChange',
  26. pattern: /(\s+)onVisibleChange(\s*=\s*\{)/g,
  27. replacement: '$1onOpenChange$2',
  28. filePattern: /\.(tsx?|jsx?)$/,
  29. description: 'onVisibleChange 改为 onOpenChange'
  30. },
  31. // 2. moment → dayjs
  32. {
  33. name: 'moment import to dayjs',
  34. pattern: /import\s+moment\s+from\s+['"]moment['"]/g,
  35. replacement: "import dayjs from 'dayjs'",
  36. filePattern: /\.(tsx?|jsx?)$/,
  37. description: 'moment.js import 改为 dayjs'
  38. },
  39. {
  40. name: 'moment() to dayjs()',
  41. pattern: /\bmoment\(/g,
  42. replacement: 'dayjs(',
  43. filePattern: /\.(tsx?|jsx?)$/,
  44. description: 'moment() 调用改为 dayjs()'
  45. },
  46. // 3. Dropdown overlay → menu
  47. {
  48. name: 'Dropdown overlay to menu',
  49. pattern: /(\s+)overlay(\s*=\s*\{)/g,
  50. replacement: '$1menu$2',
  51. filePattern: /\.(tsx?|jsx?)$/,
  52. description: 'Dropdown 的 overlay 改为 menu',
  53. warning: '⚠️ 注意:menu 需要返回 Menu 组件,可能需要手动调整'
  54. },
  55. // 4. PageHeader → 需要手动处理,这里只标记
  56. // 不做自动替换,因为需要手动迁移到 ProComponents
  57. // 5. BackTop → FloatButton.BackTop
  58. {
  59. name: 'BackTop to FloatButton.BackTop',
  60. pattern: /<BackTop(\s+[^>]*)?>/g,
  61. replacement: '<FloatButton.BackTop$1>',
  62. filePattern: /\.(tsx?|jsx?)$/,
  63. description: 'BackTop 改为 FloatButton.BackTop',
  64. warning: '⚠️ 注意:需要确保已导入 FloatButton'
  65. },
  66. {
  67. name: 'BackTop closing tag',
  68. pattern: /<\/BackTop>/g,
  69. replacement: '</FloatButton.BackTop>',
  70. filePattern: /\.(tsx?|jsx?)$/,
  71. description: 'BackTop 结束标签'
  72. },
  73. // 6. 添加必要的 import (如果文件中使用了 FloatButton 但未导入)
  74. // 这个需要更智能的处理,暂时跳过
  75. ];
  76. // 统计信息
  77. const stats = {
  78. totalFiles: 0,
  79. modifiedFiles: 0,
  80. totalReplacements: 0,
  81. replacementsByType: new Map(),
  82. errors: []
  83. };
  84. // 递归扫描目录
  85. function scanDirectory(dir, options) {
  86. const files = fs.readdirSync(dir);
  87. files.forEach(file => {
  88. const filePath = path.join(dir, file);
  89. const stat = fs.statSync(filePath);
  90. if (stat.isDirectory()) {
  91. if (!['node_modules', '.git', 'build', 'dist', '.next'].includes(file)) {
  92. scanDirectory(filePath, options);
  93. }
  94. } else if (stat.isFile()) {
  95. if (/\.(tsx?|jsx?)$/.test(file)) {
  96. stats.totalFiles++;
  97. processFile(filePath, options);
  98. }
  99. }
  100. });
  101. }
  102. // 处理单个文件
  103. function processFile(filePath, options) {
  104. let content = fs.readFileSync(filePath, 'utf-8');
  105. const originalContent = content;
  106. let fileModified = false;
  107. let fileReplacements = [];
  108. replacements.forEach(rule => {
  109. if (!rule.filePattern.test(filePath)) {
  110. return;
  111. }
  112. const matches = content.match(rule.pattern);
  113. if (matches && matches.length > 0) {
  114. content = content.replace(rule.pattern, rule.replacement);
  115. const count = matches.length;
  116. fileReplacements.push({
  117. rule: rule.name,
  118. count,
  119. description: rule.description,
  120. warning: rule.warning
  121. });
  122. stats.totalReplacements += count;
  123. stats.replacementsByType.set(
  124. rule.name,
  125. (stats.replacementsByType.get(rule.name) || 0) + count
  126. );
  127. fileModified = true;
  128. }
  129. });
  130. if (fileModified) {
  131. stats.modifiedFiles++;
  132. if (options.dryRun) {
  133. console.log(`\n📝 [DRY RUN] 将修改: ${filePath}`);
  134. fileReplacements.forEach(rep => {
  135. console.log(` ✓ ${rep.description} (${rep.count} 处)`);
  136. if (rep.warning) {
  137. console.log(` ${rep.warning}`);
  138. }
  139. });
  140. } else {
  141. // 备份原文件
  142. if (options.backup) {
  143. fs.writeFileSync(filePath + '.bak', originalContent);
  144. }
  145. // 写入修改后的内容
  146. try {
  147. fs.writeFileSync(filePath, content, 'utf-8');
  148. console.log(`✅ 已修改: ${filePath}`);
  149. fileReplacements.forEach(rep => {
  150. console.log(` ✓ ${rep.description} (${rep.count} 处)`);
  151. if (rep.warning) {
  152. console.log(` ${rep.warning}`);
  153. }
  154. });
  155. } catch (error) {
  156. stats.errors.push({ file: filePath, error: error.message });
  157. console.error(`❌ 修改失败: ${filePath}`);
  158. console.error(` 错误: ${error.message}`);
  159. }
  160. }
  161. }
  162. }
  163. // 生成报告
  164. function generateReport(options) {
  165. console.log('\n' + '='.repeat(80));
  166. console.log('📊 Ant Design v4 → v6 批量替换报告');
  167. if (options.dryRun) {
  168. console.log(' [DRY RUN 模式 - 未实际修改文件]');
  169. }
  170. console.log('='.repeat(80) + '\n');
  171. console.log('📁 文件统计:');
  172. console.log(` 扫描文件总数: ${stats.totalFiles}`);
  173. console.log(` 修改文件数: ${stats.modifiedFiles}`);
  174. console.log(` 替换总次数: ${stats.totalReplacements}\n`);
  175. if (stats.replacementsByType.size > 0) {
  176. console.log('📋 替换详情:');
  177. const sorted = Array.from(stats.replacementsByType.entries())
  178. .sort((a, b) => b[1] - a[1]);
  179. sorted.forEach(([rule, count]) => {
  180. console.log(` ${rule}: ${count} 次`);
  181. });
  182. console.log('');
  183. }
  184. if (stats.errors.length > 0) {
  185. console.log('❌ 错误列表:');
  186. stats.errors.forEach(({ file, error }) => {
  187. console.log(` ${file}`);
  188. console.log(` ${error}`);
  189. });
  190. console.log('');
  191. }
  192. console.log('='.repeat(80));
  193. if (options.dryRun) {
  194. console.log('💡 提示: 这是 DRY RUN 模式,文件未被实际修改');
  195. console.log(' 如果确认无误,请去掉 --dry-run 参数重新运行');
  196. } else {
  197. console.log('✅ 替换完成!');
  198. if (options.backup) {
  199. console.log(' 原文件已备份为 .bak 文件');
  200. }
  201. console.log(' 建议使用 git diff 检查改动,确保无误后再提交');
  202. }
  203. console.log('='.repeat(80) + '\n');
  204. // 特别提醒
  205. if (stats.totalReplacements > 0) {
  206. console.log('⚠️ 重要提醒:');
  207. console.log(' 1. 某些替换可能需要手动调整(如 Dropdown 的 menu 属性)');
  208. console.log(' 2. 建议运行 scan-api-diff.js 再次检查是否有遗漏');
  209. console.log(' 3. 必须手动处理以下情况:');
  210. console.log(' - PageHeader 组件(已移除,需迁移到 ProComponents)');
  211. console.log(' - message/notification 静态方法(需要 App.useApp())');
  212. console.log(' - Modal.confirm 等静态方法(需要 Modal.useModal())');
  213. console.log(' - Form.getFieldDecorator(已废弃)\n');
  214. }
  215. }
  216. // 主函数
  217. function main() {
  218. const args = process.argv.slice(2);
  219. if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
  220. console.log(`
  221. Ant Design v4 → v6 Import 批量替换工具
  222. 用法:
  223. node import-replace.js <目标目录> [选项]
  224. 选项:
  225. --dry-run 只显示将要修改的内容,不实际修改文件
  226. --backup 修改前备份原文件(.bak)
  227. --help, -h 显示帮助信息
  228. 示例:
  229. # 预览将要修改的内容
  230. node import-replace.js ../old-project/src --dry-run
  231. # 执行替换并备份
  232. node import-replace.js ../old-project/src --backup
  233. # 直接执行替换(谨慎使用)
  234. node import-replace.js ../old-project/src
  235. `);
  236. process.exit(0);
  237. }
  238. const targetDir = args[0];
  239. const options = {
  240. dryRun: args.includes('--dry-run'),
  241. backup: args.includes('--backup')
  242. };
  243. if (!fs.existsSync(targetDir)) {
  244. console.error(`❌ 目录不存在: ${targetDir}`);
  245. process.exit(1);
  246. }
  247. if (!options.dryRun && !options.backup) {
  248. console.log('⚠️ 警告: 您没有启用备份选项,文件将直接被修改!');
  249. console.log('建议先使用 --dry-run 预览,或添加 --backup 备份原文件\n');
  250. }
  251. console.log(`\n🔧 开始处理目录: ${targetDir}`);
  252. if (options.dryRun) {
  253. console.log(' [DRY RUN 模式]');
  254. }
  255. if (options.backup) {
  256. console.log(' [备份模式]');
  257. }
  258. console.log('');
  259. scanDirectory(targetDir, options);
  260. generateReport(options);
  261. }
  262. main();