ESLint的新配置系统第二部分:扁平配置介绍

636 阅读17分钟

我的上一篇文章中,我谈到了eslintrc的配置系统是如何通过一系列小的、渐进的变化而变得比必要的更复杂。另一方面,扁平化的配置系统从一开始就被设计成在许多方面都比较简单。我们从过去六年的ESLint开发中吸取了所有的经验,想出了一个整体的配置方法,它吸收了eslintrc的优点,并与其他JavaScript相关工具处理配置的方式相结合。其结果是,希望现有的ESLint用户能感到熟悉,并且比以前的功能要强大得多。

flat config的目标

为了给flat config的变化创造条件,我们有几个目标:

  1. 逻辑默认--在过去的9年中,人们编写JavaScript的方式发生了很大的变化,我们希望新的配置系统能够反映我们当前的现实,而不是我们在ESLint刚发布时的生活。
  2. 一种方式来定义配置--我们不希望人们再有多种方式来做同样的事情。对于任何给定的项目,应该有一种方法来定义配置。
  3. 规则配置应该保持不变--我们觉得规则的配置方式已经很好了,所以为了让它更容易过渡到平面配置,我们不想对规则配置做任何改变。同样的rules ,可以在平面配置中以同样的方式使用。
  4. 所有东西都使用本地加载- 我们对eslintrc最大的遗憾之一是以自定义的方式重新创建Node.jsrequire 分辨率。这是一个重要的复杂性来源,而且事后看来是不必要的。展望未来,我们希望直接利用JavaScript运行时的加载能力。
  5. 更好地组织顶层键- 自从ESLint发布以来,eslintrc顶层键的数量急剧增加。我们需要看看哪些键是必要的,它们之间的关系如何。
  6. 现有的插件应该能够工作--ESLint的生态系统充满了数百个插件。这些插件继续工作是很重要的。
  7. 向后兼容应该是一个优先事项--即使我们正在转移到一个新的配置系统,我们也不想把所有现有的生态系统留在后面。特别是,我们希望有办法让可共享的配置继续尽可能地工作。虽然我们知道100%的兼容性可能是不现实的,但我们想尽最大努力确保现有的可共享配置能够正常工作。

考虑到这些目标,我们想出了新的扁平化配置系统。

为linting设置逻辑默认值

当ESLint刚创建时,ECMAScript 5是JavaScript的最新版本,大多数文件都被写成 "共享一切 "的脚本或CommonJS模块(针对Node.js)。ECMAScript 6即将到来,但没有人知道它的实施速度会有多快,也没有人知道模块(ESM)最终会被如何使用。所以ESLint的默认值是假设所有文件都是ECMAScript 5。我们最终采用了ecmaVersion 解析器的配置,允许人们在准备好后选择加入ECMAScript 6。

快进到2022年:ECMAScript在不断发展,ESM是大家都在使用的标准模块格式。我们无法改变eslintrc的默认设置,否则可能会破坏很多现有的配置,但我们绝对可以用flat config进行改变。

Flat config具有以下默认值:

  • ecmaVersion: "latest" 为所有的JavaScript文件 - 没错,默认情况下,所有的JavaScript文件将被设置为最新版本的ECMAScript。这模仿了JavaScript运行时的工作方式,即每一次升级都意味着你选择了最新、最好的JavaScript版本。这一变化应该意味着你可能不必在配置中手动设置 ,除非你想由于运行时间的限制而强制使用以前的版本。如果有必要,你仍然可以将 一直设置到 。ecmaVersion ecmaVersion 3
  • sourceType: "module" 为所有 和 文件 - 默认情况下,flat config 假设你正在编写ESM。如果不是,你可以随时将 设为 。.js .mjs sourceType "script"
  • sourceType: "commonjs" 对于 文件 - 我们仍然处于过渡期,很多Node.js代码是用CommonJS编写的。为了支持这些用户,我们添加了一个新的 ,为该环境正确配置了一切。.cjs sourceType "commonjs"
  • ESLint搜索.js.mjs.cjs 文件--在eslintrc中,ESLint只在你在命令行中传递目录名时搜索.js 文件,而你需要使用--ext 标志来定义更多的文件。有了flat config,所有三个最常见的JavaScript文件名扩展名都会被自动搜索到。

