Skip to content

Commit

Permalink
add file
Browse files Browse the repository at this point in the history
  • Loading branch information
Notryag committed Jan 1, 2024
1 parent 4501661 commit 482c01c
Showing 1 changed file with 319 additions and 0 deletions.
319 changes: 319 additions & 0 deletions 杂乱/ni源码阅读.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
# 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": "[email protected]",
"description": "Use the right package manager",
"author": "Anthony Fu <[email protected]>",
"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
+ 测试
+ 文件处理
...

如果有机会可以下次讲

0 comments on commit 482c01c

Please sign in to comment.