Skip to content
On this page

ni

ni 使用正确的包管理工具

  • 根据执行参数,获取到 cwd 执行命令目录
  • detect 函数根据执行目录获取到锁文件拿到对应包管理工具
  • parseNi -> getCommand 根据包管理工具获取对应的执行命令
  • execaCommand 执行获取到的命令

全局安装

bash
npm i -g @antfu/ni

可执行命令

json
{
  "bin": {
    "ni": "bin/ni.mjs",
    "nci": "bin/nci.mjs",
    "nr": "bin/nr.mjs",
    "nu": "bin/nu.mjs",
    "nx": "bin/nx.mjs",
    "na": "bin/na.mjs",
    "nun": "bin/nun.mjs"
  },
}

解析 ni - install

执行 ni 命令

bash
ni

# npm install
# yarn install
# pnpm install
# bun install

会运行 bin/ni.mjs,执行 dist/ni.mjs 文件

js
#!/usr/bin/env node
'use strict'
import '../dist/ni.mjs' // -> 对应源码 src/commands/ni.ts

主执行函数

ts
import { parseNi } from '../parse'
import { runCli } from '../runner'

// 运行主函数
runCli(parseNi)

runCli(parseNi)

ts
export async function runCli(fn: Runner, options: DetectOptions = {}) {
  // 获取命令执行的参数 如: ni vite -D -> ['vite', '-D']
  const args = process.argv.slice(2).filter(Boolean)
  try {
    await run(fn, args, options)
  }
  catch (error) {
    // 执行错误退出
    process.exit(1)
  }
}

run(fn, args, options)

ts
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.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    // 打印 ni 的版本信息
    console.log(`@antfu/ni v${version}`)
    return
  }

  if (args.length === 1 && ['-h', '--help'].includes(args[0])) {
    // 打印 ni 的可运行帮助命令
    const dash = c.dim('-')
    console.log(c.green(c.bold('@antfu/ni')) + c.dim(` use the right package manager v${version}\n`))
    console.log(`ni   ${dash}  install`)
    console.log(`nr   ${dash}  run`)
    console.log(`nx   ${dash}  execute`)
    console.log(`nu   ${dash}  upgrade`)
    console.log(`nun  ${dash}  uninstall`)
    console.log(`nci  ${dash}  clean install`)
    console.log(`na   ${dash}  agent alias`)
    console.log(c.yellow('\ncheck https://github.com/antfu/ni for more documentation.'))
    return
  }

  if (args[0] === '-C') {
    // 指定执行的目录
    // ni -C packages/foo vite -D 在 packages/foo 下执行安装 vite 的依赖
    cwd = resolve(cwd, args[1])
    // ['-C', 'packages/foo', 'vite', '-D'] -> ['vite', '-D']
    args.splice(0, 2)
  }

  const isGlobal = args.includes('-g')
  if (isGlobal) {
    // 全局安装
    command = await fn(await getGlobalAgent(), args)
  }
  else {
    // detect 根据锁文件,选择使用哪个包管理工具。
    // 选取不到使用默认的 prompt 让用户选择
    let agent = await detect({ ...options, cwd }) || await getDefaultAgent()
    if (agent === 'prompt') {
      // 默认用户选择使用哪个包管理工具
      agent = (await prompts({
        name: 'agent',
        type: 'select',
        message: 'Choose the agent',
        choices: agents.filter(i => !i.includes('@')).map(value => ({ title: value, value })),
      })).agent
      if (!agent)
        return
    }
    // 通过 parseNi 解析出需要执行的命令
    // agent: pnpm  args: ['vite', '-D'] -> pnpm add vite -D
    command = await fn(agent as Agent, args, {
      hasLock: Boolean(agent),
      cwd,
    })
  }

  // 没有则返回
  if (!command)
    return

  const voltaPrefix = getVoltaPrefix()
  if (voltaPrefix)
    command = voltaPrefix.concat(' ').concat(command)

  if (debug) {
    // 调试模式,直接打印出命令
    console.log(command)
    return
  }

  // 在 cwd 执行目录,执行解析好的 command。
  await execaCommand(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}

