一行代码统一项目包管理器使用,解析only-allow实现

164 阅读2分钟

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

这是源码共读的第16期 | # 一行代码统一规范 包管理器

前言

本篇笔记将解析Vue3 、 Vite中用到过的统一包管理器的工具 only-allow, 我们将:

  1. 学习 npm 钩子使用
  2. 学习 only-allow 源码

原理

本质上就是通过 npm的钩子 先执行特定文件,通过读取 命令参数来判断是否执行了正确包管理工具的命令

"preinstall": "npx only-allow pnpm"

任何scripts 脚本都可以通过 pre/post 声明钩子, 一个是之前,一个是之后触发,除此还有特殊的如prepublishOnly npm publish前触发,

准备

官方仓库:only-allow

川哥整理好的仓库:only-allow-analysis

使用

只需要在项目总的package.json 加入钩子 声明指定的包

{
  "scripts": {
    "preinstall": "npx only-allow pnpm" //npm yarn
  }
}

npx 将会从指定的包中运行命令, 如果这个包不存在本地中则会安装在临时目录中执行

源码

核心只有 bin.js 一个文件 比较简单, 在这里还要了解下使用到的两个库boxen/which-pm-runs

boxen 用于在控制台输出带盒子形状的输出内容

which-pm-runs 用于检测当前命令是被哪个包管理器执行了

which-pm-runs

在看only-allow 源码实现前,先来看下 which-pm-runs这个包的实现

'use strict'

module.exports = function () {
// npm_config_user_agent 当前包管理工具的信息头
  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通过不同的包管理工具命令执行,会得到对应这样的内容:

//npm/8.19.3 node/v14.19.0 win32 x64 workspaces/false
//pnpm/7.29.2 npm/? node/v14.19.0 win32 x64
//yarn/1.22.4 npm/? node/v14.19.0 win32 x64

const pmSpec = userAgent.split(' ')[0] // npm/8.19.3
    const separatorPos = pmSpec.lastIndexOf('/')
    return {
      name: pmSpec.substr(0, separatorPos), //npm
      version: pmSpec.substr(separatorPos + 1) //8.19.3
    }

主源码实现

// 裁剪默认的node 文件名参数 得到后面的参数
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)
}

整体代码只有这么多就结束了, 比较简单, 源码主体就是得到命令行执行的环境变量 与 指定的包名称进行对比,如果不匹配则结束提示, 最后通过npm钩子使用这个库达到统一包管理工具的目的。

轮子

简单改造下,将代码抽离,得到一份完整实现的代码

#!/usr/bin/env node
const ColorInfo = (color:string) => (text:string) => {
  const whichColor = {
    'green':92,
    'red':91,
    'yellow':93,
    'blue':94,
    'purple':95,
  }
  console.log(`\x1b[${whichColor[color as keyof typeof whichColor]}m${text}\x1b[0m`)
}
const Red = ColorInfo('red')
const Green = ColorInfo('green')
const Blue = ColorInfo('blue')
const Purple = ColorInfo('purple')
const Yellow = ColorInfo('yellow')

const getPackageAgent = () =>{
    if (!process.env.npm_config_user_agent) {
      return undefined
    }
    return getAgentInfo(process.env.npm_config_user_agent)
  }
  
const getAgentInfo =  (userAgent:string) => {
  const Spec = userAgent.split(' ')[0]
  const separatorPos = Spec.lastIndexOf('/')
  return {
    name: Spec.slice(0, separatorPos),
    version: Spec.slice(separatorPos + 1)
  }
}

const usedPM = getPackageAgent()

const argv = process.argv.slice(2)
if (argv.length === 0) {
  Blue('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') {
  Yellow(`"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`)
  process.exit(1)
}
if (usedPM && usedPM.name !== wantedPM) {
  switch (wantedPM) {
    case 'npm':
      Red('Use "npm install" for installation in this project')
      break
    case 'pnpm':
      Red(`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':
      Red(`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
  }
  process.exit(1)
}
Green(`Only Allow Done.`)

总结

本期源码比较简单,推荐阅读起来,自己动手实现一个 only allow, 最核心的就是通过npm钩子来实现 任何 install 前的触发, 除此之外 还有 post 钩子, 以后自己写包或者公司项目统一规范时就可以用上,很nice