【源码阅读】拜托!包管理工具统一一下好吗?

366 阅读5分钟

在团队协作中有一个场景很常见: 同一个项目但是包管理器却是不同的。这样不是不可以,但是总感觉别扭。

解决掉它!!!

将项目中采用什么包管理器写到文档中,可以一定程度上解决这个问题。

但是每个人都还需要去看这个文档,这增加了一定的成本,有没有更好的办法呢?

看一下 vue3vitepnpm 分别都是怎么实现的吧。

借鉴优秀项目

vue3

  1. package.jsonscripts 中定义了 preinstall 的钩子,通过 preinstall 去执行指定脚本。

    "preinstall": "node ./scripts/preinstall.mjs"
    
  2. 脚本内部实现如下:

    // 通过 process.env.npm_execpath 获取对应包管理器名称,通过正则校验。
    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)
    }
    

vite

  1. vue3 有些许的相似,都是利用 preinstall 钩子,但是它是通过 only-allow 依赖包来实现的。

    "preinstall": "npx only-allow pnpm"
    

pnpm

  1. vite 一样,也是通过 preinstall 钩子,执行 only-allow 依赖包实现的。

    "preinstall": "npx only-allow pnpm"
    

小结

三者都是使用了 preinstall 钩子,而在实现的时候 vue3 是自己实现的,vitepnpm 是利用了 only-allow 包。

这样的话,可以猜到 preinstall 钩子是在 install 之前执行的,npmyarnpnpm 在文档中都有说明。

关于 preinstall 有两点需要注意:

  1. 针对 npm,理论上 preinstall 是在 install 之前,但是在 npm 的不同版本执行顺序是有些许差异的,在使用的时候还要多注意。

    github 比较激烈的讨论

  1. pnpm 默认不会执行用户自定义的以 prepost 前缀的脚本。目的是避免混淆了执行流程,例如在不知情的情况下执行一些脚本。但可以通过 enable-pre-post-scripts 选项开启。

采用 only-allow 依赖包的都是采用 npx 来执行的,这个 npx 都做了什么呢?

create-vue 实现原理omit.js 剔除对象中的属性 文章中都有提到。

  1. npx 在执行的时候会到本地或者全局的 node_modules/.bin/ 目录下查找对应的命令执行

  2. 依赖包的 package.json 中的 bin 字段可以将命令注册到本地或者全局的 node_modules/.bin/ 目录下

    如果 bin 字段值是一个字符串的话,该库的 name 字段就是对应的命令名称。

结合前两点就可以知道 npx 都做了什么、是怎么做的。

下面就可以进到 only-allow 源码中看看它是怎么实现的。

only-allow 源码

源码目录结构很简单,从 package.json 中的 main 字段可以知道入口文件是根目录下的 bin.js

image-20230310193649814.png

核心实现

源码内容相当少,速速把它拿下。

#!/usr/bin/env node

文件第一行有个这

#!/usr/bin/env node

这是啥意思呢?在之前的文章: omit.js 剔除对象中的属性 中也有说明。

#!/usr/bin/env 后面跟的是解释器,使用 #!/usr/bin/env [[解释器名称]] 的好处是可以避免在不同系统上使用不同的解释器路径。比如: #!/usr/bin/env node 因为在不同的系统上 node 解释器的路径可能会不同,而使用 #!/usr/bin/env node,就可以保证在任何系统上都可以找到可用的 node 解释器。

总的来说,only-allow 可以在终端执行,并且是通过 node 执行的,但是在不同系统中 node 的安装路径及全局环境变量 PATH 路径可能不一样,所以用 #!/usr/bin/env 来做个兼容。

解决这个疑问,继续往下看。

获取执行参数并做判断

代码中引入了两个依赖包,这里先忽略,到下边用到的时候再来看他们的作用。

const argv = process.argv.slice(2)
// 没有参数
if (argv.length === 0) {
  console.log('Please specify the wanted package manager: only-allow <npm|cnpm|pnpm|yarn>')
  process.exit(1)
}
​
const wantedPM = argv[0]
// npm/cmpm/pnpm/yarn 都不是
if (wantedPM !== 'npm' && wantedPM !== 'cnpm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn') {
  console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, cnpm, pnpm, or yarn.`)
  process.exit(1)
}

首先是通过 process.argv.slice(2) 获取到执行 only-allow 时传的参数,也就是指定的包管理工具:

only-allow yarn

这里获取到的就是 yarn

下边又判断了如果 npmcnpmpnpmyarn 都不是的话则退出进程并提示用户。

process.argv

  • process.argv 获取到的是一个数组。
  • 第一个元素是启动 Node.js 进程的可执行文件所在的绝对路径。
  • 第二个元素是当前所执行 js 文件的绝对路径。
  • 第三个及后边元素都是参数。

判断当前执行的和预期是否相符

// 获取用户当前正在执行的包管理器
const usedPM = whichPMRuns()
// 返回运行当前脚本的工作目录的路径。
const cwd = process.env.INIT_CWD || process.cwd()
const isInstalledAsDependency = cwd.includes('node_modules')
if (usedPM && usedPM.name !== wantedPM && !isInstalledAsDependency) {
  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
    // ...
  }
  process.exit(1)
}

这里就用到了一开始引入的依赖包 which-pm-runsboxen

  • which-pm-runs 用来获取用户当前正在执行的包管理器

    核心就是通过 process.env.npm_config_user_agent 拿到当前执行的包管理工具,经过处理后返回 nameversion

  • boxen 作用是美化日志输出

首先获取了用户正在执行的包管理器和当前运行脚本的工作目录的路径,后续就是判断期望的包管理工具和正在执行的包管理工具是否相符,不相符则给出提示,在输出提示的时候利用 boxen 美化日志。

这里有一个点需要注意,判断了当前脚本工作目录中如果包含了 node_modules 则不进行判断。

原因是什么?想了一下应该是如果目录中包含了 node_modules,说明被执行的脚本属于 node_modules 中的依赖包,这里限制的只是当前项目中,而不是所有依赖包,所以在 node_modules 中的不需要检测。

总结

一开始没想到源码实现这么简单,后续看了比较惊讶。这篇文章除了学到的知识更多的是用来复习之前源码学习中的知识点。

加油!!!