我们对这些新的默认值感到非常兴奋,因为我们认为这将帮助人们更快地加入ESLint,并减少混乱。

新的配置文件:eslint.config.js

与eslintrc相比,它允许在多个位置有多个配置文件,多个配置文件格式,甚至是基于package.json ,flat config只有一个位置,用于你项目的所有配置:eslint.config.js 。通过将配置限制在一个位置和一种格式,我们可以直接利用JavaScript运行时的加载机制,并避免对配置文件进行自定义解析的需要。

当使用ESLint CLI时,它从当前工作目录中搜索eslint.config.js ,如果没有找到,将继续在目录的祖先上搜索,直到找到该文件或击中根目录。这一个eslint.config.js 文件包含了ESLint运行的所有配置信息,所以与eslintrc相比,它极大地减少了所需的磁盘访问,eslintrc必须检查从linted文件位置到根目录的每一个目录,以找到任何额外的配置文件。

此外,使用一个JavaScript文件允许我们依靠用户来加载他们的配置文件可能需要的额外信息。与其说是extendsplugins 按名称加载东西,现在你只需在必要时使用importrequire 来引入这些附加资源。下面是一个关于eslint.config.js 文件的例子:

export default [
    {
        files: ["**/*.js"],
        rules: {
            "semi": "error",
            "no-unused-vars": "error"
        }  
    }
];

一个eslint.config.js 文件导出一个配置对象的数组。继续阅读以了解更多关于这个例子的信息。

基于全局的配置无处不在

虽然eslintrc中的overrides 键是很多复杂问题的来源,但有一点非常清楚:人们非常喜欢在他们的配置文件中通过glob模式来定义配置。因为我们想消除eslintrc的配置级联,我们必须使用glob模式来实现相同类型的配置覆盖。我们使用overrides configs 作为 flat config 的基础。

每个 config 对象都可以有可选的filesignores 键,指定基于最小匹配的 glob 模式来匹配文件。一个配置对象只适用于一个文件,如果该文件名与files 中的模式相匹配(或者没有files 键,在这种情况下,它将匹配所有文件)。ignores 键从files 列表中过滤掉文件,因此你可以限制配置对象适用于哪些文件。例如,也许你的测试文件和你的源文件生活在同一个目录下,你希望一个配置对象只适用于源文件。你可以像这样做:

export default [
    {
        files: ["**/*.js"],
        ignores: ["**/*.test.js"],
        rules: {
            "semi": "error",
            "no-unused-vars": "error"
        }  
    }
];

这里,配置对象将匹配所有的JavaScript文件,然后过滤掉任何以.test.js 结尾的文件。

如果你想完全忽略文件呢?你可以通过指定一个只有一个ignores 关键的配置对象来做到这一点,像这样:

export default [
    {
        ignores: ["**/*.test.js"]
    },
    {
        files: ["**/*.js"],
        rules: {
            "semi": "error",
            "no-unused-vars": "error"
        }  
    }
]

有了这个配置,所有以.test.js 结尾的JavaScript文件将被忽略。你可以认为这相当于eslintrc中的ignorePatterns ,尽管有最小匹配模式。

再见,extends ,你好,扁平级联

虽然我们想摆脱基于目录的配置级联,但flat config实际上仍然有一个直接在你的eslint.config.js 文件中定义的扁平级联。在数组中,ESLint找到所有与被提示的文件相匹配的配置对象,并以与eslintrc相同的方式将它们合并在一起。唯一真正的区别是,合并是从数组的顶部到底部进行的,而不是使用目录结构中的文件。比如说:

export default [
    {
        files: ["**/*.js", "**/*.cjs"],
        rules: {
            "semi": "error",
            "no-unused-vars": "error"
        }  
    },
    {
        files: ["**/*.js"],
        rules: {
            "no-undef": "error",
            "semi": "warn"
        }  
    }
];

这个配置有两个配置对象,有重叠的files 模式。第一个配置对象适用于所有.js.cjs 文件,而第二个对象只适用于.js 文件。当对一个以.js 结尾的文件进行检查时,ESLint结合这两个配置对象来创建该文件的最终配置。因为第二个配置将semi 设置为"warn" 的严重性,它优先于第一个配置中设置的"error" 。当有冲突时,最后匹配的配置总是获胜。

