Yarn源码中的Config

246 阅读7分钟

Config

本文发布于掘金专栏

文章原文位于github

yarnConfig是一个很重要的类,在main函数里面和Reporter一起初始化,这两个实例也会通过参数传给后续的命令的run函数。本章会详细讲解Config有关的逻辑。

Reporter的代码没有复杂的逻辑这里直接跳过

Config的声明位于src/config.js文件,调用位于src/cli/index.js里面,除了新建一个Config实例外还调用了init方法。

Config类的属性主要由基本的配置以及ConstraintResolver RequestManager ConfigRegistries实例构成。方法主要由_init init两个方法构成。

下面是省略后的代码

class Config {
  constructor(reporter: Reporter) {
    this.constraintResolver = new ConstraintResolver(this, reporter);
    this.requestManager = new RequestManager(reporter);
    this.reporter = reporter;
    this._init({});
  }

  // 基本的属性
  enableDefaultRc: boolean;
  extraneousYarnrcFiles: Array<string>;
  // ...

  // 主要的实例
  constraintResolver: ConstraintResolver;
  requestManager: RequestManager;
  registries: ConfigRegistries;
}

基础的属性

基础属性分为两类,一类是通过命令行参数和常量就可以直接得到的,另一类则是需要一些计算才能得到的参数。在方法_init里面直接使用命令行参数对基础属性进行赋值。

_init

另外一种是通过计算得到的属性,逻辑位于init方法里面。这个详见下文。

ConstraintResolver

这个实例会在构造函数被实例化出来,参数是当前的Config实例以及Reporter实例。类的源码声明于文件src/package-constraint-resolver,这个类的逻辑十分简单。下面是省略后的代码

const semver = require('semver');

export default class PackageConstraintResolver {
  // ...

  reporter: Reporter;
  config: Config;

  reduce(versions: Array<string>, range: string): Promise<?string> {
    if (range === 'latest') {
      return Promise.resolve(versions[versions.length - 1]);
    } else {
      return Promise.resolve(semver.maxSatisfying(versions, range, this.config.looseSemver));
    }
  }
}

其中只有一个reduce方法,这个方法是找到一个版本string对应的真正版本。比如运行命令yarn add react@latest,这里的参数range就是latest,第一个参数versions会从其他地方获取到,这里就会走到第一个if语句里面。如果使用的是一个semver的版本号,比如yarn add react@^18,就会走到else语句里面,这里就直接调用semvermaxSatisfying方法,这里的looseSemver默认是true,是Config的一个基础属性,从命令行参数得到的。

looseSemver的由来

RequestManager

RequestManager也是在构造函数中被实例化的一个类,原为位于src/util/request-manager.js,这是一个网络请求的基础类,其中yarn对其做了一些封装,比如重试超时以及请求队列等等,这里不做细讲。RequestManager底层的网络请求库是使用的早已废弃的request,这里也能看出yarn很久没有进行大的迭代。

ConfigRegistries

Config中的registries分别由两个属性构成,一个npm一个yarn,两个属性分别是NpmRegistry以及YarnRegistry的实例,源码位于src/registries里面。这两个类主要是和.npmrc以及.yarnrc里面的配置以及和远端的npm源有关。

这里有一个继承,BaseRegistry -> NpmRegistry -> YarnRegistry

这里本地的Registry类统一使用英文,远端npm源统一使用npm源

远端的npm源指的是比如yarn add react@18命令时需要去哪个链接下载,以及publish时需要发布到哪个网站。npm官方的源链接是https://registry.npmjs.org/,通常国内会使用淘宝的源https://registry.npmmirror.com/,源配置在.npmrc文件的registry字段。

NpmRegistry

NpmRegistry管理和.npmrc有关的配置及其相关远端npm源的相关逻辑。

由于继承BaseRegistry,所以NpmRegistry也有一个config属性对象表示当前的配置。其中最重要的方法是loadConfig方法,这里给出NpmRegistry的简洁代码。

class NpmRegistry extends Registry {
    config: Object;   // 这个config是继承来的,

    async loadConfig(): Promise<void> {
        // 初始化config里面的值
        this.mergeEnv('npm_config_');
        // 下面这个是伪代码
        this.getPossibleConfigLocationsAndSet();
    }
}

