【若川视野 x 源码共读】第12期 | 尤雨溪推荐 的ni 神器

405 阅读4分钟

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

包管理器你用哪家?npm、yarn、 pnpm,真是萝卜白菜各有所爱,本期要学习的ni可以根据锁文件自动判断你用的包管理器并执行相应的脚本,快来学习吧!

1.学习参考资料

github1s.com/antfu/ni

github.com/antfu/ni

川哥文章: juejin.cn/post/702391…

2.了解用法-readme

package.json bin选项对应:

"bin": {
    "ni": "bin/ni.js",
    "nci": "bin/nci.js",
    "nr": "bin/nr.js",
    "nu": "bin/nu.js",
    "nx": "bin/nx.js",
    "nrm": "bin/nrm.js"
  },

2.1 ni

ni - install

npm install
yarn install
pnpm install

ni axios

npm i axios
yarn add axios
pnpm i axios

ni --frozen

npm ci
yarn install --frozen-lockfile
pnpm install --frozen-lockfile

ni -g iroiro

npm i -g iroiro
yarn global add iroiro
pnpm i -g iroiro

2.2 nr

nr - run

nr dev --port=3000

npm run dev -- --port=3000
yarn run dev --port=3000
pnpm run dev -- --port=3000

nr -

rerun the last command

2.3 nx

nx - execute

nx jest

npx jest
yarn dlx jest
pnpm dlx jest

2.4 nu

nu- upgrade

nu

npm upgrade
yarn upgrade
pnpm upgrade

2.5 nci

nci - clean install

npm ci
yarn install --frozen-lockfile
pnpm install --frozen-lockfile

2.6 nrm

nrm - remove

nrm axios

npm uninstall axios
yarn remove axios
pnpm remove axios

nrm @types/node -D

npm uninstall @types/node -D
yarn remove @types/node -D
pnpm remove @types/node -D

nrm -g iroiro

npm uninstall -g iroiro
yarn global remove iroiro
pnpm remove -g iroiro

3.了解原理-readme

检查lockfiles:在其运行之前检查yarn.lock、pnpm-lock.yaml、package-lock.json 了解包管理工具,然后运行相关的命令

4.猜测实现原理

(1) ni根据运行相关命令执行不同文件

(2)检查lockfiles 从而决定使用何种包管理工具(file,path相关api)

(3)执行命令,解析命令参数(执行命令的相关依赖,库)

5.浏览代码

5.1 ni - bin/ni.js

import { parseNi } from './commands'
import { runCli } from './runner'

runCli(parseNi)

parseNi的定义:

import { version } from '../package.json'
import { Agent, AGENTS, Command } from './agents'
import { exclude } from './utils'
import { Runner } from './runner'

export function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
) {
  // 判断是不是npm,yarn,pnpm这几个包管理器,如果不是报错
  if (!(agent in AGENTS))
    throw new Error(`Unsupported agent "${agent}"`)
	// 获取具体的命令,是 run ,install, add, upgrade, uninstall 等等
  const c = AGENTS[agent][command]
  
  //如果是函数  'run': npmRun('npm')
  if (typeof c === 'function')
    return c(args)
	// 没有c,参数错误
  if (!c)
    throw new Error(`Command "${command}" is not support by agent "${agent}"`)
  // 替换 global': 'npm i -g {0}'
  return c.replace('{0}', args.join(' ')).trim()
}

export const parseNi = <Runner>((agent, args, ctx) => {
  //如果参数的长度是1,并且是 -v ,则输出版本
  if (args.length === 1 && args[0] === '-v') {
    // eslint-disable-next-line no-console
    console.log(`@antfu/ni v${version}`)
    process.exit(0)
  }
  
  // 参数数组的长度如果是0,则执行install命令
  if (args.length === 0)
    return getCommand(agent, 'install')
  // 如果参数里面包含-g则全局安装
  if (args.includes('-g'))
    return getCommand(agent, 'global', exclude(args, '-g'))
  // -f 是啥???
  if (args.length === 1 && args[0] === '-f')
    return getCommand(agent, 'install', args)

  if (args.includes('--frozen-if-present')) {
    args = exclude(args, '--frozen-if-present')
    return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
  }

  if (args.includes('--frozen'))
    return getCommand(agent, 'frozen', exclude(args, '--frozen'))

  return getCommand(agent, 'add', args)
})

exclude的定义:

// 从arr中删除T
export function remove<T>(arr: T[], v: T) {
  const index = arr.indexOf(v)
  if (index >= 0)
    arr.splice(index, 1)

  return arr
}

export function exclude<T>(arr: T[], v: T) {
  // 深拷贝arr,并从副本中删除v 
  return remove(arr.slice(), v)
}

