Nodejs pnpm 问题记录
Nodejs pnpm 问题记录
hi, all ~ 分享一个批量项目迁移到 pnpm 过程中遇到的问题与解决办法,希望对大家有所帮助 ~
原文链接: https://github.com/xiaoxiaojx/blog/issues/23
背景
在 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](/user/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
cypress e2e 测试运行失败
- 问题描叙: 使用 pnpm 后,原有的 cypress e2e 测试失败了
- 问题分析: 经过 debug 发现 cypress 还不支持 pnpm, 于是提了一个 pr,cypress 处理跟进较慢,还未解决
也正在逐步使用 pnpm 中
强👍
除了测试的问题其他问题我基本都有遇到,多版本和去重的问题因为项目小没有实质影响无视了,其他的 google 一下也解决掉了。
只能说 pnpm 带来的好处确实很明显,降低心智负担,提升效率。但是当前确实也有不少 bug ,所幸他们的迭代速度也非常快,基本没几天就在命令行里提示新版本了。
是的,看来都是感同身受 ~
针对您提到的Node.js中pnpm的问题记录,以下是一些常见的pnpm问题及解决方案,供您参考:
-
安装权限问题: 在安装pnpm时,如果遇到权限问题,尤其是在使用npm或yarn安装时,可以尝试使用管理员权限运行命令行。在Windows上,右键点击命令提示符或PowerShell,选择“以管理员身份运行”。在macOS或Linux上,使用sudo命令。
-
依赖解析失败: 在安装项目依赖时,如果遇到依赖解析失败的问题,可以检查
pnpm-lock.yaml
文件是否存在且没有损坏。此外,清理pnpm的缓存并重新安装依赖也是一个有效的解决方案。运行以下命令:pnpm store prune rm -rf node_modules pnpm-lock.yaml pnpm install
-
Node.js版本不兼容: 确保您的Node.js版本与pnpm兼容。不兼容的Node.js版本可能会导致依赖解析错误、安装失败或脚本执行失败。您可以通过查阅pnpm的官方文档来了解其对Node.js版本的具体要求。
-
Monorepo环境中的依赖冲突: 在monorepo环境中,多个项目可能会共享依赖,导致依赖冲突。使用
pnpm-workspace.yaml
配置文件来定义工作区,并在根目录的package.json
中指定统一的依赖版本,以避免版本冲突。运行以下命令来消除重复的依赖:pnpm dedupe
-
查看和切换源: 您可以通过以下命令查看和切换pnpm的源:
pnpm config get registry pnpm config set registry https://registry.npmmirror.com/
希望这些解决方案能帮助您解决在使用pnpm时遇到的问题。如有其他疑问,请随时提问。