读39行代码的小工具 install-pkg并解析其实现

1,030 阅读4分钟

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

这是源码共读的第20期 | # Vue团队核心成员开发的39行小工具 install-pkg 安装包

install-pkg

以编程方式安装程序包。并且自动检测包装管理器(npm、yarn和pnpm)

npm i @antfu/install-pkg

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

也就是通过编程方式去安装某个包到本地中,它让我想起了14期remote-git-tags中的实现 , 本质上是通过代码执行去调用 命令安装

仓库

这里推荐使用川哥整理好的同步文章仓库

//川
git clone https://github.com/lxchuan12/install-pkg-analysis.git
cd install-pkg-analysis/install-pkg && pnpm i

//官方项目
git clone https://github.com/antfu/install-pkg.git
cd install-pkg && pnpm i

package.json 解析

ni

自动检测包管理工具, 使用正确的包管理器。 它可以根据你的锁文件去推断你当前的包管理器, 根据相应的命令 去做转化

相关的script:

    "prepublishOnly": "nr build",
    "dev": "nr build --watch",
    "start": "esno src/index.ts",
    "build": "tsup src/index.ts --format cjs,esm --dts --no-splitting",
    "release": "bumpp --commit --push --tag && pnpm publish",
    "lint": "eslint \"{src,test}/**/*.ts\"",
    "lint:fix": "nr lint -- --fix"

prepublishOnly

npm钩子, 在执行 npm publish 之前会执行这个命令

nr

ni 下 可执行命令, 对应的 是 npm run /yarn run / pnpm run

nr build , 如果你使用npm包管理工具,对应的就是 npm run build

esno

具有自动CJS/ESM模式和缓存的ts编译工具, 本质就是一个tsx

tsup

使用Esbuild,无需配置即可打包你的TS库

tsup src/index.ts --format cjs,esm --dts --no-splitting

image.png

bumpp

更好的进行版本号提升的工具,基于version-bump-prompt改造

image.png

生产依赖

execa

命令执行相关的库, 基于 child_process 进行了封装,使用更加友好

import {execa} from 'execa';
const {stdout} = await execa('echo', ['unicorns']);
console.log(stdout);

find-up

通过查找父目录查找文件或目录 , 字面意思 就是找到指定文件路径的工具

import path from 'node:path';
import {findUp, pathExists} from 'find-up';

console.log(await findUp('unicorn.png'));
//=> '/Users/sindresorhus/unicorn.png'

代码

index.ts

export * from './detect'
export * from './install'

统一出口,将detect 和 install的导出 从index统一导出

install.ts

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

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

export async function installPackage(names: string | string[], options: InstallPackageOptions = {}) {
  const agent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
  if (!Array.isArray(names))
    names = [names]

  const args = options.additionalArgs || []

  if (options.preferOffline)
    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,
    },
  )
}

detect.ts

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

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

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

export async function detectPackageManager(cwd = process.cwd()) {
  const result = await findUp(Object.keys(LOCKS), { cwd })
  const agent = (result ? LOCKS[path.basename(result)] : null)
  return agent
}

逐行分析

const agent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
  if (!Array.isArray(names))
    names = [names]

agent 从默认的配置对象中以及 detectPackageManager 函数去读取我们使用的是什么包管理工具,不成立的情况下默认为npm, 并将第一个传入的非数组参数包装了为数组类型

import findUp from 'find-up'
export async function detectPackageManager(cwd = process.cwd()) {
  const result = await findUp(Object.keys(LOCKS), { cwd })
  const agent = (result ? LOCKS[path.basename(result)] : null)
  return agent
}

detectPackageManager 函数 通过当前执行命令的目录,使用 find-up去查找 锁文件(LOCKS),得到对应的 包管理工具, 否则为null。 知道findUp 函数是干啥的这里就很好理解了

 const args = options.additionalArgs || []

  if (options.preferOffline)
    args.unshift('--prefer-offline')

args变量 即 执行执行时的 附加参数

一条没见过的新命令: --prefer-offline

--prefer-offline将使npm跳过任何条件请求(304检查)直接使用缓存数据,只有在缓存无法匹配到的时候,才去访问网络。这样我们将依赖包添加到项目的过程就会快很多,可以理解为 优先使用本地或者全局的缓存 ,没有才进行联网安装

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

奇怪的Boolean

在读这一块代码时,产生了一个疑问, Boolean这干啥的,第一眼想起了装箱类型, 即它本身是一个function, 所以它被作为callback 给了filter使用。 接着我就去研究这个函数的作用,它会根据传入的参数是否有效 决定返回值。 即 1 返回 true , 0 返回false

Boolean(1)  true
Boolean(0)  false

此时就明白了, 它被作为了filter的回调函数使用, filter将数组成员依次传给Boolean, 再根据结果过滤数据! 也就是数组内的参数 如果是空字符, 或者 判断得到的空字符成员 最终被会移除

stdio

将决定输入输出流的显示,当指定silent: true , 安装包时的信息会在控制台回显, 默认是ignore 不显示。

最后execa将结果对象通过Promise返回

总结

至此 整个工具的分析就结束了, 主要文件仅39行, 却涉及到了很多知识点。

让我耳目一新的就是 filter 结合 Boolean 的用法, 又学到一招, 类似的还有之前全局总线那一期的 handlers.splice(handlers.indexOf(handler) >>> 0, 1), 查找不到的话 返回-1 >>> 0 返回的结果是4294967295 就相当于无效了, 查找到的话 任意数字 >>> 0 还是这个任意数字,省去if判断