这对可共享的配置意味着你可以直接把它们插入数组,而不是使用extends ,例如:

import customConfig from "eslint-config-custom";

export default [
    customConfig,
    {
        files: ["**/*.js", "**/*.cjs"],
        rules: {
            "semi": "error",
            "no-unused-vars": "error"
        }  
    },
    {
        files: ["**/*.js"],
        rules: {
            "no-undef": "error",
            "semi": "warn"
        }  
    }
];

这里,customConfig 被首先插入数组中,这样它就成为这个文件的配置基础。下面的每个配置对象都建立在这个基础上,为一个给定的JavaScript文件创建最终的配置。

重新设计的语言选项

ESLint一直有一个奇怪的选项组合,影响着JavaScript的解释方式。有一个顶级的globals 键,可以修改可用的全局变量,还有ecmaVersionsourceType 作为parserOptions ,更不用说env 来增加更多的全局变量。也许最令人困惑的是,你必须同时设置ecmaVersion 和添加像es6 这样的环境,以启用你想要的语法并确保正确的全局变量可用。

在flat config中,我们将所有与JavaScript评估有关的键移到一个新的顶级键中,称为languageOptions

在flat config中设置ecmaVersion

最大的变化是,我们把ecmaVersionparserOptions 中移出,直接移到languageOptions 中。这更好地反映了这个键的新行为,即根据指定的ECMAScript版本来启用语法和全局变量。比如说:

export default [
    {
        files: ["**/*.js"],
        languageOptions: {
            ecmaVersion: 6
        }
    }
];

这个配置将ecmaVersion 降级为6 。这样做可以确保所有的ES6语法和所有的ES6全局变量都可用。(任何使用的自定义解析器仍然会收到这个值ecmaVersion 。)

在平面配置中设置sourceType

接下来,我们把sourceType 移到languageOptions 。与ecmaVersion 相似,这个键不仅影响文件的解析方式,也影响ESLint对其范围结构的评估方式。我们为ESM保留了传统的"module" ,为脚本保留了"script" ,同时还增加了"commonjs" ,它让ESLint知道它应该把文件当作CommonJS来处理(这也使得CommonJS特有的globals得以实现)。如果你使用ecmaVersion: 3ecmaVersion: 5 ,一定要设置sourceType: script ,像这样:

export default [
    {
        files: ["**/*.js"],
        languageOptions: {
            ecmaVersion: 5,
            sourceType: "script"
        }
    }
];

再见环境,你好globals

eslintrc中的环境提供了一组已知的globals,并且一直是用户困惑的来源。它们需要保持更新(尤其是在browser ),而这种更新需要等待ESLint的发布。另外,我们把一些额外的功能挂在环境上,使其更容易与Node.js一起工作,最后,我们弄得一团糟。

对于平面配置,我们决定完全删除env 关键。为什么?因为它不再需要了。所有我们挂在环境上用于Node.js的自定义功能现在都被sourceType: "commonjs" ,所以剩下的就是让环境来管理全局变量。对于ESLint来说,在核心部分做这些是没有意义的,所以我们把这个责任交还给了你。

几年前,我们与Sindre Sorhus合作,创建了 globals包,它从ESLint中提取了所有的环境信息,这样它就可以被其他包使用。然后ESLint使用globals 作为其环境的来源。

有了flat config,你可以直接使用globals 包,随时更新它,以获得所有过去由环境提供的相同功能。例如,这里是你如何将浏览器球状物添加到你的配置中:

import globals from "globals";

export default [
    {
        files: ["**/*.js"],
        languageOptions: {
            globals: {
                ...globals.browser,
                myCustomGlobal: "readonly"
            }
        }
    }
];

languageOptions.globals 键的工作原理与eslintrc中的相同,只是现在,你可以使用JavaScript来动态地插入你想要的任何全局变量。

自定义分析器和分析器选项大多是相同的

