|
|
@@ -1,162 +1,133 @@
|
|
|
import type { CAC } from 'cac';
|
|
|
|
|
|
-import { getPackages } from '@vben/node-utils';
|
|
|
+import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
|
+import { createRequire } from 'node:module';
|
|
|
+import { tmpdir } from 'node:os';
|
|
|
+import { dirname, join } from 'node:path';
|
|
|
|
|
|
-import depcheck from 'depcheck';
|
|
|
+import { execa } from '@vben/node-utils';
|
|
|
+
|
|
|
+const require = createRequire(import.meta.url);
|
|
|
+const knipMain = require.resolve('knip');
|
|
|
+const knipCli = join(dirname(knipMain), '..', 'bin', 'knip.js');
|
|
|
|
|
|
-// 默认配置
|
|
|
const DEFAULT_CONFIG = {
|
|
|
- // 需要忽略的依赖匹配
|
|
|
- ignoreMatches: [
|
|
|
- 'vite',
|
|
|
- 'vitest',
|
|
|
- 'tsdown',
|
|
|
- '@vben/tailwind-config',
|
|
|
- '@vben/tsconfig',
|
|
|
- '@vben/vite-config',
|
|
|
- '@types/*',
|
|
|
+ ignore: ['dist/**', 'docs/**', 'node_modules/**', 'public/**'],
|
|
|
+ ignoreBinaries: [] as string[],
|
|
|
+ ignoreDependencies: [
|
|
|
+ '@iconify/json',
|
|
|
'@vben-core/design',
|
|
|
- ],
|
|
|
- // 需要忽略的包
|
|
|
- ignorePackages: [
|
|
|
- '@vben/backend-mock',
|
|
|
'@vben/commitlint-config',
|
|
|
'@vben/eslint-config',
|
|
|
- '@vben/node-utils',
|
|
|
- '@vben/oxfmt-config',
|
|
|
- '@vben/oxlint-config',
|
|
|
'@vben/stylelint-config',
|
|
|
- '@vben/tsconfig',
|
|
|
+ '@vben/tailwind-config',
|
|
|
'@vben/vite-config',
|
|
|
- '@vben/vsh',
|
|
|
+ '@vben/oxlint-config',
|
|
|
+ 'playwright',
|
|
|
+ 'rimraf',
|
|
|
+ 'tailwindcss',
|
|
|
],
|
|
|
- // 需要忽略的文件模式
|
|
|
- ignorePatterns: ['dist', 'node_modules', 'public'],
|
|
|
+ ignoreWorkspaces: ['internal/lint-configs/*', 'scripts/*'],
|
|
|
};
|
|
|
|
|
|
-interface DepcheckResult {
|
|
|
- dependencies: string[];
|
|
|
- devDependencies: string[];
|
|
|
- missing: Record<string, string[]>;
|
|
|
-}
|
|
|
-
|
|
|
-interface DepcheckConfig {
|
|
|
- ignoreMatches?: string[];
|
|
|
- ignorePackages?: string[];
|
|
|
- ignorePatterns?: string[];
|
|
|
+interface KnipDependency {
|
|
|
+ col: number;
|
|
|
+ line: number;
|
|
|
+ name: string;
|
|
|
+ pos: number;
|
|
|
}
|
|
|
|
|
|
-interface PackageInfo {
|
|
|
- dir: string;
|
|
|
- packageJson: {
|
|
|
- name: string;
|
|
|
- };
|
|
|
+interface KnipFileIssue {
|
|
|
+ dependencies: KnipDependency[];
|
|
|
+ devDependencies: KnipDependency[];
|
|
|
+ file: string;
|
|
|
+ optionalPeerDependencies: KnipDependency[];
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 清理依赖检查结果
|
|
|
- * @param unused - 依赖检查结果
|
|
|
- */
|
|
|
-function cleanDepcheckResult(unused: DepcheckResult): void {
|
|
|
- // 删除file:前缀的依赖提示,该依赖是本地依赖
|
|
|
- Reflect.deleteProperty(unused.missing, 'file:');
|
|
|
-
|
|
|
- // 清理路径依赖
|
|
|
- Object.keys(unused.missing).forEach((key) => {
|
|
|
- unused.missing[key] = (unused.missing[key] || []).filter(
|
|
|
- (item: string) => !item.startsWith('/'),
|
|
|
- );
|
|
|
- if (unused.missing[key].length === 0) {
|
|
|
- Reflect.deleteProperty(unused.missing, key);
|
|
|
- }
|
|
|
- });
|
|
|
+interface KnipResult {
|
|
|
+ issues: KnipFileIssue[];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 格式化依赖检查结果
|
|
|
- * @param pkgName - 包名
|
|
|
- * @param unused - 依赖检查结果
|
|
|
+ * @param result - 依赖检查结果
|
|
|
*/
|
|
|
-function formatDepcheckResult(pkgName: string, unused: DepcheckResult): void {
|
|
|
- const hasIssues =
|
|
|
- Object.keys(unused.missing).length > 0 ||
|
|
|
- unused.dependencies.length > 0 ||
|
|
|
- unused.devDependencies.length > 0;
|
|
|
+function formatResult(result: KnipResult): void {
|
|
|
+ let hasIssues = false;
|
|
|
|
|
|
- if (!hasIssues) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ for (const issue of result.issues) {
|
|
|
+ const hasDeps = issue.dependencies.length > 0;
|
|
|
+ const hasDevDeps = issue.devDependencies.length > 0;
|
|
|
+
|
|
|
+ if (!hasDeps && !hasDevDeps) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- console.log('\n📦 Package:', pkgName);
|
|
|
+ hasIssues = true;
|
|
|
+ console.log(`\n📦 ${issue.file}`);
|
|
|
|
|
|
- if (Object.keys(unused.missing).length > 0) {
|
|
|
- console.log('❌ Missing dependencies:');
|
|
|
- Object.entries(unused.missing).forEach(([dep, files]) => {
|
|
|
- console.log(` - ${dep}:`);
|
|
|
- files.forEach((file) => console.log(` → ${file}`));
|
|
|
- });
|
|
|
- }
|
|
|
+ if (hasDeps) {
|
|
|
+ console.log('⚠️ Unused dependencies:');
|
|
|
+ for (const dep of issue.dependencies) {
|
|
|
+ console.log(` - ${dep.name}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (unused.dependencies.length > 0) {
|
|
|
- console.log('⚠️ Unused dependencies:');
|
|
|
- unused.dependencies.forEach((dep) => console.log(` - ${dep}`));
|
|
|
+ if (hasDevDeps) {
|
|
|
+ console.log('⚠️ Unused devDependencies:');
|
|
|
+ for (const dep of issue.devDependencies) {
|
|
|
+ console.log(` - ${dep.name}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if (unused.devDependencies.length > 0) {
|
|
|
- console.log('⚠️ Unused devDependencies:');
|
|
|
- unused.devDependencies.forEach((dep) => console.log(` - ${dep}`));
|
|
|
+ if (!hasIssues) {
|
|
|
+ console.log('\n✅ Dependency check completed, no issues found');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 运行依赖检查
|
|
|
- * @param config - 配置选项
|
|
|
*/
|
|
|
-async function runDepcheck(config: DepcheckConfig = {}): Promise<void> {
|
|
|
+async function runKnipCheck(): Promise<void> {
|
|
|
+ const cwd = process.cwd();
|
|
|
+ const tempDir = await mkdtemp(join(tmpdir(), 'vsh-check-dep-'));
|
|
|
+ const configFile = join(tempDir, 'knip.json');
|
|
|
+
|
|
|
try {
|
|
|
- const finalConfig = {
|
|
|
- ...DEFAULT_CONFIG,
|
|
|
- ...config,
|
|
|
+ await writeFile(configFile, JSON.stringify(DEFAULT_CONFIG));
|
|
|
+
|
|
|
+ const args = [
|
|
|
+ knipCli,
|
|
|
+ '--config',
|
|
|
+ configFile,
|
|
|
+ '--include',
|
|
|
+ 'dependencies',
|
|
|
+ '--reporter',
|
|
|
+ 'json',
|
|
|
+ '--no-config-hints',
|
|
|
+ ];
|
|
|
+
|
|
|
+ await execa(process.execPath, args, { cwd });
|
|
|
+ console.log('\n✅ Dependency check completed, no issues found');
|
|
|
+ } catch (error: unknown) {
|
|
|
+ const execaError = error as {
|
|
|
+ exitCode?: number;
|
|
|
+ stdout?: string;
|
|
|
};
|
|
|
|
|
|
- const { packages } = await getPackages();
|
|
|
-
|
|
|
- let hasIssues = false;
|
|
|
-
|
|
|
- await Promise.all(
|
|
|
- packages.map(async (pkg: PackageInfo) => {
|
|
|
- // 跳过需要忽略的包
|
|
|
- if (finalConfig.ignorePackages.includes(pkg.packageJson.name)) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const unused = await depcheck(pkg.dir, {
|
|
|
- ignoreMatches: finalConfig.ignoreMatches,
|
|
|
- ignorePatterns: finalConfig.ignorePatterns,
|
|
|
- });
|
|
|
-
|
|
|
- cleanDepcheckResult(unused);
|
|
|
-
|
|
|
- const pkgHasIssues =
|
|
|
- Object.keys(unused.missing).length > 0 ||
|
|
|
- unused.dependencies.length > 0 ||
|
|
|
- unused.devDependencies.length > 0;
|
|
|
-
|
|
|
- if (pkgHasIssues) {
|
|
|
- hasIssues = true;
|
|
|
- formatDepcheckResult(pkg.packageJson.name, unused);
|
|
|
- }
|
|
|
- }),
|
|
|
- );
|
|
|
-
|
|
|
- if (!hasIssues) {
|
|
|
- console.log('\n✅ Dependency check completed, no issues found');
|
|
|
+ if (execaError.exitCode === 1 && execaError.stdout) {
|
|
|
+ const result: KnipResult = JSON.parse(execaError.stdout);
|
|
|
+ formatResult(result);
|
|
|
+ return;
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
+
|
|
|
console.error(
|
|
|
'❌ Dependency check failed:',
|
|
|
error instanceof Error ? error.message : error,
|
|
|
);
|
|
|
+ } finally {
|
|
|
+ await rm(tempDir, { force: true, recursive: true });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -164,31 +135,13 @@ async function runDepcheck(config: DepcheckConfig = {}): Promise<void> {
|
|
|
* 定义依赖检查命令
|
|
|
* @param cac - CAC实例
|
|
|
*/
|
|
|
-function defineDepcheckCommand(cac: CAC): void {
|
|
|
+function defineCheckDepCommand(cac: CAC): void {
|
|
|
cac
|
|
|
.command('check-dep')
|
|
|
- .option(
|
|
|
- '--ignore-packages <packages>',
|
|
|
- 'Packages to ignore, comma separated',
|
|
|
- )
|
|
|
- .option(
|
|
|
- '--ignore-matches <matches>',
|
|
|
- 'Dependency patterns to ignore, comma separated',
|
|
|
- )
|
|
|
- .option(
|
|
|
- '--ignore-patterns <patterns>',
|
|
|
- 'File patterns to ignore, comma separated',
|
|
|
- )
|
|
|
- .usage('Analyze project dependencies')
|
|
|
- .action(async ({ ignoreMatches, ignorePackages, ignorePatterns }) => {
|
|
|
- const config: DepcheckConfig = {
|
|
|
- ...(ignorePackages && { ignorePackages: ignorePackages.split(',') }),
|
|
|
- ...(ignoreMatches && { ignoreMatches: ignoreMatches.split(',') }),
|
|
|
- ...(ignorePatterns && { ignorePatterns: ignorePatterns.split(',') }),
|
|
|
- };
|
|
|
-
|
|
|
- await runDepcheck(config);
|
|
|
+ .usage('Analyze project dependencies using knip')
|
|
|
+ .action(async () => {
|
|
|
+ await runKnipCheck();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-export { defineDepcheckCommand, type DepcheckConfig };
|
|
|
+export { defineCheckDepCommand };
|