摘要

可以通过 package.json 文件中的 resolutions 字段,来选择嵌套依赖的版本。

动机

该问题最初是在 yarnpkg/yarn#2763 讨论过的。

总的来说,yarn 目前的行为存在的问题是,无法强制使用特定版本的嵌套依赖。

案例

例如,给定 package.ison 中的以下内容:

  "devDependencies": {
    "@angular/cli": "1.0.3",
    "typescript": "2.3.2"
  }

yarn.lock 文件将包含:

"typescript@>=2.0.0 <2.3.0":
  version "2.2.2"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.2.tgz#606022508479b55ffa368b58fee963a03dfd7b0c"
typescript@2.3.2:
  version "2.3.2"
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984"

此外,还将有:

  • typescript@2.3.2 位于 node_modules/typescript
  • typescript@2.2.2 位于 node_modules/@angular/cli/node_modules.

问题

在这种情况下,不可能强制整个项目都是用 typescript@2.3.2 (除非将整个项目扁平化,但这不是我们想要的)。

这对 TypeScript 来说是有意义的,因为用户的意图显然是要在编译其整个项目时使用 typeScript@2.3.2,而使用当前的行为,Angular CLI(负责编译 .ts 文件)将仅使用其 node_modules 中的 2.2.2 版本。

同样的,即使使用以下的 package.json 内容:

  "devDependencies": {
    "@angular/cli": "1.0.3"
  }

可能需要强制使用 typescript@2.3.2(或者使用 typescript@2.1.0)。

为什么?

在这些示例中,该需求似乎并不是非常重要(用户可能可以使用 typescript@2.2.2 或者要求 @angular/cli 开发团队放宽对 TypeScript 的限制),但可能存在嵌套依赖引入 bug 的情况,项目开发人员希望为其设置特定版本。(可以看这个示例 comment)。

相关场景(不在本文档的范围内)

这个动机的另一个扩展需求是可能需要将嵌套依赖映射到其他依赖项。例如,项目开发人员可能希望将 typescript@>=2.0.0 <2.3.0 映射到 my-typescript-fork@2.0.0

请参见下面的替代方案。

详细设计

提出的解决方案是始终考虑 package.json 文件的 resolutions 字段,并且基于每个包而不是仅在使用 --flat 参数时。

当 yarn 解析嵌套依赖项时,如果 resolutions 字段包含此包的规范,则将使用该规范。

在这个 RFC 中,特别关注了 npm 生态系统中包管理的特殊性质:实际上,在多个包的嵌套依赖项中有相同的包存在,它们的版本不同。因此,在 resolutions 字段中可以使用依赖项树的整体或其中一个子集的语法,使用基于 glob 模式的语法表达版本。

大多数示例都是使用精确的依赖项,但请注意,resolutions 字段中使用非精确规范应该被 yarn 接受并像通常一样解决。此主题也在下面讨论。

任何可能导致违反直觉的情况都会发出警告。此主题将在本节末尾讨论。

示例

当我们有以下包及其依赖项时:

package-a@1.0.0
 |_ package-d1@1.0.0
     |_ package-d2@1.0.0
package-a@2.0.0
 |_ package-d1@2.0.0
     |_ package-d2@1.0.0
package-b@1.0.0
 |_ package-d1@2.0.0
     |_ package-d2@1.0.0
package-c@1.0.0
 |_ package-a@2.0.0
     |_ package-d1@2.0.0
         |_ package-d2@1.0.0
  • 使用:
  "dependencies": {
    "package-a": "1.0.0",
    "package-b": "1.0.0"
  },
  "resolutions": {
    "**/package-d1": "2.0.0"
  }

yarn 将使用 package-d1@2.0.0 作为 package-d1 的每个嵌套依赖项,并且与 node_modules 文件夹的预期行为一致,不会复制 package-d1 安装。

  • 使用:
  "dependencies": {
    "package-a": "1.0.0",
    "package-b": "1.0.0"
  },
  "resolutions": {
    "package-a/package-d1": "3.0.0"
  }

yarn 将仅对 package-a 使用 package-d1@3.0.0,而 package-b 仍将在自己的 node_modules 中拥有 package-d1@2.0.0

  • 使用:
  "dependencies": {
    "package-a": "1.0.0",
    "package-c": "1.0.0"
  },
  "resolutions": {
    "**/package-a": "3.0.0"
  }

package-a 仍将被解析为 1.0.0,但是 package-c 将在其自己的 node_modules 中拥有 package-a@3.0.0

  • 使用:
  "dependencies": {
    "package-a": "1.0.0",
    "package-c": "1.0.0"
  },
  "resolutions": {
    "package-a": "3.0.0"
  }

yarn 不会执行任何操作(请见下文解释为什么)。

  • 使用:
  "dependencies": {
    "package-a": "1.0.0",
    "package-c": "1.0.0"
  },
  "resolutions": {
    "**/package-a/package-d1": "3.0.0"
  }

yarn 将同时将 package-apackage-c 中嵌套的依赖项 package-a 解析为 package-d1@3.0.0

解决方案

