
pnpm deploy 重新注入工作区依赖构建自包含部署目录的修复与原理【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpmpnpm deploy用于把工作区workspace中选中的单个项目连同其依赖部署到一个独立目录供后续打包、上传或直接运行。本仓库的变更记录 deploy-injects-workspace-packages-again.md 记录了一次针对该命令的关键修复deploy 重新将工作区依赖以注入injected方式落地使部署目录保持自包含self-contained不再通过符号链接回指源工作区同时在普通安装流程中开启injectWorkspacePackages而关闭dedupeInjectedDeps时原本以link:协议存在的工作区依赖也会被重写为注入副本。读完本文你将理解这两处行为变更的来龙去脉、底层实现位置以及如何在项目中配置与验证。一次针对pnpm deploy的 patch 级修复该变更记录的内容非常聚焦全文如下pnpm/installing.deps-resolver: patch pacquet: patch pnpm: patch pnpm deploy injects workspace dependencies again, so the deploy directory is self-contained instead of symlinking back into the source workspace [#13754]. Enabling injectWorkspacePackages with dedupeInjectedDeps disabled now also rewrites already-linked workspace dependencies to injected copies.它同时包含两层信息影响面pnpm/installing.deps-resolver安装期依赖解析器、pacquetpnpm 的 Rust 重实现与pnpm三个包均以patch级别升版说明这是一次行为修正而非新功能。两个行为变更pnpm deploy恢复注入工作区依赖修复 issue #13754 暴露的回链问题injectWorkspacePackages配合关闭dedupeInjectedDeps时会把已经link:的工作区依赖改写为注入副本。下文分别从deploy 为何必须自包含deploy 如何强制注入注入与去重选项的组合行为三个层面展开。背景为什么部署目录必须自包含pnpm deploy的命令帮助文本将其定位为实验性功能Deploy a package from a workspace。其基本用法是pnpm --filterdeployed project name deploy target directory从 deploy.ts 的handler可以看出该命令会校验目标目录安全性不能等于/包含工作区根、被选项目根、当前目录目标路径上不允许出现符号链接把选中项目复制到目标目录copyProject在目标目录内重新执行一次installvirtualStoreDir被指向node_modules/.pnpmsaveLockfile: false确保部署目录内有一个完整的node_modules。问题出在工作区依赖上当被部署项目依赖同工作区的其他包时pnpm 默认用link:协议把它们链接起来。如果部署目录里的node_modules/xxx只是指向源工作区packages/xxx的符号链接部署产物就依附于源仓库——拷走部署目录、删掉源工作区或换台机器运行时链接随即失效。这正是 issue #13754 所描述的场景部署产物中存在指向源工作区的悬空/回链符号链接。因此自包含把所有工作区包以实体拷贝形式注入才是 deploy 正确的目标语义。关键配置injectWorkspacePackages与dedupeInjectedDeps要理解这次修复先要分清两个相关的配置项。二者均在配置读取层定义见 Config.tsinjectWorkspacePackages?: boolean第 244 行开启后pnpm 会把工作区依赖以注入方式处理——把包内容拷贝进虚拟仓库并链接到node_modules而不是简单link:回源目录dedupeInjectedDeps?: boolean第 322 行控制是否对已注入的工作区依赖做去重forceLegacyDeploy?: boolean第 185 行强制使用旧版 deploy 实现命令行对应--legacy快捷参数见 deploy.ts。dedupeInjectedDeps的底层实现在 dedupeInjectedDeps.ts。从代码可以看出它的工作方式收集各项目已注入的工作区依赖getInjectedDepsByProjects构建去重映射getDedupeMap然后把满足条件的注入条目改写为指向源工作区项目目录的相对link:引用applyDedupeMap中pkgId: link:...与directory: path.join(opts.lockfileDir, dedupedProjectId)。换句话说去重会倾向于把工作区包合并回其原始目录的链接——这本身是节省磁盘与避免多副本的合理优化但对 deploy 而言恰恰是反效果。源码剖析deploy 如何强制注入工作区依赖deploy.ts 在调用安装流程时对相关选项做了明确且带注释的硬编码// If enabled, dedupe-injected-deps will symlink workspace packages in the // deployed dir to their original (non-deployed) directory in an attempt to // dedupe workspace packages that dont need to be injected. The deployed // dir shouldnt have symlinks to the original workspace. Disable // dedupe-injected-deps to always inject workspace packages since copying is // desirable. dedupeInjectedDeps: false,这段注释直接点明了本次修复的动机deploy 场景下必须关闭去重强制所有工作区依赖以拷贝形式注入从而保证部署目录自包含。同区块还配套了其他几个保证自包含的选项enableGlobalVirtualStore: false——注释明确指出全局虚拟仓库会在部署目录之外创建符号链接必须关闭virtualStoreDir: path.join(deployDir, node_modules/.pnpm)——虚拟仓库落在部署目录内部pruneLockfileImporters: true——仅保留被部署项目对应的 importer避免node-linkerhoisted场景下把其他项目的空 importer 错误提升到根node_modulesuseLockfile: opts.nodeLinker ! hoisted——deploy 当前不兼容 hoisted 布局。而在使用共享工作区锁文件的部署路径deployFromSharedLockfile中createDeployFiles.ts 负责在内存中重新生成部署目录专用的package.json与pnpm-lock.yaml。这里的关键机制是把link:/file:引用解析为部署目录内的真实路径再转换成file:URL 形式的 dep pathresolveLinkOrFilecreateDeployFiles.ts解析link:相对被部署项目根与file:相对锁文件目录引用并保留 peer 依赖哈希后缀createFileUrlDepPathcreateDeployFiles.ts把解析出的路径转换为namefile://...形态的依赖路径使工作区包在部署锁文件中成为可安装的普通快照convertProjectSnapshotToPackageSnapshot把其他工作区项目从importers转换为带type: directoryresolution 的包快照peer 绑定信息则通过bindSingletonPeers在部署图中补齐对同一 peer 出现多个候选版本时会抛出DEPLOY_AMBIGUOUS_PEER并提示启用injectWorkspacePackages。由此部署锁文件中的工作区依赖以可注入的包存在后续install会把它们实体安装进部署目录的node_modules而非链接回源仓库。这也解释了为何在共享锁文件路径下injectWorkspacePackages、overrides、packageExtensions、configDependencies等选项会被显式置为undefined——它们的效应应该已经固化在包快照里见 deploy.ts 的注释。组合行为开启注入、关闭去重时重写已链接依赖变更记录的第二句话描述的是普通安装流程pnpm/installing.deps-resolver所属路径中的行为当用户同时设置injectWorkspacePackages: true与dedupeInjectedDeps: false时原本以link:协议保留的工作区依赖现在也会被改写为注入副本file:协议。仓库中的测试 injectWorkspacePackagesPersistence.test.ts 为这一行为提供了直接证据其中第 116 行的测试用例名即是 workspace packages switch to file: protocol when injected with dedupeInjectedDeps disabled。该测试还揭示了实现上的微妙之处在dedupeInjectedDeps正常运行时解析器会用保留既有link:的保护逻辑来补偿去重未覆盖的路径如单项目pnpm rm之后但该保护只在去重通过时会执行一旦dedupeInjectedDeps被关闭记录的link:就是过期的——因为注入语义要求工作区包必须落到部署/安装目录内而不是回链源目录因此修复后injectWorkspacePackages: truededupeInjectedDeps: false的组合会主动把这类遗留的link:依赖重写为注入副本保证注入承诺的一致性。换句话说这一修复让两个配置项的组合语义变得自洽dedupeInjectedDeps关闭意味着不做去重回链那么所有工作区依赖都应被注入拷贝若此时仍残留link:则说明状态与配置不符应当重写。测试与手动验证仓库在 pnpm/test/deploy.ts 中覆盖了注入场景下的 deploy 行为例如第 126-129 行在工作区配置injectWorkspacePackages: true后执行 install第 134-137 行pnpm --filterapp deploy --prod --no-optional后断言部署目录中node_modules/lib真实存在即被注入而被排除的optional-only不存在第 144-147 行进一步校验部署锁文件中不残留被裁剪的可选依赖边避免后续安装产生悬空符号链接关联 issue #13623。如果你希望在自己的工作区里手动验证可以按以下步骤操作# pnpm-workspace.yaml packages: - packages/* injectWorkspacePackages: truepnpm install pnpm --filterapp deploy ./deploy-out # 检查部署产物中的工作区依赖是否为实体拷贝而非符号链接 ls -l deploy-out/node_modules/workspace-pkg readlink deploy-out/node_modules/workspace-pkg || echo not a symlink: self-contained ✓如果readlink输出为空并打印 not a symlink说明该依赖已被实体注入部署目录可以独立拷贝运行如果输出指向packages/workspace-pkg则说明仍存在回链需要检查dedupeInjectedDeps相关配置或尝试--legacy标志回退到旧实现。小结与注意事项deploy 语义回归正确部署目录必须自包含工作区依赖一律注入拷贝不允许通过dedupeInjectedDeps回链到源工作区注入与去重互斥组合得到修正injectWorkspacePackages: true且dedupeInjectedDeps: false时遗留的link:依赖会被重写为注入副本代价注入是拷贝相比符号链接会占用更多磁盘空间——这正是源码注释中 copying is desirable 所隐含的权衡用磁盘换部署产物的可移植性与独立性兼容性出口若新的共享锁文件部署路径在你的场景下遇到问题可通过在pnpm-workspace.yaml中设置forceLegacyDeploy: true或命令行--legacy回退到旧实现详见 deploy.ts 中的报错提示该变更以 patch 级别随pnpm/installing.deps-resolver、pacquet、pnpm三个包发布升级到包含该修复的版本即可获得上述行为。对于需要把 monorepo 中某个服务独立部署到服务器或容器的团队而言这一修复保证了部署产物可独立运行的最小单元从此不再依赖源仓库的存在。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考