前言
对于 only-allow 这个npm包还是前几天看公司代码的时候了解到的。
那个项目使用的包管理器是 pnpm
,package.json
中 scripts
脚本中如下所示:
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
然后我就去看了下这个包的文档和源码。
对于上面的 preinstall
script: npx only-allow pnpm
,意思就是强制在该项目中使用 pnpm
作为包管理器,而如果使用其他的包管理器,比如 npm, yarn
来安装依赖就会报错。
或许我们会好奇这是如何实现的呢?
然后去扒拉下源码,看到了这样一段代码:
if (wantedPM !== 'npm' && wantedPM !== 'cnpm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn' && wantedPM !== 'bun') {
// ...
}
当时的想法是:这段代码可以使用 Array.prototype.incldues()
:
if (!['npm', 'cnpm', 'pnpm', 'yarn', 'bun'].includes(wantedPM)) {
// ...
}
来简化下逻辑。
撸起袖子说干就干,然后就提了个 pr。作者也回复我了,不过最后并没有被 merge。原因我猜是: 虽然这样写简化了一些逻辑,但是可读性反而降低了。
目录结构
├──📄.gitignore
├──📄bin.js
├──📄LICENSE
├──📄package.json
├──📄pnpm-lock.yaml
├──📄README.md
└──📁__fixtures__
| ├──📁npm
| | ├──📄package.json
| | └──📄pnpm-debug.log
| ├──📁pnpm
| | └──📄package.json
| └──📁yarn
| | ├──📄package.json
| | └──📄pnpm-debug.log
package.json
{
"name": "only-allow",
"bin": "bin.js"
...
}
这里的 bin
字段直接指定为 bin.js
文件,那么 only-allow
(包的名称) 将成为这个包的可执行命令。
关于 bin
字段
package.json
文件中的 bin
字段用于指定一个或多个命令行工具的入口文件。当你安装一个npm包时,这些指定的命令行工具将会被链接到全局或当前项目的 node_modules/.bin
目录下,使得你可以在命令行中直接执行它们而无需知道它们的具体路径。
下面是一个 package.json
中 bin
字段的示例:
{
"name": "my-package",
"version": "1.0.0",
"bin": {
"my-command": "./bin/my-command.js"
}
}
在这个示例中,bin
字段指定了一个命令行工具 my-command
,其对应的入口文件为 ./bin/my-command.js
。
当你在终端中全局或局部安装了这个包后,npm会自动将 my-command
这个命令链接到全局或当前项目的 node_modules/.bin
目录下。这样,如果是全局安装,你就可以直接通过命令行输入 my-command
;如果是局部安装,就可以使用 npx my-command
来执行对应的脚本了。
需要注意的是,入口文件中需要有可执行的权限(例如在Unix系统中需要执行 chmod +x ./bin/my-command.js
),以及在文件开头添加 shebang 指令,告诉系统使用哪个解释器来执行这个脚本,例如:
#!/usr/bin/env node
这样的话,当你在命令行中输入 my-command
时,系统会自动调用 Node.js 解释器来执行 ./bin/my-command.js
文件。
bin.js
可以看到 bin.js
文件总共就五十多行代码,其中依赖了 which-pm-runs
这个 npm 包。
关于 which-pm-runs
的原理可以看看我的上一篇文章 【每天一个npm包】which-pm-runs
which-pm-runs
会获取正在执行的包管理器信息,格式如下
{ name: '包管理器的名称', version: '包管理器的版本' }
const whichPMRuns = require('which-pm-runs')
console.log(whichPMRuns())
// 一个可能的值
// { name: 'pnpm', version: '8.6.12' }
bin.js
:
注意:
#!/usr/bin/env node
声明了该文件为可执行脚本且使用node
来执行
#!/usr/bin/env node
const whichPMRuns = require('which-pm-runs')
function box(s) {
const lines = s.trim().split("\n")
const width = lines.reduce((a, b) => Math.max(a, b.length), 0)
const surround = x => '║ \x1b[0m' + x.padEnd(width) + '\x1b[31m ║'
const bar = '═'.repeat(width)
const top = '\x1b[31m╔═══' + bar + '═══╗'
const pad = surround('')
const bottom = '╚═══' + bar + '═══╝\x1b[0m'
return [top, pad, ...lines.map(surround), pad, bottom].join('\n')
}
const argv = process.argv.slice(2)
if (argv.length === 0) {
console.log('Please specify the wanted package manager: only-allow <npm|cnpm|pnpm|yarn|bun>')
process.exit(1)
}
// 包管理器的名称
const wantedPM = argv[0]
if (wantedPM !== 'npm' && wantedPM !== 'cnpm' && wantedPM !== 'pnpm' && wantedPM !== 'yarn' && wantedPM !== 'bun') {
console.log(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, cnpm, pnpm, yarn or bun.`)
process.exit(1)
}
const usedPM = whichPMRuns()
const cwd = process.env.INIT_CWD || process.cwd()
const isInstalledAsDependency = cwd.includes('node_modules')
if (usedPM && usedPM.name !== wantedPM && !isInstalledAsDependency) {
switch (wantedPM) {
case 'npm':
console.log(box('Use "npm install" for installation in this project'))
break
case 'cnpm':
console.log(box('Use "cnpm install" for installation in this project'))
break
case 'pnpm':
console.log(box(`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/`))
break
case 'yarn':
console.log(box(`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/`))
break
case 'bun':
console.log(box(`Use "bun install" for installation in this project.
If you don't have Bun, go to https://bun.sh/docs/installation and find installation method that suits your environment".`))
break
}
process.exit(1)
}
稍微了解下 process.argv
:
process.argv
会获取执行命令时传入的参数,比如执行 npx only-allow pnpm
命令,console.log(process.argv)
的结果如下:
[
'D:\\software\\nvm\\v18.17.0\\node.exe', // node 执行程序路径
'D:\\www\\github\\only-allow\\bin.js', // 当前执行文件的路径
'pnpm' // 参数
]
因此,下面这行代码会拿到传入的参数:
const argv = process.argv.slice(2)
如果 argv.length === 0
,说明没传入参数,报错并退出执行;否则 argv[0]
拿到的就是包管理器的名称。
代码整体逻辑还是比较清晰的,就不仔细赘述了。
结语
每天一个 npm 包,每天进步一点点。