loadConfig中主要是两个逻辑

  1. this.mergeEnv('npm_config_') 从环境变量中取值并赋值到this.config

    这个方法会遍历当前进程中的环境变量,环境变量名转换为小写。以npm_config_开头的环境变量会被加入到this.config中,其中前缀npm_config_会被去掉。

    这里注入一个名为npm_config_test_name的变量,值为test_value,在经过mergeEnvthis.config中会有一个名为test_name的属性值为test_value

    mergeEnv后的config

  2. 寻找可能的.npmrc文件并使用文件中的配置赋值

    这里会递归的寻找所有的.npmrc文件,找到文件后取出里面的值和当前的this.config进行合并,从当前目录一直找到根目录,每层的.npmrc文件都有效,层数约接近当前的目录优先级越高,越接近根目录优先级越低。如果下层目录目录和上层目录都有同一个key,则会以底层目录的.npmrc中的值为准。很多人的home目录~里面都有一个.npmrc,这个.npmrc可以看作是全局的npm配置。

    这里在调试目录下新增.npmrc文件并新增内容rctestvalue=rctestvalue。调试结果如下图。rctestvalue的值是从当前目录的.npmrc中取的,而其他的值是从上层目录的.npmrc中取得。

    .npmrc递归寻找

    不止是yarn是这个逻辑,其他的包管理器npmpnpm都会往上寻找所有的.npmrc并进行合并。

YarnRegistry

YarnRegistry管理和.yarnrc文件有关的配置和相关npm源的内容

.yarnrc的配置页面

他继承NpmRegistry,也有一个config对象属性,其中最重要的两个方法分别是init以及loadConfig

  1. init 调用this.mergeEnv('yarn_'),逻辑和上面的this.mergeEnv('npm_config_')一样,只不过前缀换成了yarn_开头。如果给一个环境变量yarn_ytest=test_value,过了这步后this.config中会有一个ytest=test_value的值。

    mergeEnv('yarn_')

  2. 寻找可能的.yarnrc文件并使用文件中的配置赋值

    这个逻辑和寻找.npmrc的逻辑一致知识文件名换成了.yarnrc

    这里在调试目录下新建一个.yarnrc文件并写入yarn_test_key yarn_test_value。调试结果如下

    .yarnnrc

this.config的使用

在获取了this.config的值之后就是使用了,其中使用最多的字段就是registry,这是远端npm源的链接,包管理器可以从这里下载包,发布包以及登录等一系列操作。

NpmRegistryYarnRegistry都有一个getOption方法,这个方法返回this.config中的值。Config也有一个getOption方法,这个方法会先调用YarnRegistrygetOption如果有值直接返回,如果没有值会去调用NpmRegistrygetOption。这说明无论层级.yarnrc中的配置比.npmrc中的配置高。

这里不建议使用.yarnrc建议使用.npmrc.yarnrcyarn自己的逻辑太多,而.npmrc是大部分包管理器通用的一个配置文件。

Config的init函数

上面一直没有讲的是ConfigRegistries就是在init函数中被初始化的,这个函数还负责初始化一些需要计算得到的基本属性(这些属性需要根据ConfigRegistries里面的config字段来进行初始化)。主要有以下几个类型的基础属性。

  1. 全局的目录

    包括全局包的安装目录以及全局link包的目录,全局cache的目录等

  2. workspace相关的

    如果有workspace相关的目录以及配置也会被初始化

  3. 网络相关的配置

    Config构造函数中实例化了一个RequestManager,这里会根据配置文件中的网络配置来设置这个实例的一些属性。

  4. 其他

    其他一些配置比如pnp插件相关

总结

到了这里Config的内容就结束了,Config是一个很重要的概念,后续命令的运行依赖他的一个实例。在main函数中被实例化后会被传到每个命令运行的参数里面。同时ConfigResolvers这个重要的类也是在Config中被使用。在配置项中使用最多的是registry,这个配置指示了包管理器从哪个地方获取包以及发布到哪里。

下一节开始正式进入具体命令的讲解

author: xiaochuan

date: 2024.12.29