parserparserOptions 键现在已经移到了languageOptions 键中,但它们的工作方式大多与eslintrc中相同,有两个具体的区别。

  1. 你现在可以直接在配置中插入解析器对象。
  2. 解析器现在可以和插件捆绑在一起,你可以为parser 指定一个字符串值,以使用来自插件的解析器。(在下一节有更多描述)。

下面是一个使用Babel ESLint解析器的例子:

import babelParser from "@babel/eslint-parser";

export default [
    {
        files: ["**/*.js", "**/*.mjs"],
        languageOptions: {
            parser: babelParser
        }
    }
];

这个配置确保Babel解析器,而不是默认的,将被用来解析所有以.js.mjs 结尾的文件。

你也可以通过使用parserOptions 键直接将选项传递给自定义解析器,这与eslintrc中的工作方式相同:

import babelParser from "@babel/eslint-parser";

export default [
    {
        files: ["**/*.js", "**/*.mjs"],
        languageOptions: {
            parser: babelParser,
            parserOptions: {
                requireConfigFile: false,
                babelOptions: {
                    babelrc: false,
                    configFile: false,
                    // your babel options
                    presets: ["@babel/preset-env"],
                }
            }
        }
    }
];

更加强大和可配置的插件

ESLint的优势在于个人和公司维护的插件生态系统,以定制他们的语义分析策略。因此,我们希望确保现有的插件能够继续工作而不需要修改,同时也允许插件做一些他们过去从来没有做过的事情。

表面上看,在flat config中使用一个插件与在eslintrc中使用一个插件非常相似。最大的区别是,eslintrc使用字符串,而flat configs使用对象。你不需要指定一个插件的名字,而是直接导入插件并将其放入plugins ,就像这个例子一样:

import jsdoc from "eslint-plugin-jsdoc";

export default [
    {
        files: ["**/*.js"],
        plugins: {
            jsdoc
        }
        rules: {
            "jsdoc/require-description": "error",
            "jsdoc/check-values": "error"
        }  
    }
];

这个配置使用 eslint-plugin-jsdoc插件,将其作为一个本地jsdoc 变量导入,然后将其插入到配置中的plugins 键中。之后,插件内的规则是使用jsdoc 命名空间来引用的。

注意:因为插件现在像其他的JavaScript模块一样被导入,所以不再严格执行插件包的名称。你不再需要把eslint-plugin- 作为你的包名的前缀......但我们希望你能这样做。

个性化的插件命名空间

由于你的配置中的插件名称现在与插件包的名称脱钩,你可以选择任何你想要的名称,就像在这个例子中:

import jsdoc from "eslint-plugin-jsdoc";

export default [
    {
        files: ["**/*.js"],
        plugins: {
            jsd: jsdoc
        }
        rules: {
            "jsd/require-description": "error",
            "jsd/check-values": "error"
        }  
    }
];

这里,插件在配置中被命名为jsd ,所以规则也使用jsd ,以表明它们来自哪个插件。

--rulesdir 到运行时插件

在eslintrc中,规则需要由CLI直接加载,以便在配置文件中可用。这意味着要么在一个插件中捆绑自定义规则,要么使用--rulesdir 标志来指定ESLint应该从哪个目录加载自定义规则。这两种方法都需要一些额外的工作来设置,并且经常让我们的用户感到沮丧。

有了平面配置,你可以直接在配置文件中加载自定义规则。因为插件现在是直接在配置中的对象,你可以很容易地创建只存在于你的配置文件中的运行时插件,比如说:

import myrule from "./custom-rules/myrule.js";

export default [
    {
        files: ["**/*.js"],
        plugins: {
            custom: {
                rules: {
                    myrule
                }
            }
        }
        rules: {
            "custom/myrule": "error"
        }  
    }
];

这里,一个自定义规则被导入为myrule ,然后创建了一个名为custom 的运行时插件,将该规则作为custom/myrule 提供给配置。

因此,一旦完成向平面配置的过渡,我们将删除--rulesdir

插件中的自定义分析器

ESLint开发过程中的一个奇怪的工件是,解析器从来都不是插件的一部分。这是因为自定义解析器早在插件出现之前就已经存在了,而且我们从来没有回去让这两者很好地一起工作。在扁平化配置中,我们借机解决了这个问题,允许插件像暴露处理器和规则一样暴露解析器。例如,你现在可以定义一个看起来像这样的插件:

