install-pkg 源码学习

181 阅读2分钟

前言

install-pkg 是什么

以编程方式安装包。自动检测包管理器(npmyarn 和 pnpm

源码地址 install-pkg

用法

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

代码结构

源码分为查找包管理器和安装包两部分

// detect.ts
export async function detectPackageManager(cwd = process.cwd()) {
  // ...根据lock文件识别当前使用的包管理器名称并返回
}
// install.ts
import execa from 'execa'
export async function installPackage(names: string | string[], options: InstallPackageOptions = {}) {
  // ...整理执行命令用 execa 执行
}

查找包管理器模块

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
}

detect.ts文件文件中使用 find-up查找目录下包管理器的lock文件,然后根据对应的映射得到当前项目使用的包管理器

find-up

import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {locatePath, locatePathSync} from 'locate-path';

const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;

export const findUpStop = Symbol('findUpStop');

export async function findUpMultiple(name, options = {}) {
	let directory = path.resolve(toPath(options.cwd) || '');
	const {root} = path.parse(directory);
	const stopAt = path.resolve(directory, options.stopAt || root);
	const limit = options.limit || Number.POSITIVE_INFINITY;
	const paths = [name].flat();

	const runMatcher = async locateOptions => {
		if (typeof name !== 'function') {
			return locatePath(paths, locateOptions);
		}

		const foundPath = await name(locateOptions.cwd);
		if (typeof foundPath === 'string') {
			return locatePath([foundPath], locateOptions);
		}

		return foundPath;
	};

	const matches = [];
	// eslint-disable-next-line no-constant-condition
	while (true) {
		// eslint-disable-next-line no-await-in-loop
		const foundPath = await runMatcher({...options, cwd: directory});

		if (foundPath === findUpStop) {
			break;
		}

		if (foundPath) {
			matches.push(path.resolve(directory, foundPath));
		}

		if (directory === stopAt || matches.length >= limit) {
			break;
		}

		directory = path.dirname(directory);
	}

	return matches;
}
export async function findUp(name, options = {}) {
	const matches = await findUpMultiple(name, {...options, limit: 1});
	return matches[0];
}

查找目录下包管理器的lock文件名

安装包模块

通过 execa 来执行安装脚本

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

package.json script使用的工具