ESLint's new config system - part two 直译

93 阅读17分钟

ESLint的新配置系统-2(平面配置简介)

写在最前面的话:本文是对 Nicholas C. Zakas (eslint 作者)关于elint新配置系统说明的直译,翻译未经校准,看不懂的地方请配合原文食用,谢谢!!!


ESLint 的新配置系统,绰号为“扁平配置”,其设计比原始配置系统更熟悉且简单得多。

在我的上一篇文章中,我讨论了 eslintrc 配置系统如何通过一系列小的增量更改变得比必要的更加复杂。另一方面,扁平配置系统从一开始就在很多方面被设计得更简单。我们吸取了前六年 ESLint 开发的所有经验教训,提出了一种整体的配置方法,该方法充分利用了 eslintrc 的优点,并将其与其他 JavaScript 相关工具处理配置的方式相结合。希望现有的 ESLint 用户会感到熟悉,并且比以前更强大。

文档:在官方文档中阅读有关平面配置系统的更多信息。

扁平化配置的目标

为了为平面配置的变化奠定基础,我们有几个目标:

  • 1.逻辑默认值 - 人们编写 JavaScript 的方式在过去九年中发生了很大变化,我们希望新的配置系统能够反映我们当前的现实,而不是 ESLint 首次发布时我们所生活的现实。
  • 2.定义配置的一种方法 - 我们不希望人们再有多种方法来做同样的事情。应该有一种方法来为任何给定的项目定义配置。
  • 3.规则配置应该保持不变 - 我们觉得规则的配置方式已经运行良好,因此为了更容易过渡到平面配置,我们不想对规则配置进行任何更改。在平面配置中可以以相同的方式使用相同的规则密钥。
  • 4.一切都使用本机加载 - 我们对 eslintrc 最大的遗憾之一是以自定义方式重新创建 Node.js 需求解析。这是复杂性的一个重要来源,事后看来,这是不必要的.展望未来,我们希望直接利用 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 的默认设置,但我们绝对可以使用平面配置进行更改。

平面配置具有以下默认设置:

  • ecmaVersion: "latest"对于所有 JavaScript 文件 - 没错,默认情况下所有 JavaScript 文件都将设置为最新版本的 ECMAScript。这模仿了 JavaScript 运行时的工作方式,每次升级都意味着您选择使用最新、最好的 JavaScript 版本。此更改意味着您可能不必在配置中手动设置 ecmaVersion ,除非您由于运行时限制而想要强制执行以前的版本。如有必要,您仍然可以将 ecmaVersion 一直设置为 3。
  • sourceType: "module"对于所有 .js 和 .mjs 文件 - 默认情况下,平面配置假定您正在编写 ESM。如果没有,您可以随时将 sourceType 设置回“script”。
  • sourceType: "commonjs"对于 .cjs 文件 - 我们仍处于过渡期,大量 Node.js 代码是用 CommonJS 编写的。为了支持这些用户,我们添加了新的“commonjs”源类型,可以为该环境正确配置所有内容。
  • ESLint 搜索 .js、.mjs 和 .cjs 文件 - 使用 eslintrc,ESLint 仅当您在命令行上传递目录名称时搜索 .js 文件,并且您需要使用 --ext 标志来定义更多。使用平面配置,会自动搜索所有三种最常见的 JavaScript 文件扩展名。

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

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

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

使用 ESLint CLI 时,它会从当前工作目录搜索 eslint.config.js,如果未找到,将继续搜索该目录的祖先,直到找到该文件或命中根目录。该 eslint.config.js 文件包含该次 ESLint 运行的所有配置信息,因此与 eslintrc 相比,它大大减少了所需的磁盘访问,它必须检查从 linted 文件位置到根目录的每个目录是否有任何其他配置文件。

此外,使用 JavaScript 文件使我们能够依赖用户加载其配置文件可能需要的其他信息。现在,您可以根据需要使用 import 和 require 来引入这些附加资源,而不是通过扩展和插件按名称加载内容。以下是 eslint.config.js 文件的示例:

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

eslint.config.js 文件导出配置对象数组。请继续阅读以了解有关此示例的更多信息。

无处不在的基于全局的配置

虽然 eslintrc 中的 overrides 键是很多复杂性的根源,但有一件事非常明确:人们真的很喜欢能够通过配置文件中的 glob 模式来定义配置。因为我们想要消除 eslintrc 的配置级联,所以我们必须使用 glob 模式来启用相同类型的配置覆盖。我们使用覆盖配置作为平面配置的基础。

每个配置对象都可以有可选文件,并忽略指定基于 minimatch 的 glob 模式来匹配文件的键。仅当文件名与文件中的模式匹配时(或者如果没有文件键,在这种情况下它将匹配所有文件),配置对象才适用于文件。忽略键会从文件列表中过滤掉文件,因此您可以限制配置对象适用于哪些文件。例如,也许您的测试文件与源文件位于同一目录中,并且您希望配置对象仅应用于源文件。你可以这样做:

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

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

如果你想完全忽略文件怎么办?您可以通过指定一个仅具有忽略键的配置对象来做到这一点,如下所示:

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

使用此配置,所有以 .test.js 结尾的 JavaScript 文件将被忽略。您可以将其视为 eslintrc 中的ignorePatterns 的等效项,尽管具有 minimatch 模式。

再见 extends, 你好 flat 级联

虽然我们想摆脱基于目录的配置级联,但平面配置实际上仍然有一个直接在 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"
        }  
    }
];

