【源码共读】编程式安装依赖 install-pkg

简介: 【源码共读】编程式安装依赖 install-pkg


通常安装依赖都是通过命令式的方式来安装,有没有想过可以通过编程式的方式来安装依赖呢?


install-pkg是一个用于安装依赖的工具,它可以在不同的环境下安装依赖,比如 npm、yarn、pnpm 等。


使用


install-pkg的使用非常简单,根据README的说明,就通过下面的代码就可以安装依赖了:

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

源码分析


install-pkg的源码非常简单,只有 100 行左右,我们来看看它的实现原理。


根据README的说明,我们可以通过installPackage方法来安装依赖,那么我们先来看看installPackage方法的实现:


installPackage方法在src/index.ts文件中,转成 js 代码如下:

import execa from 'execa'
import { detectPackageManager } from '.'
export async function installPackage(names, options = {}) {
  const detectedAgent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
  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')
  }
  return execa(
    agent,
    [
      agent === 'yarn'
        ? 'add'
        : 'install',
      options.dev ? '-D' : '',
      ...args,
      ...names,
    ].filter(Boolean),
    {
      stdio: options.silent ? 'ignore' : 'inherit',
      cwd: options.cwd,
    },
  )
}

可以看到是一个异步方法,它接收两个参数,第一个参数是要安装的依赖名称,第二个参数是配置项。


在方法内部,首先通过传入的配置项options来获取packageManager,如果没有传入packageManager,则通过detectPackageManager方法来获取packageManager,如果detectPackageManager方法也没有获取到packageManager,则默认使用npm


来看看detectPackageManager方法的实现:

import fs from 'fs'
import path from 'path'
import findUp from 'find-up'
const AGENTS = ['pnpm', 'yarn', 'npm', 'pnpm@6', 'yarn@berry', 'bun']
const LOCKS = {
  '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()) {
  let agent = null
  const lockPath = await findUp(Object.keys(LOCKS), { cwd })
  let packageJsonPath
  if (lockPath)
    packageJsonPath = path.resolve(lockPath, '../package.json')
  else
    packageJsonPath = await findUp('package.json', { cwd })
  if (packageJsonPath && fs.existsSync(packageJsonPath)) {
    try {
      const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
      if (typeof pkg.packageManager === 'string') {
        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)]
  return agent
}

findUp是一个用于查找文件的工具,它可以从当前目录向上查找文件,直到找到为止。


我们来逐行分析:

const lockPath = await findUp(Object.keys(LOCKS), {cwd})
let packageJsonPath
if (lockPath)
    packageJsonPath = path.resolve(lockPath, '../package.json')
else
    packageJsonPath = await findUp('package.json', {cwd})

最开始是获取package-lock.jsonyarn.lockpnpm-lock.yaml等文件的路径;


如果找到就好办了,直接在这个文件目录下找package.json文件即可;


如果没找到就继续使用findUp方法来查找package.json文件。

if (packageJsonPath && fs.existsSync(packageJsonPath)) {
    try {
        const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
        // ...
    } catch {
    }
}


如果找到了package.json文件,就读取文件内容,然后解析成 JSON 对象。

if (typeof pkg.packageManager === 'string') {
    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)
}


这里是用过packageManager来判断使用哪个包管理器;


  1. 如果packageManageryarn,并且版本号大于1,则使用yarn@berry
  2. 如果packageManagerpnpm,并且版本号小于7,则使用pnpm@6
  3. 如果packageManageryarnpnpmnpmbun中的一个,则直接使用;
  4. 否则就打印一个警告。
// detect based on lock
if (!agent && lockPath)
    agent = LOCKS[path.basename(lockPath)]

如果没有通过package.json来获取packageManager,则通过lockPath来获取packageManager


这个方法的核心就是通过两个方式来获取packageManager


  1. 通过package.json中的packageManager字段;
  2. 通过lock文件来获取。


可以说是非常巧妙。


我们继续看installPackage方法:

const detectedAgent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
const [agent] = detectedAgent.split('@')

这里是第一行,就是获取packageManager,和上面讲的方法相辅相成,继续往下看:

if (!Array.isArray(names))
    names = [names]

这里是将name统一变成数组,方便后面处理。

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

这里是处理preferOffline参数,如果设置了这个参数,就会在args中添加--prefer-offline或者--cached参数,因为yarn@berrynpm的参数不一样。

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

这里的命令是根据packageManager来拼接的,yarnnpm的命令不一样,所以需要判断一下。


最后就是执行安装命令了,这里使用了execa来执行命令,这个库的用法和child_process差不多,但是更加方便,

总结


通过学习这个库,我们可以学到很多东西,比如:


  1. 如何判断用户使用的包管理器;
  2. 如何查找文件;
  3. 如何使用execa来执行命令。


同时这里面还穿插着很多node的知识和包管理器的知识,比如:


  1. nodepath.basename方法;
  2. 包管理器的lock文件;
  3. 包管理器的参数和命令。


目录
相关文章
|
6月前
|
Windows
CMake基础(5)安装项目
CMake基础(5)安装项目
69 3
|
6月前
|
算法 Unix C++
【Conan 基础知识 】灵活指定CMake构建目录:Conan 2.1中的实践与思考
【Conan 基础知识 】灵活指定CMake构建目录:Conan 2.1中的实践与思考
199 1
|
6月前
|
JavaScript
Nodejs 第七章(发布npm包)
Nodejs 第七章(发布npm包)
59 0
|
18天前
|
关系型数据库 MySQL Linux
文章对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择最合适的安装方法
【10月更文挑战第8天】本文介绍了在 CentOS 7 中通过编译源码安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务及登录设置等。同时,文章对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择最合适的安装方法。
46 4
|
4月前
npm-check【实用教程】升级项目中的依赖
npm-check【实用教程】升级项目中的依赖
66 0
|
5月前
|
Python
python项目依赖库打包发布方法
python项目依赖库打包发布方法
|
6月前
|
Ubuntu 编译器 C++
【Conan 入门教程 】在Ubuntu上使用Conan编译C++第三方库:一站式解决方案
【Conan 入门教程 】在Ubuntu上使用Conan编译C++第三方库:一站式解决方案
1617 1
|
6月前
|
Python
【Python笔记】pip intall -e命令:让你的工程直接使用开源包的源码,可断点调试,修改源码!
【Python笔记】pip intall -e命令:让你的工程直接使用开源包的源码,可断点调试,修改源码!
199 0
|
6月前
|
程序员 Linux C语言
【cmake 项目依赖冲突】CMake进阶:优雅解决目标依赖和安装问题
【cmake 项目依赖冲突】CMake进阶:优雅解决目标依赖和安装问题
357 0
|
6月前
|
Linux 开发工具 C语言
【Linux系统编程】项目自动化构建工具make/Makefile--1
【Linux系统编程】项目自动化构建工具make/Makefile--1