
pnpm 工作区项目图构建器 pnpm/workspace.projects-graph 深度解析从包清单到依赖图【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpmpnpm/workspace.projects-graph是 pnpm 工作区workspace体系中负责把一组包packages的清单manifest转化为依赖关系图的核心工具包。它在 pnpm 的安装、过滤、拓扑排序、发布审批等流程中被广泛使用本文将以 pnpm11/workspace/projects-graph/README.md 为主线结合仓库源码与测试用例完整讲解其安装方式、API 用法、内部实现原理及在 pnpm 实际流程中的调用链。读完本文你将掌握如何用createProjectsGraph在自己的工具链中构建工作区依赖图并理解unmatched、linkWorkspacePackages、ignoreDevDeps等关键行为背后的设计意图。一、这个包解决什么问题在 pnpm 的 monorepo 工作区中pnpm-workspace.yaml声明了哪些目录属于工作区成员每个成员都有自己的package.json即 manifest。为了执行安装、--filter递归过滤、按依赖顺序运行脚本等操作pnpm 需要回答一个核心问题这些包之间谁依赖谁。pnpm/workspace.projects-graph的职责正是把“一个包数组”变成“一张以目录为节点的有向图”。它位于 pnpm11 的 workspace 模块之下与 projects-reader负责读取工作区项目、projects-sorter负责拓扑排序、projects-filter负责按选择器过滤协同工作构成工作区元数据处理的完整链路。二、安装在任意使用 pnpm 作为包管理器的项目中直接添加该依赖pnpm add pnpm/workspace.projects-graph从该包的 package.json 可以看到它是一个type: module的 ESM 包入口为lib/index.js对 Node.js 的要求是22.13。其运行时依赖包括pnpm/npm-package-arg解析npanpm package arg规格pnpm/resolving.npm-resolver提供parseBareSpecifier与workspacePrefToNpm等规格解析工具pnpm/workspace.range-resolver负责把 semver 范围与工作区可用版本做匹配pnpm/types类型定义ramda提供map等函数式工具。三、基本用法README 中的核心用法如下原示例中函数名为createPkgsGraph仓库源码中实际导出名为createProjectsGraph下文以源码实现为准import createPkgsGraph from pkgs-graph const { graph } createPkgsGraph([ { dir: /home/zkochan/src/foo, manifest: { name: foo, version: 1.0.0, dependencies: { bar: ^1.0.0, }, }, }, { dir: /home/zkochan/src/bar, manifest: { name: bar, version: 1.1.0, }, } ]) console.log(graph) // { // /home/zkochan/src/foo: { // dependencies: [/home/zkochan/src/bar], // manifest: { // name: foo, // version: 1.0.0, // dependencies: { // bar: ^1.0.0, // }, // }, // }, // /home/zkochan/src/bar: { // dependencies: [], // manifest: { // name: bar, // version: 1.1.0, // }, // }, // }从源码 src/index.ts 来看实际的 API 签名是export interface BaseProject { manifest: BaseManifest rootDir: ProjectRootDir } export interface ProjectGraphNodePkg extends BaseProject { package: Pkg dependencies: ProjectRootDir[] } export function createProjectsGraphPkg extends BaseProject ( projects: Pkg[], opts?: { ignoreDevDeps?: boolean linkWorkspacePackages?: boolean } ): { graph: RecordProjectRootDir, ProjectGraphNodePkg unmatched: Array{ pkgName: string, range: string } }3.1 输入包数组每个输入项是一个BaseProject包含两个字段rootDir项目根目录的绝对路径作为图中节点的唯一标识README 示例中的dir即对应源码中的rootDirmanifest该项目的package.json内容BaseManifest类型至少应包含name与version以及可选的dependencies、devDependencies、optionalDependencies、peerDependencies。3.2 输出graph 与 unmatched函数返回一个对象包含两部分graph以ProjectRootDir为键、ProjectGraphNode为值的映射。每个节点的package保留原始项目对象dependencies是解析出的工作区内部依赖目录列表unmatched本次解析中未能匹配到任何工作区成员的依赖清单每项形如{ pkgName: bar, range: ^10.0.0 }。未匹配的依赖说明它需要从 registry 安装这正是 pnpm 区分“链接本地包”与“下载外部包”的依据。四、内部实现依赖边是如何算出来的createProjectsGraph的实现分为三部分建索引、遍历依赖、输出结果。4.1 建立索引在 src/index.ts 中首先通过createProjectMap把项目数组转为RecordProjectRootDir, BaseProject随后按需惰性创建两个辅助索引仅当遇到相应类型的依赖时才构建getProjectMapByManifestName以 manifest 的name为键指向同名项目列表因为工作区中允许存在同名不同版本的项目例如foo1与foo2getProjectMapByDir以path.resolve(rootDir)为键的目录索引用于快速定位目录依赖。4.2 合并四类依赖createNode函数按如下顺序合并依赖对象src/index.tsconst dependencies { ...project.manifest.peerDependencies, ...(!opts?.ignoreDevDeps project.manifest.devDependencies), ...project.manifest.optionalDependencies, ...project.manifest.dependencies, }这里有两个关键点四类依赖全被纳入图peer、dev默认、optional、普通 dependencies 都会被当作工作区内部的潜在依赖边ignoreDevDeps: true时 devDependencies 被排除。这对应测试create package graph respects ignoreDevDeps true当bar的foo依赖只写在devDependencies中时开启该选项后bar的dependencies变为[]见 test/index.ts。4.3 对每条依赖逐一解析对每个[depName, rawSpec]条目解析流程如下判断是否为 workspace 协议若rawSpec.startsWith(workspace:)则先通过workspacePrefToNpm将其转换为普通 npm 规格。若转换后是相对路径形式workspace:../foo、workspace:./foo则降级为目录依赖处理解析 bare specifier对workspace:foo*这类别名语法用parseBareSpecifier拆出真实包名foo与范围*npa 解析调用npa.resolve(depName, rawSpec, project.rootDir)得到规格对象spec解析失败如格式怪异则跳过该依赖按类型分流spec.type directory走目录解析路径见下文 4.4spec.type version | range走按包名 版本匹配路径见下文 4.5其他类型git、tag、file 等不构成工作区内部边直接返回空。测试中有一项专门验证“怪异依赖被跳过”weird-dep: :aaaaa不会出现在结果边中见create package graph for local directory dependencies测试。4.4 目录依赖的解析当规格是directory类型如../foo、file:../foo2、workspace:./nested-foo时以project.rootDir为基准用path.resolve计算出绝对路径resolvedPath在projectMapByDir中精确查找若未命中再走“慢路径”——在全部项目中做一次path.relative比较用于兼容大小写不敏感文件系统上的大小写不一致情况命中则返回对应项目的rootDir作为依赖边未命中则返回空。测试create package graph for local directory dependencies与create package graph for local directory dependencies using the workspace protocol with a ./ prefix分别验证了../foo、file:../foo2、workspace:./nested-foo三种写法的解析结果。4.5 按名称 版本范围匹配对于 version/range 类型的依赖在projectMapByManifestName[depName]中找出同名候选项目收集它们的version列表linkWorkspacePackages false的严格模式若显式传入false注意代码注释向后兼容undefined不算且依赖不是workspace 协议写法则该依赖一律视为“不链接工作区包”记入unmatched并跳过见 src/index.tsworkspace 协议 无版本项目当候选项目都没有version字段例如仅声明name且规格为 workspace 协议时直接取同名项目。测试successfully create a package graph even when a workspace package has no version正是针对此场景对应 issue #3933 的修复精确版本命中若versions中包含rawSpec则找到同名同版本的项目范围匹配否则调用resolveWorkspaceRange(rawSpec, versions)求最大满足版本仍不匹配则记入unmatched。范围匹配的实现在 range-resolver/src/index.tsexport function resolveWorkspaceRange (range: string, versions: string[]): string | null { if (range * || range ^ || range ~ || range ) { return semver.maxSatisfying(versions, *, { includePrerelease: true, }) } return semver.maxSatisfying(versions, range, { loose: true, }) }值得注意的是*、^、~、空字符串这四种“裸范围”会被统一当作*处理并且includePrerelease: true——这正是测试* matches prerelease versions中foo: *能匹配到1.0.0-0的原因。4.6 拓扑排序下游如何消费这张图createProjectsGraph只负责建图排序由 projects-sorter/src/index.ts 中的sequenceGraph完成它把graph转成MapProjectRootDir, ProjectRootDir[]后交给graphSequencer来自pnpm/deps.graph-sequencer返回确定性的拓扑顺序与环检测结果。这也印证了 projects-graph 在整个工作区流水线中处于“地基”位置。五、关键选项的行为语义5.1 linkWorkspacePackages链接工作区包开关该选项控制“是否把工作区成员当作本地可链接依赖”不传或传true默认凡是能在工作区内找到匹配版本的依赖都解析为内部边传false只有显式使用workspace:协议的依赖才被链接普通 semver 范围如foo: 1.0.1一律视为外部依赖记入unmatched。测试create package graph respects linked-workspace-packages false给出了完整对照bar2依赖foo: 1.0.1非 workspace 协议在linkWorkspacePackages: false下其dependencies为[]并进入unmatched而bar1的workspace:*、bar3的workspace:~1.0.0、bar4的workspace:^、bar5的workspace:~仍全部链接到FOO1_PATH。5.2 ignoreDevDeps忽略开发依赖当为true时devDependencies不再参与建图。这通常用于“生产依赖闭包”的计算场景——例如发布或生产安装时不需要 dev 依赖产生的内部边。六、在 pnpm 实际流程中的调用链createProjectsGraph并非孤立工具它在 pnpm 的多条核心路径上被调用安装流程installDeps.ts 在安装时通过createProjectsGraph(allProjects, { linkWorkspacePackages: Boolean(opts.linkWorkspacePackages) }).graph构建全量工作区图作为allProjectsGraph传给后续流程——这也解释了linkWorkspacePackages选项与 pnpm 配置项link-workspace-packages的直接对应关系过滤流程projects-filter/src/index.ts 导入createProjectsGraph在--filter选择器解析、变更项目检测中基于全量图计算“被选中项目之间的依赖关系”从而支持pnpm --filter ... run时按依赖顺序执行发布审批approvalOrder.ts 在发布流程中依据该图确定审批顺序。配合测试目录 test/index.ts 中的十余个用例可以完整覆盖基本建图、peer 依赖、目录依赖、workspace 协议含./前缀、别名workspace:foo*、workspace:^/workspace:~、linkWorkspacePackagesfalse、ignoreDevDepstrue、预发布版本匹配、无版本工作区包等边界场景。七、总结pnpm/workspace.projects-graph以极简的 API一个函数、两个可选开关封装了 pnpm 工作区依赖解析的完整逻辑四类依赖合并、workspace 协议转换、目录依赖定位、semver 范围匹配与未匹配收集。它是 pnpm 安装、过滤、排序、发布等上层能力的“图数据源”理解它的输入输出与选项语义是理解 pnpm monorepo 行为的关键一步。若需继续深入可阅读其依赖的 workspace.range-resolver版本匹配、workspace.projects-reader项目读取以及消费方 workspace.projects-sorter拓扑排序和 workspace.projects-filter过滤。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考