🎙️ 前言
在前端工程规范化高度成熟的今天,不管你有没有代码洁癖,你一定知道这些保证代码的 显性规范 的工具:
- 各种 Linter:eslint、oxlint、stylelint、markdownlint、commitlint,甚至管
package.json的 npmpackagejsonlint 和管中英文书写规范的 zhlint - 代码格式化工具:prettier、biome、editorconfig
我们会纠结一个空行、一个空格、一个分号、未使用的变量、不规范的命名,会容忍不了 IDE 里任何一条黄色波浪线和背景色。
然而真正脏的工程问题,并不在代码写法上,而藏在项目依赖、版本冗余、垃圾文件、代码体量这些隐形角落。而这些问题,往往却是被大多数开发者无视或忽略的。
代码写得优雅,只是表层整洁;项目工程干净、依赖健康、版本可控、体量透明,才是高阶前端洁癖的终极追求。
很多项目看似能跑,实则暗藏大量技术隐债:
- 陈旧依赖严重脱节:漏洞、老旧语法、废弃 API 默默堆积
- 冗余依赖越积越多:永远不会被用到的包常年盘踞在
node_modules,浪费流量和磁盘空间 - 幽灵依赖定时炸弹:受包提升(package hoisting)影响引入间接依赖,让你的代码可以引用未安装的三方包,项目照跑无误,殊不知定时炸弹就这样埋下,尤其是你的项目还会发布 NPM 包,等于把问题扩散到你的用户,失掉信任
- 项目代码体量模糊:无效代码、冗余文件、重复逻辑无人统计
以上任何一个问题,都非人力易解。本篇就来聊聊,作者(重度代码洁癖患者)极度依赖的几款极简、高效、专治项目脏乱差的 CLI 神器。
TL;DR
AI 开发工具盛行的时代,坚持古法制码已经不再现实。人们开始逐渐一有问题就向 AI 求救,比如「更新项目所有的依赖到最新」、「帮我查一下 Workspace 下所有的包的项目依赖是否有冗余或缺失」、「帮我计算一下代码量」,你可能第一时间会想到 AI,确实 AI 看起来很适合做这类苦力活,但…
「人之所思,鲜有全新」,你想到的问题,大抵早已有人求索过,并已有成熟解法可循。
是的,已经有很成熟的解决方案了,不仅免费,效率还远比 AI 工具高(在解决有明确方向问题的时候,传统的方案目前还是要远高效于 AI 的),何必浪费 Token 呢。说到底,AI 最终也是有可能就调用了传统工具来实现你想让它做的苦力活。
本文将介绍以下几款 CLI 工具:
| 工具 | 作用 | 说明 |
|---|---|---|
| taze | 更新依赖 | 用过就忘了旧爱 ncu |
| ncu | 更新依赖 | 老牌工具,用了很多年 |
| knip | 检查依赖冗余或缺失(还有更多能力) | depcheck 自闭前推荐了它,强大到配享官网 |
| depcheck | 检查依赖冗余或缺失 | 于 2023 年自闭 |
| syncpack | 依赖统一 | 这次写文章的惊喜发现,也是强大到配享官网 |
| tokei | 代码行统计 | 更快更智能 |
| cloc | 代码行统计 | 老牌工具,用了很多年 |
🥦 依赖保鲜 - taze / ncu
依赖版本是否保持鲜活,是判断一个前端和 Node 项目是否健康的重要依据。
每次接手一个旧的项目,我第一时间会看看项目依赖陈旧腐朽的程度,然后制定一个稳定升级的计划。我长期以来保持的一个习惯就是,每天打开工作区的第一件事情就是查看并更新项目依赖的版本。
这个习惯给我带来了不少好处,可以第一时间感知业界知名框架、库、工具链的更新,学习并了解新的 API;但也会第一时间受到二、三方包的问题影响。不稳定,你会说?毕竟升级是一把双刃剑,我们需要通过升级享受社区的红利(Bug 修复、功能增强、性能提升),但同时也要承受可能新引入的 Bug。
实际上,通过对二三包版本迭代的感知,我可以将出现问题的版本局限在更小的范围,并给二方包的同事或三方包的作者以更精准和及时的反馈,避免问题扩大,长期的稳定才是真的稳定。
维持惰性稳定,拒绝升级,导致依赖版本陈旧滞后带来的问题,除了无法享受社区带来的红利之外,更严重的,可能导致项目无法维护。我也见过不少这样的项目,开发依赖已经完全和现有的 Node 环境等脱节,甚至在安装阶段就报错。
你可以用以下两个工具:
全局安装
非必需,但推荐。
全局安装,获得全局命令 taze、ncu:
npm install -g taze
npm install -g npm-check-updates # 注意不是 ncu
小试一下
小试一下做个简单的对比。
以下我们要对整个 Workspace(包括根目录)下的所有依赖进行检查并更新到 packages.json,使用的命令分别是 taze -r -w --force 和 ncu -w -u:
ncu 略慢,但差异并不明显,然而如果你用 pnpm -r exec 的话,相差就很明显了(taze 28s,ncu 约 4min):
后话:这也是我从
ncu换成taze的最大原因,因为之前我都是用pnpm -r exec ncu来更新整个 Workspace,并不知道ncu还有个-w参数。这样体现了些文章的重要性,为了写的文章更严谨,我仔细翻看了ncu的参数,发现原来一直误会它了。
常用参数
taze 和 ncu 都提供了非常丰富的命令行参数,以下整理了一份常用的参数对比:
| 功能 | taze | ncu | 备注 |
|---|---|---|---|
| 递归扫描 | -r / --recursive | w / --workspaces --deep | taze -r 和 ncu --deep 对等,即使不属于 Workspace 的目录也会扫 |
| 写入模式 | -w / --write | ‑u / --upgrade | 更新所有 package.json 下包的版本号到制定范围的最新版本 |
| 更新全局 | ‑g / --global | ‑g / --global | ncu 明确说自己不会更新全局,但会给出更新命令;taze 可以用 taze -g --install 一键更新 |
| 忽略缓存 | --force / -f | --cacheClear | taze 默认缓存,需要用 --force 绕过,强制拉取新数据;ncu 则需要通过 --cache 主动开启缓存;不一样的是 taze --force 不会清缓存,而只是绕过,而 ncu --cacheClear 会清掉缓存 |
| 交互模式 | ‑I / --interactive | ‑i / --interactive | 交互模式,由你自己决定哪些升级包的最新版本写入 pacakge.json,即默认写入模式;注意 taze 是大写的 I |
| 忽略包 | --exclude pkg1,pkg2 | ‑x / --reject pkg1,pkg2 | 排除不升级的包;taze 支持 pkg@versionRange 精细排除特定版本范围,比如 taze --exclude typescript@7 只屏蔽 v7,允许 v6 小版本升级 |
| 指定包 | --include pkg1,pkg2 | ‑f / --filter pkg1,pkg2 | 仅更新特定包,两者都支持正则 |
| 忽略路径 | --ignore-paths <paths> | 无 | |
| 分组展示 | --group | --format group | 更直观,也更美观;taze 默认,ncu 需加参数 |
| 超时设置 | --request-timeout 120000 | --timeout 120000 | 设置请求超时时间(ms),默认都是 30 秒 |
| 更新 Peer | --peer | --dep peer | ncu 允许 --dep 参数更细节地控制更新哪类依赖,taze 只有 --peer 开关 |
| 执行安装 | --install | --install <always|never|prompt> | 更新版本完成后,是否执行安装;ncu 控制得更细 |
| 并发控制 | --concurrency N | --concurrency N | |
| 日志控制 | --loglevel <level> | -l, --loglevel <level> | 设置日志输出级别,控制终端输出信息量 |
| 输出 JSON | --json | --jsonUpgraded | 以 JSON 格式输出升级内容(机器可读),命令执行后需要等待一定的时间 |
其他的一些不同:
- 关于更新到目标版本范围,
taze和ncu的设计是完全不一样的,taze 使用了子命令模式 ncu通过--errorLevel N可以控制退出码,而taze没有对等的能力,因此ncu更时长 CI 场景ncu提供了-r, --registry参数;taze没有,但你可以用.npmrc
配置 taze
第一步,安装开发依赖:
pnpm -w add -D taze
第二步,在 package.json 的 scripts 添加命令:
{
...
"scripts": {
"taze": "taze -r"
}
}
第三步,根目录下添加配置文件 taze.config.ts(或.tazerc、.tazerc.json、taze.config.{js,mjs,cjs}):
import {
defineConfig
} from 'taze';
// https://github.com/antfu-collective/taze?tab=readme-ov-file#config-file
export default defineConfig({
mode: 'major', // 更新版本范围
write: true, // 写入 pacakge.json
exclude: [ // 忽略哪些包,建议写好注释
'typescript', // 暂不升 7,@typescript-eslint 不支持,会导致 Eslint 报错
'eslint', // TODO up 10
'@babel/*' // 暂不升 8,间接依赖的 @babel/runtime 导致构建失败
]
});
配置 ncu
第一步,安装开发依赖:
pnpm -w add -D npm-check-updates
第二步,在 package.json 的 scripts 添加命令:
{
...
"scripts": {
"ncu": "ncu -w"
}
}
第三步,根目录下添加配置文件 .ncurc.yaml(或 .ncurc.yml、.ncurc、.ncurc.json、.ncurc.{js,mjs,cjs}:
# 写入 pacakge.json
upgrade: true
# 更新 dependencies 和 devDependencies(忽略 peerDependencies 等)
dep: dev,prod
# 分组展示(更美观)
format: group
# 超时设置
timeout: 120000
# 不要提示安装可以省时间
install: never
# 哪些包暂时不升级(可以加注释说明原因)
reject:
- 'typescript', # 暂不升 7,@typescript-eslint 不支持,会导致 Eslint 报错
- 'eslint' # TODO up 10
- '@babel/*' # 暂不升 8,间接依赖的 @babel/runtime 导致构建失败
小结
taze 和 ncu 在更新依赖方面,都十分强大且好用,个人目前已经全面切换到了 taze,但全局命令依然两个都装。
每次升级完,仔细看输出,尤其注意大版本更新,若大版本不兼容,建议先把对应包配置到忽略列表。
依赖保洁 - knip / depcheck
项目依赖的另外一个很重要又容易被忽视的问题是依赖混乱的问题:
- 版本号混乱,同名包在不同的 Workspace Package 下不一样,容易造成构建体积变大,甚至运行时问题
- 依赖冗余,浪费流量
- 依赖缺失,在有幽灵依赖的情况下较难及时发现,但却是一个隐藏的定时炸弹
版本号混乱的问题,可以用 taze 和 ncu 搞定,剩下的「多了和少了」的问题,可以用下面两个工具:
全局安装
非必需,但推荐。
全局安装,获得全局命令 knip、depcheck:
npm install -g knip
npm install -g depcheck
小试一下
利用两个工具做差不多的任务:检查整个 Workspace(包括根目录)。
可以看到 knip 完胜,简单且高效,还功能更多,而 depcheck 则需要借助 pnpm 的能力(并且需要借助参数 --no-bail 来保证不会提前退出)。
常用参数
depcheck 的功能相对简单,参数也比较少,以下整理了一份常用的参数对比:
| 功能 | knip | depcheck | 备注 |
|---|---|---|---|
| 指定目录 | 直接传路径 knip ./packages/a | depcheck ./packages/a | 两者均支持直接传入项目目录路径作为第一个位置 |
| 递归扫描 | 自动识别 Workspaces | ❌ 不支持,需借助 Shell 循环或 pnpm -r | |
| 输出 JSON | --reporter json | --json | knip 支持多种输出;depcheck 只有简单 JSO |
| 忽略包 | 配置 ignoreDependencies(CLI 无直接 flag,推荐配置文件) | --ignores="pkg1,@scope/*" | |
| 忽略路径 | 配置 ignore | --ignore-patterns=dist,covera | |
| 忽略 bin 包 | 配置 ignoreBinaries | --ignore-bin-package knip 会自动忽略常用的 cross-env、rimraf 等,而 depcheck 只能配置在 ignores(这个配置项貌似无效) 只能配置在 | |
读取 .xxignore | 无 CLI 参数,配置内设置 | --ignore-path=.gitignore | |
| 跳过依赖 | --include dependencies 只看无用依赖 | --skip-missing=trueGitHub | |
| 仅检查某类问题 | --include dependencies/files/exports | 无 | |
| 写入模式 | --fix | ❌ 不支持 | knip 独有,从 package.json 移除未使用的依赖,不建议使用 |
配置 knip
knip 提供了在项目中初始化的命令 pnpm create @knip/config,但生的配置文件是 knip.json。由于配置相对简单,完全可以手动做。
第一步,安装开发依赖:
pnpm -w add -D knip
第二步,在 package.json 的 scripts 添加命令:
{
...
"scripts": {
"knip": "knip"
}
}
需要说明一下 --strict 参数,这是一个只在命令行的参数,配置文件中并没有它的位置。
有的时候带上 --strict 可以对齐 pnpm 的严格幽灵依赖检测,但 --strict 的隐含意思是仅扫描生产用的代码,什么意思呢,就是配置文件、单测、Storybook 等它不会管。
正因如此,并不建议你在 NPM scripts 中加上 --sctrict,而是你可以偶尔执行的时候带上这个参数检查看看有没有幽灵依赖。
第三步,根目录下添加配置文件 knip.config.ts(或 .knip.{json,jsonc}、 knip.{json,jsonc}、knip.{ts,js}、knip.config.{ts,js},也可以在 pacakge.json 添加 knip 字段):
import {
KnipConfig
} from 'knip';
export default {
workspaces: {
'.': {
entry: [ // 这是一个 Taro 小程序,因此会有多个 entry
'src/app.tsx',
'src/app.config.ts',
'src/{pages,subpackages}/**/index.{ts,tsx,config.ts}'
],
project: [
'src/**/*.{ts,tsx,less,css}'
],
paths: {
'@/*': ['./src/*']
}
}
},
tags: ['-lintignore'],
ignoreDependencies: [/^@tarojs\//]
} satisfies KnipConfig;
希望在 VSCode 或 WebStorm 下用 knip 的,官方也提供了插件,甚至还提供了给 AI 的 MCP Server 等,详见 Knip - Integrations。
配置 depcheck
第一步,安装开发依赖:
pnpm -w add -D depcheck
第二步,在 package.json 的 scripts 添加命令:
{
...
"scripts": {
"depcheck": "pnpm -r --include-workspace-root --no-bail exec depcheck"
}
}
第三步,根目录下添加配置文件 .depcheck.yaml(或 .depcheck.yml、.depcheck.json):
ignorePatterns:
- dist
- __*
ignores:
- '@/*'
- '@babel/*'
- '@commitlint/*'
- '@kcuf/*config'
- '@tarojs/*'
- '@vitest/coverage-v8'
- eslint-import-resolver-custom-alias
- cross-env
- lint-staged
- rimraf
- markdownlint-cli2
- npm-package-json-lint
- typescript
- autoprefixer
- depcheck
- husky
- lerna
- less
- postcss
- react-refresh
- stylelint
可以看到为了避免干扰,需要写好多忽略项。depcheck 默认只扫描 TS/JS 代码中的 import/require,无法识别像 package.json#scripts 和其他配置文件中的隐性依赖,因此会产生大量的误报。虽然它也提供了 --ignore-bin-package 和 --specials 参数,但后者貌似不起作用,就是还需要写一大堆的 ignores。
小结
无论是功能的强大性,还是配置的简单来说,knip 都略胜 depcheck 好几筹。
虽然我建议把更新 package.json(taze 的 write: true,ncu 的 update: true)的配置项开启,但提交改动到 Git 前请一定仔细看好了哪些包升级了,并着重关注大版本。若本地运行发现问题,及时回滚版本变更。
若发现小版本升级的包有问题,可以设置 override 或 package.json#resolutions(pnpm <11)暂时绕过。
🪢 版本混乱 - syncpack
写这篇文章的时候,从 knip 的文档 了解到 syncpack,一个能够梳理并解决依赖版本混乱的 CLI 工具,也是一个配享官网的工具。
syncpack 的功能和 knip 有一定的重叠,也可以检查和升级更新 syncpack update,但它更核心的能力是检查大型 Monorepo 项目下依赖项是否一致,这也正是它名字的由来。
全局安装
非必需,但推荐。
全局安装,获得全局命令 syncpack:
npm install -g syncpack
小试一下
和上边介绍的几个小工具不一样,syncpack 主要以子命令的形式:
| 命令 | 功能 | 说明 |
|---|---|---|
list | 列出所有依赖及出现次数 | |
lint | 列出版本不一致性问题 | |
fix | 修复 lint 查出的问题 | |
format | 修改 package.json 的格式 | 它会修改字段顺序,不会修改版本号,而字段顺序我一般交给 npmpackagejsonlint 管 |
update | 更新依赖版本号 | 交给 taze 吧 |
以下是 syncpack list 和 syncpack lint 的效果:
配置 syncpack
由于我刚认识
syncpack,而且它跟taze有一定的功能重叠,所以目前我暂时还没有将它写到 NPM scripts 下。但你依然可以试试装到项目依赖。
第一步,安装开发依赖:
pnpm -w add -D syncpack
第二步,在 package.json 的 scripts 添加命令:
{
...
"scripts": {
"syncpack": "syncpack lint"
}
}
第三步,根目录下添加配置文件 syncpack.config.ts(或 .syncpackrc、.syncpackrc.config.{json,yaml,yml,js,ts,mjs,cjs}、 syncpack.config.{js,mjs,cjs},也可以在 pacakge.json 添加 syncpack 或 config.syncpack 字段):
export default {
indent: " ",
} satisfies import("syncpack").RcFile;
小结
syncpack 可以帮你查你的项目下总共有多少依赖,每个依赖被分别声明了多少次,并且哪些依赖的声明可能不一致或有问题,从而使你的项目依赖更整洁。它同时具备更新依赖版本的能力(类似 taze)。
🧮 项目体量 - tokei / cloc
我比较喜欢数代码:
- 新的代码方案替换原有的糟糕实现,想看一下新方案能够带来的代码量是否减少
- 新写完一个功能,估算新增的代码量
- 老板问项目体量
- 想查一下项目中的巨型文件(可维护性杀手)
可用工具:
我用了很久的 cloc,但 cloc 不够智能,首先它必须带路径参数,输入 cloc 不会给结果,而 cloc . 会把巨大的 node_modules 也算进去,耗时耗力还不是我们想要的结果。我需要一个更智能、更快的工具,就是 tokei,你只需要在项目目录下执行 tokei 即可得到答案,完全不用等,也不需要写复杂的参数。
tokei 之所以快,除了它是 Rust 写的之外,它还会主动忽略 node_modules 和 .gitignore 等 Ignore 文件的内容,因此构建路基等也会被忽略。
我转投 tokei 另外一个重要的原因,也是第一大原因,是 cloc 会区分 JS 和 JSX,却不区分 TS 和 TSX,而 tokei 会。
安装
cloc 是 Perl 写的,而 tokei 是 Rust,两者都可以用 brew 安装:
brew install tokei
brew install cloc
小试一下
来查一个项目所有源码部分,包含工程配置、文档,但不包含 node_modules 和构建产物目录:
tokei
cloc . --exclude-dir=node_modules,dist
可以看出 tokei 无论在性能、易用性,还是美观上,都比老牌的 cloc 略胜一筹。两者数据稍有差异。另外就是,最好排除一下 pnpm-lock.yaml,这玩意儿可有万行呢。
常用参数
摊牌了,以下表格由 AI 整理(但也是经过了 Review 的)。
| 功能说明 | tokei | cloc | 备注 |
|---|---|---|---|
| 目录 / 文件 | tokei src | cloc src | tokei 默认当前目录,可以不写;但 cloc 必须写路径 |
| 排除 | -e, --exclude <glob> | --exclude-dir=dir1,dir2--exclude-ext=js,min.js | tokei 支持 glob;cloc 区分目录和后缀,不支持 glob |
| 明细 | -f, --files | --by-file | 输出每个文件的统计结果 |
| 指定语言 | -t, --type TypeScript,TSX | --include-lang=TypeScript | tokei 语言名大小写敏感 |
| 排除指定语言 | 无原生参数 | --exclude-lang=Markdown | tokei 可搭配--exclude过滤后缀 |
| 输出格式 | -o,--output json/yaml/cbor | --json / --yaml / --xml / --csv / --sql | cloc 输出格式更多,含 SQL、XML |
| 输出文件 | 重定向 > out.json | --report-file=out.txt | cloc 原生支持输出文件参数 |
| 排序输出 | -s,--sort code/files/lines/comments/blanks | --sort=code | tokei 可按空白行、注释排序;cloc 仅支持 files、code、comment |
读取 .gitignore | 默认读取,可以 --no-ignore 关闭默认行为 | 不管 | tokei 尊重 .gitignore 等,不会碰哪些不算在源码中的文件和目录,所以快;cloc 需要手动排除 node_modules、dist 等 |
| 统计隐藏 | --hidden | --include-hidden | 默认两者都跳过隐藏文件 |
| 不递归 | 无,总是递归 | --no-recurse | tokei 无法关闭递归子目录,只能 -e 排除子目录 |
| 查看支持语言列表 | -l, --languages | --show-ext | 打印全部识别语言和后缀映射 |
| 差异对比 | 不支持 diff | --diff dirA dirB | cloc 王牌能力,对比两套代码行数增减,支持 git commit |
| 处理压缩包 | 不支持 | 原生支持 zip/tar.gz 等归档 | cloc 可直接统计压缩包内部代码,tokei 需要先解压 |
小结
这两个工具都不需要在项目中安装和配置(因为不是 npm 包),虽然也有同名的 npm 包,切不要搞混了。虽然我是 cloc 的老用户, 但 tokei 更智能、更简单、更快,很难不让人「移情别恋」。
🏓 小技巧汇总
taze
taze -r # 递归扫描,包括根目录和所有 Workspace 包
taze --force # 绕过本地 npm meta 缓存(不是强制升级版本)
taze --maturity‑period‑exclude # 忽略包发布冷却时间,直接看最新版
ncu
ncu --registry https://registry.npmmirror.com # 指定 Resistry 提速
ncu -w # 扫描整个 Workspace,包括根目录和所有 Workspace Packages
ncu -u # 扫描当前目录,并更新
ncu -wu # 扫描整个 Workspace,并更新
ncu -ui # 交互式更新
ncu -u --prod # 仅升级生产依赖 dependencies
ncu -u --dev # 仅升级生产依赖 devDependencies
ncu -u vite,vitest # 仅更新制定包
knip
knip --include unlisted # 幽灵依赖
knip --include dependencies # 未使用依赖
knip --include files # 未使用文件
knip --include exports # 未使用 exports
tokei
tokei -f -s lines # 项目文件大小明细,以代码行数排序以便找到巨屎
tokei -t TypeScript,TSX -f -s lines # 更精确一些,查找巨屎 TS 或 TSX 文件
cloc
cloc src packages-*/*/src # 统计所有 src 下的代码
cloc src --by-file # 项目文件大小明细,自动以代码行数排序
🙋 FAQ
❓ 明明刚发的包,taze 怎么没找到?
可能被缓存了,需要 taze --force。
❓ 利用 pnpm -r 的 depcheck 如何忽略某 Workspace?
使用 pnpm --filter 反向过滤:
pnpm -r --no-bail --filter '!documentation' exec depcheck
❓ 如何升级到 升级到 alpha、beta、rc 等预发布版本?
用 taze newest。
ncu 则提供了 --target 参数,取值 latest, newest, greatest, minor, patch, semver, @[tag],还有一个 --pre 参数专门用来升级预发布版本。
📌 链接
🪭 写在最后
不要把项目依赖成为黑盒,作为合格的开发者,0999
如果说 ESLint、Prettier 是代码级的洁癖,那 taze、ncu、knip、depcheck、tokei 这类 CLI 工具,就是 工程级的洁癖解药。
它们不帮你格式化代码、不约束语法风格,却能帮你清理项目冗余、更新依赖版本、统计代码结构,让整个项目从「能跑」变成「干净、通透、健康、可维护」。