【每天一个npm包】only-allow

1,236 阅读4分钟

前言

对于 only-allow 这个npm包还是前几天看公司代码的时候了解到的。

那个项目使用的包管理器是 pnpmpackage.jsonscripts 脚本中如下所示:

{
    "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 字段

bin

package.json 文件中的 bin 字段用于指定一个或多个命令行工具的入口文件。当你安装一个npm包时,这些指定的命令行工具将会被链接到全局或当前项目的 node_modules/.bin 目录下,使得你可以在命令行中直接执行它们而无需知道它们的具体路径。

下面是一个 package.jsonbin 字段的示例:

{
  "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 包,每天进步一点点。