包管理器开发工具CLI【免费下载链接】pnpmFast, disk space efficient package manager项目地址https://gitcode.com/gh_mirrors/pn/pnpm点击查看免费下载导读在 pnpm 的 TypeScript 源码仓库中pnpm11/pkg-manifest/utils包名pnpm/pkg-manifest.utils是专门用于处理 package manifest即package.json解析后的对象的工具集。它承载了 pnpm 在安装、添加、更新依赖时对依赖字段dependencies、devDependencies、optionalDependencies、peerDependencies的合并、筛选、读取与写回逻辑并实现了版本范围range风格的推断与格式化。本文将以该包的源码与测试为据逐函数讲解其能力边界、实现原理与在 pnpm 中的实际应用场景帮助你理解 pnpm 是如何安全、精准地操作 manifest 的。包概览与工程结构官方 README 对该包的定位只有一句话Utils for dealing with package manifest处理 package manifest 的工具函数。从其 package.json 可以确认包名pnpm/pkg-manifest.utils版本1100.4.9采用 ESMtype: module依赖了pnpm/core-loggers、pnpm/deps.peer-range、pnpm/error、pnpm/types以及semver、semver-utils运行时要求 Node.js22.13测试基于 Jestpnpm/jest-config通过pn compile pn .test执行。该包的源码位于 src 目录共 9 个模块全部由 index.ts 统一导出模块功能定位getAllDependenciesFromManifest将 manifest 中各类依赖字段合并为一份扁平依赖表getAllUniqueSpecs跨多个 manifest 汇总“无冲突”的依赖规格getDependencyTypeFromManifest查询某个包名所属的依赖字段类型getSpecFromPackageManifest查询某个包名的版本规格specfilterDependenciesByType按字段开关筛选依赖inferRangeSpecStyle/rangeSpecStyle版本范围风格的推断与格式化updateProjectManifestObject将依赖规格写回 manifest 对象convertEnginesRuntimeToDependencies将engines/devEngines的 runtime 转换为runtime:依赖安装与导入按官方文档安装方式为标准 pnpm 安装pnpm i pnpm/pkg-manifest.utils在代码中通过 ESM 导入import { getAllDependenciesFromManifest, getDependencyTypeFromManifest, getSpecFromPackageManifest, filterDependenciesByType, getAllUniqueSpecs, updateProjectManifestObject, calcVersionRange, getRangeSpecStyle, } from pnpm/pkg-manifest.utils注意该包是 pnpm monorepo 内部工作区包package.json 中依赖声明为workspace:*作为对外发布包使用时其类型定义pnpm/types中ProjectManifest、Dependencies、IncludedDependencies、RangeSpecStyle等类型贯穿所有 API。依赖字段的读取四类字段的合并、归属与规格查询合并全部依赖getAllDependenciesFromManifest在 pnpm 的安装流程中往往需要把某项目的全部依赖包括需要自动安装的 peer 依赖合并成一张扁平的Dependencies表。getAllDependenciesFromManifest 的实现如下export function getAllDependenciesFromManifest ( manifest: PickProjectManifest, DependenciesOrPeersField, opts?: { autoInstallPeers?: boolean, peerAliases?: Setstring } ): Dependencies { const selectedPeerDependencies opts?.peerAliases null ? {} : Object.fromEntries(Object.entries(manifest.peerDependencies ?? {}).filter(([alias]) opts.peerAliases!.has(alias))) return { ...(opts?.autoInstallPeers ? manifest.peerDependencies : selectedPeerDependencies), ...manifest.devDependencies, ...manifest.dependencies, ...manifest.optionalDependencies, } }从 测试用例 可以精确归纳其合并优先级后展开者覆盖先展开者optionalDependencies优先级最高其次是dependencies、devDependencies同名条目会覆盖 peer 字段当autoInstallPeers: true时peerDependencies整体纳入合并但同样会被 dev/deps/optional 字段覆盖当autoInstallPeers: false时peer 依赖默认被排除除非通过peerAliases显式指定要包含的别名。这一“peer 依赖可选加入”的行为正是 pnpm 安装时auto-install-peers配置在 manifest 读取层面的落点。查询依赖归属字段getDependencyTypeFromManifestgetDependencyTypeFromManifest 用于回答“某个包名记录在哪个依赖字段下”export function getDependencyTypeFromManifest ( manifest: PickProjectManifest, DependenciesOrPeersField, depName: string ): DependenciesOrPeersField | null { if (manifest.optionalDependencies?.[depName]) return optionalDependencies if (manifest.dependencies?.[depName]) return dependencies if (manifest.devDependencies?.[depName]) return devDependencies if (manifest.peerDependencies?.[depName]) return peerDependencies return null }其判定顺序optional → dependencies → dev → peer与字段优先级一致测试getDependencyTypeFromManifest.test.ts覆盖了五种分支未命中时返回null。查询版本规格getSpecFromPackageManifestgetSpecFromPackageManifest 使用??nullish 合并按同一优先级取规格未找到时返回空字符串return manifest.optionalDependencies?.[depName] ?? manifest.dependencies?.[depName] ?? manifest.devDependencies?.[depName] ?? manifest.peerDependencies?.[depName] ?? 测试getSpecFromPackageManifest.test.ts验证了同名依赖在不同字段存在时返回optionalDependencies中的规格1.0.0。按字段类型筛选依赖filterDependenciesByType在按需安装如只装生产依赖等场景中需要依据IncludedDependencies开关筛选依赖。filterDependenciesByType 按布尔开关逐字段展开export function filterDependenciesByType ( manifest: ProjectManifest, include: IncludedDependencies ): Dependencies { return { ...(include.peerDependencies ? manifest.peerDependencies : {}), ...(include.devDependencies ? manifest.devDependencies : {}), ...(include.dependencies ? manifest.dependencies : {}), ...(include.optionalDependencies ? manifest.optionalDependencies : {}), } }其中IncludedDependencies类型定义于 options.ts由DependenciesField的三个布尔开关组成peerDependencies为可选开关。测试updateProjectManifestObject.test.ts验证了未显式开启时peerDependencies默认被排除开启后则被包含。跨 manifest 汇总唯一规格getAllUniqueSpecs当 pnpm 需要从多个子包的 manifest 中提炼“一份共同的依赖规格”例如推断 workspace 根项目应写入的公共依赖时会使用 getAllUniqueSpecsexport function getAllUniqueSpecs (manifests: DependencyManifest[]): Recordstring, string { const allSpecs new Mapstring, string() const ignored new Setstring() for (const manifest of manifests) { const specs getAllDependenciesFromManifest(manifest) for (const [name, spec] of Object.entries(specs)) { if (ignored.has(name)) continue if (isConflictingOrProtocolSpec(allSpecs.get(name), spec)) { ignored.add(name) allSpecs.delete(name) continue } allSpecs.set(name, spec) } } return Object.fromEntries(allSpecs) }其去重规则由isConflictingOrProtocolSpec定义同一包名在不同 manifest 中规格不一致冲突或规格中包含协议前缀:则该包名被整体忽略。这与测试getAllUniqueSpecs.test.ts中的行为完全对应foo: 1.0.0在两个 manifest 中一致 → 保留qar在两个 manifest 中分别为1.0.0与2.0.0→ 冲突被忽略zoo: link:../zoo、alpha: npm:beta2含协议前缀 → 被忽略名为constructor、toString的依赖通过Map结构安全保留不受对象原型成员干扰。版本范围风格的推断与格式化inferRangeSpecStyle 与 rangeSpecStyle五种风格与粒度pnpm 将版本范围range的风格抽象为RangeSpecStyle定义于 misc.ts风格示例语义major^1.2.3允许同 major 内升级minor~1.2.3允许同 minor 内升级patch1.2.3精确固定版本无前缀exact1.2.3显式精确固定none*无固定实际按^写入rangeSpecGranularity将exact折叠为patch粒度用于判断“锁定的最小升级粒度”。从规格文本推断风格inferRangeSpecStyleinferRangeSpecStyle 先通过getRangeOfSpecifier剥掉协议前缀npm:、jsr:、workspace:等与别名foo、scope/foo再用semver-utils解析范围并映射风格。测试inferRangeSpecStyle.test.ts给出完整映射表^1.0.0 → major ~1.0.0 → minor 1.0.0 → patch 1.0.0 → exact 1.0 → minor按粒度推断 1 → major * → none 1.0.0 → undefined无法映射 workspace:^1.0.0 → major npm:foo1.0.0 → patch npm:foo/foo^1.0.0 → major jsr:foo^1.0.0 → major catalog:default → undefined catalog:express4-21 → undefined特别值得注意的是catalog:引用永远返回undefined因为 catalog 引用的锁定由 catalog 条目自身定义即使 catalog 名称形似版本号如express4-21也不能被误读为固定版本。从配置计算风格getRangeSpecStylegetRangeSpecStyle 把用户配置save-exact与save-prefix解释为一种风格export function getRangeSpecStyle (opts: { saveExact?: boolean, savePrefix?: string }): RangeSpecStyle { if (opts.saveExact true || opts.savePrefix ) return patch switch (opts.savePrefix) { case : return exact case ~: return minor default: return major } }优先级规则由 rangeSpecStyle.test.ts 验证save-exact或空save-prefix优先直接保存裸版本save-prefix: 保存带显式的版本默认含^为major。格式化版本versionWithRangeSpecStyleexport function versionWithRangeSpecStyle (version: string, rangeSpecStyle: RangeSpecStyle): string { switch (rangeSpecStyle) { case none: case major: return ^${version} case minor: return ~${version} case patch: return version case exact: return ${version} } throw new Error(Unknown range spec style: ${String(rangeSpecStyle)}) }测试确认none与major都输出^1.2.3非预期风格会抛出异常Unknown range spec style。计算新版本范围calcVersionRangecalcVersionRange 是“添加/更新依赖时写入什么范围”的核心决策函数其完整优先级为保留原有特殊范围若原规格无法映射为五种风格如 3.0.0、1 2、1 || 2且请求未指定自己的规格、新版本仍满足该范围则原样保留范围边界对应 pnpm/pnpm#6714 的行为一旦新版本超出原范围则退回默认前缀风格预发布版本若新版本含 prerelease如3.0.0-rc.11非更新场景下忽略请求方风格、保留原风格或直接写裸版本更新场景则优先保留原范围风格一般版本非更新场景按“请求规格 → 原规格 → 默认风格”取风格更新isUpdate: true场景按“原规格 → 请求规格 → 默认风格”取风格保证pnpm update不改变既有范围写法。测试rangeSpecStyle.test.ts中的代表性案例prev 1.2.5 version 1.2.0 → 1.2.5保留 prev 1.2.5 version 100.1.0 → ^100.1.0超出回退默认 prev ^0.5.0 bare 1.0.0 → 1.0.0请求为精确版本 prev ^0.5.0 bare 1.0.0 isUpdate → ^1.0.0更新保留前缀 prev ^3.0.0-rc.0 3.0.0-rc.1 → ^3.0.0-rc.1预发布保留风格写回 manifestupdateProjectManifestObject 与原型污染防护核心入口updateProjectManifestObject 是pnpm add/pnpm remove/pnpm update等命令写回package.json的底层实现。其输入是PackageSpecObject[]export interface PackageSpecObject { alias: string peer?: boolean bareSpecifier?: string resolvedVersion?: string rangeSpecStyle?: RangeSpecStyle saveType?: DependenciesOrPeersField }写回逻辑applyPackageSpecs按以下规则展开指定了saveType时写入对应字段并从其余依赖字段中删除该包名避免同一包名同时出现在多个依赖字段saveType非法时抛出PnpmErrorINVALID_SAVE_TYPE错误码ERR_PNPM_INVALID_SAVE_TYPEsaveType: peerDependencies时peer 规格经由getPeerSpecifier计算见下未指定saveType时通过guessDependencyType猜测依赖已存在的字段找不到则默认写入dependenciespeer: true时在写入常规字段的同时还会在peerDependencies中写入计算后的 peer 规格。getPeerSpecifier对 peer 规格的计算策略合法的 semver 版本原样保留合法的 peer range经由pnpm/deps.peer-range的isValidPeerRange校验保留否则依据resolvedVersion生成范围拿不到解析版本时回退为*。场景验证来自测试的证据updateProjectManifestObject.test.ts 中的关键场景git / tarball 依赖且无解析版本时peer 规格写为*有解析版本2.1.0时peer 规格写为^2.1.0指定rangeSpecStyle: minor时写为~1.4.0jsr:^0.1.0协议依赖解析为0.1.0后peer 规格写为^0.1.0预发布解析版本2.1.0-rc.1不加前缀、原样写入peer 更新时优先更新peerDependencies而非同名的普通依赖npm 别名npm:bar*的 peer 更新会依据解析版本刷新为npm:bar^2.0.0无版本别名npm:bar会补全为npm:bar^2.0.0而npm:^1.0.0这类“裸注册表范围”不会被误判为别名更新为^2.0.0。原型污染防护Object.defineProperty Object.hasOwn该模块对安全性的处理是本次源码阅读中最值得学习的细节。写入依赖条目时使用Object.defineProperty而非直接赋值defineDepEntry删除时先做Object.hasOwn守卫deleteDepEntryfunction defineDepEntry (target: Recordstring, string, alias: string, value: string): void { Object.defineProperty(target, alias, { value, enumerable: true, writable: true, configurable: true, }) } function deleteDepEntry (target: Recordstring, string | undefined, alias: string): void { if (target ! null Object.hasOwn(target, alias)) { delete target[alias] } }原因在于即使包名是__proto__、constructor、prototype这类与Object.prototype冲突的名字Object.defineProperty也会创建普通自有数据属性而不会触发__proto__的 setter 去改写Object.prototype删除时Object.hasOwn防止误删原型链上的继承属性。测试专门断言写入这些别名后deps.__proto__ 1.0.0、原型链未变化、Object.prototype没有新增属性非法saveType包括空串与原型冲突名被拒绝时同样不污染原型。将 engines/devEngines 的 runtime 转写为依赖convertEnginesRuntimeToDependencies设计动机pnpm 支持在package.json的engines/devEngines中声明runtime要求如 Node 版本当onFail: download时pnpm 会自动安装对应 runtime 并将其作为依赖写入 manifest。convertEnginesRuntimeToDependencies 正是这一转换的实现。RUNTIME_NAMES定义于 package.ts[node, deno, bun]。EngineDependency的结构为{ name, version?, onFail? }其中onFail可取ignore | warn | error | download。转换逻辑for (const runtimeName of RUNTIME_NAMES) { const runtimes toRuntimeList(manifest[enginesFieldName]?.runtime) if (runtimes null || manifest[dependenciesFieldName]?.[runtimeName]) continue const runtime runtimes.find((runtime) runtime.name runtimeName) if (runtime?.onFail ! download) continue if (typeof runtime.version ! string) { globalWarn(Cannot download ${runtimeName} because no version is specified ...) continue } addRuntimeDependency(manifest, dependenciesFieldName, { runtimeName, version: runtime.version.trim() }) }要点仅处理onFail: download的 runtime 条目目标依赖字段中已存在同名依赖如显式声明的node: runtime:22.20.0时跳过不覆盖用户显式声明版本会被trim()规范化测试验证 22 →runtime:22写入的规格为runtime:version形式WebContainer 环境process.versions.webcontainer中不支持 runtime 安装会告警并跳过与依赖写入一样使用Object.defineProperty定义属性防止未来新增 runtime 名称与继承属性冲突时污染原型。applyRuntimeOnFailOverride用于整体改写onFail改为download时执行转换生成runtime:依赖改为其他值时删除所有runtime:前缀的自动生成依赖保留用户显式声明的同名依赖对应测试convertEnginesRuntimeToDependencies.test.ts验证了 skip、normalize、override 三类行为。在 pnpm 生态中的定位与典型调用场景从源码结构可以推断该包是 pnpm 众多上层命令与安装流程的公共底座安装与解析流程getAllDependenciesFromManifest与filterDependenciesByType决定“要装哪些依赖”getAllUniqueSpecs用于跨子包汇总公共规格add/update/remove 命令updateProjectManifestObject与calcVersionRange、getRangeSpecStyle决定“写回 package.json 时用什么范围风格”同时保障原型污染安全版本策略inferRangeSpecStyle帮助 pnpm 理解用户既有 range 的写法从而在更新时保持风格一致runtime 管理convertEnginesRuntimeToDependencies支撑devEngines.runtime.onFail: download的自动安装能力。这些函数均以纯函数或“原地修改 manifest 对象”的形式设计易于被 index.ts 统一导出后被installing/commands、pkg-manifest/commands等上层模块复用并配套了完整 Jest 测试test 目录行为边界清晰。小结pnpm/pkg-manifest.utils虽是一个仅 9 个源文件的工具包却集中体现了 pnpm 对 manifest 操作的全部核心约束四类依赖字段的合并优先级与归属查询、跨 manifest 的规格去重与冲突规避、五种版本范围风格的推断与写回、更新时对既有 range 写法的尊重以及贯穿始终的原型污染防护。理解这个包就等于理解了 pnpm 在“读 package.json → 解析依赖 → 写回 package.json”这一闭环中的所有关键决策。赞分享包管理器开发工具CLI【免费下载链接】pnpmFast, disk space efficient package manager项目地址https://gitcode.com/gh_mirrors/pn/pnpm点击查看免费下载相关推荐pnpm 工作区版本范围解析深入理解 pnpm/workspace.range-resolverpnpm 工作区版本范围解析深入理解 pnpm/workspace.range resolver 导读 在 pnpm 的 monorepo 中 works包管理器开发工具CLIEva Icons 依赖版本范围package.json中的^与~符号使用Eva Icons 依赖版本范围package.json中的^与~符号使用 在前端项目开发中依赖管理是确保项目稳定性和一致性的关键环节。Eva Icons作前端pnpm 的 package.json 读取引擎pnpm/pkg-manifest.reader 实战与源码解析pnpm 的 package.json 读取引擎pnpm/pkg manifest.reader 实战与源码解析 导读 pnpm/pkg manifest包管理器开发工具CLI上一篇Mochi Diffusion在Mac上快速运行Stable Diffusion的终极免费方案下一篇Godot CircleShape2D 圆碰撞形状完全指南radius 参数、2D 物理性能与实战用法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考