每个 resolutions 字段的子字段称为 resolution。它是由两个字符串表示的 JSON 字段:左侧是包名称,右侧是版本规范。

包标识

一份 resolution 包含在左侧应用于依赖树的 glob 模式(而不是 node_modules 目录树上的 glob 模式,因为后者是 yarn 解析的结果,受 resolution 影响)。

  • a/b 指的是项目依赖 a 的直接嵌套依赖 b
  • **/a/b 表示项目的所有依赖和嵌套依赖 a 的直接嵌套依赖 b
  • a/**/b 表示项目依赖 a 的所有嵌套依赖中的 b
  • **/a 表示项目中所有的嵌套依赖项 a
  • a**/a 的别名(出于向后兼容性考虑,如下所述,而且因为如果它不是这样的别名,那么它将表示一个非嵌套的项目依赖项,不能被覆盖,如下所述)。
  • ** 表示项目的所有嵌套依赖(通常不是一个好的选择,以及所有以 ** 结尾的其他指定)。

关于单星号(*)的说明:在包的解析中,* 是不被允许的,因为它会引入过多的不确定性。例如,有一种情况是在某个地方使用 package-* 来匹配 package-apackage-b,然而之后它会匹配一个新的嵌套依赖项 package-c,而这并不是期望匹配的。

版本规范化

一个 resolution 的右侧包含一个版本规范,该规范通常由 yarn 通过 semver 包进行解释。

与非嵌套依赖项的关系

devDependenciesoptionalDependenciesdependencies 字段始终优先于 resolutions 字段:如果用户在这里明确定义了依赖项,则表示他想要该版本,即使它是使用非精确规范指定的。因此,resolutions 字段仅适用于嵌套依赖项。尽管如此,在非嵌套依赖项版本规范与解析之间存在不兼容性的情况下,将发出警告。

这与使用包名称 package-a 作为 **/package-a 的别名是一致的,这是安全的:如果不是这种情况,package-a 将指代其中一个非嵌套依赖项并被忽略。

resolutions 字段的兼容性

直到现在,resolutions 字段可以包含以下形式的 resolution(由 add --flatinstall --flat 填充):

  "resolutions": {
    "package-a": "1.0.0"
  }

根据当前的提议,package-a 这个包名的行为是 **/package-a 的别名:这意味着如果一个项目的 resolutions 字段包含了之前的版本的 yarn 生成的 resolutions,那么在新版本的 yarn 中行为将会如预期,即对于嵌套的依赖项将会有固定的版本。

--flat 选项的关系

在这个 RFC 提出之前,--flat 既用于填充 resolutions 字段,也用于在执行 install 命令时(包括作为 add 命令的一部分进行安装时)考虑 resolutions 字段。

这个 RFC 的主要内容是在执行 install 命令(包括 add 命令的安装部分)时考虑 resolutions 字段。

因此,根据此 RFC,--flat 现在只用于填充 resolutions 字段。它的使用方式与之前相同(使用形式为 package-name 的包指定)。

该RFC唯一的破坏性更改是,yarn 现在总是会考虑 resolutions 字段,即使未指定 --flat 参数!

这也解决了这样一个奇怪的情况:当两个开发人员在同一个项目上工作时,一个使用 --flat,而另一个没有使用,他们可能会因此获得不同的 node_modules 内容。

注意,--flat 与安装模式有关(它通过 install 命令使用,但也通过 add 命令使用,但与安装本身有关,而不是添加),即使在 add 命令中也会继续以前的行为,通过请求项目的所有嵌套依赖项的 resolutions

在未来,--flat 命令需要重新考虑,但目前我们将保持其现有行为。

yarn.lock

该设计意味着对于给定的版本规范(例如 >=2.0.0 <2.3.0),可能存在一个与之不兼容的已解决版本(例如 2.3.2)。 只要用户通过 resolution 明确要求,就是可以接受的。

