Nodejs pnpm 问题记录

发布于 1周前 作者 yibo5220 来自 nodejs/Nestjs

Nodejs pnpm 问题记录

image

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 版本即可
  • 报错信息: ca958dffa2ef75ad06f068262b471a7ea952fc25
  • 问题分析:
    1. 根据报错的堆栈找到报错的包,发现其 packge.json dependencies 字段明确声明了依赖的 [@xxx](/user/xxx)/fetch 的版本。但是从报错的信息看,实际运行测试时 import 的却是错误的版本了!
    2. 这里就需要思考一下 jest 是如何运行一个单测用例。如果是简单的 node xxx.test.js 运行一个单测那就不会有上面引用到错误版本依赖的问题,因为按照 node require 模块的规则是不会解析出错。
    3. 让我们回头看一个简单的 jest 单测用例,可以大胆推测一下每个 describe 或者是 it 语句就是一个单独的沙盒环境。如果简单的运行 node xxx.test.js 那就只会存在一个沙盒环境,所有的测试用例会共用一个上下文,这样明显不利于 jest 每个单测隔离的原则
    4. 所以我们初步判断 jest 会自己创建若干个沙盒环境去运行对应的测试代码。而用户的 src 待测试的代码在 jest 看来会通过 fs.readFileSync 去获取到内容字符串,然后不同类型文件经过 babelTransform 或者 tsTransform 得到 js 代码,最后通过 vm 或者 eval, new Function 这样去运行。
    5. 所以说代码中的 import require 等语句的路径是 jest 去静态分析补充完整的,低版本的 jest resolve 不支持软链接是完全有可能的,所以我们顺着 jest 的发版日志,找到最近支持软链接的版本问题解决。
    6. 同理 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, }, }

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 处理跟进较慢,还未解决

5 回复

也正在逐步使用 pnpm 中


强👍
除了测试的问题其他问题我基本都有遇到,多版本和去重的问题因为项目小没有实质影响无视了,其他的 google 一下也解决掉了。
只能说 pnpm 带来的好处确实很明显,降低心智负担,提升效率。但是当前确实也有不少 bug ,所幸他们的迭代速度也非常快,基本没几天就在命令行里提示新版本了。

快速且安全 ~

是的,看来都是感同身受 ~

针对您提到的Node.js中pnpm的问题记录,以下是一些常见的pnpm问题及解决方案,供您参考:

  1. 安装权限问题: 在安装pnpm时,如果遇到权限问题,尤其是在使用npm或yarn安装时,可以尝试使用管理员权限运行命令行。在Windows上,右键点击命令提示符或PowerShell,选择“以管理员身份运行”。在macOS或Linux上,使用sudo命令。

  2. 依赖解析失败: 在安装项目依赖时,如果遇到依赖解析失败的问题,可以检查pnpm-lock.yaml文件是否存在且没有损坏。此外,清理pnpm的缓存并重新安装依赖也是一个有效的解决方案。运行以下命令:

    pnpm store prune
    rm -rf node_modules pnpm-lock.yaml
    pnpm install
    
  3. Node.js版本不兼容: 确保您的Node.js版本与pnpm兼容。不兼容的Node.js版本可能会导致依赖解析错误、安装失败或脚本执行失败。您可以通过查阅pnpm的官方文档来了解其对Node.js版本的具体要求。

  4. Monorepo环境中的依赖冲突: 在monorepo环境中,多个项目可能会共享依赖,导致依赖冲突。使用pnpm-workspace.yaml配置文件来定义工作区,并在根目录的package.json中指定统一的依赖版本,以避免版本冲突。运行以下命令来消除重复的依赖:

    pnpm dedupe
    
  5. 查看和切换源: 您可以通过以下命令查看和切换pnpm的源:

    pnpm config get registry
    pnpm config set registry https://registry.npmmirror.com/
    

希望这些解决方案能帮助您解决在使用pnpm时遇到的问题。如有其他疑问,请随时提问。

回到顶部