@ -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/ts config' ,
'@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 : {
nam e : string ;
} ;
interface KnipFileIssue {
dependencies : KnipDependency [ ] ;
devDependencies : KnipDependency [ ] ;
fil e : 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 defineDepcheck Command ( cac : CAC ) : void {
function defineCheckDep Command ( 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 } ;