{"id":16634924,"url":"https://github.com/ruochuan12/ni-analysis","last_synced_at":"2025-10-30T06:30:28.113Z","repository":{"id":123045139,"uuid":"420606277","full_name":"ruochuan12/ni-analysis","owner":"ruochuan12","description":"尤雨溪推荐神器 ni ，能替代 npm/yarn/pnpm ？简单好用！源码揭秘！","archived":false,"fork":false,"pushed_at":"2021-11-01T02:17:00.000Z","size":1868,"stargazers_count":14,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-02T06:31:44.808Z","etag":null,"topics":["cli","npm"],"latest_commit_sha":null,"homepage":"https://juejin.cn/post/7023910122770399269","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruochuan12.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2021-10-24T06:31:53.000Z","updated_at":"2023-12-22T02:43:28.000Z","dependencies_parsed_at":null,"dependency_job_id":"afa77242-ccea-4618-8e04-7bacd15602c1","html_url":"https://github.com/ruochuan12/ni-analysis","commit_stats":null,"previous_names":["ruochuan12/ni-analysis"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fni-analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fni-analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fni-analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fni-analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruochuan12","download_url":"https://codeload.github.com/ruochuan12/ni-analysis/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238937490,"owners_count":19555376,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","npm"],"created_at":"2024-10-12T05:49:29.862Z","updated_at":"2025-10-30T06:30:22.756Z","avatar_url":"https://github.com/ruochuan12.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\nhighlight: darcula\ntheme: smartblue\n---\n\n# 尤雨溪推荐神器 ni ，能替代 npm/yarn/pnpm ？简单好用！源码揭秘！\n\n## 1. 前言\n\n\u003e大家好，我是[若川](https://lxchuan12.gitee.io)。欢迎关注我的[公众号若川视野](https://lxchuan12.gitee.io)，最近组织了[**源码共读活动**](https://www.yuque.com/ruochuan12)，感兴趣的可以加我微信 [ruochuan12](https://juejin.cn/pin/7005372623400435725) 参与，已进行两个多月，大家一起交流学习，共同进步，很多人都表示收获颇丰。\n\n想学源码，极力推荐之前我写的[《学习源码整体架构系列》](https://juejin.cn/column/6960551178908205093) 包含`jQuery`、`underscore`、`lodash`、`vuex`、`sentry`、`axios`、`redux`、`koa`、`vue-devtools`、`vuex4`、`koa-compose`、`vue-next-release`、`vue-this`、`create-vue`、`玩具vite`等10余篇源码文章。\n\n[本文仓库 ni-analysis，求个star^_^](https://github.com/lxchuan12/ni-analysis.git)\n\n最近组织了[源码共读活动](https://www.yuque.com/ruochuan12)，大家一起学习源码。于是搜寻各种值得我们学习，且代码行数不多的源码。\n\n之前写了 `Vue3` 相关的两篇文章。\n- [初学者也能看懂的 Vue3 源码中那些实用的基础工具函数](https://juejin.cn/post/6994976281053888519)\n- [Vue 3.2 发布了，那尤雨溪是怎么发布 Vue.js 的？](https://juejin.cn/post/6997943192851054606)\n\n文章里都是写的使用 `yarn` 。参加源码共读的小伙伴按照我的文章，却拉取的最新仓库代码，发现 `yarn install` 安装不了依赖，向我反馈报错。于是我去 `github仓库` 一看，发现尤雨溪把 `Vue3仓库` 从 `yarn` 换成了 [`pnpm`](https://github.com/vuejs/vue-next/pull/4766/files)。[贡献文档](https://github.com/vuejs/vue-next/blob/master/.github/contributing.md#development-setup)中有一句话。\n\n\u003eWe also recommend installing [ni](https://github.com/antfu/ni) to help switching between repos using different package managers. `ni` also provides the handy `nr` command which running npm scripts easier.\n\n\u003e我们还建议安装 [ni](https://github.com/antfu/ni) 以帮助使用不同的包管理器在 repos 之间切换。 `ni` 还提供了方便的 `nr` 命令，可以更轻松地运行 npm 脚本。\n\n这个 `ni` 项目源码虽然是 `ts`，没用过 `ts` 小伙伴也是很好理解的，而且主文件其实不到 `100行`，非常适合我们学习。\n\n阅读本文，你将学到：\n\n```sh\n1. 学会 ni 使用和理解其原理\n2. 学会调试学习源码\n3. 可以在日常工作中也使用 ni\n4. 等等\n```\n\n## 2. 原理\n\n[github 仓库 ni#how](https://github.com/antfu/ni#how)\n\n**ni** 假设您使用锁文件（并且您应该）\n\n在它运行之前，它会检测你的 `yarn.lock` / `pnpm-lock.yaml` / `package-lock.json` 以了解当前的包管理器，并运行相应的命令。\n\n单从这句话中可能有些不好理解，还是不知道它是个什么。我解释一下。\n\n```bash\n使用 `ni` 在项目中安装依赖时：\n   假设你的项目中有锁文件 `yarn.lock`，那么它最终会执行 `yarn install` 命令。\n   假设你的项目中有锁文件 `pnpm-lock.yaml`，那么它最终会执行 `pnpm i` 命令。\n   假设你的项目中有锁文件 `package-lock.json`，那么它最终会执行 `npm i` 命令。\n\n使用 `ni -g vue-cli` 安装全局依赖时\n    默认使用 `npm i -g vue-cli`\n\n当然不只有 `ni` 安装依赖。\n    还有 `nr` - run\n    `nx` - execute\n    `nu` - upgrade\n    `nci` - clean install\n    `nrm` - remove\n```\n\n**我看源码发现：`ni`相关的命令，都可以在末尾追加`\\?`，表示只打印，不是真正执行**。\n\n所以全局安装 `ni` 后，可以尽情测试，比如 `ni \\?`，`nr dev --port=3000 \\?`，因为打印，所以可以在各种目录下执行，有助于理解 `ni` 源码。我测试了如下图所示：\n\n![命令测试图示](./images/terminal-debugger-v2.png)\n\n假设项目目录下没有锁文件，默认就会让用户从`npm、yarn、pnpm`选择，然后执行相应的命令。\n但如果在`~/.nirc`文件中，设置了全局默认的配置，则使用默认配置执行对应命令。\n\n**Config**\n\n```ini\n; ~/.nirc\n\n; fallback when no lock found\ndefaultAgent=npm # default \"prompt\"\n\n; for global installs\nglobalAgent=npm\n```\n\n**因此，我们可以得知这个工具必然要做三件事**：\n\n```bash\n1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm \n2. 抹平不同的包管理器的命令差异\n3. 最终运行相应的脚本\n```\n\n接着继续看看 `README` 其他命令的使用，就会好理解。\n\n## 3. 使用\n\n看 [ni github文档](https://github.com/antfu/ni)。\n\n\u003e\n\u003e~~npm i in a yarn project, again? F**k!~~\n\u003e\n\u003eni - use the right package manager\n\n全局安装。\n\n```bash\nnpm i -g @antfu/ni\n```\n\n如果全局安装遭遇冲突，我们可以加上 `--force` 参数强制安装。\n\n举几个常用的例子。\n\n### 3.1 ni - install\n\n```bash\nni\n\n# npm install\n# yarn install\n# pnpm install\n```\n\n```bash\nni axios\n\n# npm i axios\n# yarn add axios\n# pnpm i axios\n```\n\n### 3.2 nr - run\n\n```bash\nnr dev --port=3000\n\n# npm run dev -- --port=3000\n# yarn run dev --port=3000\n# pnpm run dev -- --port=3000\n```\n\n```bash\nnr\n# 交互式选择命令去执行\n# interactively select the script to run\n# supports https://www.npmjs.com/package/npm-scripts-info convention\n```\n\n```bash\nnr -\n\n# 重新执行最后一次执行的命令\n# rerun the last command\n```\n\n### 3.3 nx - execute\n\n```bash\nnx jest\n\n# npx jest\n# yarn dlx jest\n# pnpm dlx jest\n```\n\n## 4. 阅读源码前的准备工作\n\n### 4.1 克隆\n\n```sh\n# 推荐克隆我的仓库（我的保证对应文章版本）\ngit clone https://github.com/lxchuan12/ni-analysis.git\ncd ni-analysis/ni\n# npm i -g pnpm\n# 安装依赖\npnpm i\n# 当然也可以直接用 ni\n\n# 或者克隆官方仓库\ngit clone https://github.com/antfu/ni.git\ncd ni\n# npm i -g pnpm\n# 安装依赖\npnpm i\n# 当然也可以直接用 ni\n```\n\n众所周知，看一个开源项目，先从 package.json 文件开始看起。\n\n### 4.2 package.json 文件\n\n```js\n{\n    \"name\": \"@antfu/ni\",\n    \"version\": \"0.10.0\",\n    \"description\": \"Use the right package manager\",\n    // 暴露了六个命令\n    \"bin\": {\n        \"ni\": \"bin/ni.js\",\n        \"nci\": \"bin/nci.js\",\n        \"nr\": \"bin/nr.js\",\n        \"nu\": \"bin/nu.js\",\n        \"nx\": \"bin/nx.js\",\n        \"nrm\": \"bin/nrm.js\"\n    },\n    \"scripts\": {\n        // 省略了其他的命令 用 esno 执行 ts 文件\n        // 可以加上 ? 便于调试，也可以不加\n        // 或者是终端 npm run dev \\?\n        \"dev\": \"esno src/ni.ts ?\"\n    },\n}\n```\n\n根据 `dev` 命令，我们找到主入口文件 `src/ni.ts`。\n\n### 4.3 从源码主入口开始调试\n\n```ts\n// ni/src/ni.ts\nimport { parseNi } from './commands'\nimport { runCli } from './runner'\n\n// 我们可以在这里断点\nrunCli(parseNi)\n```\n\n找到 `ni/package.json` 的 `scripts`，把鼠标移动到 `dev` 命令上，会出现`运行脚本`和`调试脚本`命令。如下图所示，选择调试脚本。\n\n![VSCode 调试](./images/vscode-debugger.png)\n\n![VSCode 调试 Node.js 说明](./images/node.js-debugger.jpg)\n\n## 5. 主流程 runner - runCli 函数\n\n这个函数就是对终端传入的命令行参数做一次解析。最终还是执行的 `run` 函数。\n\n对于 `process` 不了解的读者，可以看[阮一峰老师写的 process 对象](http://javascript.ruanyifeng.com/nodejs/process.html)\n\n```ts\n// ni/src/runner.ts\nexport async function runCli(fn: Runner, options: DetectOptions = {}) {\n  // process.argv：返回一个数组，成员是当前进程的所有命令行参数。\n  // 其中 process.argv 的第一和第二个元素是Node可执行文件和被执行JavaScript文件的完全限定的文件系统路径，无论你是否这样输入他们。\n  const args = process.argv.slice(2).filter(Boolean)\n  try {\n    await run(fn, args, options)\n  }\n  catch (error) {\n    // process.exit方法用来退出当前进程。它可以接受一个数值参数，如果参数大于0，表示执行失败；如果等于0表示执行成功。\n    process.exit(1)\n  }\n}\n```\n\n我们接着来看，`run` 函数。\n\n## 6. 主流程 runner - run 主函数\n\n**这个函数主要做了三件事**：\n\n```bash\n1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm - detect 函数\n2. 抹平不同的包管理器的命令差异 - parseNi 函数\n3. 最终运行相应的脚本 - execa 工具\n```\n\n```ts\n// ni/src/runner.ts\n// 源码有删减\nimport execa from 'execa'\nconst DEBUG_SIGN = '?'\nexport async function run(fn: Runner, args: string[], options: DetectOptions = {}) {\n  // 命令参数包含 问号? 则是调试模式，不执行脚本\n  const debug = args.includes(DEBUG_SIGN)\n  if (debug)\n    // 调试模式下，删除这个问号\n    remove(args, DEBUG_SIGN)\n\n  // cwd 方法返回进程的当前目录（绝对路径）\n  let cwd = process.cwd()\n  let command\n\n  // 支持指定 文件目录\n  // ni -C packages/foo vite\n  // nr -C playground dev\n  if (args[0] === '-C') {\n    cwd = resolve(cwd, args[1])\n    // 删掉这两个参数 -C packages/foo\n    args.splice(0, 2)\n  }\n\n  // 如果是全局安装，那么实用全局的包管理器\n  const isGlobal = args.includes('-g')\n  if (isGlobal) {\n    command = await fn(getGlobalAgent(), args)\n  }\n  else {\n    let agent = await detect({ ...options, cwd }) || getDefaultAgent()\n    // 猜测使用哪个包管理器，如果没有发现锁文件，会返回 null，则调用 getDefaultAgent 函数，默认返回是让用户选择 prompt\n    if (agent === 'prompt') {\n      agent = (await prompts({\n        name: 'agent',\n        type: 'select',\n        message: 'Choose the agent',\n        choices: agents.map(value =\u003e ({ title: value, value })),\n      })).agent\n      if (!agent)\n        return\n    }\n    // 这里的 fn 是 传入解析代码的函数\n    command = await fn(agent as Agent, args, {\n      hasLock: Boolean(agent),\n      cwd,\n    })\n  }\n\n  // 如果没有命令，直接返回，上一个 runCli 函数报错，退出进程\n  if (!command)\n    return\n\n  // 如果是调试模式，那么直接打印出命令。调试非常有用。\n  if (debug) {\n    // eslint-disable-next-line no-console\n    console.log(command)\n    return\n  }\n\n  // 最终用 execa 执行命令，比如 npm i\n  // https://github.com/sindresorhus/execa\n  // 介绍：Process execution for humans\n\n  await execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })\n}\n```\n\n我们学习完主流程，接着来看两个重要的函数：`detect` 函数、`parseNi` 函数。\n\n根据入口我们可以知道。\n\n```ts\nrunCli(parseNi)\n\nrun(fn)\n\n这里 fn 则是 parseNi\n```\n\n\n### 6.1 根据锁文件猜测用哪个包管理器（npm/yarn/pnpm） - detect 函数\n\n代码相对不多，我就全部放出来了。\n\n```bash\n主要就做了三件事情\n\n1. 找到项目根路径下的锁文件。返回对应的包管理器 `npm/yarn/pnpm`。\n2. 如果没找到，那就返回 `null`。\n3. 如果找到了，但是用户电脑没有这个命令，则询问用户是否自动安装。\n```\n\n```js\n// ni/src/agents.ts\nexport const LOCKS: Record\u003cstring, Agent\u003e = {\n  'pnpm-lock.yaml': 'pnpm',\n  'yarn.lock': 'yarn',\n  'package-lock.json': 'npm',\n}\n```\n\n```ts\n// ni/src/detect.ts\nexport async function detect({ autoInstall, cwd }: DetectOptions) {\n  const result = await findUp(Object.keys(LOCKS), { cwd })\n  const agent = (result ? LOCKS[path.basename(result)] : null)\n\n  if (agent \u0026\u0026 !cmdExists(agent)) {\n    if (!autoInstall) {\n      console.warn(`Detected ${agent} but it doesn't seem to be installed.\\n`)\n\n      if (process.env.CI)\n        process.exit(1)\n\n      const link = terminalLink(agent, INSTALL_PAGE[agent])\n      const { tryInstall } = await prompts({\n        name: 'tryInstall',\n        type: 'confirm',\n        message: `Would you like to globally install ${link}?`,\n      })\n      if (!tryInstall)\n        process.exit(1)\n    }\n\n    await execa.command(`npm i -g ${agent}`, { stdio: 'inherit', cwd })\n  }\n\n  return agent\n}\n```\n\n接着我们来看 `parseNi` 函数。\n\n### 6.2 抹平不同的包管理器的命令差异 - parseNi 函数\n\n```ts\n// ni/src/commands.ts\nexport const parseNi = \u003cRunner\u003e((agent, args, ctx) =\u003e {\n  // ni -v 输出版本号\n  if (args.length === 1 \u0026\u0026 args[0] === '-v') {\n    // eslint-disable-next-line no-console\n    console.log(`@antfu/ni v${version}`)\n    process.exit(0)\n  }\n\n  if (args.length === 0)\n    return getCommand(agent, 'install')\n  // 省略一些代码\n})\n```\n\n通过 `getCommand` 获取命令。\n\n```ts\n// ni/src/agents.ts\n// 有删减\n// 一份配置，写个这三种包管理器中的命令。\n\nexport const AGENTS = {\n  npm: {\n    'install': 'npm i'\n  },\n  yarn: {\n    'install': 'yarn install'\n  },\n  pnpm: {\n    'install': 'pnpm i'\n  },\n}\n```\n\n```ts\n// ni/src/commands.ts\nexport function getCommand(\n  agent: Agent,\n  command: Command,\n  args: string[] = [],\n) {\n  // 包管理器不在 AGENTS 中则报错\n  // 比如 npm 不在\n  if (!(agent in AGENTS))\n    throw new Error(`Unsupported agent \"${agent}\"`)\n\n  // 获取命令 安装则对应 npm install\n  const c = AGENTS[agent][command]\n\n  // 如果是函数，则执行函数。\n  if (typeof c === 'function')\n    return c(args)\n\n  // 命令 没找到，则报错\n  if (!c)\n    throw new Error(`Command \"${command}\" is not support by agent \"${agent}\"`)\n  // 最终拼接成命令字符串\n  return c.replace('{0}', args.join(' ')).trim()\n}\n```\n\n\n### 6.3 最终运行相应的脚本\n\n得到相应的命令，比如是 `npm i`，最终用这个工具 [execa](https://github.com/sindresorhus/execa) 执行最终得到的相应的脚本。\n\n```ts\nawait execa.command(command, { stdio: 'inherit', encoding: 'utf-8', cwd })\n```\n\n## 7. 总结\n\n**我们看完源码，可以知道这个神器 `ni` 主要做了三件事**：\n\n```bash\n1. 根据锁文件猜测用哪个包管理器 npm/yarn/pnpm - detect 函数\n2. 抹平不同的包管理器的命令差异 - parseNi 函数\n3. 最终运行相应的脚本 - execa 工具\n```\n\n我们日常开发中，可能容易 `npm`、`yarn`、`pnpm` 混用。有了 `ni` 后，可以用于日常开发使用。`Vue` 核心成员 [Anthony Fu](https://antfu.me) 发现问题，最终开发了一个工具 [ni](https://github.com/antfu/ni) 解决问题。而这种发现问题、解决问题的能力正是我们前端开发工程师所需要的。\n\n另外，我发现 `Vue` 生态很多基本都切换成了使用 [pnpm](https://pnpm.io)。\n\n因为文章不宜过长，所以未全面展开讲述源码中所有细节。非常建议读者朋友按照文中方法使用`VSCode`调试 `ni` 源码。**学会调试源码后，源码并没有想象中的那么难**。\n\n最后可以持续关注我@若川。欢迎加我微信 [ruochuan12](https://juejin.cn/pin/7005372623400435725) 交流，参与 [源码共读](https://www.yuque.com/ruochuan12) 活动，大家一起学习源码，共同进步。\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fni-analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruochuan12%2Fni-analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fni-analysis/lists"}