編程式安裝依賴install-pkg源碼解析
正文
通常安裝依賴都是通過(guò)命令式的方式來(lái)安裝,有沒(méi)有想過(guò)可以通過(guò)編程式的方式來(lái)安裝依賴呢?
install-pkg是一個(gè)用于安裝依賴的工具,它可以在不同的環(huán)境下安裝依賴,比如 npm、yarn、pnpm 等。
使用
install-pkg的使用非常簡(jiǎn)單,根據(jù)README的說(shuō)明,就通過(guò)下面的代碼就可以安裝依賴了:
import { install } from 'install-pkg'
await installPackage('vite', { silent: true })
源碼分析
install-pkg的源碼非常簡(jiǎn)單,只有 100 行左右,我們來(lái)看看它的實(shí)現(xiàn)原理。
根據(jù)README的說(shuō)明,我們可以通過(guò)installPackage方法來(lái)安裝依賴,那么我們先來(lái)看看installPackage方法的實(shí)現(xiàn):
installPackage方法在src/index.ts文件中,轉(zhuǎn)成 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,
},
)
}
可以看到是一個(gè)異步方法,它接收兩個(gè)參數(shù),第一個(gè)參數(shù)是要安裝的依賴名稱,第二個(gè)參數(shù)是配置項(xiàng)。
在方法內(nèi)部,首先通過(guò)傳入的配置項(xiàng)options來(lái)獲取packageManager,如果沒(méi)有傳入packageManager,則通過(guò)detectPackageManager方法來(lái)獲取packageManager,如果detectPackageManager方法也沒(méi)有獲取到packageManager,則默認(rèn)使用npm。
來(lái)看看detectPackageManager方法的實(shí)現(xiàn):
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是一個(gè)用于查找文件的工具,它可以從當(dāng)前目錄向上查找文件,直到找到為止。
我們來(lái)逐行分析:
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})
最開(kāi)始是獲取package-lock.json、yarn.lock、pnpm-lock.yaml等文件的路徑;
如果找到就好辦了,直接在這個(gè)文件目錄下找package.json文件即可;
如果沒(méi)找到就繼續(xù)使用findUp方法來(lái)查找package.json文件。
if (packageJsonPath && fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
// ...
} catch {
}
}
如果找到了package.json文件,就讀取文件內(nèi)容,然后解析成 JSON 對(duì)象。
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)
}
這里是用過(guò)packageManager來(lái)判斷使用哪個(gè)包管理器;
- 如果
packageManager是yarn,并且版本號(hào)大于1,則使用yarn@berry; - 如果
packageManager是pnpm,并且版本號(hào)小于7,則使用pnpm@6; - 如果
packageManager是yarn、pnpm、npm、bun中的一個(gè),則直接使用; - 否則就打印一個(gè)警告。
// detect based on lock
if (!agent && lockPath)
agent = LOCKS[path.basename(lockPath)]
如果沒(méi)有通過(guò)package.json來(lái)獲取packageManager,則通過(guò)lockPath來(lái)獲取packageManager。
這個(gè)方法的核心就是通過(guò)兩個(gè)方式來(lái)獲取packageManager:
- 通過(guò)
package.json中的packageManager字段; - 通過(guò)
lock文件來(lái)獲取。
可以說(shuō)是非常巧妙。
我們繼續(xù)看installPackage方法:
const detectedAgent = options.packageManager || await detectPackageManager(options.cwd) || 'npm'
const [agent] = detectedAgent.split('@')
這里是第一行,就是獲取packageManager,和上面講的方法相輔相成,繼續(xù)往下看:
if (!Array.isArray(names))
names = [names]
這里是將name統(tǒng)一變成數(shù)組,方便后面處理。
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參數(shù),如果設(shè)置了這個(gè)參數(shù),就會(huì)在args中添加--prefer-offline或者--cached參數(shù),因?yàn)?code>yarn@berry和npm的參數(shù)不一樣。
return execa(
agent,
[
agent === 'yarn'
? 'add'
: 'install',
options.dev ? '-D' : '',
...args,
...names,
].filter(Boolean),
{
stdio: options.silent ? 'ignore' : 'inherit',
cwd: options.cwd,
},
)
這里的命令是根據(jù)packageManager來(lái)拼接的,yarn和npm的命令不一樣,所以需要判斷一下。
最后就是執(zhí)行安裝命令了,這里使用了execa來(lái)執(zhí)行命令,這個(gè)庫(kù)的用法和child_process差不多,但是更加方便,參考:execa。
總結(jié)
通過(guò)學(xué)習(xí)這個(gè)庫(kù),我們可以學(xué)到很多東西,比如:
- 如何判斷用戶使用的包管理器;
- 如何查找文件;
- 如何使用
execa來(lái)執(zhí)行命令。
同時(shí)這里面還穿插著很多node的知識(shí)和包管理器的知識(shí),比如:
node的path.basename方法;- 包管理器的
lock文件; - 包管理器的參數(shù)和命令。
以上就是編程式安裝依賴install-pkg源碼解析的詳細(xì)內(nèi)容,更多關(guān)于編程式安裝依賴install-pkg的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript知識(shí):構(gòu)造函數(shù)也是函數(shù)
構(gòu)造函數(shù)就是初始化一個(gè)實(shí)例對(duì)象,對(duì)象的prototype屬性是繼承一個(gè)實(shí)例對(duì)象。本文給大家分享javascript構(gòu)造函數(shù)詳解,對(duì)js構(gòu)造函數(shù)相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2021-08-08
微信小程序動(dòng)態(tài)的加載數(shù)據(jù)實(shí)例代碼
這篇文章主要介紹了 微信小程序動(dòng)態(tài)的加載數(shù)據(jù)實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04
JavaScript選擇器函數(shù)querySelector和querySelectorAll
這篇文章主要介紹了?JavaScript選擇器函數(shù)querySelector和querySelectorAll,下面文章圍繞querySelector和querySelectorAll的相關(guān)資料展開(kāi)詳細(xì)內(nèi)容,需要的朋友可以參考一下2021-11-11
Three.js添加陰影和簡(jiǎn)單后期處理實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了Three.js添加陰影和簡(jiǎn)單后期處理實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
JavaScript代碼不能被阻斷的穩(wěn)定性建設(shè)
這篇文章主要為大家介紹了JavaScript代碼不能被阻斷的穩(wěn)定性建設(shè)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
JavaScript中MutationObServer監(jiān)聽(tīng)DOM元素詳情
這篇文章主要給大家分享的是?JavaScript中MutationObServer監(jiān)聽(tīng)DOM元素詳情,DOM的MutationObServer接口,可以在DOM被修改時(shí)異步執(zhí)行回調(diào)函數(shù),我的理解就是可以監(jiān)聽(tīng)DOM修改。下面來(lái)看看文章的詳細(xì)內(nèi)容,需要的朋友可以參考一下2021-11-11
Web?Animations?API實(shí)現(xiàn)一個(gè)精確計(jì)時(shí)的時(shí)鐘示例
這篇文章主要為大家介紹了Web?Animations?API實(shí)現(xiàn)一個(gè)精確計(jì)時(shí)的時(shí)鐘示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

