blog
blog copied to clipboard
pnpm 问题记录
背景
在 2022 前端技术领域会有哪些新的变化? 话题中我曾回答到,越来越多的项目会开始使用 pnpm。
这是我正在推动的一件事,使用 pnpm 替换现在的 yarn 。无论是 csr 、ssr、monorepos 等类型项目都正在进行中,有近 10个项目已经迁移完成。 当时 yarn 的 pnp 特性出来的时候,观望过一阵子,没有大面积火起来,遂放弃 ... 现在是注意到 vite、modernjs 等使用了 pnpm,其设计理念与node_modules的目录结构也能让业务更加快速安全,所以决定开始全面使用 pnpm。
下面记录与分享一下最近使用 pnpm 遇到的问题与解决的过程~
✅ 已解决的问题
jest 单测运行失败
- 问题描叙: 使用 pnpm 后,原有的 jest 单测失败了
- 问题解决: jest 低版本不支持软链接, 升级 jest 大于等于 25.2.0 版本即可
- 报错信息:
- 问题分析:
- 根据报错的堆栈找到报错的包,发现其 packge.json dependencies 字段明确声明了依赖的 @xxx/fetch 的版本。但是从报错的信息看,实际运行测试时 import 的却是错误的版本了!
- 这里就需要思考一下 jest 是如何运行一个单测用例。如果是简单的 node xxx.test.js 运行一个单测那就不会有上面引用到错误版本依赖的问题,因为按照 node require 模块的规则是不会解析出错。
- 让我们回头看一个简单的 jest 单测用例,可以大胆推测一下每个 describe 或者是 it 语句就是一个单独的沙盒环境。如果简单的运行 node xxx.test.js 那就只会存在一个沙盒环境,所有的测试用例会共用一个上下文,这样明显不利于 jest 每个单测隔离的原则
- 所以我们初步判断 jest 会自己创建若干个沙盒环境去运行对应的测试代码。而用户的 src 待测试的代码在 jest 看来会通过 fs.readFileSync 去获取到内容字符串,然后不同类型文件经过 babelTransform 或者 tsTransform 得到 js 代码,最后通过 vm 或者 eval, new Function 这样去运行。
- 所以说代码中的 import require 等语句的路径是 jest 去静态分析补充完整的,低版本的 jest resolve 不支持软链接是完全有可能的,所以我们顺着 jest 的发版日志,找到最近支持软链接的版本问题解决。
- 同理 nextjs 等项目如果有问题也需要找到最近支持软链接的版本进行升级
// Copyright 2004-present Facebook. All Rights Reserved.
'use strict';
jest.useFakeTimers();
describe('timerGame', () => {
beforeEach(() => {
jest.spyOn(global, 'setTimeout');
});
it('waits 1 second before ending the game', () => {
const timerGame = require('../timerGame');
timerGame();
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toBeCalledWith(expect.any(Function), 1000);
});
it('calls the callback after 1 second via runAllTimers', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toBeCalled();
// Fast-forward until all timers have been executed
jest.runAllTimers();
// Now our callback should have been called!
expect(callback).toBeCalled();
expect(callback).toBeCalledTimes(1);
});
});
node-gyp rebuild failures
- 问题描叙: 使用 pnpm 后,项目中依赖的 Node.js C++ 插件 rebuild 失败
- 问题解决: pnpm 低版本 bug,升级 pnpm 大于等于 6.23.1 版本即可,相关 issue issues/2135
- 报错信息:
# pnpm i better-sqlite3
Packages: +11
+++++++++++
Resolving: total 11, reused 11, downloaded 0, done
node_modules/.pnpm/registry.npmjs.org/integer/2.1.0/node_modules/integer: Running install script, done in 2s
node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3: Running install script, failed in 393ms
.../5.4.3/node_modules/better-sqlite3 install$ node-gyp rebuild
│ gyp info it worked if it ends with ok
│ gyp info using [email protected]
│ gyp info using [email protected] | linux | x64
│ gyp info find Python using Python version 3.6.8 found at "/usr/bin/python3"
│ gyp info spawn /usr/bin/python3
│ gyp info spawn args [
│ gyp info spawn args '/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/gyp_main.py',
│ gyp info spawn args 'binding.gyp',
│ gyp info spawn args '-f',
│ gyp info spawn args 'make',
│ gyp info spawn args '-I',
│ gyp info spawn args '/root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3/build/config.gypi',
│ gyp info spawn args '-I',
│ gyp info spawn args '/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/addon.gypi',
│ gyp info spawn args '-I',
│ gyp info spawn args '/root/.cache/node-gyp/12.13.0/include/node/common.gypi',
│ gyp info spawn args '-Dlibrary=shared_library',
│ gyp info spawn args '-Dvisibility=default',
│ gyp info spawn args '-Dnode_root_dir=/root/.cache/node-gyp/12.13.0',
│ gyp info spawn args '-Dnode_gyp_dir=/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp',
│ gyp info spawn args '-Dnode_lib_file=/root/.cache/node-gyp/12.13.0/<(target_arch)/node.lib',
│ gyp info spawn args '-Dmodule_root_dir=/root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3',
│ gyp info spawn args '-Dnode_engine=v8',
│ gyp info spawn args '--depth=.',
│ gyp info spawn args '--no-parallel',
│ gyp info spawn args '--generator-output',
│ gyp info spawn args 'build',
│ gyp info spawn args '-Goutput_dir=.'
│ gyp info spawn args ]
│ Traceback (most recent call last):
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/gyp_main.py", line 50, in <module>
│ sys.exit(gyp.script_main())
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 554, in script_main
│ return main(sys.argv[1:])
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 547, in main
│ return gyp_main(args)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/__init__.py", line 532, in gyp_main
│ generator.GenerateOutput(flat_list, targets, data, params)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 2215, in GenerateOutput
│ part_of_all=qualified_target in needed_targets)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 794, in Write
│ extra_mac_bundle_resources, part_of_all)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 978, in WriteActions
│ part_of_all=part_of_all, command=name)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 1724, in WriteDoCmd
│ force = True)
│ File "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/gyp/pylib/gyp/generator/make.py", line 1779, in WriteMakeRule
│ cmddigest = hashlib.sha1(command if command else self.target).hexdigest()
│ TypeError: Unicode-objects must be encoded before hashing
│ gyp ERR! configure error
│ gyp ERR! stack Error: `gyp` failed with exit code: 1
│ gyp ERR! stack at ChildProcess.onCpExit (/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/lib/configure.js:351:16)
│ gyp ERR! stack at ChildProcess.emit (events.js:210:5)
│ gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
│ gyp ERR! System Linux 4.15.0-33-generic
│ gyp ERR! command "/usr/bin/node" "/usr/lib/node_modules/pnpm/lib/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
│ gyp ERR! cwd /root/2/node_modules/.pnpm/registry.npmjs.org/better-sqlite3/5.4.3/node_modules/better-sqlite3
│ gyp ERR! node -v v12.13.0
│ gyp ERR! node-gyp -v v6.0.0
│ gyp ERR! not ok
└─ Failed in 393ms
ERROR Command failed with exit code 1.
- 问题分析: 会不会是 c++ 版本的问题?而得到的信息为该镜像使用 yarn 却是好的! 最终换了同一个 Node.js 版本的镜像又好了,辗转反侧才在 pnpm 的 issue 中找到真正的原因,为 pnpm 低版本的 bug。
同一个版本的包有两份副本
- 问题描述: 同一个版本的包在 .pnpm 目录下却有两份副本
- 问题解决: 添加 .pnpmfile.cjs 文件,忽略 peerDependencies,使其对 peer 的处理与 yarn 保持一致
// .pnpmfile.cjs
function readPackage(pkg, context) {
if (pkg.name && pkg.peerDependencies) {
// https://pnpm.io/zh/how-peers-are-resolved
pkg.peerDependencies = {}
}
return pkg
}
module.exports = {
hooks: {
readPackage,
},
}
- 报错信息:
- 问题分析: 这个问题困扰了很久,后仔细阅读了 pnpm 的 How peers are resolved 文档,确认了造成的原因。详细的讨论可见: discussions/4051
only-allow 命令影响到了业务项目
- 问题描叙: 当你在一个公共包的项目中添加了 preinstall 的勾子,但是实际依赖该包的业务并未使用 pnpm,造成报错
- 问题解决: only-allow 当作为依赖时不应该进行检查,暂时使用支持了该功能的 only-allow-test 包代替。对应的讨论见 : discussions/4131
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
❌ 未解决的问题
pnpm add 与 pnpm i 命令不会去重
- 问题描叙: 当使用 pnpm add 或者 pnpm i 升级某个包时,会存在某几个版本兼容的包没有进行合并,导致存在多个版本。如 sass: ^1.30.0 和 sass: '^1.44.0'没有被合并,但是使用 pnpm update 去升级这个包是会进行合并
- 问题分析: 由于使用 yarn 的习惯不小心就会发生这种情况,所以希望支持 pnpm deduplicate 去重的命令,每次构建前强制运行一次,反馈后作者表示 pnpm add 命令会保持和 update 命令同样的行为。对应的讨论见 discussions/4143
- 临时解决: 找到了一个 pnpm update / install / add 命令后都会运行的一个勾子,在其中写了一个自定义的校验函数,如果pkgName 存在 dependencies 或者 devDependencies 中,就只能使用 pnpm update 命令去更新升级
// .pnpmfile.cjs
const argv = process.argv.slice(2)
function readPackage(pkg, context) {
// Override the manifest of [email protected] after downloading it from the registry
if (pkg.name && pkg.peerDependencies) {
// Replace [email protected] with [email protected]
pkg.peerDependencies = {}
}
return pkg
}
function checkCommand() {
const command = argv[0]
const pkgJson = require('./package.json')
const deps = Object.assign({}, pkgJson.dependencies || {}, pkgJson.devDependencies || {})
if (['add', 'i', 'install'].some((name) => command === name) && typeof argv[1] === 'string') {
const { name, version } = getNameAndVersion(argv[1])
if (deps[name]) {
throw new Error(
`【Inspector 依赖检查】: 更新升级依赖请用 "pnpm update ${name}${
version ? '@' + version : ''
}" 命令 !!!`
)
}
}
}
function getNameAndVersion(nameAndVersion) {
let name = ''
let version = ''
let splitName = nameAndVersion.split('@')
if (nameAndVersion.startsWith('@')) {
// @xxxx/pkg@latest or @xxxx/pkg
if (splitName.length === 3) {
// @xxxx/pkg@latest
name = `@` + splitName[1]
version = splitName[splitName.length - 1]
} else {
// @xxxx/pkg
name = `@` + splitName[1]
version = ''
}
} else {
// react@latest or react
if (splitName.length === 2) {
// @xxxx/pkg@latest
name = splitName[0]
version = splitName[1]
} else {
// @xxxx/pkg
name = splitName[0]
version = ''
}
}
return {
name,
version: version || 'latest',
}
}
module.exports = {
hooks: {
readPackage,
afterAllResolved(lockfile) {
checkCommand()
return lockfile
},
},
}
cypress e2e 测试运行失败
- 问题描叙: 使用 pnpm 后,原有的 cypress e2e 测试失败了
- 问题分析: 经过 debug 发现 cypress 还不支持 pnpm, 于是提了一个 pr,cypress 处理跟进较慢,还未解决
竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
评论区惊现大佬,哈哈,竟然还有这么一段吐槽的故事。我注意到软链接还是从 lerna 开始,而 pnpm 的软硬链接其上阵的架构又让人学习了不少 ~
竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
其实 npm 也在做软链模式了 https://github.com/npm/rfcs/pull/436
竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
其实 npm 也在做软链模式了 npm/rfcs#436
哈哈,用户已经损失严重了 ~
竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
其实 npm 也在做软链模式了 npm/rfcs#436
哈哈,用户已经损失严重了 ~
就目前来说, pnpm 的确比 npm 好用太多了,npm 正在被广大node.js开发者抛弃
竟然看到类似的经历,现在挺好的,大家已经大规模接受软链接,当年 pnpm 出来的时候大家都吐槽为啥是软链接。 不过事情总是非常奇妙,可遇见的 2022 年,回归到 npm 最原始的文件目录格式,又会引发一轮浪潮。
其实 npm 也在做软链模式了 npm/rfcs#436
哈哈,用户已经损失严重了 ~
就目前来说, pnpm 的确比 npm 好用太多了,npm 正在被广大node.js开发者抛弃
谁好用,就用谁 🤔
2022年应该会出现一个回归普通文件的模式,期待变化。
2022年应该会出现一个回归普通文件的模式,期待变化。
使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本
一时很难想到更优秀的方案了,期待出现 ~
2022年应该会出现一个回归普通文件的模式,期待变化。
使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本
一时很难想到更优秀的方案了,期待出现 ~
明天的 SEEConf https://seeconf.antfin.com/ ,有一个分享会讲到 npm fs 的概念,可以围观。
16: 50 来看尽然就结束了 😂 , 遗憾错过,后面去找下 ppt 学习一下
2022年应该会出现一个回归普通文件的模式,期待变化。
使用 yarn 的 pnp 需要用它的 resolver 才能从映射表找到正确的资源 使用 pnpm 后通过硬链接达到资源共享,只需支持软链接即可,降低了其他工具支持的成本 一时很难想到更优秀的方案了,期待出现 ~
明天的 SEEConf https://seeconf.antfin.com/ ,有一个分享会讲到 npm fs 的概念,可以围观。
有 PPT 的链接吗? 想看看
作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路
详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0
本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug
目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。
但是没有搞清楚是为什么,以及是否有更好的解决方案。
作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路
详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0
本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug
目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。
但是没有搞清楚是为什么,以及是否有更好的解决方案。
app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧
作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路 详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0 本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug 目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。 但是没有搞清楚是为什么,以及是否有更好的解决方案。
app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧
上述情况算是举例吧,实际遇到的情况相当于 moment作为依赖的依赖,是在antd中写死了版本号的,所以按道理不应该出现版本依赖到错误版本的问题,但实际上出现了。
作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路 详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0 本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug 目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。 但是没有搞清楚是为什么,以及是否有更好的解决方案。
app A 打包后的 antd 肯定是用了正确的 2.29.0 版本的 moment (除非 antd 没有写死版本), 至于 app A 中如果某个依赖用到了 moment, 但没声明版本这些包则可能用到 2.30.0 的 moment 吧
上述情况算是举例吧,实际遇到的情况相当于 moment作为依赖的依赖,是在antd中写死了版本号的,所以按道理不应该出现版本依赖到错误版本的问题,但实际上出现了。
进入.pnpm的任意模块目录,在当前目录下执行
$ node
$ require.resolve('moment')
返回的结果应该都是两个版本中的一个,可能是这个原因导致了你的项目中antd解析到的moment的也只会是其中一个版本
@wwh0219 此时应该到 antd 目录下去运行 require.resolve('moment') , baseDir 为当前代码 import 的目录
@xiaoxiaojx 但是antd里边引用moment的时候应该是没办法指定badedir的吧,根本原因还是在.pnpm目录下会有个node_modules目录,其中的moment指向了某个版本,所以在.pnpm/antd下require moment会默认引用到.pnpm/node_modules
pnpm add 与 pnpm i 命令不会去重 这个问题目前依然存在,怎么说呢,不算大问题,还是真遇到了还是挺头疼的,以前yarn用习惯了
@Mrcxt 可以用这个库 ocavue/pnpm-deduplicate , 目前看来没什么问题
作者你好,用pnpm workspace搭的monorepo项目,或多或少的遇到依赖冲突的问题,不知道是否遇到过以及是否有相关的思路
详细描述: 假设我有一个app A,依赖了antd: 3.18.0,antd自身又依赖了moment: 2.29.0。 另外有一个app B,依赖了antd: 3.20.0,依赖了moment: 2.30.0
本来预期情况是各依赖各自的版本,岁月静好。 但是会出现app A在跑dev或者打包后,moment依赖会变成用的2.30.0,从而造成一些bug
目前我的解决方法是在app A中显式的装一下[email protected]版本的依赖。
但是没有搞清楚是为什么,以及是否有更好的解决方案。
关于这个问题,这篇文章应该是说清楚了😂 https://blog.csdn.net/qq_21567385/article/details/121088506
23年8月,pnpm版本:v8.7.0 “pnpm add 与 pnpm i 命令不会去重”,这个问题,现在执行pnpm update依然不会去重,代码引用不会自动变更,感觉是个bug了