Yarn Modern 工程手册:从依赖模型到可复现安装
先分清 Classic、Modern 与 linker 冲突
Yarn Modern 不是把 npm install 换成 yarn 那么简单。它同时改变了包管理器版本入口、配置文件、依赖落盘方式、工作区协议、缓存策略和安装脚本边界。个人项目只要“能装上”就可能暂时工作,团队仓库却必须回答:谁决定 Yarn 版本,依赖究竟从哪里解析,为什么某台机器有 node_modules 而另一台没有,以及 CI 如何证明没有悄悄改写锁文件。
第一步不是安装,而是识别仓库属于哪一种状态。Yarn Classic 是 1.x,通常读取 .yarnrc;Yarn Modern 使用 .yarnrc.yml,并把 PnP 作为默认安装模型。Classic 与 Modern 的差异是工具代际差异,PnP、node-modules 和 pnpm 则是 Modern 内部的 linker 选择,二者不能混为一谈。选择 nodeLinker: node-modules 仍然是在使用 Yarn Modern,并不等于退回 Classic。
遇到旧仓库时,先查看 yarn --version、配置文件名、锁文件和 .yarn/ 目录,再按 Classic 迁移指南 识别需要迁移的命令与配置。若问题已经进入包发布、制品库服务端、Changesets,或 Nx、Turborepo 的远端任务缓存,应切换到对应工具链;Yarn workspace 在这里负责依赖边界和本地脚本入口,不替代完整的 Monorepo 任务编排。
动手前确认版本入口与仓库状态
准备一台可运行项目批准版 Node.js 的 Windows、macOS 或 Linux 开发机,并确认:
node --version
npm --versionYarn 与 Node.js 的兼容边界会随版本演进。升级前检查 Yarn 安装文档、Yarn Changelog 和目标 Node.js 支持状态,再把批准后的精确 Yarn 版本写入仓库。跨平台项目还要在 Windows、macOS/Linux、企业代理和原生依赖代表样本上执行同一验证链路,不能把一台开发机的成功当成团队兼容结论。
旧教程最容易在以下位置造成误判:
Yarn Classic 是 1.x,Modern 使用 .yarnrc.yml,Classic 的 .yarnrc 和许多命令不能原样沿用。Yarn Modern 默认使用 PnP,但 nodeLinker: node-modules 与 nodeLinker: pnpm 都是稳定、正式支持的模式。选择 node-modules 不是“降级到 Classic”。
yarn install 分为 resolution、fetch、link、build 四个阶段。报错发生在哪一阶段,决定应该查 manifest、网络缓存、linker 还是安装脚本。CI 中不可变安装默认更严格;旧 --frozen-lockfile 只是兼容别名,现代基线应使用 --immutable。Yarn 的安全默认值会继续演进。当前官方安全文档说明,较新的 Yarn 4 已调整第三方 postinstall 默认行为,并提供发布年龄门槛与 Hardened Mode;升级不能只看命令是否还存在。
安装入口不是全局 Yarn
团队不要用 npm install -g yarn 把某个 Yarn 版本永久装进每台开发机。全局二进制由机器状态决定,仓库的 yarn.lock 却由项目状态决定;二者脱节时,同一提交可能由不同 Yarn 版本重写。
推荐入口是 Corepack 或项目内 Yarn binary。先检查当前环境:
corepack --versionNode 25 起不再随 Node 分发 Corepack,因此命令不存在并不代表 Node 安装损坏。按组织批准的方式安装 Corepack,再启用 shim:
npm install --global corepack
corepack enable然后在项目根选择团队批准的 Yarn 稳定版本:
corepack use yarn@stable
yarn --version
node -p "require('./package.json').packageManager"第一次 stable 解析依赖网络,不能直接作为 CI 的漂移入口。它的作用是把当时的稳定版解析成精确版本并写入 package.json 的 packageManager;代码审查应确认该字段不再是浮动标签。后续开发机和 CI 读取同一字段。
也可以执行:
yarn set version stable
yarn installYarn 会更新项目的版本声明;在无法由 Corepack 表达的来源场景,它可能把 binary 存到 .yarn/releases,并通过 .yarnrc.yml 的 yarnPath 引用。团队只能选一种主线:优先使用 packageManager,确有离线或自定义 binary 需求时才提交 yarnPath,避免两套入口互相覆盖。
最小版本验收不是“命令能运行”,而是三处证据一致:
yarn --version
node -p "require('./package.json').packageManager"
yarn config get yarnPathyarn --version 应与 packageManager 中的精确版本一致;未采用项目 binary 时,yarnPath 通常为空。CI 还应把版本打印到日志,升级失败时才能判断是依赖变化还是包管理器变化。
用双 workspace 看清依赖模型
创建实验目录并初始化项目:
mkdir yarn-modern-lab
cd yarn-modern-lab
yarn init -p
mkdir packages
mkdir packages/app
mkdir packages/shared根 package.json 必须保留 Yarn 已经写入的 packageManager。不要覆盖整个文件;把下面字段合并进去:
{
"name": "yarn-modern-lab",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"verify": "yarn workspace @lab/app run verify"
}
}合并后再次执行 node -p "require('./package.json').packageManager",确认 corepack use 或 yarn set version 写入的精确版本仍在。
创建 packages/shared/package.json:
{
"name": "@lab/shared",
"version": "1.0.0",
"main": "index.cjs"
}创建 packages/shared/index.cjs:
module.exports = "shared-ok";创建 packages/app/package.json:
{
"name": "@lab/app",
"private": true,
"dependencies": {
"@lab/shared": "workspace:^"
},
"scripts": {
"verify": "node index.cjs"
}
}创建 packages/app/index.cjs:
console.log(require("@lab/shared"));运行:
yarn install
yarn verify预期最后输出:
shared-ok这个实验没有把 shared 发布到 registry。workspace:^ 要求 Yarn 在当前项目内按名称找到 workspace,并在解析图中建立本地依赖边。若名称写错或 workspace glob 没覆盖目录,安装会在 resolution 阶段失败;这比依赖意外回退到同名远程包更安全。
提交前至少检查:
yarn workspaces list
yarn why @lab/shared
git status --shortworkspaces list 应列出根和两个子 workspace,yarn why 应显示 @lab/app 的依赖关系,Git 变更中应包含 package.json、yarn.lock 和明确选择的 Yarn 配置产物。
一次安装为什么分四个阶段
Yarn 首先解析每个 descriptor,例如 workspace:^、^1.2.0 或 npm: 协议,形成确定的 locator;然后抓取归一化包进入缓存;接着由 linker 把依赖图暴露给运行时;最后按拓扑顺序处理获准执行的构建脚本。
这四阶段给排障提供了稳定坐标:
resolution 报错先查包名、版本范围、workspace 名称、registry metadata 与 resolutions。fetch 报错先查代理、DNS、CA、认证、缓存校验和与 registry 可达性。link 报错先查 PnP 兼容、符号链接权限、Windows junction、磁盘和文件系统。
build 报错先查生命周期脚本、编译工具链、平台架构、环境变量与脚本准入。
不要看到 yarn install 失败就删除所有缓存。清缓存可能让 fetch 证据消失,还会把本来可离线复现的问题变成网络问题。先用日志中的阶段和错误码缩小范围,再做有目的的清理。
PnP、node-modules 与 pnpm linker
.yarnrc.yml 是 Yarn Modern 的项目配置入口。一个最小 PnP 配置可以是:
nodeLinker: pnp安装后不会出现传统 node_modules,而会生成 .pnp.cjs 等 loader 文件。运行项目应通过 yarn node、yarn run 或已经加载 PnP 的项目入口。PnP 的价值不只是少文件:它按声明的依赖图解析,能更早暴露 ghost dependency,也能给出更有语义的 peer dependency 诊断。
当 React Native、旧插件、扫描器或某些工具无法理解 PnP 时,可以明确切到:
nodeLinker: node-modules然后重新安装并验证:
yarn install
yarn verify此时会生成传统 node_modules,兼容面最大,但幽灵依赖更难被发现,hoisting 结果也会受文件系统模型影响。不要仅凭“团队以前都用 node_modules”做决定;应列出真实不兼容工具、复现命令和迁移成本。
第三种模式是:
nodeLinker: pnpm它在 node_modules/.store 形成内容寻址存储,并用链接暴露包。它是 Yarn 的 linker,不等于改用 pnpm CLI,也不读取 pnpm lockfile。团队若已选择 pnpm 包管理器,应使用 pnpm 正文中的治理方式;不要把两者混成一套责任不清的方案。
切换 linker 会改变安装产物和工具兼容面,必须作为架构变更处理:在独立分支修改 nodeLinker,删除当前 linker 产生且确认可重建的产物,重新安装,运行构建、测试、IDE、打包和容器验证,再决定是否合并。回滚就是恢复 .yarnrc.yml 与锁定版本,并由旧 linker 重新生成产物,而不是把两种产物混留在工作区。
缓存、锁文件和不可变安装
yarn.lock 锁的是解析结果和校验信息,不是包管理器版本;packageManager 锁 Yarn 自身;.yarnrc.yml 锁安装模型。缺任何一层,都不能称为可复现。
本地正常安装:
yarn installCI 的基线入口:
yarn install --immutable
yarn verify--immutable 在安装需要修改 lockfile 时直接失败。它不是“禁止下载”,也不保证缓存内容可信。若团队提交项目缓存并采用 Zero-Installs,外部贡献场景应额外验证:
yarn install --immutable --immutable-cache --check-cache--immutable-cache 禁止增删缓存文件,--check-cache 会重新抓取并比较 lockfile 与现有缓存的校验信息。它增加网络与时间成本,适合至少一个受保护 CI Job,而不必机械复制到每个并行 Job。
Yarn 默认可使用全局缓存。若希望把 offline mirror 放入仓库,可配置:
enableGlobalCache: false这会把缓存放到项目 .yarn/cache。是否提交它是一项仓库治理决策:提交可降低 registry 故障影响,但会增加仓库体积、审查二进制归档的难度和外部 PR 篡改风险。Zero-Installs 还通常依赖 PnP loader;包含原生构建的依赖在平台变化后仍可能需要安装步骤,不能宣传成“永远不运行 install”。
清理时先观察配置:
yarn config get cacheFolder确认范围后再选择 yarn cache clean 清理当前项目本地缓存、--mirror 清理全局镜像,或 --all 同时清理两者。当前官方命令没有 dry-run;执行前应先记录路径并确认影响面。不要在共享缓存目录上直接执行系统级递归删除,也不要把删除 yarn.lock 当成修复缓存的方法;前者可能影响其他项目,后者会触发全量重新解析。
从 Classic 迁移到 Modern
迁移前先在 Classic 基线上保存证据:
yarn --version
yarn install --frozen-lockfile
yarn test
git status --short若基线本来就失败,先修复基线,不能把旧问题归咎于 Modern。随后在迁移分支执行项目版本设置,先保留兼容性最高的 linker:
nodeLinker: node-modules再执行:
yarn install
yarn test重点审查以下变化:
.yarnrc 和 .npmrc 中的 Yarn 配置要迁到 .yarnrc.yml;Modern 不会把旧文件当作等价配置。私有源 token 使用 npmAuthToken 等 Modern 键,并应来自环境变量,不能迁入明文。--frozen-lockfile 改成 --immutable。
yarn global 不再是团队项目工具入口;临时执行使用 yarn dlx,可复现工具应写入 devDependencies。任意 pre* / post* 脚本与生命周期行为存在差异,必须逐条验证,而不是假定名称相同就会执行。Classic 的 nohoist 不能原样照搬,应按实际兼容问题评估 nmHoistingLimits。
迁移验收应比较构建输出、测试、打包清单和运行行为,而不只是比较 lockfile。Modern 会转换旧 lockfile,首次安装还可能需要 registry metadata;离线迁移前应先在可联网的隔离环境完成并审查结果。
不要在同一个变更中同时完成 Classic -> Modern、PnP 切换、Node 大版本升级和大规模依赖升级。一次只移动一个变量,失败时才有可回退的因果边界。
私有源、代理、CA 与凭证
项目可以在 .yarnrc.yml 声明无秘密的源边界:
npmRegistryServer: "https://registry.example.invalid"
npmAlwaysAuth: true
npmScopes:
acme:
npmRegistryServer: "https://packages.example.invalid"
npmAlwaysAuth: true
npmAuthToken: "${YARN_NPM_AUTH_TOKEN}"示例域名不可直接使用,真实地址由组织配置。token 通过开发机凭证系统或 CI Secret 注入;.yarnrc.yml 只引用变量名。执行前验证变量存在,但不要打印值:
yarn config get npmScopes --json
yarn npm whoami --scope acmeyarn config get 对秘密值有脱敏能力,但团队仍不应把完整配置和环境转储上传工单。whoami 成功只能证明当前凭证能访问该 scope,不能证明它权限最小、不会过期或未泄露。
代理和 CA 问题要分层判断:先确认操作系统和 Node.js 信任链,再确认 Yarn 的 httpProxy、httpsProxy 与 httpsCaFilePath 等官方配置。企业 TLS 中间人证书应由安全团队分发可信 CA;不要用关闭 TLS 校验换取安装成功。出现证书错误时记录请求域名、证书颁发者、Node/Yarn 版本和代理链,修复信任根后再验证。
dependency confusion 的防线不是“内网包名看起来独特”。私有 scope 必须固定到受控 registry,公共与私有源的回退规则要经过验证;CI token 只授予读依赖所需权限,并限定项目、环境和有效期。
脚本、插件与供应链执行面
依赖安装不是纯文件下载。生命周期脚本可能编译原生模块、下载二进制,也可能读取环境变量和发起网络请求。Yarn 的脚本默认策略会随版本演进,团队升级前必须查看实际值:
yarn config get enableScripts
yarn config get npmMinimalAgeGate
yarn config get enableHardenedMode不要根据旧文章假定 postinstall 一定开启或一定关闭。若关闭第三方脚本导致某个依赖不可用,应通过根 manifest 的 dependenciesMeta 对经审查包做最小允许,而不是全局放开所有脚本。原生模块升级时还要在目标 OS、CPU、libc 和 Node ABI 上复测。
Yarn 插件本质上会被 Yarn 进程动态加载,可新增 resolver、fetcher、linker、命令和 hook。它们拥有包管理器进程的能力,不是静态配置片段。只允许官方内置插件或经过源码、版本、来源和校验审查的插件;插件升级应与 Yarn 升级一样进入变更窗口。
Hardened Mode 会检查 resolution 与 registry metadata,可缓解 lockfile poisoning,但会增加 registry 查询和安装时延。合理做法是在外部 PR 的受保护 Job 或专门校验 Job 开启并留存证据,其余复用已验证结果;不能因为性能慢就关闭所有安全校验,也不能把它宣称为完整供应链防护。
常用操作要保留意图
添加依赖:
yarn add <package>
yarn add --dev <package>升级整个项目的目标依赖:
yarn up <package>解释依赖来源与 peer 问题:
yarn why <package>
yarn explain peer-requirements执行项目脚本与单个 workspace 脚本:
yarn run verify
yarn workspace @lab/app run verify这些命令必须与变更意图一起审查。yarn up 可能影响多个 workspace;yarn dlx 会临时获取并执行远程包,不能替代受锁文件约束的开发依赖;resolutions 是有期限的根级覆盖,必须记录原因、责任人和退出条件。
从现象回到原因
显示的是 Yarn 1.x
项目文档要求 Modern,但 yarn --version 显示 1.x,.yarnrc.yml 也不生效。 检查 package.json#packageManager、corepack --version,Windows 用 Get-Command yarn -All,POSIX Shell 用 command -v -a yarn。 全局 Classic 抢占 PATH、Corepack shim 未启用,或终端仍缓存旧路径。 移除不受控的全局 Yarn,按批准方式安装并启用 Corepack,重开终端。 yarn --version 与 packageManager 一致,yarn config get nodeLinker 能读取项目配置。
MODULE_NOT_FOUND 只在 PnP 出现
切到 PnP 后某工具找不到包,node-modules 模式正常。 运行 yarn why <package>,确认使用 yarn run 启动,并检查报错包是否声明了实际依赖。 应用依赖幽灵包、工具绕过 PnP loader,或第三方 manifest 缺失依赖声明。 先补自己项目的依赖;第三方问题可用经审查的 packageExtensions 临时修正,确实无法兼容时才选择 node-modules。 干净安装后运行构建、测试、IDE 和打包,不再依赖偶然 hoisting。
CI 报 lockfile 会被修改
本机成功,CI 的 yarn install --immutable 失败。 在与 CI 相同的 Node、Yarn、OS 和配置下运行不可变安装,检查 manifest 与 lockfile 是否同一提交。 修改 package.json 后漏提交 lockfile、Yarn 版本漂移、条件依赖或配置差异触发重新解析。 用批准版本重新生成并审查 lockfile;不要在 CI 临时关闭 immutable。 干净检出执行相同命令,Git 状态保持为空。
私有包返回 401、403 或公共源 404
公共包可下载,私有 scope 失败。 检查 npmScopes、变量是否注入、token 是否过期,并用 yarn npm whoami --scope <scope> 验证身份。 scope 路由错误、凭证未注入、权限不足、代理剥离认证头,或私有源错误回退到公共源。 固定 scope registry,轮换最小权限 token,修复代理转发;不要把 token 写进配置或日志。 身份校验和不可变安装成功,日志不含明文凭证。
原生依赖在一台机器构建失败
lockfile 相同,但 Windows、Alpine 或新 Node 版本上 build 阶段失败。 记录 OS、CPU、libc、Node ABI、Yarn 版本和失败脚本,使用 --inline-builds 获取受控日志。 缺编译工具链、预编译二进制不覆盖平台、脚本被禁用,或缓存复用了不兼容产物。 补齐受支持工具链或选择兼容依赖;按平台隔离构建缓存,避免跨 ABI 复用。 在目标平台干净构建并执行运行时烟雾测试,而非只看安装退出码。
linker 是仓库级架构决策
PnP 能暴露幽灵依赖并减少文件树,node-modules 兼容面更广,pnpm linker 位于两者之间。判断标准应包括 IDE、测试运行器、原生扩展、容器、扫描器和部署打包链,而不是安装耗时单指标。每次切换都要有代表性项目、失败清单和回滚提交。
Zero-Installs 把缓存变成代码资产
提交 .yarn/cache 后,依赖归档进入代码审查、仓库增长、备份和外部 PR 信任边界。必须开启 immutable cache 和独立校验,规定谁能更新缓存、怎样识别异常体积、何时清理旧归档。不能一边接受不可信 PR 修改缓存,一边跳过 --check-cache。
脚本默认值升级会改变构建结果
新 Yarn 版本可能收紧安装脚本、增加年龄门槛或改变安全模式。升级验证必须覆盖需要 postinstall 的原生模块、二进制下载和代码生成,并比较产物,而非只运行 yarn --version。允许列表应具体到包,并定期删除不再需要的例外。
私有源失败可能变成供应链事件
401 可能只是 token 过期,也可能意味着 scope 路由丢失后请求落到公共 registry。监控需要区分认证失败、源路由和不存在包;私有包命名、scope 强制、代理规则和最小权限凭证要共同落地。任何临时公共回退都应视为高风险变更。
workspace 规模不等于任务编排能力
Yarn workspace 能统一安装、引用本地包和批量运行脚本,但不会自动提供组织需要的 affected 分析、远端任务缓存和跨语言构建图。仓库规模增长后,应把这些需求交给 31 家族选型,而不是继续往根 package.json 堆并行 Shell。
本地快不代表 CI 可复现
开发机可能命中全局缓存、残留 node_modules 或旧 PnP 产物。团队基线必须从干净检出验证 packageManager、不可变安装、构建、测试和 Git 清洁状态;缓存只优化 fetch,不能绕过版本契约与产物验证。
团队落地基线
一个可维护的 Yarn Modern 仓库至少应明确:
packageManager 固定精确版本,升级由专门变更完成。.yarnrc.yml 明确 nodeLinker、源和无秘密策略,真实 token 只由环境注入。yarn.lock 必须提交,CI 使用 yarn install --immutable。
是否提交 .yarn/cache 有书面决定;若提交,至少一个 CI Job 校验缓存。workspace 之间使用 workspace: 表达本地依赖,不依赖偶然 hoisting。生命周期脚本、插件、resolutions 和 packageExtensions 都有审查与退出条件。
本地和 CI 调用同一 yarn run 入口,日志打印 Node 与 Yarn 版本。升级窗口覆盖 Windows、macOS/Linux、容器和原生依赖代表样本。
已确认项目使用 Yarn Modern,而不是 PATH 中的 Yarn Classic。packageManager 是工具写入并提交的精确版本,不是 stable 或 latest。已明确 PnP、node-modules 或 pnpm linker 的选型证据和回滚方式。
yarn.lock 已提交,干净检出可通过 yarn install --immutable。workspace 名称、glob 和 workspace: 引用可由 yarn workspaces list、yarn why 解释。私有 scope 固定到受控 registry,凭证未写入仓库或构建日志。
代理与企业 CA 已按信任链配置,没有关闭 TLS 校验。生命周期脚本和 Yarn 插件已按可执行代码审查。缓存提交策略、外部 PR 校验和清理责任已经明确。
Classic 迁移没有同时混入 Node 大版本、PnP 和依赖大升级。CI 打印 Node/Yarn 版本,并与本地调用相同脚本入口。失败排查先定位 resolution、fetch、link 或 build 阶段,再清理对应对象。
遇到不同问题时查哪个入口
安装入口、Corepack 与项目版本锁定发生冲突时,查 Yarn Installation、Yarn Manifest 和 yarn set version。
PnP、node-modules、pnpm linker 的目录结构或兼容性不符合预期时,查 Yarn Install Modes 与 Yarn Plug'n'Play。workspace 解析、workspace: 协议或跨包依赖失败时,查 Yarn Workspaces。
不可变安装、缓存提交和离线恢复出现差异时,查 Yarn Cache Strategies 与 yarn install 命令。
registry、脚本、插件和安全默认值变化时,查 .yarnrc.yml 配置、Yarn Security、Yarn Extensibility 与 Yarn Changelog。
