pnpm 依赖清单引擎 `@pnpm/deps.inspection.list` 深度剖析:pnpm list / pnpm why 的实现基石

原创2026-09-19 21:47:191,730 阅读
文章标签:包管理器开发工具CLI

本文以 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 层的 listwhyllls 等命令复用。其版本信息与依赖声明见 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 的依赖树,再交给 getPrinterreportAs 选择渲染器输出字符串。这里有一个关键分支(src/index.ts):

  • depth === -1(即 pnpm list --depth -1)时,完全不构建依赖树,每个项目仅返回空层级,仅展示项目本身;
  • 否则调用 buildDependenciesTree 构建树,并把每个 importer 的 nameversionprivatepath 与树信息合并成 PackageDependencyHierarchy(类型定义见 src/types.ts)。

listForPackages 则在 searchForPackages 内部通过 createPackagesSearcher 创建查找器(可自定义 finders),并把 showDedupedSearchMatches: true 传给 tree-builder,让被搜索包的命中信息(searchMessagesearched 标记)随树一起返回,从而在渲染时对命中包加粗高亮。

whyForPackages:反向依赖追溯

whyForPackagessrc/index.ts)与 list 方向相反:它优先读取当前锁文件readCurrentLockfile,位置在 <lockfileDir>/node_modules/.pnpm),不存在时回退到 wanted 锁文件(readWantedLockfile);随后从锁文件的 importers 构建 importerInfoMap,再调用 buildDependentsTree 得到每个目标包的反向依赖树,最后同样按 tree / json / parseable 三种格式渲染。这也解释了 pnpm whypnpm list 使用同一渲染引擎、但语义完全相反的设计。

三种输出格式的渲染管线

渲染函数集中在 src/renderTree.tssrc/renderJson.tssrc/renderParseable.tssrc/renderDependentsTree.ts,通过 reportAs: 'tree' | 'json' | 'parseable' 切换(getPrintersrc/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:额外显示每个包的 descriptionrepositoryhomepage 与物理路径;
  • 支持 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 数组,每个元素包含 nameversionpathprivate,并按字段组织 dependenciesdevDependenciesoptionalDependenciesunsavedDependencies。非 --long 模式下每个依赖项为 { from, version, resolved, path } 结构;--long 模式则合并 getPkgInfodescriptionlicenseauthorhomepagerepository 等完整元数据。去重节点会附带 deduped: truededupedDependenciesCount(见 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@versionpeersSuffixHash 集合;
  • filterMultiPeerEntries 只保留出现多于一种 peer 哈希的包;
  • 渲染时对这类包追加红色后缀 peer#<hash> (N variations)(见 peerHashSuffix),让用户直观看到 peer 变体的数量;
  • 被去重的节点追加灰色 [deduped] 标签;当树中该节点无子节点时直接标记而不展开。

这套逻辑同时应用于前向树(renderTreefindMultiPeerPackages)与反向树(renderDependentsTree 的对应实现)。反向树还会在形成环的依赖上标注 [circular],并在叶子(importer)节点显示其所属依赖字段,如 (dependencies) / (devDependencies)

pnpm why 输出与汇总

renderDependentsTreesrc/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.tspnpm11/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.jspackage.jsonpnpm-lock.yaml),可直接作为最小可运行样例。

小结

@pnpm/deps.inspection.list 是 pnpm 依赖清单能力的统一出口:它把"符号链接式 node_modules 中究竟装了什么、为什么装着"这一复杂问题,收敛为 list / listForPackages / whyForPackages 三个核心 API + 三套渲染器,并用颜色图例、peer 变体后缀、[deduped]/[circular] 标注把 pnpm 依赖图的特殊性完整呈现给用户。无论是理解 pnpm listpnpm why 的行为,还是在自定义工具中复用其能力,这份源码都是最直接的参考实现。

登录后查看全文
pnpm