本文参加了由公众号@若川视野 ****发起的每周源码共读活动, 点击了解详情一起参与。
包管理器你用哪家?npm、yarn、 pnpm,真是萝卜白菜各有所爱,本期要学习的ni可以根据锁文件自动判断你用的包管理器并执行相应的脚本,快来学习吧!
1.学习参考资料
川哥文章: juejin.cn/post/702391…
2.了解用法-readme
package.json bin选项对应:
"bin": {
"ni": "bin/ni.js",
"nci": "bin/nci.js",
"nr": "bin/nr.js",
"nu": "bin/nu.js",
"nx": "bin/nx.js",
"nrm": "bin/nrm.js"
},
2.1 ni
ni - install
npm install
yarn install
pnpm install
ni axios
npm i axios
yarn add axios
pnpm i axios
ni --frozen
npm ci
yarn install --frozen-lockfile
pnpm install --frozen-lockfile
ni -g iroiro
npm i -g iroiro
yarn global add iroiro
pnpm i -g iroiro
2.2 nr
nr - run
nr dev --port=3000
npm run dev -- --port=3000
yarn run dev --port=3000
pnpm run dev -- --port=3000
nr -
rerun the last command
2.3 nx
nx - execute
nx jest
npx jest
yarn dlx jest
pnpm dlx jest
2.4 nu
nu- upgrade
nu
npm upgrade
yarn upgrade
pnpm upgrade
2.5 nci
nci - clean install
npm ci
yarn install --frozen-lockfile
pnpm install --frozen-lockfile
2.6 nrm
nrm - remove
nrm axios
npm uninstall axios
yarn remove axios
pnpm remove axios
nrm @types/node -D
npm uninstall @types/node -D
yarn remove @types/node -D
pnpm remove @types/node -D
nrm -g iroiro
npm uninstall -g iroiro
yarn global remove iroiro
pnpm remove -g iroiro
3.了解原理-readme
检查lockfiles:在其运行之前检查yarn.lock、pnpm-lock.yaml、package-lock.json 了解包管理工具,然后运行相关的命令
4.猜测实现原理
(1) ni根据运行相关命令执行不同文件
(2)检查lockfiles 从而决定使用何种包管理工具(file,path相关api)
(3)执行命令,解析命令参数(执行命令的相关依赖,库)
5.浏览代码
5.1 ni - bin/ni.js
import { parseNi } from './commands'
import { runCli } from './runner'
runCli(parseNi)
parseNi的定义:
import { version } from '../package.json'
import { Agent, AGENTS, Command } from './agents'
import { exclude } from './utils'
import { Runner } from './runner'
export function getCommand(
agent: Agent,
command: Command,
args: string[] = [],
) {
// 判断是不是npm,yarn,pnpm这几个包管理器,如果不是报错
if (!(agent in AGENTS))
throw new Error(`Unsupported agent "${agent}"`)
// 获取具体的命令,是 run ,install, add, upgrade, uninstall 等等
const c = AGENTS[agent][command]
//如果是函数 'run': npmRun('npm')
if (typeof c === 'function')
return c(args)
// 没有c,参数错误
if (!c)
throw new Error(`Command "${command}" is not support by agent "${agent}"`)
// 替换 global': 'npm i -g {0}'
return c.replace('{0}', args.join(' ')).trim()
}
export const parseNi = <Runner>((agent, args, ctx) => {
//如果参数的长度是1,并且是 -v ,则输出版本
if (args.length === 1 && args[0] === '-v') {
// eslint-disable-next-line no-console
console.log(`@antfu/ni v${version}`)
process.exit(0)
}
// 参数数组的长度如果是0,则执行install命令
if (args.length === 0)
return getCommand(agent, 'install')
// 如果参数里面包含-g则全局安装
if (args.includes('-g'))
return getCommand(agent, 'global', exclude(args, '-g'))
// -f 是啥???
if (args.length === 1 && args[0] === '-f')
return getCommand(agent, 'install', args)
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'))
return getCommand(agent, 'add', args)
})
exclude的定义:
// 从arr中删除T
export function remove<T>(arr: T[], v: T) {
const index = arr.indexOf(v)
if (index >= 0)
arr.splice(index, 1)
return arr
}
export function exclude<T>(arr: T[], v: T) {
// 深拷贝arr,并从副本中删除v
return remove(arr.slice(), v)
}
其他的命令 nr nx nu 的流程类似,不逐一分析了。看runCli方法:
5.2 runCli
~/src/runner.ts:
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方法:
const DEBUG_SIGN = '?'
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
// ?? 参数里面有?则删除?
const debug = args.includes(DEBUG_SIGN)
if (debug)
remove(args, DEBUG_SIGN)
let cwd = process.cwd()
let command
if (args[0] === '-C') {
cwd = resolve(cwd, args[1])
args.splice(0, 2)
}
// ?? 全局npm
const isGlobal = args.includes('-g')
if (isGlobal) {
command = await fn(getGlobalAgent(), args)
}
else {
// 检测
let agent = await detect({ ...options, cwd }) || getDefaultAgent()
if (agent === 'prompt') {
// 选择 npm , yarn, pnpm
agent = (await prompts({
name: 'agent',
type: 'select',
message: 'Choose the agent',
choices: agents.map(value => ({ title: value, value })),
})).agent
if (!agent)
return
}
command = await fn(agent as Agent, args, {
hasLock: Boolean(agent),
cwd,
})
}
if (!command)
return
if (debug) {
// eslint-disable-next-line no-console
console.log(command)
return
}
// 执行命令
await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })
}
getGlobalAgent:
export function getGlobalAgent() {
return getConfig().globalAgent
}
调用了getConfig:
export function getConfig() {
if (!config) {
// 不存在全局默认配置
if (!fs.existsSync(rcPath))
config = defaultConfig
else
//~/.nirc 文件中存在默认的全局配置
config = Object.assign({}, defaultConfig, ini.parse(fs.readFileSync(rcPath, 'utf-8')))
}
return config
}
detect函数的实现:
export async function detect({ autoInstall, cwd }: DetectOptions) {
// Find a file or directory by walking up parent directories
// 寻找一个文件或者目录通过遍历父级目录
//cwd: The directory to start from. 开始搜索的目录
const result = await findUp(Object.keys(LOCKS), { cwd })
// 返回的目录, 例如 /xxxx/pnpm-lock.yaml, 取basename就是pnpm-lock.yaml,根据这个获取是哪一种目录
const agent = (result ? LOCKS[path.basename(result)] : null)
// 没有安装
if (agent && !cmdExists(agent)) {
// 没有选择自动安装
if (!autoInstall) {
console.warn(`Detected ${agent} but it doesn't seem to be installed.\n`)
// ???
if (process.env.CI)
process.exit(1)
// Create clickable links in the terminal
// 在终端创建一个可以点击的链接
const link = terminalLink(agent, INSTALL_PAGE[agent])
const { tryInstall } = await prompts({
name: 'tryInstall',
type: 'confirm',
message: `Would you like to globally install ${link}?`,
})
// 询问是否要安装,如果不,则退出
if (!tryInstall)
process.exit(1)
}
// 安装包管理工具
await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })
}
return agent
}
LOCKS的定义如下:
export const LOCKS: Record<string, Agent> = {
'pnpm-lock.yaml': 'pnpm',
'yarn.lock': 'yarn',
'package-lock.json': 'npm',
}
包管理工具的下载链接:
export const INSTALL_PAGE: Record<Agent, string> = {
pnpm: 'https://pnpm.js.org/en/installation',
yarn: 'https://yarnpkg.com/getting-started/install',
npm: 'https://www.npmjs.com/get-npm',
}
cmdExists: 根据系统类型判断是否是否安装了相应的包管理工具:
export function cmdExists(cmd: string) {
try {
// #8
execSync(
os.platform() === 'win32'
? `cmd /c "(help ${cmd} > nul || exit 0) && where ${cmd} > nul 2> nul"`
: `command -v ${cmd}`,
)
return true
}
catch {
return false
}
}
在Linux中,command -v 可以判断一个命令是否支持:
6.收获
了解到ni的用法和实现原理;
了解到有用的依赖:find-up, terminal-link
学完本期源码,您可以思考如下问题:
1.package.json文件中bin选项的含义是什么?
2.ni如何判断当前使用的是什么包管理器?
3.ni最终是如何运行相应的脚本的?
4.简述ni的核心流程?
5.思考ni的使用场景?