export default {
    parsers: {
        parserName: {
            parse() { /*... */ }
        }
    }
}

这个插件在parsers 关键下暴露了一个叫做parserName 的分析器。然后你可以在你的配置中使用这个分析器,像这样:

import custom from "./custom-plugin.js";

export default [
    {
        files: ["**/*.js"],
        plugins: {
            custom
        },
        languageOptions: {
            parser: "custom/parserName"
        }
    }
];

这个配置创建了一个名为custom 的插件命名空间。然后可以使用字符串"custom/parserName" 来访问这个自定义的分析器。

处理程序的工作方式与eslintrc类似

processor 顶层键的工作方式与eslintrc中基本相同,主要的使用情况是使用一个在插件中定义的处理器,比如说:

import markdown from "eslint-plugin-markdown";

export default [
    {
        files: ["**/*.md"],
        plugins: {
            markdown
        },
        processor: "markdown/markdown"
    }
];

这个配置对象指定在名为"markdown" 的插件中包含一个名为"markdown" 的处理器,并将该处理器应用于所有以.md 结尾的文件。

flat config中的一个新增内容是:processor 现在也可以是一个同时包含preprocess()postprocess() 方法的对象。

有组织的linter选项

在eslintrc中,有几个键直接与linter的操作方式相关,即noInlineConfigreportUnusedDisableDirectives 。这些键已经移到了新的linterOptions ,但工作方式与eslintrc中完全相同。这里有一个例子:

export default [
    {
        files: ["**/*.js"],
        linterOptions: {
            noInlineConfig: true,
            reportUnusedDisableDirectives: true
        }
    }
];

共享设置是完全一样的

顶层的settings key的行为方式与eslintrc中完全相同。你可以定义一个带有键值对的对象,这些键值对应该对所有的规则都可用。这里有一个例子:

export default [
    {
        settings: {
            sharedData: "Hello"
        }
    }
];

使用预定义的配置

ESLint有两个预定义的配置:

  • eslint:recommended - 启用ESLint推荐大家使用的规则以避免潜在的错误
  • eslint:all - 启用所有ESLint提供的规则。

为了包括这些预定义的配置,你可以把字符串值插入导出的数组中,然后在随后的配置对象中对其他属性进行任何修改:

export default [
    "eslint:recommended",
    {
        rules: {
            semi: ["warn", "always"]
        }
    }
];

这里,eslint:recommended 预定义的配置首先被应用,然后另一个配置对象为semi 添加所需的配置。

向后兼容工具

如前所述,我们觉得需要与eslintrc有很好的向后兼容性,以缓解过渡。该 @eslint/eslintrc包提供了一个FlatCompat 类,使得在一个平面配置文件中继续使用eslintrc风格的共享配置和设置变得容易。这里有一个例子:

import { FlatCompat } from "@eslint/eslintrc";

const compat = new FlatCompat({
    baseDirectory: __dirname
});

export default [

    // mimic ESLintRC-style extends
    compat.extends("standard", "example"),

    // mimic environments
    compat.env({
        es2020: true,
        node: true
    }),

    // mimic plugins
    compat.plugins("airbnb", "react"),

    // translate an entire config
    compat.config({
        plugins: ["airbnb", "react"],
        extends: "standard",
        env: {
            es2020: true,
            node: true
        },
        rules: {
            semi: "error"
        }
    })
];

使用FlatCompat 类允许你继续使用你现有的所有eslintrc文件,同时优化它们以便与flat config一起使用。我们设想这是一个必要的过渡步骤,允许生态系统慢慢地转换到flat config。

总结

团队花了很长时间来设计flat config,使它既能让现有的用户感到熟悉,又能提供新的功能,使每个人都受益。我们保持了规则、设置和处理器等内容的不变,同时将插件、语言选项和linter选项等内容扩展得更加统一。我们认为扁平化配置在这两极之间找到了一个很好的平衡,一旦新的配置系统普遍可用,你会更喜欢使用ESLint。同时,兼容性工具将允许你继续使用现有的共享配置。