【若川视野 x 源码共读】第16期 | only-allow

842 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

目标

  1. 学习调试源码
  2. 学会 npm 钩子
  3. 学会 preinstall: npx only-allow pnpm一行代码统一规范包管理器
  4. 学习 only-allow原理

背景

团队协作开发时,对于包管理工具可能并没有什么强制要求,有的人习惯用 npm,而有的人习惯用 yarn,还有现在也会有 pnpmtnpm等等。这种情况下很容易出现一些问题,严重时可能会导致线上 BUG。所以这里我们需要借助一些工具(代码)来进行约束。

在 Vue3 源码中,利用 npmpreinstall钩子进行约束,只能使用 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的实现其本质上是:

  1. 获取当前包管理器
  2. 判断是否是规定的包管理器
  3. 若不是则退出当前进程
// 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-runs
  • boxen

其中 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