ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

pnpm monorepo实战:从安装配置到常见报错排查

2026/9/16 9:27:47 拓冰建站 浏览量
pnpm monorepo实战:从安装配置到常见报错排查 1. 项目概述与思路拆解1.1 什么是pnpm与monorepo为什么要一起用先聊一个很多人在一开始就会混淆的概念。pnpm是一个包管理器monorepo是一种仓库组织方式这两者并不是同一层面的东西但对前端工程化来说它们几乎是天生一对。先从最直观的场景说起。假设你们团队有3个项目分别依赖了5个公共组件。用传统的多仓库方式每个项目各自维护一份组件代码改个bug要跑3个仓库、发3次包、再更新3次依赖中间的沟通成本和版本错位问题非常折磨人。monorepo的思路是把这些项目统一放进一个仓库用工作区workspace把它们隔离成一个个独立的包共享一份依赖树、一套配置、一个提交历史。代码复用从“发版—升级”变成了“本地直接引用”效率提升非常明显。那为什么偏偏选pnpm而不是npm或者yarn这里就要说到pnpm最核心的优势—硬链接与内容寻址存储。npm安装依赖时每个项目的node_modules都是独立完整的一份复制10个项目装同一个lodash磁盘上就有10份lodash。pnpm则把依赖包全局存储在一个统一的store里项目里通过硬链接指向store中的文件多个项目共享同一份物理文件。磁盘占用大幅下降安装速度也会快不少尤其在依赖多、项目多的场景下省出来的空间和时间都是实打实的。再加上pnpm对monorepo工作区有原生支持通过一个pnpm-workspace.yaml文件就能声明所有子包配合--filter参数可以精准操作某个包体验非常顺滑。1.2 热词背后用户最关心的到底是什么我大致扫了一圈和这个标题相关的搜索热词发现一个很有意思的现象搜索“pnpm”相关排在最前面的不是“pnpm monorepo最佳实践”这类进阶问题反而是这些“pnpm 不是内部或外部命令也不是可运行的程序”“pnpm : 无法将‘pnpm’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”“pnpm 安装到D盘”“pnpm下载失败”“pnpm和npm区别”这说明了什么问题说明真正卡住大部分人脚步的不是monorepo本身的架构设计而是最基础的安装和环境配置。很多人在npm的世界里待惯了第一次接触pnpm下载之后发现命令行完全用不了瞬间就懵了。这批热词其实就是一份真实的“新手踩坑清单”我后面会专门拿出一个章节来逐个拆解。还有一类热词是关于版本迭代的比如“run pnpm approve-builds to pick which dependencies should be allowed to run”这是pnpm 10.x版本之后新增的构建脚本审批机制带来的新问题很多从旧版本升上来的项目都会遇到这里也需要单独说明。所以说搭建一个pnpm monorepo项目本质上包含两条线一条是思维线理解为什么用pnpm、为什么用monorepo另一条是工程线从安装、初始化、配置到日常命令的完整实操。这篇博文两条线都会讲尽量让新手能照着走完也让有经验的人能补上几个容易忽略的坑。2. 环境准备与安装把pnpm装好2.1 安装前的核心认知pnpm如何与Node.js协同开始安装之前必须先把一个基础概念讲清楚因为这个问题直接关系到后面所有报错的理解。pnpm是一个Node.js生态的包管理器它本身就是一个Node.js编写的命令行工具。这意味着它依赖Node.js运行时才能执行。你的电脑里先要有Node.js然后才有pnpm。这就像你要装一个App手机得先有操作系统一样。Node.js装好之后不管是npm、yarn还是pnpm它们本质上都是通过Node.js运行的JS文件。npm是Node.js自带的所以装完Node.js就能直接用npm不需要额外安装。但pnpm不是自带的得自己装一遍。很多新手在命令行敲pnpm -v报“不是内部或外部命令”根本原因就是pnpm这个可执行文件的路径没有被注册到系统的PATH环境变量里或者说macOS/Linux用户遇到“command not found”也是同一个道理。明白了这一点再去看各种安装教程就不会云里雾里了。因为你会发现所有安装方式其实都在做同一件事把pnpm的可执行文件放到一个系统能找得到的位置。这些位置可能是Node.js的全局安装目录、用户目录下的某个特定文件夹或者系统级的环境变量目录。2.2 五步装好pnpm涵盖Windows与macOS/Linux第一步确认Node.js已经装好打开命令行工具Windows可以用PowerShellmacOS可以用终端输入node -v如果能输出类似v18.20.4这样以v开头的版本号说明Node.js已经就绪。如果提示“node不是内部或外部命令”那问题就出在Node.js本身得先去Node.js官网下载安装包或者用nvmNode Version ManagerNode.js版本管理器来安装。这里更推荐nvm的方式因为它方便切换版本这在多项目协作场景下非常实用。第二步用corepack或npm全局安装pnpm在Node.js 14.19.0及以上版本中其实自带了一个叫corepack的工具可以把它理解成“包管理器的管理器”。启用pnpm的命令是corepack enable具体到这个场景还可以用更直接的方式npm install -g pnpm这条命令会把pnpm作为一个全局包安装安装位置就是Node.js的全局npm目录。安装完成后验证一下pnpm -v如果安装成功会输出pnpm的版本号比如10.6.2。第三步处理Windows特有的Path路径问题如果第二步输入pnpm -v报错提示“pnpm不是内部或外部命令”大概率是npm的全局安装目录没有被加入系统PATH。解决办法是先查一下全局目录在哪执行npm config get prefix会输出一个路径可能是类似C:\Users\你的用户名\AppData\Roaming\npm这样的位置。把这个路径手动添加到系统PATH环境变量中。Windows的操作为右键“此电脑” → 属性 → 高级系统设置 → 环境变量 → 在“系统变量”里找到Path → 编辑 → 新建 → 粘贴上面查到的路径 → 确定。重新打开一个命令行窗口注意不是当前窗口要重新开一个才能生效再执行pnpm -v验证。顺便提一下在Windows上如果PowerShell执行pnpm时报错“无法将‘pnpm’项识别为 cmdlet...”多半是执行策略execution policy限制导致的。可以先执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser然后再试一次。第四步修改全局安装位置安装到D盘关于“pnpm安装到D盘”这个热词很多人的真实痛点是C盘空间不够。这个目标可以通过两个层面来实现。如果是想把pnpm这个工具本身装到D盘可以在安装时指定前缀npm install -g pnpm --prefix D:\nodejs\pnpm-global然后同样把D:\nodejs\pnpm-global加入系统PATH。但更常见的诉求其实是把依赖包的全局存储位置改掉。pnpm的默认store路径在用户目录下时间久了会吃掉大量的C盘空间可以通过以下方式改到D盘pnpm config set store-dir D:\pnpm-store这个配置会写入你的全局配置文件中后续所有pnpm安装的依赖其物理存储都会放在D盘C盘压力自然就小下来了。第五步验证安装并查看配置pnpm -v pnpm config list第二条命令会列出当前的配置项确认store-dir、registry这些关键配置是否符合预期。这里也建议把镜像源换成国内常用镜像安装速度会有一个质的提升pnpm config set registry https://registry.npmmirror.com2.3 额外补充Linux离线安装pnpm对于内网环境或者网络受限的场景离线安装pnpm也有成熟的方案。思路是找一台能联网的机器下载pnpm的standalone可执行文件npm install -g pnpm --registry https://registry.npmmirror.com安装之后找到pnpm的全局安装路径把整个目录压缩打包拷到目标Linux机器上解压后把bin目录加入PATH即可。更简单的方案是直接下载官方发布的单文件二进制版本从GitHub Releases页面找到对应Linux平台的tar.gz包解压后放到/usr/local/bin或/opt目录下。注意离线机器上如果跑pnpm需要联网下载依赖还得提前配好内网镜像源或者用离线缓存包。提示任何涉及内网环境的配置一定要确认符合公司的网络管理规范不要私下搭建与外部网络的特殊通道按合规流程操作即可。3. 核心细节解析与实操要点3.1 pnpm与npm的核心差异硬链接、内容寻址存储与依赖隔离前文已经提到了硬链接这里再深挖一层因为这对理解pnpm的行为模式至关重要。传统的npm在安装依赖时是“平铺”式的。项目的node_modules目录下直接放着一层的包看起来整洁但隐含着两个问题。第一个问题是依赖提升带来的幽灵依赖项目里明明没有在package.json里声明某个包但代码里可以直接require到它因为它是别的依赖的次级依赖被npm提升到了顶层。第二个问题是安装速度慢、占空间大。pnpm的解决方案很有意思。它引入了三个概念全局store、硬链接、符号链接。全局store是pnpm所有依赖的物理存储仓库每个版本的包只在store里存一份。当项目需要某个依赖时pnpm不会复制一份到项目的node_modules里而是在项目自己的node_modules中创建一个硬链接指向store里那个文件。硬链接的特点是多个链接共享同一个物理文件内容但不额外占用磁盘空间。这就是为什么pnpm多项目场景下能省出大量磁盘空间的原因。但硬链接有个局限不能跨分区而且文件与文件之间的目录结构并不能完全通过硬链接合理组织。所以pnpm在项目的node_modules里还做了一层符号链接的映射。简单的理解方式是node_modules下的pnpm目录是真实的内容地址目录按照“包名版本号”的哈希结构存放然后在node_modules根目录和其他包内部用符号链接指向这个目录。这个设计同时解决了另一个重要问题——严格依赖隔离项目只能访问package.json里明确声明的依赖不再存在幽灵依赖。用一个生活化的类比来说npm是把同样的东西每户都发一份大家各自保管东西多、空间占得大pnpm是市中心建了一个中央仓库每家每户的门上挂一个指向中央仓库对应货架的指示牌要用的时候顺着指示牌去拿空间占用大幅下降而且每户只能拿自己采购单上列的东西不会出现邻居堆的东西被自己顺手拿走的情况也就是幽灵依赖。3.2 monorepo工作区的设计思路packages目录怎么规划pnpm对monorepo的支持同样非常成熟核心配置就在pnpm-workspace.yaml文件里。假设我们要搭建一个包含两个业务包和一个共享组件库的monorepo项目目录结构如下my-monorepo/ ├── packages/ │ ├── app-admin/ # 后台管理端应用 │ ├── app-mobile/ # 移动端应用 │ └── shared-utils/ # 公共工具库 ├── package.json └── pnpm-workspace.yaml在pnpm-workspace.yaml中声明工作区packages: - packages/** # - !packages/deprecated/** # 如果某些目录想排除用这个写法这一行配置的含义是packages目录下所有子目录每一个包含package.json的目录都会被自动识别为一个独立的工作区包。sub-package之间的依赖关系可以通过workspace协议来引用后面实操部分会细说。关于目录规划我建议遵循几个常见的拆分原则。第一粒度不要太细拆到“一个包对应一个明确的业务模块或一组强相关的工具函数”即可拆得太细会造成管理负担。第二如果项目里有公共的组件库、配置库、类型定义库单独建一个packages下的子目录作为独立包便于其他业务包复用。第三包的名称建议使用统一的组织名/包名命名规范这样在依赖引用时语义更清晰。3.3 初始化与基础配置package.json的高级字段在项目根目录生成一个package.json这是monorepo的灵魂。一个典型的配置长这样{ name: my-monorepo, private: true, scripts: { dev:admin: pnpm --filter mono/admin dev, dev:mobile: pnpm --filter mono/mobile dev, build: pnpm -r build, clean: pnpm -r exec rimraf node_modules rimraf node_modules }, devDependencies: { typescript: ^5.4.5, rimraf: ^6.0.1 } }这里有几个值得说明的细节。private: true非常关键。它告诉npm/pnpm这个包永远不会被发布到npm仓库因为它是仓库的根容器没有独立发布的含义。如果不设置这项某些发布命令默认会自动执行容易出问题。scripts里的--filter是pnpm操作monorepo子包的核心语法。--filter mono/admin表示只对mono/admin这个包执行后面的命令。而-rrecursive表示对工作区里所有包递归执行。关于根目录的node_modulespnpm会默认把公共依赖提升到根目录子包目录内的依赖则按需安装。这样可以有效减少重复安装同时每个子包依然能保持声明上的完整性。3.4 子包的依赖声明如何使用workspace协议当一个子包需要引用另一个子包时例如app-admin要引用shared-utils在packages/app-admin/package.json中这样写{ name: mono/admin, dependencies: { mono/shared-utils: workspace:* } }workspace:*是pnpm特有的协议标记含义是“使用工作区内对应包的最新版本”。这样写的好处有几点。其一pnpm install时不会去npm仓库搜索这个包而是直接链接到本地的packages/shared-utils目录开发时可以实时感知修改。其二当整个monorepo作为产物发布时可以通过命令批量把workspace:协议替换为实际版本号发布到仓库。如果觉得workspace:*和本地实际版本号不一致会产生困惑也可以用workspace:^或者直接写成workspace:1.0.0。但在开发阶段最省心的就是workspace:*不用频繁跟着版本号走。3.5 TypeScript与构建配置的配合monorepo场景下TypeScript的配置需要一点技巧因为在npm传统模式下类型解析遵循“就近查找”原则很容易因为包的链接关系导致类型找不对。通常的做法是每个子包维护自己的tsconfig.json然后在根目录提供一个基础配置{ compilerOptions: { module: ESNext, moduleResolution: bundler, target: ES2020, strict: true, declaration: true, esModuleInterop: true } }子包的配置通过extends继承基础配置{ extends: ../../tsconfig.base.json, include: [src] }如果某个子包是纯函数库需要生成.d.ts类型声明文件供其他包使用还需要在它的package.json里配置main、module、types字段{ name: mono/shared-utils, main: dist/index.js, module: dist/index.mjs, types: dist/index.d.ts, scripts: { build: tsup src/index.ts --format cjs,esm --dts } }实际开发中比较省心的构建工具是tsup配置简单还能同时产出CommonJS和ESM两种格式非常适合monorepo里的子包构建。4. 实操过程与核心环节实现4.1 完整搭建流程从零到一跑通一个最小monorepo下面这个流程是我推荐的最小可运行版本大家可以照着一步步操作整个过程大约10分钟就能走完。步骤一创建根目录并初始化mkdir my-monorepo cd my-monorepo步骤二创建pnpm工作区配置文件新建pnpm-workspace.yamlpackages: - packages/**步骤三创建根package.json在根目录执行npm init -y然后手动修改为私有项目并加入必要的脚本参照前文给出的示例。注意这里用npm init生成初始配置只是图省事之后的依赖安装和脚本执行都用pnpm。步骤四创建子包创建packages/shared-utils目录在里面初始化一个包mkdir -p packages/shared-utils/src cd packages/shared-utils npm init -y编辑它的package.json为{ name: mono/shared-utils, version: 1.0.0, main: dist/index.js, types: dist/index.d.ts, scripts: { build: tsup src/index.ts --format cjs,esm --dts } }在src下创建一个工具函数文件src/index.tsexport function add(a: number, b: number): number { return a b; } export function isEven(n: number): boolean { return n % 2 0; }步骤五创建业务应用包回到根目录mkdir -p packages/app-admin cd packages/app-admin npm init -y将package.json改成{ name: mono/admin, version: 0.1.0, private: true, scripts: { dev: vite, build: vite build }, dependencies: { mono/shared-utils: workspace:* } }步骤六安装依赖并验证在根目录执行pnpm install这条命令会读取根目录和所有子包的package.json将mono/shared-utils链接到工作区内的本地包并从npm仓库安装其他所有依赖。执行完成后在app-admin的源码里这样引用import { add } from mono/shared-utils; console.log(add(1, 2)); // 输出 3在本地开发时对shared-utils的任何修改app-admin都能实时感知不需要重新build也不需要改版本号这体验和拆仓库完全不同。步骤七常用命令速记安装所有子包的依赖pnpm install给某个特定子包安装依赖pnpm --filter mono/admin add lodash移除某个子包的依赖pnpm --filter mono/admin remove lodash在某个子包执行命令pnpm --filter mono/admin build在所有子包中依次执行命令pnpm -r build查看所有子包pnpm -r list4.2 根依赖与子包依赖怎么区分一个常见困惑是某个依赖到底应该装到根package.json还是子包的package.json里。我的经验规则是这样的。开发工具类的依赖比如typescript、vite、rimraf、eslint这种全仓库通用的工具装到根目录的devDependencies即可。这样所有子包共享同一份工具版本不会出现各个子包维护不同typescript版本导致行为不一致的问题。业务运行时的依赖比如某个子包单独用到axios、lodash就应该装到这个子包的dependencies里。这样这个子包在发布时依赖才能正确声明。有些工具像React、Vue这类运行时库如果在多个子包中都会用到那么每个子包都应该在自己的package.json中声明。虽然硬链接和store会让物理存储共享但依赖的语义完整性仍然需要每个包自己声明清楚这也方便将来某个包拆出去单独维护。4.3 生产场景的构建顺序与缓存管理多子包存在依赖关系的场景下构建顺序是一个容易被忽视的坑。比如app-admin依赖shared-utils如果直接并行构建所有包可能会先构建app-admin而此时shared-utils还没有构建出dist目录导致打包失败。pnpm提供了--filter的依赖链条语法来应对这个问题。例如pnpm --filter mono/admin... build这个命令中的...表示“包括该包以及所有被它依赖的包”。所以它会先构建shared-utils再构建app-admin顺序由pnpm依赖分析自动完成。如果要构建所有包最安全的命令是pnpm -r --topological build--topological表示按照拓扑顺序构建即先构建被依赖的包再构建依赖别人的包。这个参数在依赖链路较长的monorepo中非常实用。pnpm还内置了一个基于内容寻址的构建缓存只要源码没有变化同样的命令第二次执行会直接走缓存返回结果CI上能省不少时间。如果想要跳过缓存强制构建可以用--force参数。4.4 代码共享的方式源码直引与构建产物引用的选择monorepo里还有一个决策子包之间到底应该引用源码还是引用构建产物。直接引源码的做法是在package.json的main字段指向src/index.ts配合打包器的alias或者tsconfig的paths解析。优点是开发调试时源码实时生效不需要重新构建但缺点是打包时会涉及对TypeScript源码的编译配置稍显复杂。引用构建产物的方式更传统就是每个子包先build出dist目录依赖方指向dist。优点是兼容性最佳任何工具链都能正确处理缺点是每次修改源码后必须重新构建否则依赖方看到的是旧代码。我的建议是纯函数工具库这类低层包开发时用源码直引提升开发体验业务应用之间不要互相引用它们应该只依赖低层工具库发布到npm的包务必用构建产物引用。这样组合下来体验和稳定性都能照顾到。5. 常见问题与排查技巧实录5.1 安装报错速查表把搜索热词里出现频率最高的问题整理成一张表方便大家对着排查。报错信息根本原因解决方案pnpm 不是内部或外部命令也不是可运行的程序pnpm可执行文件路径未加入Windows系统PATH用npm config get prefix查看全局目录手动加入PATH环境变量无法将“pnpm”项识别为 cmdlet、函数、脚本文件...PowerShell执行策略限制执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUserpnpm: command not foundmacOS/Linux未安装成功或未加入PATH检查npm全局bin目录确认PATH中包含该目录ERR_PNPM_OUTDATED_LOCKFILElockfile版本与当前pnpm不兼容执行pnpm install --lockfile-only更新或升级pnpm版本Cannot read properties of undefined等安装崩溃pnpm版本过旧或Node.js版本不匹配升级pnpm到最新版确认Node.js在支持范围内ELIFECYCLE Command failed某个子包构建脚本报错先定位具体是哪个包的脚本单独pnpm --filter 包名 run script复现ERR_PNPM_APPROVE_BUILDSpnpm 10.x开始默认禁止依赖包执行install/build脚本执行pnpm approve-builds按提示选择允许运行的依赖5.2 pnpm下载失败的排查思路“pnpm下载失败”也是一个高频热词。需要区分是安装pnpm工具本身失败还是用pnpm下载依赖时失败。如果是安装pnpm本身失败通常是npm registry连不上或者速度太慢。可以先切换npm镜像源npm config set registry https://registry.npmmirror.com然后重新安装npm install -g pnpm如果是用pnpm安装依赖时失败可以尝试清理pnpm的缓存storepnpm store prune这个命令会清除store中无用的文件但不影响正在使用的包。如果项目里的node_modules状态已经混乱可以删掉重新安装rm -rf node_modules pnpm installWindows下删除node_modules如果遇到文件被占用或路径过长的问题可以用rimrafpnpm exec rimraf node_modules还有一种常见情况是lockfile损坏或与package.json不一致。优先尝试删掉pnpm-lock.yaml重新生成但这会丢失锁定的精确版本建议只在其他手段都无效时使用。5.3 pnpm 10.x的approve-builds机制这个热词非常有代表性是新版本带来的一个“惊喜”。pnpm从10.x版本开始默认不允许依赖包在安装时执行postinstall脚本。这么做的目的是安全考虑很多npm包在被安装时会自动执行一段构建脚本有些是必要的编译步骤有些可能存在投毒风险。pnpm希望开发者对允许哪些依赖执行脚本做到“显式知情”。当遇到某个依赖需要构建脚本而未被允许时终端会出现类似提示run pnpm approve-builds to pick which dependencies should be allowed to run。解决方案是执行pnpm approve-builds它会打开一个交互式界面列出所有声明了构建脚本的依赖你按空格选中需要信任的包回车确认。确认结果会写入package.json中的一个pnpm.onlyBuiltDependencies字段{ pnpm: { onlyBuiltDependencies: [esbuild] } }如果你在一个自动化的CI环境中没法交互式执行可以手动在根package.json中维护这个字段把需要执行脚本的包名列进去pnn install时会自动遵守。我遇到过的情况是项目里依赖了esbuild没有允许构建脚本就报错因为esbuild的postinstall会下载对应平台的二进制文件。这种情况下直接把esbuild加入onlyBuiltDependencies就能解决。这个字段在pnpm的官方文档叫onlyBuiltDependencies不同版本拼写一样放心用。5.4 版本升级带来的兼容性问题pnpm版本迭代比较快从9.x升到10.x时有一些breaking changes需要特别留意。除了approve-builds机制变化之外还有一个常见变动是pnpm对node_modules目录结构的调整。升级版本后最好执行一次完整的pnpm install让依赖重新链接再跑一遍构建。如果项目使用了pnpm的overrides字段用来强制覆盖某个依赖版本升级后字段的语法在个别版本上有过调整建议查看版本对应的changelog。这里也顺便说一句不要把pnpm固定在某个老版本上不动。新版本修复了很多安全漏洞和bug尤其是涉及依赖下载、lockfile解析、以及构建脚本管理的改动都对工程稳定性有实际帮助。升级前先在分支上跑一遍全量构建心里有底再切到主干。5.5 国内镜像加速与缓存优化pnpm默认的registry是官方源在国内访问速度不稳定。除了前面提到的全局registry配置也可以在项目根目录加入一个.npmrc文件registryhttps://registry.npmmirror.com如果希望某些包走特定源的地址还可以更精细地配置scope级别的registry。另外pnpm的store本身就带有缓存效果同类依赖在不同项目中不会重复下载所以多数情况下第二次安装会非常快。如果发现安装速度变慢可以先检查磁盘空间是否影响硬链接创建再检查镜像源是否稳定。6. 进阶让monorepo项目更稳定的几条心得6.1 用Changesets管理版本发布monorepo的子包版本管理是一个难点。每个包独立发版又要保证依赖方和依赖包的版本匹配手动处理非常容易出错。推荐使用changesets这套方案。它在仓库里维护每个变更的版本号意图配合命令行工具可以自动生成CHANGELOG并更新所有相关包的版本号。和pnpm的工作区配合得很好官方文档也有专门的pnpm支持章节。基本的接入方式是pnpm add -Dw changesets/cli pnpm changeset init开发完成后执行pnpm changeset按提示选择影响范围和版本类型就会生成一个markdown文件记录变更内容。发布时执行pnpm changeset version更新版本号最后pnpm changeset publish发布所有需要发布的包。6.2 保持依赖一致性和去重monorepo项目最怕的是各子包之间依赖版本漂移比如app-admin用lodash 4.17.20app-mobile用lodash 4.17.21长期下去会极大增加维护成本。一个实用的工具是syncpack可以统一检查多个包的依赖版本并自动纠正pnpm add -Dw syncpack syncpack fix-mismatches另外pnpm的lockfile天然对同一个依赖的多个小版本会去重只要不是跨越过大版本比如lodash 3和lodash 4一般不会产生太大的冗余。6.3 CI中的缓存策略在Github Actions或GitLab CI中pnpm可以配合store缓存大幅缩短安装时间。以GitHub Actions为例- name: Install pnpm run: corepack enable - name: Setup pnpm store cache uses: actions/cachev4 with: path: ~/.local/share/pnpm/store key: pnpm-store-${{ runner.os }}把pnpm的store目录加入CI缓存后续每次跑流水线时大部分依赖都能从缓存命中不用每次重新下载。6.4 定期清理无用的node_modules与依赖monorepo跑久了每个子包下的node_modules会积累不少无用的目录尤其是那些被移除掉的依赖残留。pnpm提供了一个命令来清理pnpm prune这个命令会移除未在package.json中声明的依赖释放磁盘空间。另外建议定期审查根目录和各个子包的依赖清单把不再使用的依赖清理掉保持维护的清爽度。7. 写在最后的经验分享从npm迁移到pnpm monorepo最直接的体感变化是安装速度快了几个量级磁盘空间省了将近一半。但这个过程也遇到过不少动手之前完全没预料到的问题比如PowerShell限制、approve-builds审批、锁定文件版本不一致等。这些坑并非无解只要理解了背后的设计逻辑排查起来非常快。我个人在实际项目中的体会是pnpm的workspace能力和monorepo的代码组织方式是相辅相成的。pnpm的严格依赖隔离机制天然地促使开发者明确每个子包的依赖边界避免代码里到处都能引用未声明模块的混乱状态。而monorepo的集中式管理又让这些隔离的子包可以以很低成本共享代码和工具链。如果你正在考虑引入pnpm monorepo或者已经完成了基础搭建但还在被各种报错困扰希望这篇内容能帮你少走几个弯路。最后再分享一个实用的小技巧在根目录的package.json里增加一条脚本reinstall: rimraf node_modules pnpm install当依赖状态混乱时一条命令就能快速重置实测在长期维护的monorepo项目中非常有用。