pnpm为同一依赖项的完全相同版本提供了不同的哈希,破坏了nestjs



我有一个单回购,它有一个非常基本的设置,可以在这里重现这个问题:

它是一个单独的nestjs应用程序,有两个包可供读取。

@nestjs/core和其他依赖项是包和主应用程序工作所必需的,并且它被强制为完全相同的固定版本,不仅在它们自己的本地包.json上,而且在主包.json中的resolutions {}配置中也是如此。

我可以检查锁定文件,发现尽管使用了相同的版本,但哈希不同,导致nestjs出现重大问题,无法可靠地导入可注入的依赖项,导致它在引导时中断。

有办法防止这种情况发生吗?强制链接完全相同的散列/依赖项

编辑:

使用pnpm v7.29.0,您不再需要执行下面描述的破解操作,只需将其留在此处用于教育目的。

现在的解决方案只是设置dedupe-peer-dependents=true(例如在您的.npmrc中)。


来自pnpm文档

- foo-parent-1
- bar@1.0.0
- baz@1.0.0
- foo@1.0.0
- foo-parent-2
- bar@1.0.0
- baz@1.1.0
- foo@1.0.0

在上面的例子中,foo@1.0.0是为foo-parent-1和foo-parend-2安装的。这两个包都有bar和baz,但它们依赖于不同版本的baz。因此foo@1.0.0具有两组不同的依赖项:一组具有baz@1.0.0另一个baz@1.1.0.为了支持这些用例,pnpm必须硬链接foo@1.0.0存在不同依赖集的次数。

对于您的特定情况,foo===@nestjs/core,baz===@nessjs/micservices。尽管这里使用的例子是";不同版本";,这同样适用于可选的对等依赖关系。因此,为了重新说明这个例子,在您的上下文中:

- my-nestjs-app
- @nestjs/microservices@9.1.4
- @nestjs/core@9.1.4
- my-other-nestjs-app
- @nestjs/core@9.1.4

通常,如果一个包没有对等依赖项,它会硬链接到其依赖项符号链接旁边的node_modules文件夹,如下所示:

然而,如果foo[@nestjs/core]具有对等依赖关系,则可能有多组依赖关系,因此我们为不同的对等依赖关系解析创建不同的集合

^这通常适用于大多数软件包。然而@nestjs/core是特别的。它是有状态的,因此它可以处理所有的运行时依赖项注入。pnpm在monorepo中创建@nestjs/core的多个副本会导致您看到的令人困惑的行为,因为您的应用程序可能依赖于一个副本,而其他nestjs库则依赖于另一个副本。根据NestJS的不和,这似乎是使用pnpm+nest的开发人员普遍遇到的问题。

解决方案

使用pnpm钩子在解析时间修改nestjs包的peerDependenciesMeta

// .pnpmfile.cjs in your monorepo's root
function readPackage(pkg, context) {
if (pkg.name && pkg.name.startsWith('@nestjs/')) {
context.log(`${pkg.name}: make all peer dependencies required`);
pkg.peerDependenciesMeta = {}; 
}
return pkg;
}
module.exports = {
hooks: {
readPackage,
}
};

这是一个破解IMO,处理起来真的很烦人,因为Renovate/Dependabot在执行依赖关系更新时会忽略.pnpmfile.js。我建议使用Nx或Nest/有状态包更适合使用的其他包管理器。

当依赖关系具有对等依赖关系时,如果在依赖关系图的各个部分中对对等依赖关系进行不同的解析,则可能会多次将其写入node_modules。

在您的案例中,@nestjs/core位于graphql-server项目和@myapp/entities项目的依赖项中。CCD_ 11具有CCD_ 12作为可选的对等依赖。

@nestjs/platform-expressgraphql-server项目的依赖项中,因此pnpm将其链接到@nestjs/platform-express。你可以在锁定文件中看到它:

/@nestjs/core/8.4.7_fkqgj3xrohk2pflugljc4sz7ea:
resolution: {integrity: sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw==}
requiresBuild: true
peerDependencies:
'@nestjs/common': ^8.0.0
'@nestjs/microservices': ^8.0.0
'@nestjs/platform-express': ^8.0.0
'@nestjs/websockets': ^8.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
'@nestjs/microservices':
optional: true
'@nestjs/platform-express':
optional: true
'@nestjs/websockets':
optional: true
dependencies:
'@nestjs/common': 8.4.7_47vcjb2de6lyibr6g4enoa5lyu
'@nestjs/platform-express': 8.4.7_7tsmhnugyerf5okgqzer2mfqme # <------HERE
'@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
object-hash: 3.0.0
path-to-regexp: 3.2.0
reflect-metadata: 0.1.13
rxjs: 7.5.5
tslib: 2.4.0
uuid: 8.3.2
transitivePeerDependencies:
- encoding

