注入依赖
注入依赖是 PNPM 的一项功能,允许将本地项目文件夹安装为已发布到 NPM 注册表一样。
背景:传统的 工作区符号链接
Rush 项目通常使用 workspace:
指定符来依赖于单仓工作区中的其他项目。例如,假设 my-project
和 my-library
是 Rush 工作区中的项目
my-repo/apps/my-project/package.json
{
"name": "my-project",
"version": "1.2.3",
"dependencies": {
"react": "^18.3.1",
"my-library": "workspace:*"
}
}
在上面的示例中,react
包将通过从 NPM 注册表下载并提取到 node_modules
子文件夹中来安装。相反,workspace:*
指定符会导致 PNPM 创建一个指向 my-library
被开发的源代码文件夹的 node_modules
符号链接
符号链接: my-repo/apps/my-project/node_modules/my-library
--> my-repo/libraries/my-library/
这样,my-project
将始终使用 my-library
的最新本地构建输出。甚至可能 my-project
和 my-library
根本没有发布到 NPM 注册表。
工作区符号链接的限制
但是,假设 my-library
声明了一个像这样的对等依赖
my-repo/libraries/my-library/package.json
{
"name": "my-library",
"version": "0.0.0",
"peerDependencies": {
"react": "^18.0.0 || ^17.0.0"
},
"devDependencies": {
"react": "17.0.0"
}
}
my-library
项目声明它可以使用 React 版本 17 和 18。对于本地开发,devDependencies
会安装最旧的支持版本 17.0.0,这是一种常见的做法,用于验证向后兼容性。
为什么我们需要 peerDependencies
而不是 dependencies
?使用 dependencies
,包管理器可以自由选择任何与 "^18.0.0 || ^17.0.0"
匹配的 react
版本。例如,如果我们的应用程序使用的是 React 17,那么 my-library
可能会获得 React 18,这是错误的。对等依赖通过规定 my-library
必须获得与其使用者相同的 react
版本(实际上是相同的安装磁盘文件夹)来避免这种情况。
如果两个不同的应用程序都依赖于 my-library
,而这些应用程序的 react
版本不同会怎么样?对于外部 NPM 包,PNPM 通常会通过将两个(相同版本)的 my-library
复制到 node_modules
的两个不同子文件夹中来解决这个问题。这些副本称为 "对等依赖 Doppelgangers"。 由于 Node.js 模块解析器的设计约束,它们是必要的
无上下文解析:当磁盘上的给定文件导入 NPM 包时,模块解析器将始终以相同的方式解析该文件。
换句话说,让 my-library/lib/index.js
针对 app1
导入 React 17,而针对 app2
导入 React 18 的唯一方法是,这两个应用程序从 index.js
的两个不同(Doppelganger)副本中导入。
包管理器在将 NPM 包提取到 node_modules
文件夹中时,会根据需要自动创建 Doppelgangers。但是,在我们的示例中,my-project
使用 workspace:*
来创建一个指向 my-library
项目文件夹的符号链接,而不是将 NPM 包提取到 node_modules
文件夹中。对等依赖将如何满足?PNPM 在这种情况下 simply 会产生不正确的安装
- 当
my-project
导入 React 时,它将获得版本 18 - 当
my-project
导入my-library
,而my-library
导入 React 时,它将获得版本 17(从devDependencies
安装)
peerDependencies
被忽略。
注入依赖来拯救
为了解决这个问题,PNPM 支持 一个 package.json 设置,称为 injected
,它将导致 my-library
就像发布到 NPM 一样被安装。以下是启用它的方法
my-repo/apps/my-project/package.json
{
"name": "my-project",
"version": "1.2.3",
"dependencies": {
"react": "^18.3.1",
"my-library": "workspace:*"
},
"dependenciesMeta": {
"my-library": {
"injected": true
}
}
}
通过此更改,pnpm install
(在我们的情况下为 rush install
或 rush update
)将通过将项目内容复制到 my-project
的 node_modules
文件夹中来安装 my-library
。由于它们是传统安装的,注入依赖可以成为 Doppelgangers 并正确满足对等依赖。
第二个好处:注入安装会尊重发布过滤器,例如 .npmignore
,因此复制的内容准确反映了将 my-library
发布到 NPM 注册表时会发生的情况。因此,使用该库的测试项目可以将 injected: true
设置为捕获 .npmignore
过滤器中的错误——在使用 workspace:
符号链接时通常会忽略的错误配置。
听起来很棒——那么为什么 PNPM 不会对每个 workspace:
引用使用注入安装呢?
同步注入依赖
我们说过注入依赖在 rush install
期间被复制到 node_modules
文件夹中。但是,如果我们对 my-library
进行更改,然后运行 rush build
会怎么样?当 my-project
导入 my-library
时,它仍然会找到 node_modules
中的旧副本。为了获得正确的结果,我们需要在每次重建 my-library
后重新执行 rush install
。更准确地说,我们需要在构建任何注入项目之后但在消费者构建之前重新执行 rush install
。在最坏的情况下,这意味着在 rush build
期间可能需要重新执行 rush install
数百次。这是不现实的。
PNPM 目前还没有包含针对此问题的内置解决方案,因此注入依赖尚未得到广泛采用。但是,一个名为 pnpm-sync 的新工具提供了解决方案:每当重建 my-library
时,pnpm-sync
都可以将其输出复制到更新适当的 node_modules
子文件夹中。
通常情况下,每个项目都应该决定是否以及如何调用 pnpm-sync
命令,但 Rush 集成了此功能并自动管理它。若要在 Rush 中使用 pnpm-sync
,请启用 usePnpmSyncForInjectedDependencies
实验
common/config/rush/experiments.json
/**
* (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot
* correctly satisfy versioning requirements without installing duplicate copies of a package inside the
* node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally
* installed by making a symlink to the local project source folder. PNPM's "injected dependencies"
* feature provides a model for copying the local project folder into node_modules, however copying
* must occur AFTER the dependency project is built and BEFORE the consuming project starts to build.
* The "pnpm-sync" tool manages this operation; see its documentation for details.
* Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies
* by invoking "pnpm-sync" during the build.
*/
"usePnpmSyncForInjectedDependencies": true
这将启用以下行为
rush install
和rush update
将自动调用pnpm-sync prepare
来配置my-library
等注入依赖的复制rush build
(以及其他 Rush 自定义命令和阶段)将自动调用pnpm-sync copy
,每当像my-library
这样的项目被重建时,都会重新同步安装的文件夹rushx
将在my-library
文件夹下执行任何操作后,自动调用pnpm-sync copy
子空间的注入依赖
如果您正在使用 Rush 子空间,请考虑也启用 alwaysInjectDependenciesFromOtherSubspaces
common/config/subspaces/<subspace-name>/pnpm-config.json
/**
* When a project uses `workspace:` to depend on another Rush project, PNPM normally installs
* it by creating a symlink under `node_modules`. This generally works well, but in certain
* cases such as differing `peerDependencies` versions, symlinking may cause trouble
* such as incorrectly satisfied versions. For such cases, the dependency can be declared
* as "injected", causing PNPM to copy its built output into `node_modules` like a real
* install from a registry. Details here: https://rush.node.org.cn/pages/advanced/injected_deps/
*
* When using Rush subspaces, these sorts of versioning problems are much more likely if
* `workspace:` refers to a project from a different subspace. This is because the symlink
* would point to a separate `node_modules` tree installed by a different PNPM lockfile.
* A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`,
* which automatically treats all projects from other subspaces as injected dependencies
* without having to manually configure them.
*
* NOTE: Use carefully -- excessive file copying can slow down the `rush install` and
* `pnpm-sync` operations if too many dependencies become injected.
*
* The default value is false.
*/
"alwaysInjectDependenciesFromOtherSubspaces": true
另请参阅
- pnpm-sync GitHub 项目
- dependenciesMeta.*.injected 来自 PNPM 文档
- Rush 子空间