Skip to content
On this page

install-pkg

install-pkg 自动检测包管理器,以编程方式安装程序包。

实现原理

  • 查找当前目前或者父目录中的包 lock 文件,确定包管理器。
  • 通过 execa 执行安装命令

Used

js
import { installPackage } from '@antfu/install-pkg'
await installPackage('vite', { silent: true })

源码解析

detectPackageManager 函数

用于检测需要使用什么包管理器安装程序包

ts
import fs from 'fs'
import path from 'path'
import findUp from 'find-up'

export type PackageManager = 'pnpm' | 'yarn' | 'npm' | 'bun'

const AGENTS = ['pnpm', 'yarn', 'npm', 'pnpm@6', 'yarn@berry', 'bun'] as const
export type Agent = typeof AGENTS[number]

const LOCKS: Record<string, PackageManager> = {
  'bun.lockb': 'bun',
  'pnpm-lock.yaml': 'pnpm',
  'yarn.lock': 'yarn',
  'package-lock.json': 'npm',
  'npm-shrinkwrap.json': 'npm',
}

export async function detectPackageManager(cwd = process.cwd()): Promise<Agent | null> {
  let agent: Agent | null = null
  // 从当前目录或者父目录找到 locks 列表中的某一个路径
  const lockPath = await findUp(Object.keys(LOCKS), { cwd })
  let packageJsonPath: string | undefined

  if (lockPath)
    // 存在 locak 文件,获取 package.json 文件路径
    packageJsonPath = path.resolve(lockPath, '../package.json')
  else
    // 不存在,继续在目录或父目录查找 package.json 文件
    packageJsonPath = await findUp('package.json', { cwd })

  if (packageJsonPath && fs.existsSync(packageJsonPath)) {
    // packageJsonPath 存在
    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)
          // yarn
          agent = 'yarn@berry'
        else if (name === 'pnpm' && parseInt(version) < 7)
          // pnpm
          agent = 'pnpm@6'
        else if (name in AGENTS)
          // name 是否在 AGENTS 包管理器列表中
          agent = name
        else
          console.warn('[ni] Unknown packageManager:', pkg.packageManager)
      }
    }
    catch {}
  }

  // detect based on lock
  if (!agent && lockPath)
    // 没有取到包管理信息并且匹配到 locks 中某一个锁文件路径
    agent = LOCKS[path.basename(lockPath)]

  return agent
}

installPackage 函数

ts
import execa from 'execa'
import { detectPackageManager } from '.'

export interface InstallPackageOptions {
  cwd?: string
  dev?: boolean
  silent?: boolean
  packageManager?: string
  packageManagerVersion?: string
  preferOffline?: boolean
  additionalArgs?: string[]
}

export async function installPackage(names: string | string[], options: InstallPackageOptions = {}) {
  // 检测到包管理器或者是默认的 npm
  const detectedAgent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
  // pnpm@6 -> ['pnpm', '6']
  const [agent] = detectedAgent.split('@')

  if (!Array.isArray(names))
    // 将需要加载的程序包包装成数组
    names = [names]

  // 包管理器对应的参数
  const args = options.additionalArgs || []

  if (options.preferOffline) {
    // 是否要清除包管理器的缓存
    // yarn berry uses --cached option instead of --prefer-offline
    if (detectedAgent === 'yarn@berry')
      args.unshift('--cached')
    else
      args.unshift('--prefer-offline')
  }

  // 执行脚本 pnpm install -D vite
  return execa(
    agent,
    [
      agent === 'yarn'
        ? 'add'
        : 'install',
      options.dev ? '-D' : '',
      ...args,
      ...names,
    ].filter(Boolean),
    {
      stdio: options.silent ? 'ignore' : 'inherit',
      cwd: options.cwd,
    },
  )
}