在另一个项目(@myapp/entities)中,@nestjs/platform-express不在依赖项中,因此在安装@nestjs/core时,pnpm无法解决可选的对等依赖项。因此,pnpm需要创建@nestjs/core的另一个实例,该实例没有链接此可选对等体。正如您在锁定文件中看到的,其他条目没有@nestjs/platform-express:

/@nestjs/core/8.4.7_g7av3gvncewo44y4rurz3mgav4:
resolution: {integrity: sha512-XB9uexHqzr2xkPo6QSiQWJJttyYYLmvQ5My64cFvWFi7Wk2NIus0/xUNInwX3kmFWB6pF1ab5Y2ZBvWdPwGBhw==}
requiresBuild: true
peerDependencies:
'@nestjs/common': ^8.0.0
'@nestjs/microservices': ^8.0.0
'@nestjs/platform-express': ^8.0.0
'@nestjs/websockets': ^8.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
'@nestjs/microservices':
optional: true
'@nestjs/platform-express':
optional: true
'@nestjs/websockets':
optional: true
dependencies:
'@nestjs/common': 8.4.7_47vcjb2de6lyibr6g4enoa5lyu
'@nuxtjs/opencollective': 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
object-hash: 3.0.0
path-to-regexp: 3.2.0
reflect-metadata: 0.1.13
rxjs: 7.5.5
tslib: 2.4.0
uuid: 8.3.2
transitivePeerDependencies:
- encoding

要解决此问题,可以将@nestjs/platform-express添加到@myapp/entities项目的依赖项中。它应该与其他项目中的版本相同。

  1. .npmrc中设置use-lockfile-v6=true

  2. 然后pnpm install获取新的锁定文件

  3. 然后分析pnpm-lock.yaml的顶部,看看你的单回购的哪些包(你的)出现了不同的版本。

这是一项有点手动的工作,但从上到下开始,查看importers:块。这是您在package.json文件中列出的所有直接依赖项。

停在每个看起来有后缀的依赖项上,比如version: 0.31.1(react@18.2.0)(注意,没有重复风险的是version: 0.31.1)。

例如,在我的案例中,我为我的packageA:找到了

'@mui/material':
specifier: ^5.10.16
version: 5.10.16(@emotion/react@11.10.5)(@emotion/styled@11.10.5)(@types/react@18.0.26)(react-dom@18.2.0)(react@18.2.0)

然后将'@mui/material':复制到剪贴板中,并在importer:部分中搜索所有出现的内容。如果您发现了不同的版本模式,这意味着它已经消除了重复,例如我的packageB:

'@mui/material':
specifier: ^5.10.16
version: 5.10.16(@types/react@18.0.26)(react-dom@18.2.0)(react@18.2.0)

在我的案例中,我的packageA具有未在packageB中指定的@emotion/*依赖项,这使得它们不匹配,因为@mui/material将它们列为peerDependencies。由于我不再需要@emotion/*依赖项,我只是删除了它们。然后是CCD_ 41,现在只有CCD_。这意味着它将对packageApackageB使用完全相同的包。

如果我在packageA中需要@emotion/*,我可以将它们添加到packageB中,那么合并依赖关系也会得到同样的结果。这个想法只是修复我的不同@mui/materialpeerDependencies版本,以便它们可以匹配和合并。在中进行了解释https://pnpm.io/how-peers-are-resolved。

我认为这也有助于确保您的直接依赖项在monoreto中的任何地方都使用相同的版本(这是对齐peerDependencies之前的第一步。为此,我在根package.json:中使用

"pnpm": {
"overrides": {
"@mui/material": "^5.10.16",
"react": "^18.2.0",
"react-dom": "^18.2.0",
}
},

(对每个你不想看到"重复"的依赖项重复这个过程…)

(注意,这是根据我自己的经验,它可能并不完美,但它至少帮助了我^^…)

最新更新