背景
在 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 等项目如果有问题也需要找到最近支持软链接的版本进行升级
1 | // Copyright 2004-present Facebook. All Rights Reserved. |
node-gyp rebuild failures
- 问题描叙: 使用 pnpm 后,项目中依赖的 Node.js C++ 插件 rebuild 失败
- 问题解决: pnpm 低版本 bug,升级 pnpm 大于等于 6.23.1 版本即可,相关 issue issues/2135
- 报错信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69# 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 node-gyp@6.0.0
│ gyp info using node@12.13.0 | 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 保持一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// .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
1
2
3
4
5{
"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 命令去更新升级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// .pnpmfile.cjs
const argv = process.argv.slice(2)
function readPackage(pkg, context) {
// Override the manifest of foo@1.x after downloading it from the registry
if (pkg.name && pkg.peerDependencies) {
// Replace bar@x.x.x with bar@2.0.0
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 处理跟进较慢,还未解决