此配置有两个具有重叠文件模式的配置对象。第一个配置对象适用于所有 .js 和 .cjs 文件,而第二个配置对象仅适用于 .js 文件.当对以 .js 结尾的文件进行 linting 时,ESLint 会组合两个配置对象来创建该文件的最终配置。因为第二个配置将 semi 设置为“警告”严重性,所以它优先于第一个配置中设置的“错误”。当发生冲突时,最后匹配的配置总是获胜。

对于可共享配置来说,这意味着您可以将它们直接插入到数组中,而不是使用扩展,例如:

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 的解释方式。有修改可用全局变量的顶级全局键,还有 ecmaVersion 和 sourceType 作为 parserOptions,更不用说 env 来添加更多全局变量。也许最令人困惑的是,您必须设置 ecmaVersion 并添加像 es6 这样的环境,以启用您想要的语法并确保正确的全局变量可用。

在平面配置中,我们将与 JavaScript 评估相关的所有键移至名为 languageOptions 的新顶级键中。

在平面配置中设置 ecmaVersion

最大的变化是我们将 ecmaVersion 从 parserOptions 中移出并直接移至 languageOptions 中。这更好地反映了该键的新行为,即根据指定的 ECMAScript 版本启用语法和全局变量。例如:

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

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

在平面配置中设置 sourceType

接下来,我们将 sourceType 移至 languageOptions 中。与 ecmaVersion 类似,此键不仅影响文件的解析方式,还影响 ESLint 如何评估其作用域结构。我们保留了 ESM 的传统“模块”和脚本的“脚本”,还添加了“commonjs”,这让 ESLint 知道它应该将文件视为 CommonJS(这也启用了 CommonJS 特定的全局变量)。如果您使用 ecmaVersion: 3 或 ecmaVersion: 5,请务必设置 sourceType: script,如下所示:

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

再见 environments,你好 globals

eslintrc 中的环境提供了一组已知的全局变量,并且一直是用户困惑的根源。它们需要保持最新(特别是在浏览器的情况下)并且更新需要等待 ESLint 版本。另外,我们在环境中添加了一些附加功能,以便更轻松地使用 Node.js,但最终,我们搞砸了。

对于平面配置,我们决定完全删除 env 键。为什么?因为不再需要它了。我们连接到与 Node.js 一起使用的环境的所有自定义功能现在都由 sourceType:“commonjs”覆盖,因此剩下的就是让环境管理全局变量。 ESLint 在核心中这样做是没有意义的,因此我们将这个责任交还给您。

几年前,我们与 Sindre Sorhus 合作创建了 globals 包,该包从 ESLint 中提取了所有环境信息,以便其他包可用。然后,ESLint 使用全局变量作为其环境的来源。

通过平面配置,您可以直接使用全局包,随时更新它,以获得环境过去提供的所有相同功能。例如,以下是将浏览器全局变量添加到配置中的方法:

import globals from "globals";

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

languageOptions.globals 键的工作方式与 eslintrc 中的相同,只是现在,您可以使用 JavaScript 动态插入所需的任何全局变量。

自定义解析器和解析器选项大部分相同

parser 和 parserOptions 键现已移至 languageOptions 键中,但它们的工作方式大多与 eslintrc 中相同,但有两个具体区别:

  • 1.您现在可以将解析器对象直接插入到配置中。
  • 2.解析器现在可以与插件捆绑在一起,您可以为解析器指定一个字符串值以使用插件中的解析器。 (下一节将详细介绍。)

这是一个使用 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 的优势在于个人和公司维护的插件生态系统,用于定制其 linting 策略。因此,我们希望确保现有插件无需修改即可继续工作,并允许插件执行过去无法执行的操作。

从表面上看,在平面配置中使用插件看起来与在 eslintrc 中使用插件非常相似。最大的区别是 eslintrc 使用字符串,而平面配置使用对象。您无需指定插件的名称,而是直接导入插件并将其放入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。

处理器的工作方式与 eslintrc 类似

处理器顶级密钥的工作方式与 eslintrc 中的基本相同,主要用例是使用插件中定义的处理器,例如:

import markdown from "eslint-plugin-markdown";

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

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

平面配置中的一个补充是,处理器现在也可以是一个包含 preprocess() 和 postprocess() 方法的对象。

有组织的 linter 选项

在 eslintrc 中,有几个与 linter 操作方式直接相关的键,即 noInlineConfig 和 reportUnusedDisableDirectives。这些已移至新的 linterOptions 键中,但工作方式与 eslintrc 中完全相同。这是一个例子:

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

共享设置完全相同

顶级设置键的行为方式与 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";
import path from "path";
import { fileURLToPath } from "url";

// mimic CommonJS variables -- not needed if using CommonJS
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

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 文件,同时优化它们以与平面配置一起使用。我们认为这是一个必要的过渡步骤,让生态系统慢慢转换为扁平配置。

结论

该团队花了很长时间设计扁平化配置,以便现有用户感到熟悉,并提供让每个人受益的新功能。我们保持规则、设置和处理器等内容相同,同时扩展插件、语言选项和 linter 选项等内容以使其更加统一。我们认为扁平配置在这两个极点之间找到了良好的平衡,一旦新的配置系统普遍可用,您将更喜欢使用 ESLint。与此同时,兼容性实用程序将允许您继续使用现有的共享配置。

在本博客系列的下一部分中,您今天将学习如何开始使用平面配置。

Tags

Configuration Command Line