index.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import type { CAC } from 'cac';
  2. import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
  3. import { createRequire } from 'node:module';
  4. import { tmpdir } from 'node:os';
  5. import { extname, join } from 'node:path';
  6. import { execa, getStagedFiles } from '@vben/node-utils';
  7. const require = createRequire(import.meta.url);
  8. const circularScannerCli =
  9. require.resolve('circular-dependency-scanner/dist/cli.js');
  10. // 默认配置
  11. const DEFAULT_CONFIG = {
  12. allowedExtensions: ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
  13. ignoreDirs: [
  14. 'dist',
  15. '.turbo',
  16. 'output',
  17. '.cache',
  18. 'scripts',
  19. 'internal',
  20. 'packages/effects/request/src/',
  21. 'packages/@core/ui-kit/menu-ui/src/',
  22. 'packages/@core/ui-kit/popup-ui/src/',
  23. ],
  24. threshold: 0, // 循环依赖的阈值
  25. } as const;
  26. // 类型定义
  27. type CircularDependencyResult = string[];
  28. interface CheckCircularConfig {
  29. allowedExtensions?: string[];
  30. ignoreDirs?: string[];
  31. threshold?: number;
  32. }
  33. interface CommandOptions {
  34. config?: CheckCircularConfig;
  35. staged: boolean;
  36. verbose: boolean;
  37. }
  38. // 缓存机制
  39. const cache = new Map<string, CircularDependencyResult[]>();
  40. async function detectCircularDependencies({
  41. cwd,
  42. ignorePattern,
  43. staged,
  44. }: {
  45. cwd: string;
  46. ignorePattern: string;
  47. staged: boolean;
  48. }): Promise<CircularDependencyResult[]> {
  49. const tempDir = await mkdtemp(join(tmpdir(), 'vsh-check-circular-'));
  50. const outputFile = join(tempDir, 'circles.json');
  51. try {
  52. const args = [circularScannerCli, cwd, '--output', outputFile];
  53. if (staged) {
  54. args.push('--absolute');
  55. }
  56. args.push('--ignore', ignorePattern);
  57. await execa(process.execPath, args, {
  58. cwd,
  59. });
  60. await access(outputFile);
  61. const output = await readFile(outputFile, 'utf8');
  62. return JSON.parse(output) as CircularDependencyResult[];
  63. } catch (error) {
  64. if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
  65. return [];
  66. }
  67. throw error;
  68. } finally {
  69. await rm(tempDir, { force: true, recursive: true });
  70. }
  71. }
  72. /**
  73. * 格式化循环依赖的输出
  74. * @param circles - 循环依赖结果
  75. */
  76. function formatCircles(circles: CircularDependencyResult[]): void {
  77. if (circles.length === 0) {
  78. console.log('✅ No circular dependencies found');
  79. return;
  80. }
  81. console.log('⚠️ Circular dependencies found:');
  82. circles.forEach((circle, index) => {
  83. console.log(`\nCircular dependency #${index + 1}:`);
  84. circle.forEach((file) => console.log(` → ${file}`));
  85. });
  86. }
  87. /**
  88. * 检查项目中的循环依赖
  89. * @param options - 检查选项
  90. * @param options.staged - 是否只检查暂存区文件
  91. * @param options.verbose - 是否显示详细信息
  92. * @param options.config - 自定义配置
  93. * @returns Promise<void>
  94. */
  95. async function checkCircular({
  96. config = {},
  97. staged,
  98. verbose,
  99. }: CommandOptions): Promise<void> {
  100. try {
  101. // 合并配置
  102. const finalConfig = {
  103. ...DEFAULT_CONFIG,
  104. ...config,
  105. };
  106. // 生成忽略模式
  107. const ignorePattern = `**/{${finalConfig.ignoreDirs.join(',')}}/**`;
  108. // 检查缓存
  109. const cacheKey = `${staged}-${process.cwd()}-${ignorePattern}`;
  110. if (cache.has(cacheKey)) {
  111. const cachedResults = cache.get(cacheKey);
  112. if (cachedResults && verbose) {
  113. formatCircles(cachedResults);
  114. }
  115. return;
  116. }
  117. // 检测循环依赖
  118. const results = await detectCircularDependencies({
  119. cwd: process.cwd(),
  120. ignorePattern,
  121. staged,
  122. });
  123. if (staged) {
  124. let files = await getStagedFiles();
  125. const allowedExtensions = new Set(finalConfig.allowedExtensions);
  126. // 过滤文件列表
  127. files = files.filter((file) => allowedExtensions.has(extname(file)));
  128. const circularFiles: CircularDependencyResult[] = [];
  129. for (const file of files) {
  130. for (const result of results) {
  131. const resultFiles = result.flat();
  132. if (resultFiles.includes(file)) {
  133. circularFiles.push(result);
  134. }
  135. }
  136. }
  137. // 更新缓存
  138. cache.set(cacheKey, circularFiles);
  139. if (verbose) {
  140. formatCircles(circularFiles);
  141. }
  142. } else {
  143. // 更新缓存
  144. cache.set(cacheKey, results);
  145. if (verbose) {
  146. formatCircles(results);
  147. }
  148. }
  149. // 如果发现循环依赖,只输出警告信息
  150. if (results.length > 0) {
  151. console.log(
  152. '\n⚠️ Warning: Circular dependencies found, please check and fix',
  153. );
  154. }
  155. } catch (error) {
  156. console.error(
  157. '❌ Error checking circular dependencies:',
  158. error instanceof Error ? error.message : error,
  159. );
  160. }
  161. }
  162. /**
  163. * 定义检查循环依赖的命令
  164. * @param cac - CAC实例
  165. */
  166. function defineCheckCircularCommand(cac: CAC): void {
  167. cac
  168. .command('check-circular')
  169. .option('--staged', 'Only check staged files')
  170. .option('--verbose', 'Show detailed information')
  171. .option('--threshold <number>', 'Threshold for circular dependencies', {
  172. default: 0,
  173. })
  174. .option('--ignore-dirs <dirs>', 'Directories to ignore, comma separated')
  175. .usage('Analyze project circular dependencies')
  176. .action(async ({ ignoreDirs, staged, threshold, verbose }) => {
  177. const config: CheckCircularConfig = {
  178. threshold: Number(threshold),
  179. ...(ignoreDirs && { ignoreDirs: ignoreDirs.split(',') }),
  180. };
  181. await checkCircular({
  182. config,
  183. staged,
  184. verbose: verbose ?? true,
  185. });
  186. });
  187. }
  188. export { type CheckCircularConfig, defineCheckCircularCommand };