news 2026/9/20 15:38:47

pnpm11 @pnpm/installing.modules-yaml:node_modules/.modules.yaml 状态文件的读写实现详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pnpm11 @pnpm/installing.modules-yaml:node_modules/.modules.yaml 状态文件的读写实现详解

pnpm11 @pnpm/installing.modules-yaml:node_modules/.modules.yaml 状态文件的读写实现详解

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

node_modules/.modules.yaml是 pnpm 安装器在每次安装后落盘的一份“modules 状态清单”,记录虚拟存储位置、提升策略、跳过/待构建的包等关键信息;@pnpm/installing.modules-yaml(pnpm11 工作区包)就是负责读取与写入这份文件的唯一权威模块。本文以 包 README 为骨架,结合 源码实现 与 测试用例,完整讲解其文件命名由来、数据字段、向后兼容迁移逻辑、写入时的字段裁剪策略,以及它在 pnpm11 安装流程中的下游消费方。

包的定位与安装方式

README 给出的安装与用法非常直接:

pnpm add @pnpm/installing.modules-yaml
import {write, read} from '@pnpm/installing.modules-yaml' await write('node_modules', { hoistedAliases: {}, layoutVersion: 1, packageManager: 'pnpm@1.0.0', pendingBuilds: [], shamefullyFlatten: false, skipped: [], storeDir: '/home/user/.pnpm-store', }) const modulesYaml = await read(`node_modules`)

README 的 API 概览只有两条:

  • read(pathToDir): Promise<ModulesObject>—— 从指定目录读取.modules.yaml
  • write(pathToDir, ModulesObject): Promise<void>—— 向指定目录写入.modules.yaml

需要说明的是:README 中的read/write是对外语义的简化写法,源码实际导出的函数名 是readModulesManifest(modulesDir)writeModulesManifest(modulesDir, modules)。从 package.json 可以确认该包的信息:

  • 包名@pnpm/installing.modules-yaml,描述为 “Reads/writesnode_modules/.modules.yaml”,当前版本1101.0.2
  • 运行入口为lib/index.js"main"),纯 ESM("type": "module");
  • 运行时依赖只有 5 个:@pnpm/fs.graceful-fs@pnpm/typesis-windowsramdaread-yaml-file
  • 引擎要求"node": ">=22.13"

文件命名与存储格式:为什么叫 .modules.yaml 却常是 JSON

