pnpm 依赖清单引擎 `@pnpm/deps.inspection.list` 深度剖析:pnpm list / pnpm why 的实现基石
本文以 pnpm 仓库中的 @pnpm/deps.inspection.list 包(位于 pnpm11/deps/inspection/list)为主体,剖析它在符号链接式 node_modules 中"列出已安装包"的核心机制:从 pnpm list 的前向依赖树、pnpm why 的反向依赖追溯,到 tree / JSON / parseable 三种输出格式与完整参数体系。读完本文,你将掌握这一清单引擎的 API 设计、渲染管线与 peer 变体标注原理,并知道如何在自己的工具链中复用它。
包定位:为什么"列出依赖"在 pnpm 中是一门专门学问
@pnpm/deps.inspection.list 的官方描述只有一句话:List installed packages in a symlinked node_modules。这句话点明了它与 npm 系其他包管理器最大的不同:pnpm 的 node_modules 是符号链接结构的(软链 + 虚拟存储 .pnpm 目录),依赖的真实物理位置与逻辑导入路径并不一致,因此"列出已安装的包"不能简单地读取一层目录,而必须:
- 以锁文件(
pnpm-lock.yaml的 wanted / current 两份)与各 importer 的package.json为依据重建依赖层级; - 通过
@pnpm/deps.inspection.tree-builder构建前向依赖树(dependencies tree)与反向依赖树(dependents tree); - 在渲染层处理生产依赖、dev 依赖、optional 依赖、未保存依赖(extraneous)的分类着色,以及 peer 变体(peersSuffixHash)与去重(deduped)标注。
该包在 pnpm11 中被拆分为独立工作区包,供 CLI 层的 list、why、ll、ls 等命令复用。其版本信息与依赖声明见 pnpm11/deps/inspection/list/package.json(当前版本 1101.0.4,要求 Node >=22.13,ESM 模块)。
安装与快速上手
按 pnpm11/deps/inspection/list/README.md 的安装说明,直接通过 pnpm 添加即可:
pnpm add @pnpm/deps.inspection.list
该包以 TypeScript 编写(tsgo --build 编译,输出 lib/index.js + lib/index.d.ts),属于 MIT 许可的开源实现。在源码层面,它依赖以下工作区兄弟包(均以 workspace:* 关联,见 package.json):
@pnpm/deps.inspection.tree-builder:依赖树 / 反向依赖树的构建核心;@pnpm/lockfile.fs:读写 wanted / current 锁文件;@pnpm/workspace.project-manifest-reader:安全读取各 importer 的package.json;@pnpm/text.tree-renderer与@pnpm/text.ordinal-comparator:树形渲染与包名排序;@pnpm/pkg-manifest.reader、@pnpm/types:清单读取与类型定义。
核心 API 全景
包的公开入口是 pnpm11/deps/inspection/list/src/index.ts,导出五个主要函数与一组渲染函数:
| API | 职责 |
|---|---|
list(projectPaths, opts) |
列出给定项目已安装的依赖层级,返回格式化后的字符串(pnpm list 的底层) |
listForPackages(packages, projectPaths, opts) |
在依赖层级中检索指定包名并高亮命中项(pnpm list <pkg> 的底层) |
searchForPackages(packages, projectPaths, opts) |
返回检索命中的结构化 PackageDependencyHierarchy[](不渲染) |
whyForPackages(packages, projectPaths, opts) |
反向追溯指定包被谁依赖(pnpm why <pkg> 的底层) |
flattenSearchedPackages(pkgs, opts) |
将层级结构扁平化为 depPath(如 pkg > dep@1.0.0)序列 |
list / listForPackages:前向依赖清单
list 先通过 getPackagesForListing 收集所有 importer 的依赖树,再交给 getPrinter 按 reportAs 选择渲染器输出字符串。这里有一个关键分支(src/index.ts):
- 当
depth === -1(即pnpm list --depth -1)时,完全不构建依赖树,每个项目仅返回空层级,仅展示项目本身; - 否则调用
buildDependenciesTree构建树,并把每个 importer 的name、version、private、path与树信息合并成PackageDependencyHierarchy(类型定义见 src/types.ts)。
listForPackages 则在 searchForPackages 内部通过 createPackagesSearcher 创建查找器(可自定义 finders),并把 showDedupedSearchMatches: true 传给 tree-builder,让被搜索包的命中信息(searchMessage、searched 标记)随树一起返回,从而在渲染时对命中包加粗高亮。
whyForPackages:反向依赖追溯
whyForPackages(src/index.ts)与 list 方向相反:它优先读取当前锁文件(readCurrentLockfile,位置在 <lockfileDir>/node_modules/.pnpm),不存在时回退到 wanted 锁文件(readWantedLockfile);随后从锁文件的 importers 构建 importerInfoMap,再调用 buildDependentsTree 得到每个目标包的反向依赖树,最后同样按 tree / json / parseable 三种格式渲染。这也解释了 pnpm why 与 pnpm list 使用同一渲染引擎、但语义完全相反的设计。
三种输出格式的渲染管线
渲染函数集中在 src/renderTree.ts、src/renderJson.ts、src/renderParseable.ts 与 src/renderDependentsTree.ts,通过 reportAs: 'tree' | 'json' | 'parseable' 切换(getPrinter 见 src/index.ts)。
tree:人类可读的层级树
树形输出由 renderTree 生成,其特性包括:
- 每个项目以加粗的
name@version 路径作为根,(PRIVATE)标记私有包; - 依赖按
dependencies:、devDependencies:、optionalDependencies:分组(组标签用cyanBright),若开启showExtraneous还会输出not saved (you should add these dependencies to package.json if you need them):分组; - 每个分组的包名按字典序排序(
lexCompare),并按依赖类型着色:生产依赖默认色、optional 依赖蓝色、dev-only 依赖黄色、未保存依赖红色; - 树顶部输出图例:
Legend: production dependency, optional only, dev only; - 支持
--long:额外显示每个包的description、repository、homepage与物理路径; - 支持
showSummary:输出形如N packages in M projects的汇总(统计逻辑见listSummary)。
测试用例对该输出的精确断言可参考 pnpm11/deps/inspection/list/test/index.ts,例如外部锁文件场景下 pkg@1.0.0 根节点与 is-positive@1.0.0 的树形输出,以及多项目工作区中"图例只打印一次"的行为验证。
json:机器可读的结构化输出
renderJson 输出缩进 2 空格的 JSON 数组,每个元素包含 name、version、path、private,并按字段组织 dependencies、devDependencies、optionalDependencies、unsavedDependencies。非 --long 模式下每个依赖项为 { from, version, resolved, path } 结构;--long 模式则合并 getPkgInfo 的 description、license、author、homepage、repository 等完整元数据。去重节点会附带 deduped: true 与 dedupedDependenciesCount(见 src/renderJson.ts)。
parseable:便于脚本解析的纯路径输出
renderParseable 输出每行一个真实物理路径(短格式),--long 模式下改为 路径:alias npm:name@version 或 路径:name@version。实现上通过 flatten 递归展开并用 Set<string> 去重——因为输出是扁平的,同一包被多个父包重复依赖时只输出一次(src/renderParseable.ts)。别名包(npm: 协议)会显示为 alias@npm:name@version 形式。
关键配置项与默认值
所有 API 共享一组默认值(src/index.ts):
const DEFAULTS = {
alwaysPrintRootPackage: true, // 即使无任何依赖也打印项目根节点
depth: 0, // 递归深度,-1 表示不展开依赖
long: false, // 是否展示完整元数据
registriesByScope: undefined, // 按 scope 的 registry 映射
reportAs: 'tree', // tree | json | parseable
showExtraneous: true, // 是否显示未保存进 package.json 的依赖
}
调用方可通过参数覆盖({ ...DEFAULTS, ...maybeOpts })。其余重要选项包括:
include: { dependencies, devDependencies, optionalDependencies }:按字段过滤展示范围;onlyProjects:仅展示工作区项目本身;excludePeerDependencies:剔除 peer 依赖;checkWantedLockfileOnly:仅依据 wanted 锁文件(跳过 current 锁文件);modulesDir:指定模块目录(默认node_modules);virtualStoreDirMaxLength:虚拟存储目录最大长度,传递给 tree-builder 用于路径截断策略;finders:自定义包查找器,用于list <pkg>场景。
peer 变体与去重标注:pnpm 依赖图的独有细节
符号链接式 node_modules 中,同一个 name@version 可能因 peer 依赖组合不同而存在多个物理实例。渲染层通过 src/peerVariants.ts 处理这一复杂性:
collectHashes沿树收集每个name@version的peersSuffixHash集合;filterMultiPeerEntries只保留出现多于一种 peer 哈希的包;- 渲染时对这类包追加红色后缀
peer#<hash> (N variations)(见peerHashSuffix),让用户直观看到 peer 变体的数量; - 被去重的节点追加灰色
[deduped]标签;当树中该节点无子节点时直接标记而不展开。
这套逻辑同时应用于前向树(renderTree 的 findMultiPeerPackages)与反向树(renderDependentsTree 的对应实现)。反向树还会在形成环的依赖上标注 [circular],并在叶子(importer)节点显示其所属依赖字段,如 (dependencies) / (devDependencies)。
pnpm why 输出与汇总
renderDependentsTree(src/renderDependentsTree.ts)生成 pnpm why 的树形输出:根节点为被查询的 name@version,向下逐层展开"谁依赖了它",直到 importer 叶子;depth 可截断展开深度。输出末尾追加 Found N versions of <name> 汇总(若存在同名多版本或多实例则追加 N instances)。JSON 模式支持 --long 元数据合并与 truncateDependents 深度截断;parseable 模式输出 name@version > name@version > ... 的路径序列并反转以 importer 优先展示。
测试与工程配套
包的测试覆盖了外部锁文件、多项目工作区、无包名/无版本项目、私有包、别名依赖等多种场景(见 pnpm11/deps/inspection/list/test/index.ts 与 pnpm11/deps/inspection/list/test/renderDependentsTree.test.ts),并配有 fixture 仓库 many-deps。测试通过 @jest/globals + @pnpm/test-fixtures 运行,pretest 依赖 tree-builder 的 fixture 准备。示例工程见 pnpm11/deps/inspection/list/example(含 index.js、package.json 与 pnpm-lock.yaml),可直接作为最小可运行样例。
小结
@pnpm/deps.inspection.list 是 pnpm 依赖清单能力的统一出口:它把"符号链接式 node_modules 中究竟装了什么、为什么装着"这一复杂问题,收敛为 list / listForPackages / whyForPackages 三个核心 API + 三套渲染器,并用颜色图例、peer 变体后缀、[deduped]/[circular] 标注把 pnpm 依赖图的特殊性完整呈现给用户。无论是理解 pnpm list 与 pnpm why 的行为,还是在自定义工具中复用其能力,这份源码都是最直接的参考实现。