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
  • 测试
  • 文件处理 ...

如果有机会可以下次讲

相关推荐
敲敲了个代码4 小时前
从硬编码到 Schema 推断:前端表单开发的工程化转型
前端·javascript·vue.js·学习·面试·职场和发展·前端框架
dly_blog5 小时前
Vue 响应式陷阱与解决方案(第19节)
前端·javascript·vue.js
消失的旧时光-19435 小时前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
console.log('npc')6 小时前
Table,vue3在父组件调用子组件columns列的方法展示弹窗文件预览效果
前端·javascript·vue.js
我命由我123456 小时前
SVG - SVG 引入(SVG 概述、SVG 基本使用、SVG 使用 CSS、SVG 使用 JavaScript、SVG 实例实操)
开发语言·前端·javascript·css·学习·ecmascript·学习方法
C_心欲无痕7 小时前
vue3 - markRaw标记为非响应式对象
前端·javascript·vue.js
qingyun9897 小时前
深度优先遍历:JavaScript递归查找树形数据结构中的节点标签
前端·javascript·数据结构
胡楚昊7 小时前
NSSCTF动调题包通关
开发语言·javascript·算法
熬夜敲代码的小N7 小时前
Vue (Official)重磅更新!Vue Language Tools 3.2功能一览!
前端·javascript·vue.js
小彭努力中7 小时前
1.在 Vue 3 中使用 Cesium 快速展示三维地球
前端·javascript·vue.js·#地图开发·#cesium·#vue3