ESLint 自定义规则开发实战:从零编写、测试、打包插件到发布 npm 全流程指南

📅 发布时间:2026/9/10 16:44:50
ESLint 自定义规则开发实战:从零编写、测试、打包插件到发布 npm 全流程指南
ESLint 自定义规则开发实战从零编写、测试、打包插件到发布 npm 全流程指南【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint本教程以 ESLint 官方仓库中的 自定义规则实战教程 及其配套的 可运行示例代码 为骨架完整演示如何编写一条要求所有名为foo的const变量必须被赋值为字符串字面量bar的自定义规则并通过RuleTester测试、打包成插件、本地使用、发布到 npm 并在项目中运行含--fix自动修复。读完本文你将掌握 ESLint 自定义规则的完整生命周期并理解规则元数据、AST 访问器、context.report()、fixer 等底层机制的实际行为。为什么需要自定义规则当 ESLint 的 内置规则 和社区已发布的插件都无法满足你的需求时就需要创建自定义规则。典型场景包括强制落实公司或项目内部的编码最佳实践防止某类特定 Bug 反复出现确保代码符合既定的风格指南。在动手编写一条并非面向特定公司或项目通用的规则之前建议先在网络上搜索是否已有插件解决了你的问题——很可能现成的规则已经存在。如果确实没有再按本教程的流程自定义开发。前置条件开始之前请确保开发环境中已安装Node.jsnpm本教程还假定你已具备 ESLint 及其规则的基本了解。你可以先阅读 自定义规则 与 插件 两篇文档它们分别讲解规则结构与插件的创建方式。目标规则enforce-foo-bar本教程要实现的规则要求所有名为foo的const变量必须被赋值为字符串字面量bar。规则定义在enforce-foo-bar.js中同时它还会建议把赋给const foo的其他任何值替换为bar。例如假设有一个foo.js文件// foo.js const foo baz123;运行 ESLint 时该规则会把baz123标记为变量foo的错误取值。如果 ESLint 以 autofix 模式运行则会自动把文件修复为// foo.js const foo bar;第一步搭建项目先为自定义规则创建新项目mkdir eslint-custom-rule-example # 创建目录 cd eslint-custom-rule-example # 进入目录 npm init -y # 初始化 npm 项目 touch enforce-foo-bar.js # 创建规则文件第二步搭出规则文件骨架在enforce-foo-bar.js中写入规则的基础结构并添加一个meta对象用于存放规则的基础信息// enforce-foo-bar.js module.exports { meta: { // TODO: add metadata }, create(context) { return { // TODO: add callback function(s) }; }, };在仓库配套示例 enforce-foo-bar.js 中可以看到规则文件顶部还带有fileoverview与author的 JSDoc 注释用于描述规则用途和作者信息这是 ESLint 官方对规则文件约定的注释风格。第三步添加规则元数据meta在编写规则逻辑前需要为规则对象添加元数据ESLint 在运行规则时会使用这些信息。规则对象通过meta属性声明规则类型、文档和可修复性等信息type规则类型这里取值为problem表示该规则识别出可能引发错误或混乱的代码docs.description规则描述这里是Enforce that a variable named foo can only be assigned a value of bar.fixable值为code表示该规则可以通过修改代码来自动修复问题另一种取值是whitespaceschema规则的配置选项模式这里为空数组[]表示规则不接受任何配置项languages因为该规则针对 JavaScript添加languages: [js/js]声明它只适用于内置的 JavaScript 语言。如果该规则被启用在其他语言上ESLint 会抛出错误。// enforce-foo-bar.js module.exports { meta: { type: problem, docs: { description: Enforce that a variable named foo can only be assigned a value of bar., }, fixable: code, schema: [], languages: [js/js], }, create(context) { return { // TODO: add callback function(s) }; }, };fixable声明并非可选的装饰信息而是有强制约束力的契约。查看 linter.js 中context.report()的实现可以发现如果规则在报告中带上了fix函数但meta.fixable未被声明为code或whitespaceESLint 会直接抛出Fixable rules must set the meta.fixable property to code or whitespace.的错误。同理若规则提供suggestions建议性修改但未设置meta.hasSuggestions: true也会抛出对应错误。关于规则元数据的更多说明可参考 规则结构。第四步编写规则的访问器方法Visitor Methods定义规则的create函数它接收一个context对象并返回一个以语法节点类型为键、以回调函数为值的对象。本规则要处理的是VariableDeclarator变量声明符节点。你可以选择任意 ESTree 节点类型。::: tip 可以使用 Code Explorer 查看任意 JavaScript 代码的 AST这有助于确定你想定位的节点类型。 :::在VariableDeclarator访问器方法内部需要检查三个条件该节点是否属于const变量声明、变量名是否为foo、初始值是否不是字符串bar。判断方式是对传给访问器的node进行逐层评估// enforce-foo-bar.js module.exports { meta: { type: problem, docs: { description: Enforce that a variable named foo can only be assigned a value of bar. }, fixable: code, schema: [], languages: [js/js] }, create(context) { return { // Performs action in the function on every variable declarator VariableDeclarator(node) { // Check if a const variable declaration if (node.parent.kind const) { // Check if variable name is foo if (node.id.type Identifier node.id.name foo) { // Check if value of variable is bar if (node.init node.init.type Literal node.init.value ! bar) { /* * Report error to ESLint. Error message uses * a message placeholder to include the incorrect value * in the error message. * Also includes a fix(fixer) function that replaces * any values assigned to const foo with bar. */ context.report({ node, message: Value other than bar assigned to const foo. Unexpected value: {{ notBar }}., data: { notBar: node.init.value }, fix(fixer) { return fixer.replaceText(node.init, bar); } }); } } } } }; } };关键点拆解node.parent.kind constVariableDeclarator的父节点是VariableDeclaration其kind属性表明声明类型var/let/constnode.id.type Identifier node.id.name foo确认被声明的标识符名为foonode.init node.init.type Literal node.init.value ! bar确认有初始值、是字符串字面量且不等于barnode.init的判空是为了避免const foo;这类无初始值的情况触发误报context.report()向 ESLint 报告错误。报告对象中的message使用{{ notBar }}占位符配合data对象把实际错误值动态拼进错误信息fix(fixer)定义自动修复逻辑fixer.replaceText(node.init, bar)生成一个替换命令把const foo的初始值替换成字符串bar。从实现层面看fixer.replaceText()会先通过 SourceCode.getRange() 取得节点在源码中的起止位置再生成一个{ range, text }形式的修复命令最终由 ESLint 的 applyFixes 流程统一应用到源码上。此外在 linter.js 的规则上下文中如果报告带fix而规则未声明meta.fixable会在运行时直接抛错再次印证了第三步中元数据声明的必要性。第五步搭建测试环境规则写完后需要验证它是否符合预期。ESLint 提供了内置的RuleTester类用于测试规则。你不需要借助第三方测试库来测试 ESLint 规则但RuleTester与 Mocha、Jest 等测试框架也能无缝协作。创建测试文件enforce-foo-bar.test.jstouch enforce-foo-bar.test.js测试文件会用到eslint包把它安装为开发依赖npm install --save-dev eslint然后在package.json中添加测试脚本// package.json { // ...other configuration scripts: { test: node enforce-foo-bar.test.js }, // ...other configuration }RuleTester由 lib/rule-tester/index.js 导出。从其源码可以看到RuleTester内部封装了一个以 flat config 模式创建的Linter实例rule-tester.js并通过describe/it静态属性桥接 Mocha 等测试框架若环境中存在全局describe和it函数则直接使用否则退化为默认的同步执行处理器。这意味着你可以用node enforce-foo-bar.test.js直接运行也可以把它放进 Mocha 的测试套件中。第六步编写测试用例使用RuleTester编写测试在enforce-foo-bar.test.js中导入RuleTester类和自定义规则。RuleTester#run()方法会针对 valid合法和 invalid非法两类测试用例检验规则如果规则未通过任何测试场景该方法会抛出错误。RuleTester要求至少存在一个 valid 和一个 invalid 测试场景。// enforce-foo-bar.test.js const { RuleTester } require(eslint); const fooBarRule require(./enforce-foo-bar); const ruleTester new RuleTester({ // Must use at least ecmaVersion 2015 because // thats when const variables were introduced. languageOptions: { ecmaVersion: 2015 }, }); // Throws error if the tests in ruleTester.run() do not pass ruleTester.run( enforce-foo-bar, // rule name fooBarRule, // rule code { // checks // valid checks cases that should pass valid: [ { code: const foo bar;, }, ], // invalid checks cases that should not pass invalid: [ { code: const foo baz;, output: const foo bar;, errors: 1, }, ], }, ); console.log(All tests passed!);要点说明new RuleTester({ languageOptions: { ecmaVersion: 2015 } })至少需要 ecmaVersion 2015因为const变量从该版本才被引入同时这也展示了 RuleTester 构造参数与 flat config 中languageOptions的对齐关系——构造时传入的配置会和默认配置合并rule-tester.jsvalid数组声明应当通过检查的代码例如const foo bar;invalid数组声明应当报错的代码其中errors: 1断言恰好产生 1 个错误output字段则断言自动修复后的输出为const foo bar;。注意输出使用了双引号字符串bar因为fix(fixer)中替换文本写的是bar。从源码看RuleTester.run()会把被测规则以rule-to-test/ruleName的形式注册为插件规则并用包装过的create方法冻结context.options、context.settings等属性防止规则在测试中意外修改这些共享对象rule-tester.js。运行测试npm test如果测试通过控制台会输出All tests passed!第七步把规则打包进插件规则编写并验证通过后就可以把它放进插件中。借助插件你可以把规则封装成 npm 包在其他项目中共享复用。创建插件文件touch eslint-plugin-example.js插件本质上就是导出的 JavaScript 对象。要把规则包含进插件只需把它加入插件的rules对象——一个以规则名为键、规则源码为值的键值对集合// eslint-plugin-example.js const fooBarRule require(./enforce-foo-bar); const plugin { rules: { enforce-foo-bar: fooBarRule } }; module.exports plugin;仓库配套示例 eslint-plugin-example.js 与此完全一致。插件对象还可以包含configs、processors、parsers等其他字段规则的打包只需rules字段即可。关于插件创建的更多内容可参考 创建插件。第八步在本地使用插件你可以在项目中通过本地定义的插件来运行自定义规则。要使用本地插件只需在 ESLint 配置文件的plugins属性中指定插件的路径即可。本地使用插件的典型场景包括在发布到 npm 之前先测试插件使用一个不打算发布到 npm 的插件。在把插件加入项目之前先用 flat 配置文件eslint.config.js为项目创建 ESLint 配置touch eslint.config.js然后在eslint.config.js中添加如下代码// eslint.config.js use strict; // Import the defineConfig helper function const { defineConfig } require(eslint/config); // Import the ESLint plugin locally const eslintPluginExample require(./eslint-plugin-example); module.exports defineConfig([ { files: [**/*.js], languageOptions: { sourceType: commonjs, ecmaVersion: latest, }, // Using the eslint-plugin-example plugin defined locally plugins: { example: eslintPluginExample }, rules: { example/enforce-foo-bar: error, }, }, ]);配置要点defineConfig是从eslint/config导出的辅助函数当前仓库在 config-api.js 中将其连同globalIgnores、includeIgnoreFile一并导出用于为配置数组提供类型提示与更友好的错误信息plugins: { example: eslintPluginExample }把本地插件以命名空间example注册rules: { example/enforce-foo-bar: error }以插件名/规则名的格式启用规则严重级别设为errorfiles: [**/*.js]与languageOptionssourceType: commonjs、ecmaVersion: latest为该配置块限定了匹配的文件范围与解析语言选项。接下来创建被测文件example.jstouch example.js写入以下代码// example.js function correctFooBar() { const foo bar; } function incorrectFoo() { const foo baz; // Problem! }现在可以对example.js运行 ESLint验证本地插件的规则npx eslint example.js终端输出如下/path-to-directory/eslint-custom-rule-example/example.js 8:11 error Value other than bar assigned to const foo. Unexpected value: baz example/enforce-foo-bar ✖ 1 problem (1 error, 0 warnings) 1 error and 0 warnings potentially fixable with the --fix option.注意输出信息中的example/enforce-foo-bar前缀正是配置文件中example命名空间与规则名的组合错误消息中的Unexpected value: baz则来自context.report()的data占位符插值。仓库示例 example.js 中通过/* eslint-disable no-unused-vars */注释屏蔽了无关规则如未使用变量警告以便聚焦本规则的输出。第九步发布插件到 npm要把包含规则的插件发布到 npm需要配置好package.json在对应字段中添加以下内容name包的唯一名称npm 上不允许其他包重名main插件文件的相对路径本例为eslint-plugin-example.jsdescription在 npm 上展示的包描述peerDependencies添加eslint: 10.0.0作为 peer dependency。使用该插件需要 ESLint 版本大于等于此版本。将eslint声明为 peer dependency 要求用户把该包单独安装到项目中而不是随插件捆绑keywords包含标准关键词[eslint, eslintplugin, eslint-plugin]以便包更容易被搜索到也可以添加其他与插件相关的关键词。一个完整的、带注释的插件package.json示例// package.json { // Name npm package. // Add your own package name. eslint-plugin-example is taken! name: eslint-plugin-example, version: 1.0.0, description: ESLint plugin for enforce-foo-bar rule., main: eslint-plugin-example.js, // plugin entry point scripts: { test: node enforce-foo-bar.test.js }, // Add eslint10.0.0 as a peer dependency. peerDependencies: { eslint: 10.0.0 }, // Add these standard keywords to make plugin easy to find! keywords: [ eslint, eslintplugin, eslint-plugin ], author: , license: ISC, devDependencies: { eslint: ^10.0.0 } }仓库配套的 package.json 还额外设置了private: true这是在本地开发、尚未准备发布时的常见做法——加上它之后npm publish会被拒绝防止误发布。发布包运行npm publish并按照 CLI 的提示完成操作。发布成功后包就会出现在 npm 上。第十步使用已发布的自定义规则接下来在另一个项目中消费已发布的插件。在项目中执行以下命令下载安装该包npm install --save-dev eslint-plugin-example然后更新eslint.config.js改用从 npm 下载的插件版本// eslint.config.js use strict; // Import the plugin downloaded from npm const eslintPluginExample require(eslint-plugin-example); // ... rest of configuration配置文件中其余部分plugins: { example: eslintPluginExample }与rules: { example/enforce-foo-bar: error }保持不变只是require的来源从本地相对路径换成了 npm 包名。现在可以测试自定义规则了。对第八步创建的example.js再次运行 ESLint这次使用的是下载的插件npx eslint example.js输出与本地插件版一致/path-to-directory/eslint-custom-rule-example/example.js 8:11 error Value other than bar assigned to const foo. Unexpected value: baz example/enforce-foo-bar ✖ 1 problem (1 error, 0 warnings) 1 error and 0 warnings potentially fixable with the --fix option.从上面的输出可以看出这个错误实际上可以通过--fix标志修复把变量赋值修正为bar。带上--fix再次运行npx eslint example.js --fix这次运行终端没有任何错误输出但可以看到修复已经应用到example.js中// example.js // ... rest of file function incorrectFoo() { const foo bar; // Fixed! }自动修复之所以能生效正是因为规则在context.report()中提供了fix(fixer)函数fixer.replaceText(node.init, bar)生成修复命令ESLint 在--fix模式下收集所有修复命令并应用到源码。RuleTester的 invalid 用例中output字段的断言与这里--fix的行为是一一对应的——测试保证了修复输出const foo bar;而命令行验证了真实文件中的修复结果。总结在本教程中你完成了一条自定义规则它要求所有名为foo的const变量必须被赋值为字符串bar并建议把const foo的任何其他赋值替换为bar。你还把这条规则加入插件并发布到了 npm。通过这个过程你掌握了以下可以复用到其他自定义规则和插件开发中的实践创建自定义 ESLint 规则含meta元数据与 AST 访问器使用内置RuleTester测试自定义规则valid/invalid 用例、errors数量断言与output修复断言把规则打包进插件rules键值对发布插件到 npmname/main/description/peerDependencies/keywords等字段的配置在项目中使用插件中的规则本地插件与 npm 包两种方式配合 flat config 与--fix自动修复。如果你希望查看带注释的完整教程源码可以直接阅读仓库中的 custom-rule-tutorial-code 目录其中包含了本文涉及的全部示例文件可直接复制运行。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考