ni - 包自动选择工 源码实现 antfu的小工具

ni - 包自动选择工 源码实现 antfu的小工具

ni是用来做什么的

ni是用来判断你的项目用的npm,yarn还是pnpm等,正确使用package managers

How

ni会假定你项目中有lockfiles(也应该有),在它执行前它会检查你的lock文件,就知道了你的package manager

bash 复制代码
ni

# npm install
# yarn install
# pnpm install
# bun install

它有的命令如下

  • ni - install
  • nr - run
  • nlx - download & execute
  • nu - upgrade
  • nun - uninstall
  • nci - clean install
  • na - agent alias

调试

准备

bash 复制代码
git clone https://github.com/antfu/ni.git
cd ni
pnpm install

先查看package.json

package.json

json 复制代码
{
  "name": "@antfu/ni",
  "type": "module",
  "version": "0.21.12",
  "packageManager": "pnpm@8.11.0",
  "description": "Use the right package manager",
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
  "license": "MIT",
  "homepage": "https://github.com/antfu/ni#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/antfu/ni.git"
  },
  "bugs": {
    "url": "https://github.com/antfu/ni/issues"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  },
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "bin": {
    "ni": "bin/ni.mjs",
    "nci": "bin/nci.mjs",
    "nr": "bin/nr.mjs",
    "nu": "bin/nu.mjs",
    "nlx": "bin/nlx.mjs",
    "na": "bin/na.mjs",
    "nun": "bin/nun.mjs"
  },
  "files": [
    "bin",
    "dist"
  ],
  "scripts": {
    ...
  },
  "devDependencies": {
    ...
  }
}

这和我们平常看见的package.json有一些不同,我们通过了解package.json这个窗口可以了解整个项目的运行方式,可以逐步解析

我们不长接触的属性有

  • respository
    • 这个很明显是指定存储库的位置
  • bugs
    • 指定项目报告BUG的地址或者方式
  • exports
    • 是node12引入的特性,执行模块的入口点以及在不同环境下的导出方式
    • 本项目指定了默认的导出方式,types类型定义文件的位置,es modules环境下加载的文件位置,require commonjs环境下加载的文件路径
  • bin
    • bin属性用来指定项目中可执行文件的入口点,通常是命令行工具。 如果有bin属性,这些可执行文件会放在系统的PATH路径中,可以直接在命令行中执行
  • files
    • 这个是执行发布包的时候包含的文件或者目录
    • 保证在发布包的时候不会发布多余的文件,保证npm包的简洁

debug

在package.json中我们看到了ni的入口文件 "ni": "bin/ni.mjs",, 我们从bin/ni.mjs文件开始

在vscode中script中有默认的调试工具,在scripts上有Debug按钮,按下Debug就可以调试nodejs程序就像在浏览器中一样

ni.ts

ts 复制代码
import { parseNi } from '../parse'
import { runCli } from '../runner'

runCli(parseNi)

ni

  1. 解析命令行
  2. 执行解析出来的命令

一步一步来看他是怎么做到的

1. 解析命令行

主要代码都在runner.ts文件中

ts 复制代码
export async function runCli(fn: Runner, options: DetectOptions & { args?: string[] } = {}) {
  const {
    args = process.argv.slice(2).filter(Boolean),
  } = options
  try {
    await run(fn, args, options)
  }
  catch (error) {
    if (error instanceof UnsupportedCommand && !options.programmatic)
      console.log(c.red(`\u2717 ${error.message}`))

    if (!options.programmatic)
      process.exit(1)

    throw error
  }
}

runCli 截取有效参数,处理错误,执行run函数

ts 复制代码
export async function run(fn: Runner, args: string[], options: DetectOptions = {}) {
  const debug = args.includes(DEBUG_SIGN)
  if (debug)
    remove(args, DEBUG_SIGN)

  let cwd = options.cwd ?? process.cwd()
  if (args[0] === '-C') {
    cwd = resolve(cwd, args[1])
    args.splice(0, 2)
  }

  if (args.length === 1 && (args[0]?.toLowerCase() === '-v' || args[0] === '--version')) {
    ...
    return
  }

  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`@antfu/ni v${version}`)
    return
  }

  if (args.length === 1 && ['-h', '--help'].includes(args[0])) {
    ...
    return
  }

  let command = await getCliCommand(fn, args, options, cwd)

  if (!command)
    return

  const voltaPrefix = getVoltaPrefix()
  if (voltaPrefix)
    command = voltaPrefix.concat(' ').concat(command)

  if (debug) {
    console.log(command)
    return
  }

  await execaCommand(command, { stdio: 'inherit', cwd })
}
  1. 处理参数-v -C等 脉络出来了,通过if判断参数处理一些边缘情况,在加参数的时候返回不同的值,重要的是下面

  2. 解析参数得到命令

ts 复制代码
let command = await getCliCommand(fn, args, options, cwd)

...


export async function getCliCommand(
  fn: Runner,
  args: string[],
  options: DetectOptions = {},
  cwd: string = options.cwd ?? process.cwd(),
) {
  const isGlobal = args.includes('-g')
  if (isGlobal)
    return await fn(await getGlobalAgent(), args)

  let agent = (await detect({ ...options, cwd })) || (await getDefaultAgent(options.programmatic))
  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
  }

  return await fn(agent as Agent, args, {
    programmatic: options.programmatic,
    hasLock: Boolean(agent),
    cwd,
  })
}

就是组装参数,并且执行fn,fn是我们之前找到的parseNi

ts 复制代码
export function getCommand(
  agent: Agent,
  command: Command,
  args: string[] = [],
) {
  ...
  const c = AGENTS[agent][command]
  ...
  return c.replace('{0}', args.map(quote).join(' ')).trim()
}

export const parseNi = <Runner>((agent, args, ctx) => {
  // bun use `-d` instead of `-D`, #90
    ...
  return getCommand(agent, 'add', args)
})

删除了一些边界情况的判断逻辑,可以看的更清晰 getCommand调用getCommand,getCommand组装参数通过AGENTS对象

ts 复制代码
export 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}',
  }
  ...
}

AGENTS对象就是这个结构,我们在判断的时候可以用对象来代替if判断,当做map来用,这样顺序就很清晰了,前面一系列的处理参数,最终通过一个对象来完成最后的组装

如果是自己写一个只有自己用的install方法的工具,甚至可以直接用

ts 复制代码
export const AGENTS = {
    npm: {
        install: 'npm i'
    }
}

然后直接返回命令行,最后执行,但是发布工具的话,需要考虑的问题就多了起来,有很多判断加边界情况

2. 执行

得到命令行参数之后,执行就简单了

import { execaCommand } from 'execa' 借用的是execa包的方法

总结

我们这次只梳理的最主干的脉络,也依稀看出一个小工具中包含的繁杂细节

还有很多可以讲,里面的函数的包装,

  • 如何分割函数
  • 如果处理错误
  • 如果处理debug
  • 测试
  • 文件处理 ...

如果有机会可以下次讲

相关推荐
我码玄黄32 分钟前
THREE.js:网页上的3D世界构建者
开发语言·javascript·3d
爱喝水的小鼠1 小时前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
小晗同学1 小时前
Vue 实现高级穿梭框 Transfer 封装
javascript·vue.js·elementui
WeiShuai1 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife1 小时前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
mez_Blog3 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川3 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶3 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
深情废杨杨3 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS3 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js