本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
目标
- 学习调试源码
- 学会 npm 钩子
- 学会
preinstall: npx only-allow pnpm一行代码统一规范包管理器 - 学习
only-allow原理
背景
团队协作开发时,对于包管理工具可能并没有什么强制要求,有的人习惯用 npm,而有的人习惯用 yarn,还有现在也会有 pnpm、tnpm等等。这种情况下很容易出现一些问题,严重时可能会导致线上 BUG。所以这里我们需要借助一些工具(代码)来进行约束。
在 Vue3 源码中,利用 npm的 preinstall钩子进行约束,只能使用 pnpm安装依赖。它是怎么实现的呢?我们一起来看看。
Vue3 源码约束包管理工具
// vue-next/package.json
{
"private": true,
"version": "3.2.22",
"scripts": {
"preinstall": "node ./scripts/preinstall.js",
}
}
install 执行时的顺序
# install 之前执行此脚本
preinstall
# 然后执行 install 脚本
install
# install 之后执行此脚本
postinstall
更多可以查看官文钩子
而 preinstall的实现其本质上是:
- 获取当前包管理器
- 判断是否是规定的包管理器
- 若不是则退出当前进程
// vue-next/scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.warn(`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`)
process.exit(1)
}
process对象在我们实际项目中用到的不多,但是项目搭建及其他操作时会用到,大多数场景是用来获取一些环境相关的变量。具体可以查看阮一峰老师 process 对象
process.argv属性返回一个数组,由命令行执行脚本时的各个参数组成。
- 第一个成员总是
node - 第二个成员是脚本文件名
- 其余成员是脚本文件的参数
接下来的要说的 only-allow包实际上就是实现了 preinstall.js 中约束包管理器的部分,它的作用就是帮助我们「统一规范包管理器」。
Only-allow
我们可以从它的 github地址来查看它 README.md 里的描述:
- 强制在项目上使用特定的包管理器
- 使用时在
package.json中添加一行脚本即可 json
// 比如要强制使用 pnpm
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
调试源码
通过查看 package.json文件可知,它的入口文件为 bin.js
// only-allow/package.json
{
"bin": "bin.js"
}
我们到 only-allow/bin.js文件中给 const usedPM = whichPMRuns()打上断点。
关于如何在 VSCode 进入调试,可以参考若川大佬的文章
然后执行 npm i命令进入调试。
可以看到它会提示我们需要使用 pnpm进行安装并且退出当前进程。
过程分析
那它到底是如何执行的呢?通过 only-allow的源码(才36行)可以知道它其实还是如同 Vue3源码中 preinstall那块一样:
- 获取当前的包管理工具
- 获取项目中规定的包管理工具
- 如果不同,则抛出提示并退出当前进程
那么如果得知项目中规定的包管理工具呢?
我们在前面说过 process.argv对象,从第三项开始就是脚本文件的参数,那么 only-allow在被执行时是这样的 npx only-allow xxx,实际上它执行的是 node bin.js xxx,所以可以通过:
const argv = process.argv.slice(2)
const wantedPM = argv[0]; // 获取到规定的包管理器
获取当前使用的包管理器
only-allow源码中有引用 which-pm-runs,这个包的作用就是获取当前运行时的包管理器。
获取到「规定的包管理器」和「运行时包管理器」如果不同则退出当前进程。
which-pm-runs
它最终返回的是包管理器和版本号。
调试可知,可以得到类似这样 process.env.npm_config_user_agent的字符串。
yarn/1.22.10 npm/? node/v14.16.0 linux x64
'use strict'
module.exports = function () {
if (!process.env.npm_config_user_agent) {
return undefined
}
return pmFromUserAgent(process.env.npm_config_user_agent)
}
function pmFromUserAgent(userAgent) {
const pmSpec = userAgent.split(' ')[0]
const separatorPos = pmSpec.lastIndexOf('/')
return {
name: pmSpec.substr(0, separatorPos),
version: pmSpec.substr(separatorPos + 1),
}
}
截取字符串
vue-next源码中有 pull request => chore: remove deprecated String.prototype.substr
String.prototype.substr is deprecated.
也就是说不推荐使用 substr。推荐使用 slice。
源码
接下来我们再一起看看源码部分是如何实现的:
#!/usr/bin/env node
const whichPMRuns = require('which-pm-runs');
const boxen = require('boxen');
const argv = process.argv.slice(2)
if (argv.length === 0) {
console.log('Please specify the wanted package manager: only-allow <npm|pnpm|yarn>')
process.exit(1)
}
const wantedPM = argv[0]
if (wantedPM !== 'npm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn') {
console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`)
process.exit(1)
}
const usedPM = whichPMRuns()
if (usedPM && usedPM.name !== wantedPM) {
// 「总结」里截图中的红框
const boxenOpts = { borderColor: 'red', borderStyle: 'double', padding: 1 }
switch(wantedPM) {
case 'npm':
console.log(boxen('Use "npm install" for installation in this project', boxenOpts))
break
case 'pnpm':
console.log(boxen(`Use "pnpm install" for installation in this project.
If you don't have pnpm, install it via "npm i -g pnpm".
For more details, go to https://pnpm.js.org/`, boxenOpts))
break
case 'yarn':
console.log(boxen(`Use "yarn" for installation in this project.
If you don't have Yarn, install it via "npm i -g yarn".
For more details, go to https://yarnpkg.com/`, boxenOpts))
break
}
process.exit(1)
}
补充知识
#!/usr/bin/env node
#!是一个符号,名称为Shebang。通常在 Unix 系统脚本文件中的第一行出现。
作用:用于知名这个脚本文件的解释程序。
也就是为了在Linux或者Unix中指定用node来执行脚本文件。Window中不支持,而是通过文件扩展名来确定用什么解释器来执行脚本。
阮一峰老师的 npm scripts 使用指南中提到:当输入一个命令,npm会新建一个shell并在其中执行指定的脚本,在执行该脚本时,也就需要指定该脚本的解释程序为 node。
不同电脑的同一种解释器可能是被安装到了不同的目录下,那么系统要怎么才能知道解释器(比如 node)路径呢?这就需要环境变量,/usr/bin/env就是告诉系统到用户的环境变量中寻找node,进而动态的寻找解释器的路径。(如果根本没有配置node,那脚本也无法运行)
这里有用到两个依赖包:
which-pm-runsboxen
其中 boxen我们可以在源码的最后面几行可以看到,它包裹住了要提示给用户的信息,并传一个样式进去。所以在命令行中会展示如下:
而 which-pm-runs就是用来获取当前使用的包管理工具的。实现如下:
'use strict'
module.exports = function () {
if (!process.env.npm_config_user_agent) {
return undefined
}
return pmFromUserAgent(process.env.npm_config_user_agent)
}
function pmFromUserAgent (userAgent) {
const pmSpec = userAgent.split(' ')[0]
const separatorPos = pmSpec.lastIndexOf('/')
return {
name: pmSpec.substr(0, separatorPos),
version: pmSpec.substr(separatorPos + 1)
}
}
最终返回包管理器和版本号。
根据调试可知,process.env.npm_config_user_agent 是类似这样的字符串:
yarn/1.22.10 npm/? node/v14.16.0 linux x64