携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第9期,链接:www.yuque.com/ruochuan12/…。
摘要
本次源码阅读的内容是 ni,一款包管理的集成体,它能自动辨别项目所使用的包管理工具,以防错误的命令使用导致项目的包管理混乱。
它的实现原理是通过检测项目中的 yarn.lock / pnpm-lock.yaml / package-lock.json / bun.lockb 或者是根据在 package.json 中指定的 packageManager 来决定所使用的包管理工具。
ni 主要提供了如下几个命令:
ni- install;nr- run;nx- execute;nu- upgrade;nun- uninstall;nci- clean install;na- agent alias;ni -C- Change Directory
接下来我们深入分析下源码吧~
源码分析
项目地址 ni
ni 相关源码分析
package.json 分析
首先我们来看一下项目的 package.json ,scripts 有如下几个命令:
"scripts": {
// 打包
"prepublishOnly": "npm run build",
// 使用 esno 执行 ni.ts
"dev": "esno src/commands/ni.ts",
// 使用 unbuild 进行打包
"build": "unbuild",
"stub": "unbuild --stub",
"release": "bumpp && npm publish",
// lint 校验
"lint": "eslint .",
// 测试
"test": "vitest"
},
我们需要做的是对 dev 命令进行调试,鼠标悬浮于其上,就可以弹出如图的选项,选择调试脚本,即可进行调试,不过首先要在相应的文件中打上断点。
src/commands/ni.ts 分析
打开 dev 命令执行的文件,这里的代码比较简单,但是我们可以得到更关键的源码位置。
import { parseNi } from '../parse'
import { runCli } from '../runner'
runCli(parseNi)
parseNi 分析
parseNi 函数主要的功能是解析 args, 并对 -g --frozen-if-present --frozen 做出了额外的判断操作,最后执行了 getCommand。
export const parseNi = <Runner>((agent, args, ctx) => {
if (args.length === 1 && args[0] === '-v') {
// eslint-disable-next-line no-console
console.log(`@antfu/ni v${version}`)
process.exit(0)
}
// bun use `-d` instead of `-D`, #90
if (agent === 'bun')
args = args.map(i => i === '-D' ? '-d' : i)
if (args.includes('-g'))
return getCommand(agent, 'global', exclude(args, '-g'))
if (args.includes('--frozen-if-present')) {
args = exclude(args, '--frozen-if-present')
return getCommand(agent, ctx?.hasLock ? 'frozen' : 'install', args)
}
if (args.includes('--frozen'))
return getCommand(agent, 'frozen', exclude(args, '--frozen'))
if (args.length === 0 || args.every(i => i.startsWith('-')))
return getCommand(agent, 'install', args)
return getCommand(agent, 'add', args)
})
getCommand 分析
在这里能够获取最终要执行的命令,根据传入的 agent 和command, 在AGENTS 查找出预先设定好的命令映射。
export function getCommand(
agent: Agent,
command: Command,
args: string[] = [],
) {
if (!(agent in AGENTS))
throw new Error(`Unsupported agent "${agent}"`)
const c = AGENTS[agent][command]
if (typeof c === 'function')
return c(args)
if (!c)
throw new Error(`Command "${command}" is not support by agent "${agent}"`)
return c.replace('{0}', args.join(' ')).trim()
}
const AGENTS = {
'npm': {
'agent': 'npm {0}',
'run': npmRun('npm'),
'install': 'npm i {0}',
'frozen': 'npm ci',
'global': 'npm i -g {0}',
'add': 'npm i {0}',
'upgrade': 'npm update {0}',
'upgrade-interactive': null,
'execute': 'npx {0}',
'uninstall': 'npm uninstall {0}',
'global_uninstall': 'npm uninstall -g {0}',
},
...
}
runCli() 分析
runCli() 位于 src\runner.ts,它的内容也比较简单,获取命令行执行参数 args 并执行 run()
export async function runCli(fn: Runner, options: DetectOptions = {}) {
const args = process.argv.slice(2).filter(Boolean)
try {
await run(fn, args, options)
}
catch (error) {
process.exit(1)
}
}
run() 分析
这里分析一下关键代码:
- 如果
args里有-g, 则通过getGlobalAgent()获取globalAgent,代码里默认的是npm; - 不然的话通过
detect来自动检测项目中使用到的包管理器,原理就是通过检测项目中的yarn.lock/pnpm-lock.yaml/package-lock.json/bun.lockb或者是根据在package.json中指定的packageManager来决定所使用的包管理工具; - 要是还没获取到包管理器的话,通过
getDefaultAgent()获取defaultAgent
const isGlobal = args.includes('-g')
if (isGlobal) {
command = await fn(await getGlobalAgent(), args)
}
else {
let agent = await detect({ ...options, cwd }) || await getDefaultAgent()
if (agent === 'prompt') {
agent = (await prompts({
name: 'agent',
type: 'select',
message: 'Choose the agent',
choices: agents.filter(i => !i.includes('@')).map(value => ({ title: value, value })),
})).agent
if (!agent)
return
}
command = await fn(agent as Agent, args, {
hasLock: Boolean(agent),
cwd,
})
}
总结与收获
本次阅读的源码数量不多,但是却非常值得学习,针对 ni 命令的代码大体梳理了一遍,整体流程了解的差不多了,不够有些细节还需要在深入理解下,有些是 node 的知识,有些是包管理器的知识,这些内容后续还需要找找资料学习。
这次受到的启示是,在学习工作中遇到的一些问题,我们都可以通过技术手段来优化办事流程和效率。可以写一些小脚本,方便自己;也可以发布成包,帮助大家。