源码中文件名是一个常量,并附带了关键的注释解释(src/index.ts#L14-L16):

// The dot prefix is needed because otherwise `npm shrinkwrap` // thinks that it is an extraneous package. const MODULES_FILENAME = '.modules.yaml'

点前缀的作用是让npm shrinkwrap这类工具不会把该文件误判为一个“多余的安装产物”。

更值得注意的是存储格式。虽然文件名是.yaml,但当前版本写出的内容其实是JSON(JSON 本身是合法 YAML 子集):

await fs.mkdir(modulesDir, { recursive: true }) await fs.writeFile(modulesYamlPath, JSON.stringify(saveModules, null, 2))

而读取端采用“先 JSON、后 YAML”的双路解析策略(src/index.ts#L55-L69):

  1. 先按 UTF-8 读入文件内容,尝试JSON.parse
  2. 若 JSON 解析失败,回退到read-yaml-file按 YAML 解析——源码注释明确写道 “Manifests written by old pnpm versions are YAML”,即旧版 pnpm 写的是真正的 YAML,这一回退就是为兼容历史文件而保留的;
  3. 文件不存在(ENOENT)时返回null,而不是抛错;
  4. 空文件(0 字节)会返回undefined而不报错,这一点由 fixtures/empty-modules-yaml 中那个空的.modules.yaml对应测试用例(test/index.ts#L131-L134)验证;
  5. 解析不了的非空文件必须抛错。测试用例里有一段很有信息量的注释(test/index.ts#L136-L142):
// Callers must not mistake an unreadable state file for a missing one: // that reads as layout drift and purges node_modules on every install. const modulesDir = temporaryDirectory() fs.writeFileSync(path.join(modulesDir, '.modules.yaml'), 'not: [valid') await expect(readModulesManifest(modulesDir)).rejects.toThrow()

换言之,如果读取端把“损坏的状态文件”误当成“没有状态文件”,上层安装器会把它理解为 node_modules 布局漂移,进而每次安装都清空重建 node_modules——所以这里必须 fail loudly。

另外还有一条针对 JSON 特性的防御:重复 key 的 JSON 对象按“最后一个值生效”处理,对应测试 构造了同一条超长 dep path 出现两次的 JSON,断言读取结果保留的是后一个值('public')。

Modules 数据结构:字段逐项解析

ModulesRaw接口(src/index.ts#L22-L46)定义了状态文件的完整字段集,导出的Modules类型在其基础上把ignoredBuilds从字符串数组改成了Set。逐字段说明如下:

字段类型含义
hoistedDependenciesHoistedDependencies核心字段:dep path → 别名到public/private提升位置的映射,决定哪些包出现在 node_modules 根
hoistPattern/publicHoistPatternstring[](可选)提升模式与公共提升模式,与 pnpm 的hoist-pattern/public-hoist-pattern配置对应
includedRecord<DependenciesField, boolean>记录本次安装包含了哪几类依赖(dependencies/devDependencies/optionalDependencies
layoutVersionnumbernode_modules 布局版本号,是上层判断“能否复用现有 node_modules”的关键依据
nodeLinker'hoisted' \| 'isolated' \| 'pnp'(可选)记录本次安装使用的 node_modules 链接策略
packageManagerstring写入该文件时的包管理器版本,如pnpm@5.1.8
pendingBuildsstring[]尚未执行构建脚本的包列表
ignoredBuildsDepPath[](读入后为Set<DepPath>被忽略构建脚本的 dep path 集合
skippedstring[]因平台不匹配等原因被跳过的包
prunedAtstring最近一次修剪时间(UTC 字符串)
storeDirstring全局内容寻址存储(CAFS store)路径
virtualStoreDirstring项目内虚拟存储目录,默认<modulesDir>/.pnpm
virtualStoreDirMaxLengthnumber虚拟存储路径长度上限,缺省值 120
injectedDepsRecord<string, string[]>(可选)注入依赖的映射记录
hoistedLocationsRecord<string, string[]>(可选)提升位置的补充记录
allowBuildsRecord<string, boolean \| string>(可选)构建脚本的审批状态
virtualStoreOnlyboolean(可选)标记该 node_modules 由virtualStoreOnly安装(如pnpm fetch)产生:此时记录的提升模式被强制置空,下次安装时不得与用户配置比较
hoistedAliases/shamefullyHoist仅作向后兼容保留,读取旧文件时用于迁移(见下一节)

读取端的字段规范化逻辑(src/index.ts#L70-L112)会补齐若干默认值:

  • virtualStoreDir缺失时补为path.join(modulesDir, '.pnpm');若是相对路径则相对modulesDir解析为绝对路径;
  • prunedAt缺失时补为当前 UTC 时间;
  • virtualStoreDirMaxLength缺失时补为120

向后兼容:从 pnpm 5 的 shamefully-hoist 到 hoistedDependencies

.modules.yaml的历史格式经历过一次重要变化:旧版(如 pnpm 5)使用shamefullyHoist布尔值加hoistedAliases映射来表达提升关系;新版改用结构化的hoistedDependenciesreadModulesManifest中专门有一段switch (modules.shamefullyHoist)迁移逻辑(src/index.ts#L79-L105):

  • shamefullyHoist: true时:若publicHoistPattern缺失则补为['*'];若只有hoistedAliases而没有hoistedDependencies,则把每个别名映射为'public'
  • shamefullyHoist: false时:若publicHoistPattern缺失则补为[];同样把hoistedAliases迁移为hoistedDependencies,但所有别名标为'private'

仓库里保留了真实的旧版 fixture 文件,可以直接对照阅读。old-shamefully-hoist/.modules.yaml 是一个由pnpm@5.1.8写出的 YAML 文件:

hoistPattern: - '*' hoistedAliases: /accepts/1.3.7: - accepts /array-flatten/1.1.1: - array-flatten /body-parser/1.19.0: - body-parser included: dependencies: true devDependencies: true optionalDependencies: true layoutVersion: 4 packageManager: pnpm@5.1.8 pendingBuilds: [] registries: default: 'https://registry.npmjs.org/' shamefullyHoist: true skipped: [] storeDir: /home/zoli/.pnpm-store/v3 virtualStoreDir: .pnpm

old-no-shamefully-hoist/.modules.yaml 则是shamefullyHoist: false的对应版本。两条向后兼容测试(test/index.ts#L80-L104)断言:读入前者后publicHoistPattern变为['*']且三个包全部标记为public;读入后者后publicHoistPattern[]且全部标记为private。这两个 fixture 也顺带展示了旧文件的两个特征:内容是纯 YAML 而非 JSON,以及包含一个新版写入时会被主动丢弃的registries字段(见下文)。

写入逻辑:字段裁剪、排序与 Windows 特判

writeModulesManifest(src/index.ts#L115-L149)在落盘前做了一系列确定性处理,保证同一状态反复写入时字节稳定、且不带入过期信息:

  1. ignoredBuildsSet还原为数组——Set无法被 JSON 序列化,落盘前Array.from展开;
  2. skipped排序——if (saveModules.skipped) saveModules.skipped.sort(),消除数组顺序带来的无意义 diff;
  3. 删除空值字段hoistPatternnull或空串时删除(注释说明 YAML 写作者无法处理undefined字段);publicHoistPatternnull时删除;virtualStoreOnly为假时删除;hoistedAliases在“为 null,或两个 hoist pattern 均为 null”时删除;
  4. 主动丢弃registries字段——源码注释解释得很清楚(src/index.ts#L136-L139):pnpm 11 及更早版本会把上次安装使用的 registry 记录在文件里,而现在 registry 直接从项目配置读取;因此旧文件里残留的registries会在首次重写时丢失,避免持有一份配置变更后立即过期的拷贝。这一点有专门的测试验证(test/index.ts#L144-L171):写入一个带registries: { default: ... }的 manifest 后,读回原始文件断言registries字段已不存在;
  5. virtualStoreDir的绝对/相对路径处理(src/index.ts#L140-L146):
// We should store the absolute virtual store directory path on Windows // because junctions are used on Windows. Junctions will break even if // the relative path to the virtual store remains the same after moving // a project. if (!isWindows()) { saveModules.virtualStoreDir = path.relative(modulesDir, saveModules.virtualStoreDir) }

非 Windows 平台写相对路径(通常是.pnpm),Windows 平台保留绝对路径。原因是 Windows 上 pnpm 使用 junction,junction 在“项目目录被移动而虚拟存储相对位置不变”时同样会失效,所以必须记录绝对路径才能正确校验。测试用例(test/index.ts#L36-L39)正验证了这一点:path.isAbsolute(raw.virtualStoreDir)应当等于isWindows()的返回值; 6. 最后fs.mkdir(modulesDir, { recursive: true })确保目录存在,再以JSON.stringify(saveModules, null, 2)写入 2 空格缩进的 JSON。

测试矩阵:从往返一致到长路径场景

test/index.ts 覆盖了该包的全部关键行为,可以视为一份“可验证的规格书”:

  • 往返一致性writeModulesManifest+readModulesManifest的结果与输入深度相等(round-trip),包括node_modules目录不存在的场景(L106-L129);
  • 超长 dep path:构造@scope/package@1.0.0(${'p'.repeat(1001)})这样的 dep path,验证带 1000+ 字符 peer 依赖片段的条目也能完整往返(L41-L67)——这类长路径正是 pnpm 虚拟存储目录名长度管理(virtualStoreDirMaxLength缺省 120)要面对的现实;
  • 损坏文件必须拒绝空文件容错:见前文;
  • 旧格式迁移:两个 pnpm 5 的 fixture 验证shamefullyHoist/hoistedAliaseshoistedDependencies的迁移正确性;
  • registries字段剥离:验证新写入不再携带旧版的 registry 快照。

谁在消费这个包:pnpm11 中的下游引用

@pnpm/installing.modules-yaml是 pnpm11 中多处安装链路的公共依赖,从各包的package.json依赖关系看(均为workspace:*依赖),主要消费方包括:

  • installing/deps-installer/src/install/validateModules.ts 与 installing/deps-installer/src/install/link.ts:安装器在安装前后读取/重写该文件,校验现有 node_modules 与当前配置的兼容性;
  • building/after-install/src/index.ts 与 building/commands/src/policy/approveBuilds.ts、getAutomaticallyIgnoredBuilds.ts:安装后构建脚本的审批/忽略策略会读写pendingBuildsignoredBuildsallowBuilds等字段;
  • workspace/injected-deps-syncer/src/index.ts:工作区注入依赖的同步逻辑依赖其中的injectedDeps记录;
  • installing/deps-restorer/src/index.ts 与 installing/context/src/index.ts:锁文件快速恢复与安装上下文构建时读取该状态以决定能否复用 node_modules;
  • 此外deps/graph-builderdeps/inspection/tree-builderglobal/commandspatching/commands等包也在依赖列表中(见 pnpm11/deps/graph-builder/package.json 等),说明它同时服务于构建依赖图与检查类命令。

从源码结构看,.modules.yaml实际上是 pnpm 判断“node_modules 是否仍有效”的唯一事实来源:layoutVersionhoistedDependenciesincluded等字段共同决定了安装器是走完整重建、增量修补还是直接跳过。理解这个包的读写行为,也就理解了 pnpm 增量安装机制的底层契约。

参考文件索引

  • README:包的对外文档(安装、用法、API 概览,MIT 许可)
  • src/index.ts:readModulesManifest/writeModulesManifest完整实现与Modules类型定义
  • test/index.ts:往返、长路径、损坏文件、旧格式迁移等全部测试
  • package.json:包元信息、依赖与引擎要求
  • test/fixtures/old-shamefully-hoist/.modules.yaml / old-no-shamefully-hoist/.modules.yaml / empty-modules-yaml:pnpm 5 时代的真实 YAML 样本与空文件样本

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 15:38:14

PWM脉宽调制直流调速设计与MATLAB仿真验证全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 15:36:52

基于随机森林的蔬菜销量预测可视化系统设计与Python实现

简介&#xff1a;基于Python的蔬菜产品销售预测可视化系统完整项目实例&#xff0c;面向具备Python基础的数据分析师、算法工程师及农业数字化从业者&#xff0c;用于解决生鲜零售场景中的销量预测、库存管理与智能补货决策问题。压缩包内为1个docx文档&#xff0c;容量仅110KB…

作者头像 李华
网站建设 2026/9/20 15:35:09

连续式混合机结构参数优化:基于响应面法的混合均匀度提升实践

简介&#xff1a;这份《连续式混合机结构参数的响应面分析优化》是一篇技术文档&#xff0c;适合机械设计、离散元仿真及结构优化方向的工程师与研究者阅读。内容针对卧式强制混合机结构笨重、材料冗余的问题&#xff0c;采用ANSYS-DesignModeler建立参数化模型&#xff0c;结合…

作者头像 李华
网站建设 2026/9/20 15:33:31

Win10/Win11管理员权限完全指南:UAC提权与文件权限修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 15:33:22

Atlas 300V NPU部署YOLO实战:从环境搭建到模型转换与推理

从GPU切换到Atlas 300V 24G这个过程&#xff0c;比我想象中要曲折得多。刚拿到卡的时候&#xff0c;我的第一反应和大多数人一样&#xff1a;先找nvidia-smi&#xff0c;然后习惯性地写CUDA代码。结果发现这套思路完全走不通&#xff0c;Atlas 300V本质上不是一块GPU&#xff0…

作者头像 李华
网站建设 2026/9/20 15:33:03

Open-Code-Review:基于Git Diff的开源可验证代码评审范式

1. “open-code-review”不是工具名&#xff0c;而是一类新型代码评审范式的代号最近在几个技术社区和内部研发群聊里&#xff0c;频繁看到有人发“open-code-review”这个词&#xff0c;配图是终端里跑着一个带--diff参数的 CLI 命令&#xff0c;输出里夹杂着 Git 补丁块和带行…

作者头像 李华