其他的命令 nr nx nu 的流程类似,不逐一分析了。看runCli方法:

5.2 runCli

~/src/runner.ts:

export async function runCli(fn: Runner, options: DetectOptions = {}) {
  const args = process.argv.slice(2).filter(Boolean)
  try {
    await run(fn, args, options)
  }
  catch (error) {
    process.exit(1)
  }
}

主要定调用了run方法:

const DEBUG_SIGN = '?'
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
  // ?? 参数里面有?则删除?
  const debug = args.includes(DEBUG_SIGN)
  if (debug)
    remove(args, DEBUG_SIGN)

  let cwd = process.cwd()
  let command

  if (args[0] === '-C') {
    cwd = resolve(cwd, args[1])
    args.splice(0, 2)
  }
  // ?? 全局npm 
  const isGlobal = args.includes('-g')
  if (isGlobal) {
    command = await fn(getGlobalAgent(), args)
  }
  else {
    // 检测
    let agent = await detect({ ...options, cwd }) || getDefaultAgent()
    if (agent === 'prompt') {
      // 选择 npm , yarn, pnpm 
      agent = (await prompts({
        name: 'agent',
        type: 'select',
        message: 'Choose the agent',
        choices: agents.map(value => ({ title: value, value })),
      })).agent
      if (!agent)
        return
    }
    command = await fn(agent as Agent, args, {
      hasLock: Boolean(agent),
      cwd,
    })
  }

  if (!command)
    return

  if (debug) {
    // eslint-disable-next-line no-console
    console.log(command)
    return
  }
  // 执行命令
  await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}

getGlobalAgent:

export function getGlobalAgent() {
  return getConfig().globalAgent
}

调用了getConfig:

export function getConfig() {
  if (!config) {
    // 不存在全局默认配置
    if (!fs.existsSync(rcPath))
      config = defaultConfig
    else
      //~/.nirc 文件中存在默认的全局配置
      config = Object.assign({}, defaultConfig, ini.parse(fs.readFileSync(rcPath, 'utf-8')))
  }
  return config
}

detect函数的实现:

export async function detect({ autoInstall, cwd }: DetectOptions) {
  // Find a file or directory by walking up parent directories
  // 寻找一个文件或者目录通过遍历父级目录
  //cwd:  The directory to start from. 开始搜索的目录
  const result = await findUp(Object.keys(LOCKS), { cwd })
  // 返回的目录, 例如 /xxxx/pnpm-lock.yaml, 		取basename就是pnpm-lock.yaml,根据这个获取是哪一种目录
  const agent = (result ? LOCKS[path.basename(result)] : null)

  // 没有安装
  if (agent && !cmdExists(agent)) {
    // 没有选择自动安装
    if (!autoInstall) {
      console.warn(`Detected ${agent} but it doesn't seem to be installed.\n`)
			
      // ??? 
      if (process.env.CI)
        process.exit(1)
      // Create clickable links in the terminal
      // 在终端创建一个可以点击的链接
      const link = terminalLink(agent, INSTALL_PAGE[agent])
      const { tryInstall } = await prompts({
        name: 'tryInstall',
        type: 'confirm',
        message: `Would you like to globally install ${link}?`,
      })
      // 询问是否要安装,如果不,则退出
      if (!tryInstall)
        process.exit(1)
    }
		// 安装包管理工具
    await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
  }

  return agent
}

LOCKS的定义如下:

export const LOCKS: Record<string, Agent> = {
  'pnpm-lock.yaml': 'pnpm',
  'yarn.lock': 'yarn',
  'package-lock.json': 'npm',
}

包管理工具的下载链接:

export const INSTALL_PAGE: Record<Agent, string> = {
  pnpm: 'https://pnpm.js.org/en/installation',
  yarn: 'https://yarnpkg.com/getting-started/install',
  npm: 'https://www.npmjs.com/get-npm',
}

cmdExists: 根据系统类型判断是否是否安装了相应的包管理工具:

export function cmdExists(cmd: string) {
  try {
    // #8
    execSync(
      os.platform() === 'win32'
        ? `cmd /c "(help ${cmd} > nul || exit 0) && where ${cmd} > nul 2> nul"`
        : `command -v ${cmd}`,
    )
    return true
  }
  catch {
    return false
  }
}

在Linux中,command -v 可以判断一个命令是否支持:

6.收获

了解到ni的用法和实现原理;

了解到有用的依赖:find-up, terminal-link

学完本期源码,您可以思考如下问题:

1.package.json文件中bin选项的含义是什么?

2.ni如何判断当前使用的是什么包管理器?

3.ni最终是如何运行相应的脚本的?

4.简述ni的核心流程?

5.思考ni的使用场景?