前言
在上一篇文章 从架构和源码角度重新认识 ESLint (一) 中,我主要从 ESLint 的使用和架构出发,介绍了 ESLint 核心的功能模块,以及怎么更好地利用 debug
模式,帮助我们去定位问题。
本篇文章将深入到源码,主要围绕 ESLint 的配置文件读取、加载和合并等逻辑,这部分的源码主要在
lib/cli-engine
目录下,而该模块依赖了官方的 @eslint/eslintrc
包,实际上比较核心的配置处理逻辑全部封装在该包中。
本文的源码分析 ESLint 版本是 v8.57.0,目前官方版本已经到 9.x 版本,因为我的团队还在使用 8.x 版本,因此主要围绕 8.x 最后一个版本 8.57.0 进行源码解读。
下面开始进入正题。
CliEngine
我们首先回顾上一篇文章中的架构图:
cli-engine
作为配置处理的关键功能模块,它的调用处于比较上层的位置,而 lib/cli-engine
主要封装了一个 CliEngine
类,导出给外部调用。
cli-engine
最核心的配置处理逻辑主要集中在 @eslint/eslintrc
包中,它导出了三个关键的模块:
- CascadingConfigArrayFactory,用户配置读取、加载、合并的类,是整个 ESLint 配置处理的核心入口
- ModuleResolver,文件加载器
- getUsedExtractedConfigs,从缓存中读取处理后的最终配置
下面看 CliEngine
源码:
js
const {
Legacy: {
CascadingConfigArrayFactory,
getUsedExtractedConfigs,
ModuleResolver
}
} = require("@eslint/eslintrc");
const builtInRules = require("../rules");
const loadRules = require("./load-rules");
const debug = require("debug")("eslint:cli-engine");
/** @type {WeakMap<CLIEngine, CLIEngineInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* Core CLI.
*/
class CLIEngine {
/**
* Creates a new instance of the core CLI engine.
* @param {CLIEngineOptions} providedOptions The options for this instance.
* @param {Object} [additionalData] Additional settings that are not CLIEngineOptions.
* @param {Record<string,Plugin>|null} [additionalData.preloadedPlugins] Preloaded plugins.
*/
constructor(providedOptions, { preloadedPlugins } = {}) {
const options = Object.assign(
Object.create(null),
defaultOptions,
{ cwd: process.cwd() },
providedOptions
);
// 读取项目的缓存文件
const cacheFilePath = getCacheFile(
options.cacheLocation || options.cacheFile,
options.cwd
);
// 初始化 configArrayFactory 实例
const configArrayFactory = new CascadingConfigArrayFactory({
additionalPluginPool,
baseConfig: options.baseConfig || null,
cliConfig: createConfigDataFromOptions(options),
cwd: options.cwd,
ignorePath: options.ignorePath,
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo,
rulePaths: options.rulePaths,
specificConfigPath: options.configFile,
useEslintrc: options.useEslintrc,
builtInRules,
loadRules,
getEslintRecommendedConfig: () => require("@eslint/js").configs.recommended,
getEslintAllConfig: () => require("@eslint/js").configs.all
});
// 创建缓存和 linter 实例
const lintResultCache =
options.cache ? new LintResultCache(cacheFilePath, options.cacheStrategy) : null;
const linter = new Linter({ cwd: options.cwd });
// 省略一些代码
// setup special filter for fixes
if (options.fix && options.fixTypes && options.fixTypes.length > 0) {
debug(`Using fix types ${options.fixTypes}`);
// 省略一些代码
}
}
// Return Map<Rule>
getRules() {
// 通过一个 Map 存储所有的内置规则和用户配置的插件规则
}
/**
* Executes the current configuration on an array of file and directory names.
* @returns {LintReport} The results for all files that were linted.
*/
executeOnFiles(patterns) {
// 省略代码
}
/**
* Executes the current configuration on text.
* @returns {LintReport} The results for the linting.
*/
executeOnText(text, filename, warnIgnored) {
// 省略代码
}
/**
* Returns a configuration object for the given file based on the CLI options.
* @returns {ConfigData} A configuration object for the file.
*/
getConfigForFile(filePath) {
const { configArrayFactory, options } = internalSlotsMap.get(this);
const absolutePath = path.resolve(options.cwd, filePath);
if (directoryExists(absolutePath)) {
throw Object.assign(
new Error("'filePath' should not be a directory path."),
{ messageTemplate: "print-config-with-directory-path" }
);
}
return configArrayFactory
.getConfigArrayForFile(absolutePath)
.extractConfig(absolutePath)
.toCompatibleObjectAsConfigFileContent();
}
/**
* Returns the formatter representing the given format or null if the `format` is not a string.
* @returns {(FormatterFunction|null)} The formatter function or null if the `format` is not a string.
*/
getFormatter(format) {
// 省略代码
}
}
CLIEngine.version = pkg.version;
CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
module.exports = {
CLIEngine,
/**
* Get the internal slots of a given CLIEngine instance for tests.
* @param {CLIEngine} instance The CLIEngine instance to get.
* @returns {CLIEngineInternalSlots} The internal slots.
*/
getCLIEngineInternalSlots(instance) {
return internalSlotsMap.get(instance);
}
};
删除了一些非核心或者本次源码解读不太关注的代码,除去注释后代码在 100 行左右。
我们重点关注以下几点:
-
初始化 CliEngine 实例时会创建若干关键的功能模块实例:
- configArrayFactory,需要重点关注的配置处理实例
- lintResultCache ,ESLint lint 结果缓存实例,只有当配置中配置
cache
为true
时才创建,cli 通过--cache
开启 - linter,代码 lint 的核心类实例
-
CliEngine 封装的几个公开方法:
- getRules,这个方法会将内置的规则和用户配置的规则合并,并返回一个 Map
- executeOnFiles ,接受 glob 文件参数,核心就是读取文件范围源码,调用
linter.verify
对代码进行 lint,过程中会利用 lintResultCache 将上次结果进行缓存,下次 lint 时就可以尽量利用缓存提升 lint 速度 - executeOnText,跟 executeOnFiles 方法功能类似,区别就是 executeOnText 接收的主要参数是源码字符串,在一些场景下可以脱离文件 IO 使用
- getFormatter ,根据当前传入的参数获取 lint 结果 fomat 格式,根据 format 将结果输出到一个文本或者 JS 文件中,具体使用可以参考文档:--format,官方也支持自定义 formatter
-
CliEngine 实例化后会通过 WeakMap 将实例进行缓存,可以重复利用。毕竟一次 CliEngine 的实例初始化涉及到很多的文件 IO 操作,这个过程是比较耗时和占用 CPU 的,因此通过缓存一定程度解决这些问题
-
CliEngine 内部的一些执行日志,通过 DEBUG scope
eslint:cli-engine
开启
摘掉一些非核心代码,再去梳理 CliEngine 逻辑就相对清晰。当然一些详细的实现,例如 LintResultCache 是怎么做缓存的,什么时候缓存失效等,需要深入到这部分源码去看。这部分我个人还是很感兴趣的,后续可能会考虑再解读下这部分源码。
回到主线,接下来我们要深入到 CascadingConfigArrayFactory
的实现源码,它是 ESLint 配置处理最关键的类。它封装在 @eslint/eslintrc
包中,所以我们需要切下进程。
CascadingConfigArrayFactory
在开始解读源码之前,我们先看下 ESLint 官方是怎么介绍 @eslint/eslintrc
该包的:
This repository contains the legacy ESLintRC configuration file format for ESLint. This package is not intended for use outside of the ESLint ecosystem. It is ESLint-specific and not intended for use in other programs.
Note: This package is frozen except for critical bug fixes as ESLint moves to a new config system.
大概意思:该仓库包含旧 ESLintRC 配置文件格式,虽然是 ESLint 的规范实现,但是该包不打算暴露给 ESLint 生态或者其他开发者使用。最后提示,目前包的状态只是修复在迁移到新的配置系统时遇到的问题,不再开发新功能。
要完全理解上面的几句话,要对 ESLint 新旧配置系统有一些了解。在 8.x 及以下版本 ESLint 配置是通过 .eslintrc.*(js、json、cjs、yml)
等文件格式实现的,这也就是上述所说的旧 ESLintRC 配置系统。在 ESLint 9.x 版本,它升级了新的配置系统,配置格式为:eslint.config.*(js、json、cjs)
。因此为了迁移到新的配置系统,将 8.x 之前版本 ESLintRC 配置处理逻辑进行抽象,也是解耦了新版 9.x 的代码为了兼容旧配置系统的复杂性。
在正式解读源码前,为了帮助大家更好地理解这个包的功能模块,先看一个大概的功能模块图:
CliEngine 消费的 CascadingConfigArrayFactory 是包中最顶层的抽象类型,下面还依赖 share 、config-array-factory 等模块:
-
share ,主要封装一些通用公共的模块,例如配置校验、模块加载器:
- config-validator,主要校验 ESlint 的整份 ESLintRC 配置、规则配置、enviroment 配置、global 配置等
- relative-module-resolver,通过 Node.js 内置的
Module.createRequire
创建了一个模块加载器,这是 ESLint 内置默认的文件、插件等加载器,贯穿整个源码使用 - 除此之外还有一些其它的处理规则告警级别、三方包名等 utils,这里不一一介绍
-
config-array-factory ,配置加载的核心实现,底层还依赖 config-array 做配置合并的逻辑,特别是对于嵌套的配置,例如用户配置了一个 share config a(下面会详细介绍),a 里面配置了一些插件和其他选项,a 的配置与项目配置的合并策略
因此我们解读的源码核心聚焦在上图的左边部分 config-array-factory 以及 config-array 。
share config 和插件
为了更好理解后面的内容,在 ESLint 中有两个概念需要讲清楚: share config(根据官方的说法引入) 和插件。
在 ESLint 中有两种配置插件的方式:
js
// eslintrc.js
module.export = {
extends: ["plugin:@typescript-eslint/recommended"]
}
// or
module.export = {
plugins: ["@typescript-eslint"]
}
而上面代码示例中,第一种形式,是通过 share config 的方式配置,也就是 extends 配置项,此时通过
plugin:xxx
注册,ESLint 背后加载时会将其当成一个插件处理。
那么 share config 使用场景是什么?
例如我现在有多个核心技术栈基本一致的项目,我想使得每个项目都享用同一份 ESLintRC 配置,里面包含了一些插件、规则告警级别以及其他通用配置等。这个时候,我们就可以使用 share config 抽离一个 NPM 包:
arduino
// eslint-config-share.js
module.export = {
plugins: ["@typescript-eslint"],
globals: {
MyGlobal: true
},
rules: {
semi: [2, "always"]
}
}
安装依赖后在项目中使用:
arduino
// a 项目 .eslintrc.js
module.export = {
plugins: ["share"],
}
通过这种方式共享配置,后续迭代升级,所有项目只需要升级配置版本即可。这里有两个点需要注意:
- 只有包名为
eslint-config-xxx
才能直接通过extends
配置,而且可以忽略前面的eslint-config
- 如果要把插件当做 share config 注册,则需要使用
plugin:
前缀,如上面例子所示
其加载方式、配置合并策略和一般的插件会有区别,大家平时在开发中需要注意,要能区分当前配置的是一个插件还是一个 share config,否则 ESLint 解析配置时会报错。
而 ESLint 的插件定位就是提供一系列规则集合,正常情况下不太会提供其他配置,当然你也可以把一个插件当成 share config 使用,在注册的时候需要注意。
讲清楚这两个概念后,我们进入下面的源码解读。
cascading-config-array-factory
先从 CascadingConfigArrayFactory 入口开始:
js
// cascading-config-array-factory.js
const debug = debugOrig("eslintrc:cascading-config-array-factory");
/** @type {WeakMap<CascadingConfigArrayFactory, CascadingConfigArrayFactoryInternalSlots>} */
const internalSlotsMap = new WeakMap();
/**
* This class provides the functionality that enumerates every file which is
* matched by given glob patterns and that configuration.
*/
class CascadingConfigArrayFactory {
/**
* Initialize this enumerator.
* @param {CascadingConfigArrayFactoryOptions} options The options.
*/
constructor({
additionalPluginPool = new Map(),
baseConfig: baseConfigData = null,
cliConfig: cliConfigData = null,
cwd = process.cwd(),
ignorePath,
resolvePluginsRelativeTo,
rulePaths = [],
specificConfigPath = null,
useEslintrc = true,
builtInRules = new Map(),
loadRules,
resolver,
eslintRecommendedPath,
getEslintRecommendedConfig,
eslintAllPath,
getEslintAllConfig
} = {}) {
const configArrayFactory = new ConfigArrayFactory({
additionalPluginPool,
cwd,
resolvePluginsRelativeTo,
builtInRules,
resolver,
eslintRecommendedPath,
getEslintRecommendedConfig,
eslintAllPath,
getEslintAllConfig
});
internalSlotsMap.set(this, {
baseConfigArray: createBaseConfigArray({
baseConfigData,
configArrayFactory,
cwd,
rulePaths,
loadRules
}),
baseConfigData,
cliConfigArray: createCLIConfigArray({
cliConfigData,
configArrayFactory,
cwd,
ignorePath,
specificConfigPath
}),
cliConfigData,
configArrayFactory,
configCache: new Map(),
cwd,
finalizeCache: new WeakMap(),
ignorePath,
rulePaths,
specificConfigPath,
useEslintrc,
builtInRules,
loadRules
});
}
/**
* Get the config array of a given file.
* If `filePath` was not given, it returns the config which contains only
* `baseConfigData` and `cliConfigData`.
* @param {string} [filePath] The file path to a file.
* @param {Object} [options] The options.
* @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`.
* @returns {ConfigArray} The config array of the file.
*/
getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) {
const {
baseConfigArray,
cliConfigArray,
cwd
} = internalSlotsMap.get(this);
if (!filePath) {
return new ConfigArray(...baseConfigArray, ...cliConfigArray);
}
const directoryPath = path.dirname(path.resolve(cwd, filePath));
debug(`Load config files for ${directoryPath}.`);
return this._finalizeConfigArray(
this._loadConfigInAncestors(directoryPath),
directoryPath,
ignoreNotFoundError
);
}
/**
* Load and normalize config files from the ancestor directories.
* @param {string} directoryPath The path to a leaf directory.
* @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories.
* @returns {ConfigArray} The loaded config.
* @private
*/
_loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) {
// 省略代码
}
/**
* Finalize a given config array.
* Concatenate `--config` and other CLI options.
* @param {ConfigArray} configArray The parent config array.
* @param {string} directoryPath The path to the leaf directory to find config files.
* @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`.
* @returns {ConfigArray} The loaded config.
* @private
*/
_finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) {
// 省略代码
}
}
CascadingConfigArrayFactory 入口的作用是以下几点:
- 实例化 configArrayFactory ,通过一个 WeakMap 缓存一些配置和上下文信息
- 封装 getConfigArrayForFile 方法,根据传入的文件路径构造一个 configArrayFactory 实例,而构造 configArrayFactory 需要的配置两份关键配置,一份是用户配置,一份是 cli 参数传入的配置
- 用户指定配置文件路径和不指定的处理方式,会有一些区别,也就是我们使用
eslint . --config .eslintrc.json
是否指定了--config
参数的区别,指定了则通过私有方法_finalizeConfigArray
对配置再进行一次整合
在上面的逻辑中我们需要重点关注下 createBaseConfigArray
和 createCLIConfigArray
函数,这两个函数封装在 CascadingConfigArrayFactory 类之外的代码中:
js
/**
* Create the config array from `baseConfig` and `rulePaths`.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createBaseConfigArray({
configArrayFactory,
baseConfigData,
rulePaths,
cwd,
loadRules
}) {
const baseConfigArray = configArrayFactory.create(
baseConfigData,
{ name: "BaseConfig" }
);
/*
* Create the config array element for the default ignore patterns.
* This element has `ignorePattern` property that ignores the default
* patterns in the current working directory.
*/
baseConfigArray.unshift(configArrayFactory.create(
{ ignorePatterns: IgnorePattern.DefaultPatterns },
{ name: "DefaultIgnorePattern" }
)[0]);
/*
* Load rules `--rulesdir` option as a pseudo plugin.
* Use a pseudo plugin to define rules of `--rulesdir`, so we can validate
* the rule's options with only information in the config array.
*/
if (rulePaths && rulePaths.length > 0) {
baseConfigArray.push({
type: "config",
name: "--rulesdir",
filePath: "",
plugins: {
"": new ConfigDependency({
definition: {
rules: rulePaths.reduce(
(map, rulesPath) => Object.assign(
map,
loadRules(rulesPath, cwd)
),
{}
)
},
filePath: "",
id: "",
importerName: "--rulesdir",
importerPath: ""
})
}
});
}
return baseConfigArray;
}
/**
* Create the config array from CLI options.
* @param {CascadingConfigArrayFactoryInternalSlots} slots The slots.
* @returns {ConfigArray} The config array of the base configs.
*/
function createCLIConfigArray({
cliConfigData,
configArrayFactory,
cwd,
ignorePath,
specificConfigPath
}) {
const cliConfigArray = configArrayFactory.create(
cliConfigData,
{ name: "CLIOptions" }
);
cliConfigArray.unshift(
...(ignorePath
? configArrayFactory.loadESLintIgnore(ignorePath)
: configArrayFactory.loadDefaultESLintIgnore())
);
if (specificConfigPath) {
cliConfigArray.unshift(
...configArrayFactory.loadFile(
specificConfigPath,
{ name: "--config", basePath: cwd }
)
);
}
return cliConfigArray;
}
createBaseConfigArray 主要是通过 configArrayFactory.create 创建了一份 baseConfigArray,并且往里面增加了 rulesdir
相关的配置,看注释是为了之后校验用的,下面看到这部分源码我们再展开。
createCLIConfigArray 同样背后也是依赖 configArrayFactory.create 创建一份配置,并且根据是否传入 --config
合并配置。
到这里,我们能从源码掌握的信息还是不多,所以我们需要继续下钻到 configArrayFactory 的源码。
config-array-factory
先直接上源码:
js
// config-array-factory.js
const debug = debugOrig("eslintrc:config-array-factory");
/**
* The factory of `ConfigArray` objects.
*/
class ConfigArrayFactory {
/**
* Initialize this instance.
* @param {ConfigArrayFactoryOptions} [options] The map for additional plugins.
*/
constructor({
additionalPluginPool = new Map(),
cwd = process.cwd(),
resolvePluginsRelativeTo,
builtInRules,
resolver = ModuleResolver,
eslintAllPath,
getEslintAllConfig,
eslintRecommendedPath,
getEslintRecommendedConfig
} = {}) {
internalSlotsMap.set(this, {
additionalPluginPool,
cwd,
resolvePluginsRelativeTo:
resolvePluginsRelativeTo &&
path.resolve(cwd, resolvePluginsRelativeTo),
builtInRules,
resolver,
eslintAllPath,
getEslintAllConfig,
eslintRecommendedPath,
getEslintRecommendedConfig
});
}
/**
* Create `ConfigArray` instance from a config data.
* @param {ConfigData|null} configData The config data to create.
* @param {Object} [options] The options.
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
* @param {string} [options.filePath] The path to this config data.
* @param {string} [options.name] The config name.
* @returns {ConfigArray} Loaded config.
*/
create(configData, { basePath, filePath, name } = {}) {
if (!configData) {
return new ConfigArray();
}
const slots = internalSlotsMap.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);
const elements = this._normalizeConfigData(configData, ctx);
return new ConfigArray(...elements);
}
/**
* Load a config file.
* @param {string} filePath The path to a config file.
* @param {Object} [options] The options.
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
* @param {string} [options.name] The config name.
* @returns {ConfigArray} Loaded config.
*/
loadFile(filePath, { basePath, name } = {}) {
const slots = internalSlotsMap.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);
return new ConfigArray(...this._loadConfigData(ctx));
}
/**
* Load `.eslintignore` file.
* @param {string} filePath The path to a `.eslintignore` file to load.
* @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist.
*/
loadESLintIgnore(filePath) {
// 省略代码
}
/**
* Load a given config file.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} Loaded config.
* @private
*/
_loadConfigData(ctx) {
return this._normalizeConfigData(loadConfigFile(ctx.filePath), ctx);
}
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_normalizeConfigData(configData, ctx) {
const validator = new ConfigValidator();
validator.validateConfigSchema(configData, ctx.name || ctx.filePath);
return this._normalizeObjectConfigData(configData, ctx);
}
/**
* Normalize a given config to an array.
* @param {ConfigData|OverrideConfigData} configData The config data to normalize.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigData(configData, ctx) {
// 省略代码
}
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigDataBody(
{
// 省略代码
},
ctx
) {
// 省略代码
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtends(extendName, ctx) {
// 省略代码
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedBuiltInConfig(extendName, ctx) {
// 省略代码
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedPluginConfig(extendName, ctx) {
// 省略代码
}
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedShareableConfig(extendName, ctx) {
// 省略代码
}
/**
* Load given plugins.
* @param {string[]} names The plugin names to load.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {Record<string,DependentPlugin>} The loaded parser.
* @private
*/
_loadPlugins(names, ctx) {
// 省略代码
}
/**
* Load a given parser.
* @param {string} nameOrPath The package name or the path to a parser file.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {DependentParser} The loaded parser.
*/
_loadParser(nameOrPath, ctx) {
// 省略代码
}
/**
* Load a given plugin.
* @param {string} name The plugin name to load.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {DependentPlugin} The loaded plugin.
* @private
*/
_loadPlugin(name, ctx) {
// 省略代码
}
}
面对源码相对来说比较多的文件,其包含源码行数约 1000 多一点 ,在阅读时,我们不着急去关注细节,试图看懂所有的 API 实现,第一步我们需要理清楚它们之间的调用逻辑。然后再进行分析,我们可以发现,看起来该类实现的私有 API 比较多,但是基本分为三类:
- 第一类,_loadXXX,plugin、parser、config 等的加载处理,大同小异
- 第二类,_normalizeConfigXXX 等,主要是根据不同的配置参数和方式,然后调用上述的 _loadXXX API 加载模块
- 其它,多数是非核心逻辑封装,一些特殊和边界场景的处理,可以先不关注
分完类,理清主要矛盾,我们发现它的逻辑基本清晰了,我们只需要看懂三类 API 的一两个例子具体实现,其它的就手到擒来了。
回到正题,我们从 create 入口出发,先理清调用链,也就是 CascadingConfigArrayFactory 对 configArrayFactory.create 的 API 调用。
看一个依赖调用关系图:
有了上面这个图,我们对 ConfigArrayFactory 实现的 API 之间调用逻辑关系就有一个非常清晰的认识。从图中调用关系结合源码我们可以知道:
- 从 create API 出发,中间经过一系列 API 调用,最后其实创建的是一个 ConfigArray 实例
- 各种 share config、plugins、parser 的加载最后创建的是 ConfigDepenency 实例
- 在 normalize 配置时,会通过 share 模块导出的 ConfigValidator 对传入的配置进行校验
- 加载 share config 也就是 _loadExtends 是相对复杂的,它是一个深度遍历递归加载的过程
有了清晰的依赖关系,我们开始深入到部分 API 的细节实现,值得关注的是以下几个 API:create、_normalizeObjectConfigData、_loadExtends 等。
首先看 create 方法:
js
/**
* Create `ConfigArray` instance from a config data.
* @param {ConfigData|null} configData The config data to create.
* @param {Object} [options] The options.
* @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`.
* @param {string} [options.filePath] The path to this config data.
* @param {string} [options.name] The config name.
* @returns {ConfigArray} Loaded config.
*/
create(configData, { basePath, filePath, name } = {}) {
if (!configData) {
return new ConfigArray();
}
const slots = internalSlotsMap.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);
const elements = this._normalizeConfigData(configData, ctx);
return new ConfigArray(...elements);
}
它是整个配置创建的起点,逻辑比较简单,通过传入的配置,调用 _normalizeConfigData
进行配置处理和加载,最后创建 ConfigArray 实例。
_normalizeConfigData 经过层层调用最后核心是执行 _normalizeObjectConfigData 方法,当然 _normalizeConfigData 本身还包含了参数校验的逻辑,这部分比较容易理解,所以这里直接略过,感兴趣小伙伴可以自行去看。下面直接看 _normalizeObjectConfigData 的实现:
js
/**
* Normalize a given config to an array.
* @param {ConfigData} configData The config data to normalize.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigDataBody(
{
env,
extends: extend,
globals,
ignorePatterns,
noInlineConfig,
parser: parserName,
parserOptions,
plugins: pluginList,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings,
overrides: overrideList = []
},
ctx
) {
const extendList = Array.isArray(extend) ? extend : [extend];
const ignorePattern = ignorePatterns && new IgnorePattern(
Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
ctx.matchBasePath
);
// Flatten `extends`.
for (const extendName of extendList.filter(Boolean)) {
yield* this._loadExtends(extendName, ctx);
}
// Load parser & plugins.
const parser = parserName && this._loadParser(parserName, ctx);
const plugins = pluginList && this._loadPlugins(pluginList, ctx);
// Yield pseudo config data for file extension processors.
if (plugins) {
yield* this._takeFileExtensionProcessors(plugins, ctx);
}
// Yield the config data except `extends` and `overrides`.
yield {
// Debug information.
type: ctx.type,
name: ctx.name,
filePath: ctx.filePath,
// Config data.
criteria: null,
env,
globals,
ignorePattern,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};
// Flatten `overries`.
for (let i = 0; i < overrideList.length; ++i) {
yield* this._normalizeObjectConfigData(
overrideList[i],
{ ...ctx, name: `${ctx.name}#overrides[${i}]` }
);
}
}
该函数是一个迭代器函数,因为像 plugin、parser、share config 的加载过程涉及到一些异步逻辑,因此构造配置设计成一个迭代器函数,能更好地控制整个配置处理流程。
逻辑不算复杂,主要是根据配置中的配置项,依次加载 extends、plugin、parser 等,但需要注意一个细节,extends 加载是最前面的,并且在加载完所有的 extends 配置,才加载 plugin 和 parser,这个地方下面我们单独通过例子详细讲。
最后,如果在项目中配置了 overrides
,需要递归地进行配置拍平。这个配置在一般情况下用的比较少,使用场景为我们需要为不同的文件类型配置不同的插件、规则或者 parser ,例如下面这个例子:
json
{
"extends": "eslint:recommended",
"rules": {
"no-undef": "error"
},
"env": {
"es6": true,
"browser": true
},
"parserOptions": {
"ecmaVersion": 10
},
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-member-access": "off"
},
"parserOptions": {
"project": "tsconfig.json"
}
},
{
"files": [
"*.js",
"*.ts"
],
"rules": {
"no-unused-vars": "off",
"max-classes-per-file": "off"
}
}
]
}
至于这里的配置合并策略,我会在 config-array 详细介绍。
最后是 _loadExtends:
js
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtends(extendName, ctx) {
debug("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
try {
if (extendName.startsWith("eslint:")) {
return this._loadExtendedBuiltInConfig(extendName, ctx);
}
if (extendName.startsWith("plugin:")) {
return this._loadExtendedPluginConfig(extendName, ctx);
}
return this._loadExtendedShareableConfig(extendName, ctx);
} catch (error) {
error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
throw error;
}
}
这里根据配置的方式,会有几种加载策略,比如 eslint:xxx
表示,你配置的是 ESLint 内置的 share config,则通过 _loadExtendedBuiltInConfig 处理这部分加载逻辑。plugin:xxx
表示配置是一个插件类型的 share config ,前面有提到过,所以 _loadExtendedPluginConfig 内部本质核心逻辑调用的是 _loadPlugin API。
而 _loadPlugin 的实现比较简单,其实类似于 _loadExtendedShareableConfig ,只是插件的加载不存在递归的情况,因此看完 _loadExtendedShareableConfig 实现, 其它的 _loadXXX 逻辑就基本知道了。
我们重点看 _loadExtendedShareableConfig 的实现:
js
/**
* Load configs of an element in `extends`.
* @param {string} extendName The name of a base config.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
_loadExtendedShareableConfig(extendName, ctx) {
const { cwd, resolver } = internalSlotsMap.get(this);
const relativeTo = ctx.filePath || path.join(cwd, "__placeholder__.js");
let request;
if (isFilePath(extendName)) {
request = extendName;
} else if (extendName.startsWith(".")) {
request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior.
} else {
request = naming.normalizePackageName(
extendName,
"eslint-config"
);
}
let filePath;
try {
filePath = resolver.resolve(request, relativeTo);
} catch (error) {
/* istanbul ignore else */
if (error && error.code === "MODULE_NOT_FOUND") {
throw configInvalidError(extendName, ctx.filePath, "extend-config-missing");
}
throw error;
}
writeDebugLogForLoading(request, relativeTo, filePath);
return this._loadConfigData({
...ctx,
filePath,
name: `${ctx.name} >> ${request}`
});
}
通过 config 的名字构造 request,再通过 resolver.resolve 加载模块,最后回到 _loadConfigData 递归加载配置流程。
为什么需要递归加载? 这里光看代码有点抽象,我们看一个例子:
js
// eslint-config-share-1.js
module.export = {
plugins: ["@typescript-eslint"],
globals: {
MyGlobal: true
},
rules: {
semi: [2, "always"]
}
}
// eslint-config-share-2.js
module.export = {
extends: ["import"],
rules: {
"import/no-extraneous-dependencies": [
"error",
{"devDependencies": ["**/*.test.js", "**/*.spec.js"]}]
}
}
项目中使用两份配置:
js
module.export = {
extends: ["share1", "share2"],
// 其它配置等
}
从这个例子出发,我们发现从项目的配置出发,通过 extends 我们可以不断套娃配置,嵌套的配置里面还是可以使用 extends、plugins 等配置项,因此加载 share config 是一个递归的过程。
为什么要深度遍历?这里涉及到一个配置合并策略的问题:
- 例如有两份 share config 同时配置了一个规则的不同选项,它们的值出现冲突时,哪个优先?
- share config 配置项跟项目级别的配置项冲突,哪个优先?
这里的深度遍历构造配置项就是为了后续配置合并做准备,而这部分的配置合并策略是在 config-array 中实现,所以要搞清楚,需要去看 config-array 的源码。
config-array
首先看 ConfigArray 的实现:
js
// config-array.js
/**
* The Config Array.
*
* `ConfigArray` instance contains all settings, parsers, and plugins.
* You need to call `ConfigArray#extractConfig(filePath)` method in order to
* extract, merge and get only the config data which is related to an arbitrary
* file.
* @extends {Array<ConfigArrayElement>}
*/
class ConfigArray extends Array {
/**
* Get the plugin environments.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Environment>} The plugin environments.
*/
get pluginEnvironments() {
return ensurePluginMemberMaps(this).envMap;
}
/**
* Get the plugin processors.
* The returned map cannot be mutated.
* @type {ReadonlyMap<string, Processor>} The plugin processors.
*/
get pluginProcessors() {
return ensurePluginMemberMaps(this).processorMap;
}
/**
* Get the plugin rules.
* The returned map cannot be mutated.
* @returns {ReadonlyMap<string, Rule>} The plugin rules.
*/
get pluginRules() {
return ensurePluginMemberMaps(this).ruleMap;
}
/**
* Extract the config data which is related to a given file.
* @param {string} filePath The absolute path to the target file.
* @returns {ExtractedConfig} The extracted config data.
*/
extractConfig(filePath) {
const { cache } = internalSlotsMap.get(this);
const indices = getMatchedIndices(this, filePath);
const cacheKey = indices.join(",");
if (!cache.has(cacheKey)) {
cache.set(cacheKey, createConfig(this, indices));
}
return cache.get(cacheKey);
}
}
移除了两个非核心逻辑的函数实现代码,看起来逻辑非常简单。到这里,我们终于大概能知道前面一系列的类都是 config-array-xxx 命名,因为 ConfigArray 本身是从 Array 继承出来的类,至于为什么需要设计成数组,我们需要再继续往下看。
ConfigArray 最核心的实现是 extractConfig API,根据注释,我们知道这个 API 的作用就是导出最终的 ESLint 配置数据。在这个 API 中,调用了两个关键的函数:getMatchedIndices 和 createConfig, 这两个函数封装在 ConfigArray 类之外,也是在 config-array.js 文件中。
getMatchedIndices 返回的 indices 除了作为配置缓存的 key,也是 createConfig 接收的参数之一。我们先看其实现:
js
/**
* Get the indices which are matched to a given file.
* @param {ConfigArrayElement[]} elements The elements.
* @param {string} filePath The path to a target file.
* @returns {number[]} The indices.
*/
function getMatchedIndices(elements, filePath) {
const indices = [];
for (let i = elements.length - 1; i >= 0; --i) {
const element = elements[i];
if (!element.criteria || (filePath && element.criteria.test(filePath))) {
indices.push(i);
}
}
return indices;
}
代码实现逻辑不复杂,但是我们要完全看懂这段逻辑需要回顾下前面的源码,才能一步步揭开其神秘面纱。
首先是 elements
参数,它调用的时候传递的就是 ConfigArray 实例本身,前面我们读 ConfigArrayFactory 相关源码时知道 ConfigArrayFactory.create 创建的 ConfigArray 实例传入的是一系列配置对象,也就是 _normalizeObjectConfigDataBody 最后返回的结果:
scala
_normalizeObjectConfigDataBody () {
// 省略其它代码
// Yield the config data except `extends` and `overrides`.
yield {
// Debug information.
type: ctx.type,
name: ctx.name,
filePath: ctx.filePath,
// Config data.
criteria: null,
env,
globals,
ignorePattern,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};
}
简单来说,这个 elements 数组存的是一个个相对完整的、处理后的 ESLint 配置对象,因为大多数场景下,当一个项目配置了 share config 或者 overrides
配置项,这个时候该数组是有多个数据项的。简单解释,一个 share config 是一个 ConfigArray 的数据项,项目本身的配置也是一个配置项;如果配置了 overrides ,那这个数组的每一项也是一个数据项。
那这里的 element.criteria
又是什么?继续回溯源码:
js
// config-array-factory.js
/**
* Normalize a given config to an array.
* @param {ConfigData|OverrideConfigData} configData The config data to normalize.
* @param {ConfigArrayFactoryLoadingContext} ctx The loading context.
* @returns {IterableIterator<ConfigArrayElement>} The normalized config.
* @private
*/
*_normalizeObjectConfigData(configData, ctx) {
const { files, excludedFiles, ...configBody } = configData;
const criteria = OverrideTester.create(
files,
excludedFiles,
ctx.matchBasePath
);
const elements = this._normalizeObjectConfigDataBody(configBody, ctx);
// Apply the criteria to every element.
for (const element of elements) {
/*
* Merge the criteria.
* This is for the `overrides` entries that came from the
* configurations of `overrides[].extends`.
*/
element.criteria = OverrideTester.and(criteria, element.criteria);
/*
* Remove `root` property to ignore `root` settings which came from
* `extends` in `overrides`.
*/
if (element.criteria) {
element.root = void 0;
}
yield element;
}
}
发现原来它是在 _normalizeObjectConfigData API 中加上的,我们需要看下 OverrideTester 在做什么,看注释应该是专门为了 overrides 配置场景的。因为是 overrides 是相对来说不常见的配置,这里就不详细解读代码,感兴趣的小伙伴可以自行去看,它封装在 config-array/override-tester.js 中。
直接说明其用途:其实就是当我们配置了 overrides,我们会配置这些插件或者规则时 for 哪些文件类型的,而 criteria 就是在导出配置时去匹配文件,匹配到的文件才会存下这份配置的索引,最后在 lint 时消费到这份配置里面的选项。
到这里我们就搞清了 getMatchedIndices 的逻辑,下面继续看 createConfig:
js
/**
* Create the extracted config.
* @param {ConfigArray} instance The config elements.
* @param {number[]} indices The indices to use.
* @returns {ExtractedConfig} The extracted config.
*/
function createConfig(instance, indices) {
const config = new ExtractedConfig();
const ignorePatterns = [];
// Merge elements.
for (const index of indices) {
const element = instance[index];
// Adopt the parser which was found at first.
if (!config.parser && element.parser) {
if (element.parser.error) {
throw element.parser.error;
}
config.parser = element.parser;
}
// Adopt the processor which was found at first.
if (!config.processor && element.processor) {
config.processor = element.processor;
}
// Adopt the noInlineConfig which was found at first.
if (config.noInlineConfig === void 0 && element.noInlineConfig !== void 0) {
config.noInlineConfig = element.noInlineConfig;
config.configNameOfNoInlineConfig = element.name;
}
// Adopt the reportUnusedDisableDirectives which was found at first.
if (config.reportUnusedDisableDirectives === void 0 && element.reportUnusedDisableDirectives !== void 0) {
config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives;
}
// Collect ignorePatterns
if (element.ignorePattern) {
ignorePatterns.push(element.ignorePattern);
}
// Merge others.
mergeWithoutOverwrite(config.env, element.env);
mergeWithoutOverwrite(config.globals, element.globals);
mergeWithoutOverwrite(config.parserOptions, element.parserOptions);
mergeWithoutOverwrite(config.settings, element.settings);
mergePlugins(config.plugins, element.plugins);
mergeRuleConfigs(config.rules, element.rules);
}
// Create the predicate function for ignore patterns.
if (ignorePatterns.length > 0) {
config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse());
}
return config;
}
这个函数实现了配置合并的核心策略,除了前面 parser、processor、ignorePatterns 等配置比较简单的处理策略,主要有三类相对复杂的合并策略:
- mergeWithoutOverwrite,主要是处理 env、globals、parserOptions、settings 等对象类型配置的合并
- mergePlugins,插件配置合并
- mergePlugins,规则配置合并
看到这里,不知道大家有没有留意到一个问题,通过前面的源码我们了解到加载配置数组的方式,是深度遍历构造出来的,也就是说越底层的配置会在 elements 配置数组最前面,那这个合并策略看起来就是外部项目的配置优先级比里层 share config 、overrides 配置低,这看起来不太符合正常的设计,那用户怎么覆盖 share config 配置?
其实这里有个隐藏彩蛋,在 getMatchedIndices 中,其实在获取索引时,是倒序遍历:
js
function getMatchedIndices(elements, filePath) {
const indices = [];
for (let i = elements.length - 1; i >= 0; --i) {
// 省略代码
}
return indices;
}
因此这里的配置顺序就正好一来一回颠倒过来了,项目级别配置项优先级应该是最高的,而 ESLintRC 文件外层的配置也大于 overrides
配置。 而在合并配置时,使用倒序的索引数组得到的配置项进行配置合并。
下面我们来一一看三个合并参数的策略,首先是 mergeWithoutOverwrite:
js
/**
* Merge two objects.
*
* Assign every property values of `y` to `x` if `x` doesn't have the property.
* If `x`'s property value is an object, it does recursive.
* @param {Object} target The destination to merge
* @param {Object|undefined} source The source to merge.
* @returns {void}
*/
function mergeWithoutOverwrite(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
if (isNonNullObject(target[key])) {
mergeWithoutOverwrite(target[key], source[key]);
} else if (target[key] === void 0) {
if (isNonNullObject(source[key])) {
target[key] = Array.isArray(source[key]) ? [] : {};
mergeWithoutOverwrite(target[key], source[key]);
} else if (source[key] !== void 0) {
target[key] = source[key];
}
}
}
}
递归的对类型为对象类型的配置进行合并,只要最外层已经配置过的属性,内层的配置都会被忽略,项目级别配置项优先级始终是最高的。
mergePlugins 的合并逻辑也比较简单,不用考虑参数合并问题,合并时会检查是否出现配置相同插件而加载路径不一致的情况,也就是在上一篇文章中提到的错误之一:ESLint couldn't determine the plugin "XXX" uniquely,就是在这里抛出的错误:
js
/**
* Merge plugins.
* `target`'s definition is prior to `source`'s.
* @param {Record<string, DependentPlugin>} target The destination to merge
* @param {Record<string, DependentPlugin>|undefined} source The source to merge.
* @returns {void}
*/
function mergePlugins(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetValue = target[key];
const sourceValue = source[key];
// Adopt the plugin which was found at first.
if (targetValue === void 0) {
if (sourceValue.error) {
throw sourceValue.error;
}
target[key] = sourceValue;
} else if (sourceValue.filePath !== targetValue.filePath) {
throw new PluginConflictError(key, [
{
filePath: targetValue.filePath,
importerName: targetValue.importerName
},
{
filePath: sourceValue.filePath,
importerName: sourceValue.importerName
}
]);
}
}
}
在合并插件之前,前面加载配置时已经将每个配置项的插件配置处理成 Record 形式,合并起来比较高效。
最后是规则合并策略 mergeRuleConfigs:
js
/**
* Merge rule configs.
* `target`'s definition is prior to `source`'s.
* @param {Record<string, Array>} target The destination to merge
* @param {Record<string, RuleConf>|undefined} source The source to merge.
* @returns {void}
*/
function mergeRuleConfigs(target, source) {
if (!isNonNullObject(source)) {
return;
}
for (const key of Object.keys(source)) {
if (key === "__proto__") {
continue;
}
const targetDef = target[key];
const sourceDef = source[key];
// Adopt the rule config which was found at first.
if (targetDef === void 0) {
if (Array.isArray(sourceDef)) {
target[key] = [...sourceDef];
} else {
target[key] = [sourceDef];
}
/*
* If the first found rule config is severity only and the current rule
* config has options, merge the severity and the options.
*/
} else if (
targetDef.length === 1 &&
Array.isArray(sourceDef) &&
sourceDef.length >= 2
) {
targetDef.push(...sourceDef.slice(1));
}
}
}
我们知道规则的配置有两种选择,一种是数组,一种是告警级别的字符串,在经过处理后,最后都会是数组。当配置项是数组时,第一个项是告警级别,最外层的配置永远是优先级更高的 ;第二个项是规则的选项,一般是一个对象,合并策略比较粗暴,最外层的配置对象直接覆盖里层的配置,没有对象的深度合并。
看一个例子:
js
// eslint-config-share.js
module.export = {
plugin: ["@typescript-eslint"],
rules: {
"import/no-extraneous-dependencies": [
"error",
{"devDependencies": ["**/*.test.js", "**/*.spec.js"]}]
},
env: {
browser: true,
es6: true,
node: true,
},
}
// project A .eslintrc.js
module.export = {
extends: ["share"]
plugins: ["@typescript-eslint", "import"],
rules: {
"import/no-extraneous-dependencies": [
"error",
{"includeInternal": true, "includeTypes": true }]
},
env: {
browser: true,
es6: true,
node: false,
},
}
上面的配置经过合并后,在 ESLint 内部变成了:
json
{
"plugins": {
"@typescript-eslint": {
// 省略一些字段
},
"import": {
// 省略一些字段
}
},
"rules": {
"import/no-extraneous-dependencies": [
"error",
{"includeInternal": true, "includeTypes": true }]
},
"env": {
"browser": true,
"es6": true,
"node": false,
},
}
到这里,关于 ESLint 8.x 配置加载、配置合并等核心逻辑源码解读基本就结束了,当然这中间还有一些细节和小的功能模块没有一一解读。我个人读源码的方式,更多关注核心流程和个人感兴趣的部分,再选择性挑一些细节实现看,其它的细枝末节只要不影响我理解关键逻辑,一般后续遇到问题时时会倒过来再看。
这部分篇幅比较长,下面对一些关键信息进行小结。
小结
在看完 ESLint 配置加载和合并的源码,我个人始终觉得这部分的源码实现还是比较复杂的,一些细节不深入去看还是会出现一知半解的情况。
因此在 9.x 版本,ESLint 引入新的 Flat 配置系统 ,就是为了简化配置系统,让开发者更容易理解和使用,想了解更多细节的小伙伴可以去看关于 ESLint 新配置系统的 RFC:2019-config-simplification。
以下几个点是我看完这部分源码觉得需要重点关注的:
- CascadingConfigArrayFactory 作为 ESLint CliEngine 消费的实例,是整个配置加载模块的入口,它依赖了 ConfigArrayFactory
- ConfigArrayFactory 类实现了配置加载的功能,它从 create 出发,经过层层的 normalize 逻辑,最后返回了一个标准的配置对象数组,也就是 ConfigArray 实例;因为 extends 和 overrides 配置方式,所以需要递归地创建配置对象、加载配置,这里采用的是深度遍历的算法
- ConfigArray 本质是一个继承自 Array 的类,其核心的作用是合并多份配置为最终的配置,内部会根据不同的配置项和类型:对象、插件、规则等,实现了不同的合并算法;值得一提的是,为了保证配置优先级,通过 getMatchedIndices 函数倒序遍历配置数组,从而使得优先级高的项目配置在首位,最深层的配置在最后
- 在阅读源码过程中,可以发现代码实现的多个功能模块,当创建一些开销大的实例包括 lint 结果都使用了缓存,这是我们在设计复杂、代码执行开销大的架构场景时值得参考的地方
最后
读这部分源码除了让我增加对 ESLint 内部实现的了解,也让我从优秀社区工具中学到一些编码和设计范式。
后续再遇到 ESLint 配置问题,除了学到更多的 debug 姿势,因为对源码的了解,也能更好地排查问题。
除此之外,读这部分源码,也让我学到了一些好的编程范式,例如为了更好传递全局共享的上下文参数,我们可以构造一个 context 对象,方便传递,这也是《重构》一书中提到的最佳实践之一。
总是在设计的时候考虑缓存, 缓存能很大程度上帮我们提升工具运行的效率,避免无谓的 IO 开销。
Reference
技术之外
工作和代码之外,也需要生活。最近除了研究代码,也还在研究咖啡,尝试输出一些内容。所以,做了一个公众号,当然除了咖啡知识之外,也会分享一些读书笔记,对咖啡感兴趣,也爱读书的小伙伴可以关注下。