【第12期】 ni —— 包管理新助手

311 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

摘要

本次源码阅读的内容是 ni,一款包管理的集成体,它能自动辨别项目所使用的包管理工具,以防错误的命令使用导致项目的包管理混乱。

它的实现原理是通过检测项目中的 yarn.lock / pnpm-lock.yaml / package-lock.json / bun.lockb 或者是根据在 package.json 中指定的 packageManager 来决定所使用的包管理工具。

ni 主要提供了如下几个命令:

  1. ni - install;
  2. nr - run;
  3. nx - execute;
  4. nu - upgrade;
  5. nun - uninstall;
  6. nci - clean install;
  7. na - agent alias;
  8. ni -C - Change Directory

接下来我们深入分析下源码吧~

源码分析

项目地址 ni

ni 相关源码分析

package.json 分析

首先我们来看一下项目的 package.jsonscripts 有如下几个命令:

  "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 命令进行调试,鼠标悬浮于其上,就可以弹出如图的选项,选择调试脚本,即可进行调试,不过首先要在相应的文件中打上断点。

image.png

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 分析

在这里能够获取最终要执行的命令,根据传入的 agentcommand, 在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() 分析

这里分析一下关键代码:

  1. 如果 args 里有 -g, 则通过 getGlobalAgent() 获取globalAgent,代码里默认的是 npm
  2. 不然的话通过 detect 来自动检测项目中使用到的包管理器,原理就是通过检测项目中的 yarn.lock / pnpm-lock.yaml / package-lock.json / bun.lockb 或者是根据在 package.json 中指定的 packageManager 来决定所使用的包管理工具;
  3. 要是还没获取到包管理器的话,通过 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 的知识,有些是包管理器的知识,这些内容后续还需要找找资料学习。

这次受到的启示是,在学习工作中遇到的一些问题,我们都可以通过技术手段来优化办事流程和效率。可以写一些小脚本,方便自己;也可以发布成包,帮助大家。