detect 函数

根据猜测用哪个包管理器(npm/yarn/pnpm),不存在则询问用户是否全局安装包管理工具

ts
// 锁文件对应的包管理工具
export const LOCKS: Record<string, Agent> = {
  'bun.lockb': 'bun',
  'pnpm-lock.yaml': 'pnpm',
  'yarn.lock': 'yarn',
  'package-lock.json': 'npm',
  'npm-shrinkwrap.json': 'npm',
}
ts
export async function detect({ autoInstall, cwd }: DetectOptions) {
  let agent: Agent | null = null

  // 在执行命令的目录文件下找到锁文件路径:'user\\project\\pnpm-lock.yaml'
  const lockPath = await findUp(Object.keys(LOCKS), { cwd })
  // package.json 文件路径
  let packageJsonPath: string | undefined

  if (lockPath)
    // 'user\\project\\package.json'
    packageJsonPath = path.resolve(lockPath, '../package.json')
  else
    packageJsonPath = await findUp('package.json', { cwd })

  // read `packageManager` field in package.json
  if (packageJsonPath && fs.existsSync(packageJsonPath)) {
    // 存在 package.json 则读取文件内容
    try {
      const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
      if (typeof pkg.packageManager === 'string') {
        // 存在 packageManager 字段,截取出包管理工具名称和版本
        const [name, version] = pkg.packageManager.split('@')
        if (name === 'yarn' && parseInt(version) > 1)
          agent = 'yarn@berry'
        else if (name === 'pnpm' && parseInt(version) < 7)
          agent = 'pnpm@6'
        else if (name in AGENTS)
          agent = name
        else
          console.warn('[ni] Unknown packageManager:', pkg.packageManager)
      }
    }
    catch {}
  }

  // detect based on lock
  if (!agent && lockPath)
    agent = LOCKS[path.basename(lockPath)]

  // auto install
  if (agent && !cmdExists(agent.split('@')[0])) {
    // 不存在则尝试让用户选择全局安装包管理工具
    if (!autoInstall) {
      console.warn(`[ni] Detected ${agent} but it doesn't seem to be installed.\n`)

      if (process.env.CI)
        process.exit(1)

      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 execaCommand(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
  }

  return agent
}

parseNi 函数

根据传入的 agent 包管理工具,组装执行的命令

ts
export const parseNi = <Runner>((agent, args, ctx) => {
  // bun use `-d` instead of `-D`, #90
  if (agent === 'bun')
    args = args.map(i => i === '-D' ? '-d' : i)

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

  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'))

  if (args.length === 0 || args.every(i => i.startsWith('-')))
    return getCommand(agent, 'install', args)

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

getCommand 函数

抹平不同包管理的执行命令差异

对应包管理工具的命令

ts
const pnpm = {
  'agent': 'pnpm {0}',
  'run': 'pnpm run {0}',
  'install': 'pnpm i {0}',
  'frozen': 'pnpm i --frozen-lockfile',
  'global': 'pnpm add -g {0}',
  'add': 'pnpm add {0}',
  'upgrade': 'pnpm update {0}',
  'upgrade-interactive': 'pnpm update -i {0}',
  'execute': 'pnpm dlx {0}',
  'uninstall': 'pnpm remove {0}',
  'global_uninstall': 'pnpm remove --global {0}',
}
export const AGENTS = {
  // ... 省略部分
  'pnpm': pnpm,
  // pnpm v6.x or below
  'pnpm@6': {
    ...pnpm,
    run: npmRun('pnpm'),
  },
  // ... 省略部分
}
ts
export function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
) {
  if (!(agent in AGENTS))
  // 不存在 则丢出错误
    throw new Error(`Unsupported agent "${agent}"`)

  // 获取对应包管理工具执行的命令 pnpm add {0}
  const c = AGENTS[agent][command]

  if (typeof c === 'function')
    return c(args)

  if (!c)
    throw new Error(`Command "${command}" is not support by agent "${agent}"`)
  // 将 pnpm add {0} 替换为 'pnpm add vite -D'
  return c.replace('{0}', args.join(' ')).trim()
}