现在的情况是,这种情况会让 yarn 感到不适应,并引发 yarn.lock 的修改。(参考yarnpkg/yarn#3420)

这一特性将消除对 yarn 这种特性的需求。

check 命令的关系

默认情况下,check 命令(无特定选项)会读取 yarn.lock 文件并确保其中所有的版本与 node_modules 中的版本一致。

这样做我们就不需要额外的修改就可以免费获得这个功能。

--verify-tree

--verify-tree 旨在确保 node_modules 中的所有软件包在不考虑 yarn 的解析逻辑的情况下相互保持一致。

如果你强制使用一个不符合 semver 要求的版本号,--verify-tree 将会抛出错误。

这个 RFC 中暂时不需要对 --verify-tree 做出更改,但是以后可以扩展 --verify-tree 来支持 resolutions 字段的覆盖。

非精准版本规则

如果在 resolutions 字段中有非精确的规范,规则仍然是一样的:resolutions 字段优先于嵌套依赖中的规范。

如果 resolutions 字段的范围比嵌套依赖的规范更广,则可以发出警告。 如果 yarn 根据 resolutions 规范解析的确切版本与嵌套依赖规范不兼容,则会发生这种情况。

例如,如果 @angular/cli 依赖于 typescript@>=2.0.0 <2.3.0,并且 resolutions 字段包含 typescript@>=2.0.0 <2.4.0,那么如果 typescript 的最新可用版本是 2.2.2,则不会发出警告,如果 typescript 的最新可用版本是 2.3.2,则会发出警告。

这个做法的理由是,由于 yarn.lock 文件只能被用户(通过 yarn 命令)修改,因此在出现此类情况并写入 yarn.lock 文件之前,始终会发出警告。

日志中的警告

yarn 会警告以下情况:

  1. 未使用的 resolutions.

  2. 不兼容的解决方案(请参阅上面关于 yarn.lock 和关于扩展非精确规范的部分)。基本上,由于包没有正确表达其依赖关系,因此会使用不兼容的解决方案。在理想情况下,应在某个时间点修复包,并删除解决方案。从这个意义上说,不兼容的解决方案应该始终受到警告。此外,不兼容的解决方案是产生意外行为的潜在因素,因此用户不应忽略它。

resolutions 的局限性

resolutions 字段仅适用于本地项目,不适用于依赖它的项目。在某种程度上,这与 lock 文件是相同的。

我们如何教学

这不会有太大的影响,因为它通过添加扩展了当前的行为功能。

正如上面提到的关于 --flat 的评论,将 resolutions 总是考虑在内是唯一的破坏性更改,但这不会让人惊讶,这将使 yarn 的行为比以前更加一致。

“resolution” 这个术语的含义与以前相同,但现在不再完全由yarn控制,而且现在也受用户控制。

这是 yarn 的一个高级用法,所以新用户一开始不需要了解它。尽管如此,当项目依赖的一些包在它们的依赖关系中出现问题时,这个功能可能会被经常使用。

因此,有关这种用例并强调 resolutions 主要是暂时存在的一些文档将是有意义的。

缺点

教学

这会使得 yarn 的行为更加复杂,尽管更加有用。因此,用户可能很难理解它。RFC 提交者在 Maven 上看到过许多次这种情况,Maven 的依赖管理非常复杂但也非常完整。用户会感到困惑,理解操作 resolutions 字段的影响可能需要一些时间。

代替品

全局嵌套依赖项解析

从一个示例开始,该解决方案将采用以下形式的 package.json 文件:

  "devDependencies": {
    "@angular/cli": "1.0.3",
    "typescript": "2.3.2"
  },
  "resolutions": {
    "typescript": "2.0.2"
  }

这个例子中,yarn 将会在整个项目中使用 typescript@2.0.2。应该遵循与本 RFC 选定方案相同的考虑(除了全局模式的部分)。

通过与 yarn 维护着的讨论,这基本上太简单了。

配套版本规格

这是针对上面动机部分中提出的 “超出范围场景” 的一种简化解决方案(它映射版本但不映射依赖项名称)。

这项方案在 comment 被提议。

这种方案与上一个备选方案类似,但在包名称中加入了版本规范。在 package.json 中,它的形式如下:

  "devDependencies": {
    "@angular/cli": "1.0.3",
    "typescript": "2.2.2",
    "more dependencies..."
  },
  "resolutions": {
    "typescript@>=2.0.0 <2.3.0": "typescript@2.3.2"
  }

那么 yarn 就会将匹配到的版本规范替换成用户指定的版本规范。例如,一个本来会被解析为 typescript@2.2.2 的依赖项,现在会被解析为 typescript@2.3.2

这个是过于高级的用法,可以被认为是这个 RFC 的一个可能的扩展。

映射版本规范以及包名称

与前面的两种解决方案类似,但右侧的 resolution 使用了不同的名称:

  "devDependencies": {
    "@angular/cli": "1.0.3",
    "typescript": "2.2.2"
  },
  "resolutions": {
    "typescript@>=2.0.0 <2.3.0": "my-typescript-fork@2.3.2"
  }

甚至:

  "devDependencies": {
    "@angular/cli": "1.0.3",
    "typescript": "2.2.2"
  },
  "resolutions": {
    "typescript": "my-typescript-fork",
  }

并且版本规范将被保留。

这个是过于高级的用法,可以被认为是这个 RFC 的一个可能的扩展。

未来的扩展

在上面的部分中讨论的两种替代方案,“映射版本规范”和“映射版本规范以及包名称”,可以根据当前的提议进行调整,以支持这些用例。

flatten

这是关于 --flat 选项的一些注释以及它与这个 RFC 的未来相关性的说明。

--flat 安装选项可以转换为一个 flatten 命令,该命令将:

  1. 在所有嵌套的依赖项中填充 resolutions
  2. 设置 flat 字段到 package.json

对于 install 来说,拥有一个扁平化模式并没有太大意义:

  1. 这个 RFC 中的方案已经使 install 命令遵循了 resolutions 字段。
  2. 在我看来,install 应该只关注构建 node_modules 目录,而不是修改 package.json 文件。

那么 package.json 中的 flat 选项(以及 add--flat 选项)将不再用于安装,而是用于添加、升级等操作(即修改 package.json 中的依赖项)。这将通过填充 resolutions 字段来确保项目保持扁平化。

译源:selective-versions-resolutions
协作: chatGPT

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