4.1 Yarn install命令源码实现
本文发布于掘金专栏
文章原文位于github
本文严重依赖2. 使用 vscode 以及 chrome 调试 yarn 源码
install 命令是 yarn 命令中比较核心的一个命令,弄懂 install 其他很多命令都会明白,因为存在互相调用函数实现。
调试准备
进入一个 debug 文件夹并初始化一个npm包,安装react@18,删除node_modules只留下package.json和yarn.lock,命令在下面
bash
yarn init -y
yarn add react@18 # 这里用18是因为react18有依赖项
rm -rf node_modules
在vscode的launch.json中的配置添加调试 install 相关的配置
json
{
"type": "node",
"request": "launch",
"name": "debug yarn install",
"skipFiles": ["<node_internals>/**"],
"console": "internalConsole",
"outFiles": ["${workspaceFolder}/**/*.(m|c|)js", "!**/node_modules/**"],
"program": "${workspaceFolder}/mybuild/cli/index.js",
"cwd": "${workspaceFolder}/../yarn-source-dev",
"args": ["install"]
}
选择debug yarn install即可调试。具体的调试方法见使用 vscode 以及 chrome 调试 yarn 源码,也就是本书的第 2 章。
install 命令运行流程
install 命令源码位于src/cli/commands/install.js,查看 install 命令源码可以得到下面的运行流程图。
运行流程主要分为四个函数的执行。
run 函数
run函数是导出给commands对象的一个函数,这是install命令具体实现的入口,下面是简洁代码,run函数主要做了以下事
js
if (flags.lockfile === false) {
lockfile = new Lockfile();
} else {
lockfile = await Lockfile.fromDirectory(config.lockfileFolder, reporter);
}
wrapLifecycle();
- 生成
Lockfile实例,如果命令行参数中带有--lockfile false的话会生成一个全新的Lockfile实例,如果没有--lockfile参数的话就从当前的yarn.lock文件生成实例,这也是默认的选项。下面是从yarn.lock中生成的Lockfile实例。可以看到Lockfile是解析yarn.lock文件到对应的数据结构的类。
- 调用
wrapLifecycle。实际这里是先调用的install函数install函数再调用的wrapLifecycle。下面是install的函数实现。
wrapLifecycle 函数
这个函数的参数是一个factory函数,factory函数会被放到一系列的lifecyclescript执行中执行。下面是 wrapLifecycle 函数的简洁声明
js
async function wrapLifecycle(config: Config, flags: Object, factory) {
await config.executeLifecycleScript("preinstall");
await factory();
await config.executeLifecycleScript("install");
await config.executeLifecycleScript("postinstall");
if (!config.production) {
if (!config.disablePrepublish) {
await config.executeLifecycleScript("prepublish");
}
await config.executeLifecycleScript("prepare");
}
}
所谓的lifecyclescript实际上就是在package.json中的scripts字段中声明的script。这里执行script的函数是config.executeLifecycleScript。这个后续在yarn run命令的章节中会具体讲解。这里可以看到在执行完preinstall script 会就会立即执行factory函数。
factory 函数
factory 函数首先实例话了Install类,这个类的构造函数里面是一些属性的初始化。然后这个函数会调用Install实例上的init方法。init方法里面是后学install命令的具体实现。
init 方法
init方法的代码比较多,这里简洁分为三部分。
第一部分是执行yarn升级检测,如果yarn有版本的更新输出中会有提示,这里的yarn版本指的是yarn1的版本,20 年之前使用的人会经常看到这个提示,现在(2024)年因为yarn1的版本基本没有更新所有很难看到这个提示了。
第二部分是执行fetchRequestFromCwd函数,这个函数是获取包的入口的函数。此函数会把package.json里面依赖的包都收集到一个数组里面。这里查看代码可以看到yarn默认是安装三种类型的依赖dependencies devDependencies optionalDependencies,其中devDependencies在!this.config.production === false时是不会安装的(加上--procution 命令会使 this.config.production === true)。同时这个函数还会返回当前包的一些信息比如workspace以及package.json。
第三部分则是steps,在上文中的流程图中带有step的节点都是steps中的一个运行节点,运行节点是先收集需要运行的节点,然后对节点依次执行,如果当前节点执行报错则直接退出不执行后续的节点。steps的执行函数会使用init方法中的一些变量并进行修改,在整个步骤完成后init函数中的变量会被置为相应的值。
第四部分则是保存对应的package.json以及yarn.lock。第三部分结束后整个node_modules都已经构建好了。
steps
steps是init方法的核心部分。这里按照出现顺序依次介绍对应的step。
在运行install命令的时候控制台会输出当前在哪一个step
checkCompatibilityStep
这个step是选择加入到steps中的,其中的执行条件是package.json中有os cpu engines字段中的其中一个且命令行参数中没有对应的ignore参数,比如os和cpu对应--ignore-platform参数,engines对应--ignore-engines参数。具体检测的函数实现代码位于src/package-compatibility.js。
resolveStep
这个第一个必执行的step,resolveStep的目标是找到所有包的具体信息,包括依赖的依赖的包。找到的具体信息主要是包的具体版本是什么,从哪里下载,包依赖的具体依赖包具体信息。一般情况下是在npm远端源上下载的包文件。下面时step执行函数的内容
js
await this.resolver.init(depRequests, {
isFlat: this.flags.flat,
isFrozen: this.flags.frozenLockfile,
workspaceLayout,
});
topLevelPatterns = this.preparePatterns(rawPatterns);
flattenedTopLevelPatterns = await this.flatten(topLevelPatterns);
npm远端源的包文件是一个tarball文件,它是一个以.tgz结尾的一个压缩文件。访问源的api,比如https://registry.npmjs.org/react/18.2.0,可以在响应中的dist/tarball中找到包文件的链接。
this.resolver.init会找到所有的包的准确版本以及下载包的npm源链接。
这个函数内容的逻辑很复杂,概括大致能分成yarn.lock文件中存在以及在yarn.lock文件中不存在两种。如果包对应的版本在yarn.lock中存在,yarn会直接使用lock文件中的包版本以及lock文件中声明的tarball文件链接。如果包在yarn.lock中不存在,yarn会请求npm远端源去获取对应的信息,知道所有的依赖都被正确解析。
相关代码在
src/package-resolver
step执行函数中剩下两步很简单,把最底层的依赖拍平。
经过这一部所有包的信息,包括依赖的依赖,也就是将要安装的所有包的信息都被解析到this.resolver中。为下一步进行下载对应的包做准备。
下面是resolveStep运行完之后解析到的react@18及其所有的包的信息。
作者因为有
yarn.lock文件所有所有的包都是直接通过lock文件解析的,通过lock文件解析的包的fresh字段是false,通过源解析的是true
auditStep
这个step满足参数中有--audit才执行,因为yarn audit命令并没有修复的子命令,所有install命令这里可以加--audit来实现差不多的功能。这个在之后的yarn audit命令中会讲这里先跳过。
fetchStep
这是一个必执行的step。在上面的resolveStep把所有包的信息都解析出,这一步主要是下载包到对应的cache文件夹。逻辑也很简单,如果cache文件夹中存在对应的包,则无需操作,只需要返回包cache对应的信息,如果cache文件夹中不存在包,则下载包的tarball文件并进行解压到包的cache文件夹。
使用
yarn cache dir命令可以查看全局的cache文件夹
进入cache文件夹中搜索react-18.2.0会发现存在react@18.2.0版本的cache文件夹。这是因为作者之前已经安装过react@18.2.0导致cache命中。
linkStep
这是一个必执行的step。这个step开始构建当前目录下的node_modules。这个step简单来讲就是先把第一步解析到的所有包扁平化成一个数组,找到每个包对于的cache文件夹位置以及在当前目录node_modules下的位置,然后进行文件的复制。由于resolveStep和fetchStep已经知道所有的信息这里可以直接复制。
在把所有包从cache中的位置复制到node_modules之后,yarn还会把有bin的包的bin执行文件创建软链接到node_modules/.bin目录下。
pnpStep
这个step需要条件满足才执行,这里需要打开pnp插件的开关才能执行。这个不做多讲后续讲pnp会补充。
buildStep
这是一个必执行的step。这个step一般是在linkStep后被执行,同时这个step一般也是steps的最后一个。这个step主要是将之前resolveStep中收集到的所有包的scripts进行执行。这里会先按照当前包的依赖中的scripts进行执行,执行完当前的再执行依赖的,整体的执行顺序是一个拓扑序。这里执行的scripts是preinstall install postinstall,其中的声明在依赖包的scripts中的script不会被执行。
到了这里基本的step就走完,剩下的都是满足条件才执行的step
savingHarStep
这个step是一个满足条件才执行的step。这个step通过在命令行参数中加入--har来启用,启用之后在命令完成后会生成一个.har文件,这个文件可以使用网络分析工具进行分析。这个step在网络有问题排查十分有用。
cleaningModulesStep
这个step是一个满足条件才执行的step。启用这个step的条件是当前目录下存在.yarnclean文件。这个step和yarn autoclean命令有关后续会讲解。
总结
Yarn的install命令实现中最重要的是四个step。resolveStep负责找到包具体信息和来源,fetchStep把包从远端拉到本地的cache文件夹,linkStep负责构建完整的node_modules,buildStep负责按照拓扑序执行所有依赖包的scripts。
author: xiaochuan
date: 2025.1.1