2024-05-19 21:20:42 +08:00
|
|
|
|
import type { CAC } from 'cac';
|
|
|
|
|
|
|
2026-03-15 21:04:12 +08:00
|
|
|
|
import { access, mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
|
|
|
|
import { createRequire } from 'node:module';
|
|
|
|
|
|
import { tmpdir } from 'node:os';
|
|
|
|
|
|
import { extname, join } from 'node:path';
|
2024-05-19 21:20:42 +08:00
|
|
|
|
|
2026-03-15 21:04:12 +08:00
|
|
|
|
import { execa, getStagedFiles } from '@vben/node-utils';
|
2024-06-08 19:49:06 +08:00
|
|
|
|
|
2026-03-15 21:04:12 +08:00
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
|
|
const circularScannerCli =
|
|
|
|
|
|
require.resolve('circular-dependency-scanner/dist/cli.js');
|
2025-04-28 23:08:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 默认配置
|
|
|
|
|
|
const DEFAULT_CONFIG = {
|
|
|
|
|
|
allowedExtensions: ['.cjs', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
|
|
|
|
|
ignoreDirs: [
|
|
|
|
|
|
'dist',
|
|
|
|
|
|
'.turbo',
|
|
|
|
|
|
'output',
|
|
|
|
|
|
'.cache',
|
|
|
|
|
|
'scripts',
|
|
|
|
|
|
'internal',
|
|
|
|
|
|
'packages/effects/request/src/',
|
|
|
|
|
|
'packages/@core/ui-kit/menu-ui/src/',
|
|
|
|
|
|
'packages/@core/ui-kit/popup-ui/src/',
|
|
|
|
|
|
],
|
|
|
|
|
|
threshold: 0, // 循环依赖的阈值
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
// 类型定义
|
|
|
|
|
|
type CircularDependencyResult = string[];
|
|
|
|
|
|
|
|
|
|
|
|
interface CheckCircularConfig {
|
|
|
|
|
|
allowedExtensions?: string[];
|
|
|
|
|
|
ignoreDirs?: string[];
|
|
|
|
|
|
threshold?: number;
|
|
|
|
|
|
}
|
2024-05-19 21:20:42 +08:00
|
|
|
|
|
|
|
|
|
|
interface CommandOptions {
|
2025-04-28 23:08:05 +08:00
|
|
|
|
config?: CheckCircularConfig;
|
2024-05-19 21:20:42 +08:00
|
|
|
|
staged: boolean;
|
|
|
|
|
|
verbose: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-28 23:08:05 +08:00
|
|
|
|
// 缓存机制
|
|
|
|
|
|
const cache = new Map<string, CircularDependencyResult[]>();
|
|
|
|
|
|
|
2026-03-15 21:04:12 +08:00
|
|
|
|
async function detectCircularDependencies({
|
|
|
|
|
|
cwd,
|
|
|
|
|
|
ignorePattern,
|
|
|
|
|
|
staged,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
cwd: string;
|
|
|
|
|
|
ignorePattern: string;
|
|
|
|
|
|
staged: boolean;
|
|
|
|
|
|
}): Promise<CircularDependencyResult[]> {
|
|
|
|
|
|
const tempDir = await mkdtemp(join(tmpdir(), 'vsh-check-circular-'));
|
|
|
|
|
|
const outputFile = join(tempDir, 'circles.json');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const args = [circularScannerCli, cwd, '--output', outputFile];
|
|
|
|
|
|
|
|
|
|
|
|
if (staged) {
|
|
|
|
|
|
args.push('--absolute');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
args.push('--ignore', ignorePattern);
|
|
|
|
|
|
|
|
|
|
|
|
await execa(process.execPath, args, {
|
|
|
|
|
|
cwd,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await access(outputFile);
|
|
|
|
|
|
const output = await readFile(outputFile, 'utf8');
|
|
|
|
|
|
return JSON.parse(output) as CircularDependencyResult[];
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await rm(tempDir, { force: true, recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-28 23:08:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 格式化循环依赖的输出
|
|
|
|
|
|
* @param circles - 循环依赖结果
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatCircles(circles: CircularDependencyResult[]): void {
|
|
|
|
|
|
if (circles.length === 0) {
|
|
|
|
|
|
console.log('✅ No circular dependencies found');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('⚠️ Circular dependencies found:');
|
|
|
|
|
|
circles.forEach((circle, index) => {
|
|
|
|
|
|
console.log(`\nCircular dependency #${index + 1}:`);
|
|
|
|
|
|
circle.forEach((file) => console.log(` → ${file}`));
|
2024-05-19 21:20:42 +08:00
|
|
|
|
});
|
2025-04-28 23:08:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查项目中的循环依赖
|
|
|
|
|
|
* @param options - 检查选项
|
|
|
|
|
|
* @param options.staged - 是否只检查暂存区文件
|
|
|
|
|
|
* @param options.verbose - 是否显示详细信息
|
|
|
|
|
|
* @param options.config - 自定义配置
|
|
|
|
|
|
* @returns Promise<void>
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function checkCircular({
|
|
|
|
|
|
config = {},
|
|
|
|
|
|
staged,
|
|
|
|
|
|
verbose,
|
|
|
|
|
|
}: CommandOptions): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 合并配置
|
|
|
|
|
|
const finalConfig = {
|
|
|
|
|
|
...DEFAULT_CONFIG,
|
|
|
|
|
|
...config,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 生成忽略模式
|
|
|
|
|
|
const ignorePattern = `**/{${finalConfig.ignoreDirs.join(',')}}/**`;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查缓存
|
|
|
|
|
|
const cacheKey = `${staged}-${process.cwd()}-${ignorePattern}`;
|
|
|
|
|
|
if (cache.has(cacheKey)) {
|
|
|
|
|
|
const cachedResults = cache.get(cacheKey);
|
2026-03-15 21:04:12 +08:00
|
|
|
|
if (cachedResults && verbose) {
|
|
|
|
|
|
formatCircles(cachedResults);
|
2025-04-28 23:08:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检测循环依赖
|
2026-03-15 21:04:12 +08:00
|
|
|
|
const results = await detectCircularDependencies({
|
2025-04-28 23:08:05 +08:00
|
|
|
|
cwd: process.cwd(),
|
2026-03-15 21:04:12 +08:00
|
|
|
|
ignorePattern,
|
|
|
|
|
|
staged,
|
2025-04-28 23:08:05 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (staged) {
|
|
|
|
|
|
let files = await getStagedFiles();
|
|
|
|
|
|
const allowedExtensions = new Set(finalConfig.allowedExtensions);
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤文件列表
|
|
|
|
|
|
files = files.filter((file) => allowedExtensions.has(extname(file)));
|
2024-05-19 21:20:42 +08:00
|
|
|
|
|
2025-04-28 23:08:05 +08:00
|
|
|
|
const circularFiles: CircularDependencyResult[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
for (const result of results) {
|
|
|
|
|
|
const resultFiles = result.flat();
|
|
|
|
|
|
if (resultFiles.includes(file)) {
|
|
|
|
|
|
circularFiles.push(result);
|
|
|
|
|
|
}
|
2024-05-19 21:20:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-28 23:08:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新缓存
|
|
|
|
|
|
cache.set(cacheKey, circularFiles);
|
2026-03-15 21:04:12 +08:00
|
|
|
|
if (verbose) {
|
|
|
|
|
|
formatCircles(circularFiles);
|
|
|
|
|
|
}
|
2025-04-28 23:08:05 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 更新缓存
|
|
|
|
|
|
cache.set(cacheKey, results);
|
2026-03-15 21:04:12 +08:00
|
|
|
|
if (verbose) {
|
|
|
|
|
|
formatCircles(results);
|
|
|
|
|
|
}
|
2024-05-19 21:20:42 +08:00
|
|
|
|
}
|
2025-04-28 23:08:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果发现循环依赖,只输出警告信息
|
|
|
|
|
|
if (results.length > 0) {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
'\n⚠️ Warning: Circular dependencies found, please check and fix',
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(
|
|
|
|
|
|
'❌ Error checking circular dependencies:',
|
|
|
|
|
|
error instanceof Error ? error.message : error,
|
|
|
|
|
|
);
|
2024-05-19 21:20:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-28 23:08:05 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 定义检查循环依赖的命令
|
|
|
|
|
|
* @param cac - CAC实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
function defineCheckCircularCommand(cac: CAC): void {
|
2024-05-19 21:20:42 +08:00
|
|
|
|
cac
|
|
|
|
|
|
.command('check-circular')
|
2025-04-28 23:08:05 +08:00
|
|
|
|
.option('--staged', 'Only check staged files')
|
|
|
|
|
|
.option('--verbose', 'Show detailed information')
|
|
|
|
|
|
.option('--threshold <number>', 'Threshold for circular dependencies', {
|
|
|
|
|
|
default: 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
.option('--ignore-dirs <dirs>', 'Directories to ignore, comma separated')
|
|
|
|
|
|
.usage('Analyze project circular dependencies')
|
|
|
|
|
|
.action(async ({ ignoreDirs, staged, threshold, verbose }) => {
|
|
|
|
|
|
const config: CheckCircularConfig = {
|
|
|
|
|
|
threshold: Number(threshold),
|
|
|
|
|
|
...(ignoreDirs && { ignoreDirs: ignoreDirs.split(',') }),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await checkCircular({
|
|
|
|
|
|
config,
|
|
|
|
|
|
staged,
|
|
|
|
|
|
verbose: verbose ?? true,
|
|
|
|
|
|
});
|
2024-05-19 21:20:42 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-28 23:08:05 +08:00
|
|
|
|
export { type CheckCircularConfig, defineCheckCircularCommand };
|