Rush Stack商店博客活动
跳至主要内容

注入依赖

注入依赖是 PNPM 的一项功能,允许将本地项目文件夹安装为已发布到 NPM 注册表一样。

背景:传统的 工作区符号链接

Rush 项目通常使用 workspace: 指定符来依赖于单仓工作区中的其他项目。例如,假设 my-projectmy-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-projectmy-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 installrush update)将通过将项目内容复制到 my-projectnode_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 installrush 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

另请参阅