编程式安装依赖install-pkg源码解析
田八 人气:0正文
通常安装依赖都是通过命令式的方式来安装,有没有想过可以通过编程式的方式来安装依赖呢?
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.json
、yarn.lock
、pnpm-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
来判断使用哪个包管理器;
- 如果
packageManager
是yarn
,并且版本号大于1
,则使用yarn@berry
; - 如果
packageManager
是pnpm
,并且版本号小于7
,则使用pnpm@6
; - 如果
packageManager
是yarn
、pnpm
、npm
、bun
中的一个,则直接使用; - 否则就打印一个警告。
// detect based on lock if (!agent && lockPath) agent = LOCKS[path.basename(lockPath)]
如果没有通过package.json
来获取packageManager
,则通过lockPath
来获取packageManager
。
这个方法的核心就是通过两个方式来获取packageManager
:
- 通过
package.json
中的packageManager
字段; - 通过
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@berry
和npm
的参数不一样。
return execa( agent, [ agent === 'yarn' ? 'add' : 'install', options.dev ? '-D' : '', ...args, ...names, ].filter(Boolean), { stdio: options.silent ? 'ignore' : 'inherit', cwd: options.cwd, }, )
这里的命令是根据packageManager
来拼接的,yarn
和npm
的命令不一样,所以需要判断一下。
最后就是执行安装命令了,这里使用了execa
来执行命令,这个库的用法和child_process
差不多,但是更加方便,参考:execa。
总结
通过学习这个库,我们可以学到很多东西,比如:
- 如何判断用户使用的包管理器;
- 如何查找文件;
- 如何使用
execa
来执行命令。
同时这里面还穿插着很多node
的知识和包管理器的知识,比如:
node
的path.basename
方法;- 包管理器的
lock
文件; - 包管理器的参数和命令。
加载全部内容