在自己开发的包下执行 npm link之后,发生了什么?
概述
比如我有一个项目叫做my-cli
在项目的package.json里设置bin的值,作用是执行npm link之后,我们在任何终端地方执行my-cli命令都会执行cli.js这个文件。
{
"name": "my-cli",
"version": "1.0.0",
"main": "index.js",
"bin": {
"my-cli": "./dist/cli.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"type": "module",
"author": "",
}
而用什么环境(或者说程序)执行,则是通过在cli.js中头部加上的注释来判断
#!/usr/bin/env node
这句表示是用node来执行这个脚本文件
初探
可以观察到执行npm link之后,node安装目录下出现了几个文件,分别是:
- my-cli
- my-cli.cmd
- my-cli.ps1
而node_modules下会出现my-cli这个文件夹,可以看到他是一个软链接,即真正的文件在原来执行npm link时的路径下,这里相当于只是一个快捷方式。
我是windows系统,所以我发现在终端执行my-cli命名时,调用的是my-cli.ps1这个文件
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/node_modules/my-cli/dist/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/node_modules/my-cli/dist/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/node_modules/my-cli/dist/cli.js" $args
} else {
& "node$exe" "$basedir/node_modules/my-cli/dist/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
我想看明白上面写了什么便问问Ai:
如果顺便想要学习一下powershell脚本的内容,则可以在vscode安装个插件进行断点调试
安装完可以创建launch.json
选择powerShell(刚刚安装插件后才有这个选项),之后选择launch script(你可以选别的)
在创建的launch.json中把script的值换成my-cli,之后在my-cli.ps1文件下打上断点后运行点击运行debug按钮就可以顺利调试了
调试npm源码,看看npm link怎么实现的
我们知道安装完node,他的路径会加入到操作系统的path环境变量里,所以我们才可以在任意位置访问到node,my-cli也同理,由于脚本文件被创建到了node下,所以也能在终端随时访问。
接下来我们去npm源码看一下具体npm link的实现方式。
首先到node安装路径下的node_modules目录里,找到npm包,并查看package.json文件内容,里面有bin字段,这个字段的值就是npm的入口文件
顺着入口打上断点,
再结合vscode的调试功能,很快就可以找到npm link的实现方式了。
在launch.json里,添加Launch via npm的配置。在runtimeArgs数组里添加link,然后点击运行调试按钮,就可以开始寻找npm link的实现方式了...
{
"name": "测试npm link原理",
"request": "launch",
"runtimeArgs": [
"link"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
},
看到这一句,就知道了link命令的具体执行代码,就在commands文件夹下
return require(`./commands/${command}.js`)
从这一句可以看到npm link有没有加参数在功能上的区别。
定位到创建文件和软连接是通过@npmcli/arborist这个包的reify方法实现的
const Arborist = require('@npmcli/arborist')
const arb = new Arborist({
...this.npm.flatOptions,
Arborist,
path: globalTop,
global: true,
})
await arb.reify({
add,
})
找到创建软链接的地方
这里是创建脚本的地方,就前面提到的三个脚本 xx.ps1、xx.cmd、xx
这里可以看到之前为什么入口文件有需要指定运行环境的地方,他读取了入口文件的第一行
看到这些脚本语言,我们终于知道原来脚本是在这个函数里生成的。
总结
通过这次探索,我们学习了如何使用调试去观察代码的执行流程,并且也知道了命令行脚本的调试方法。还了解了如何借助AI帮我们辅助了解代码。在调试的过程中,我们可以根据一些现象去寻找源码的实现方式。所以前提条件需要我们对要调试的功能有一个初步的认识。
执行npm xx这种命令时,调用的是node_modules\npm\lib\commands\xx.js这种同名文件。
在调试的过程中,咱们发现了npm的核心文件是node_modules\npm\node_modules\@npmcli\arborist\lib\arborist\reify.js,因为不管是npm install或者是npm link等命令,最终都会调用该文件。
最后我们在reify.js中找到了他是调用node的symlink方法创建的软链接,并且在node_modules\npm\node_modules\cmd-shim\lib\index.js里创建了那三个脚本文件,以便于我们在任何终端中输入命令名都可以正常执行脚本。
额外
我在cursor编辑器中,用gpt4o帮我们分析了一下reify.js的功能,可供参考:
reify.js这个文件是一个 JavaScript 模块,定义了一个名为 Reifier 的类,该类继承自传入的 cls 类。Reifier 类的主要功能是处理 Node.js 项目的依赖管理,特别是处理依赖的安装、更新和锁定文件的生成。以下是对该文件的详细分析:
主要功能
-
依赖安装与更新:
reify方法是该类的核心方法,负责根据传入的选项安装或更新项目的依赖。- 该方法会根据不同的安装策略(如
linked)调整依赖树,并处理依赖的差异。
-
依赖树的管理:
- 该类通过
_loadTrees方法加载当前项目的实际依赖树和理想依赖树。 - 使用
_diffTrees方法计算实际依赖树和理想依赖树之间的差异。
- 该类通过
-
文件系统操作:
- 通过
_reifyPackages方法,执行一系列文件系统操作以安装或更新依赖。 - 包括创建目录、移动文件、创建符号链接等。
- 通过
-
错误处理与回滚:
- 在安装过程中,如果发生错误,使用一系列回滚方法(如
_rollbackRetireShallowNodes)来恢复到之前的状态。
- 在安装过程中,如果发生错误,使用一系列回滚方法(如
-
锁定文件的生成:
- 使用
_saveIdealTree方法保存理想依赖树的元数据到package-lock.json或shrinkwrap文件中。
- 使用
-
审计与报告:
- 通过
_submitQuickAudit方法进行快速审计,检查依赖的安全性问题。
- 通过
-
脚本执行:
- 在安装或更新依赖后,执行相关的 npm 脚本(如
predependencies,dependencies,postdependencies)。
- 在安装或更新依赖后,执行相关的 npm 脚本(如
代码结构
- 私有方法:以
_开头的方法是类的私有方法,用于处理内部逻辑,如_reifyPackages、_loadTrees、_diffTrees等。 - 符号属性:使用
Symbol定义了一些私有属性和方法,确保它们在类外部无法直接访问。 - 异步操作:大量使用
async/await进行异步操作,确保文件系统操作和网络请求的顺序执行。
总结
Reifier 类是一个复杂的依赖管理工具,主要用于处理 Node.js 项目的依赖安装、更新和锁定文件的生成。它通过一系列异步操作和文件系统操作,确保项目的依赖状态与预期一致,并在出现错误时提供回滚机制。