- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第16期,链接:# 【若川视野 x 源码共读】第16期 | 一行代码统一规范 包管理器。
在团队协作中有一个场景很常见: 同一个项目但是包管理器却是不同的。这样不是不可以,但是总感觉别扭。
解决掉它!!!
将项目中采用什么包管理器写到文档中,可以一定程度上解决这个问题。
但是每个人都还需要去看这个文档,这增加了一定的成本,有没有更好的办法呢?
看一下 vue3、vite、pnpm 分别都是怎么实现的吧。
借鉴优秀项目
vue3
-
在
package.json的scripts中定义了preinstall的钩子,通过preinstall去执行指定脚本。"preinstall": "node ./scripts/preinstall.mjs" -
脚本内部实现如下:
// 通过 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
-
和
vue3有些许的相似,都是利用preinstall钩子,但是它是通过only-allow依赖包来实现的。"preinstall": "npx only-allow pnpm"
pnpm
-
和
vite一样,也是通过preinstall钩子,执行only-allow依赖包实现的。"preinstall": "npx only-allow pnpm"
小结
三者都是使用了 preinstall 钩子,而在实现的时候 vue3 是自己实现的,vite 和 pnpm 是利用了 only-allow 包。
这样的话,可以猜到 preinstall 钩子是在 install 之前执行的,npm、yarn、pnpm 在文档中都有说明。
关于 preinstall 有两点需要注意:
-
针对
npm,理论上preinstall是在install之前,但是在npm的不同版本执行顺序是有些许差异的,在使用的时候还要多注意。
pnpm默认不会执行用户自定义的以pre或post前缀的脚本。目的是避免混淆了执行流程,例如在不知情的情况下执行一些脚本。但可以通过enable-pre-post-scripts选项开启。
采用 only-allow 依赖包的都是采用 npx 来执行的,这个 npx 都做了什么呢?
create-vue 实现原理 和 omit.js 剔除对象中的属性 文章中都有提到。
-
npx在执行的时候会到本地或者全局的node_modules/.bin/目录下查找对应的命令执行 -
依赖包的
package.json中的bin字段可以将命令注册到本地或者全局的node_modules/.bin/目录下如果
bin字段值是一个字符串的话,该库的name字段就是对应的命令名称。
结合前两点就可以知道 npx 都做了什么、是怎么做的。
下面就可以进到 only-allow 源码中看看它是怎么实现的。
only-allow 源码
源码目录结构很简单,从 package.json 中的 main 字段可以知道入口文件是根目录下的 bin.js。
核心实现
源码内容相当少,速速把它拿下。
#!/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。
下边又判断了如果 npm、cnpm、pnpm、yarn 都不是的话则退出进程并提示用户。
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-runs 和 boxen。
-
which-pm-runs用来获取用户当前正在执行的包管理器核心就是通过
process.env.npm_config_user_agent拿到当前执行的包管理工具,经过处理后返回name和version。 -
boxen作用是美化日志输出
首先获取了用户正在执行的包管理器和当前运行脚本的工作目录的路径,后续就是判断期望的包管理工具和正在执行的包管理工具是否相符,不相符则给出提示,在输出提示的时候利用 boxen 美化日志。
这里有一个点需要注意,判断了当前脚本工作目录中如果包含了 node_modules 则不进行判断。
原因是什么?想了一下应该是如果目录中包含了 node_modules,说明被执行的脚本属于 node_modules 中的依赖包,这里限制的只是当前项目中,而不是所有依赖包,所以在 node_modules 中的不需要检测。
总结
一开始没想到源码实现这么简单,后续看了比较惊讶。这篇文章除了学到的知识更多的是用来复习之前源码学习中的知识点。
加油!!!