news 2026/9/11 0:12:03

ESLint 自定义处理器(Custom Processors)完全指南:从零为 Markdown/HTML 文件编写 preprocess 与 postprocess

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESLint 自定义处理器(Custom Processors)完全指南:从零为 Markdown/HTML 文件编写 preprocess 与 postprocess

ESLint 自定义处理器(Custom Processors)完全指南:从零为 Markdown/HTML 文件编写 preprocess 与 postprocess

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

自定义处理器(Custom Processors)是 ESLint 扩展机制中用于处理「非标准 JavaScript 文件」的核心能力,它让 ESLint 能够从 Markdown、HTML、模板字符串等文件中提取出 JavaScript 代码进行校验,再把报告回映射到原始文件的位置。本文基于 custom-processors.md 官方文档,结合仓库内的 processor-service.js 实现与 processor-service.js 测试 测试用例,完整讲解处理器的接口规范、配置方式、自动修复支持与meta对象的使用细节,读完即可动手编写一个可用于 flat config 的自定义处理器。

::: tip 本文讲解的自定义处理器基于flat config格式(即eslint.config.js)。如果你仍在使用已废弃的.eslintrc格式,请参考 plugin-migration-flat-config.md 完成迁移。 :::

为什么需要自定义处理器

ESLint 默认只能解析标准的 JavaScript 代码。但在真实项目中,大量 JavaScript 片段散落在其他格式的文件里,例如:

  • Markdown 文档中的 ```js 代码块;
  • HTML 文件中<script>标签内的内联脚本;
  • Vue、Svelte 等单文件组件中的<script>部分;
  • 模板字符串中嵌入的代码片段。

自定义处理器(Custom Processor)的作用就是告诉 ESLint如何从这类文件中提取 JavaScript 片段并单独对其进行 lint。例如社区流行的@eslint/markdown插件就内置了一个专门从 Markdown 中提取并校验 JS 代码块的自定义处理器。

处理器的核心思想是「两步走」:

  1. preprocess(预处理):读取原始文件的全部文本,剥离非 JS 内容,把其中的 JS 片段拆分返回给 ESLint 逐一校验;
  2. postprocess(后处理):收集每个片段产生的 lint 消息,把错误位置映射回原始文件的真实行列,并合并成一维数组返回。

处理器接口规范(Custom Processor Specification)

要创建一个自定义处理器,你的模块导出的对象必须满足以下接口。处理器通常作为插件的一部分(放在processors键下),但也可以单独导出:

const plugin = { meta: { name: "eslint-plugin-example", version: "1.2.3", }, processors: { "processor-name": { meta: { name: "eslint-processor-name", version: "1.2.3", }, // 接收文件文本与文件名 preprocess(text, filename) { // 在这里剥离非 JS 内容 // 并按需拆分成多个待 lint 的代码片段 return [ // 返回待 lint 的代码块数组 { text: code1, filename: "0.js" }, { text: code2, filename: "1.js" }, ]; }, // 接收 Message[][] 与文件名 postprocess(messages, filename) { // messages 是二维数组,每个顶层元素对应 // preprocess() 返回数组中的一块代码产生的消息 // 需要返回保留的一维消息数组 return [].concat(...messages); }, supportsAutofix: true, // (可选,默认 false) }, }, }; // 用于 ESM export default plugin; // 或用于 CommonJS module.exports = plugin;

preprocess方法:拆分代码块

preprocess方法接收两个参数:文件的完整内容text和文件路径filename,返回一个待 lint 的代码块数组。每个代码块会被独立 lint,但报告仍归属到原始文件名之下。

代码块对象包含两个属性:

属性说明
text代码块的实际内容
filename代码块的「虚拟文件名」,可以任意命名,但应当包含文件扩展名

代码块的扩展名非常关键:ESLint 会根据扩展名决定如何处理该代码块——只有当代码块的虚拟文件名以.js结尾、或与项目配置中的files条目匹配时,它才会被真正 lint(详见下文源码分析中的filterCodeBlock)。因此,如果你从 Markdown 中提取的片段要按 JS 规则校验,就应命名为0.js1.js等。

至于返回一块还是多块,完全由插件自行决定:

  • 处理.html文件时,你可以把所有<script>内容合并后只返回一项
  • 处理.md文件时,由于每个 JS 代码块相互独立,你可以返回多个条目分别校验。

postprocess方法:聚合与位置映射

postprocess方法接收一个二维数组(每个顶层元素对应preprocess返回的一块代码所产生的消息列表)以及文件名,必须完成两件事:

  1. 调整所有错误的行列位置,使其对应到原始未处理文件中的真实位置(因为校验发生的位置是片段内部,需要按片段在原始文件中的偏移量换算回去);
  2. 把所有消息聚合成一个一维数组并返回。

最简单但最常见的实现就是[].concat(...messages)(即messages.flat()),它在插件不打算映射位置时原样合并所有消息。

Lint 消息的数据结构

报告出的问题(lint message)包含以下位置与修复信息:

type LintMessage = { /// 消息出现的行号(1 起始)。 line?: number; /// 消息出现的列号(1 起始)。 column?: number; /// 结束位置的行号(1 起始)。 endLine?: number; /// 结束位置的列号(1 起始)。 endColumn?: number; /// 若为 `true`,表示致命错误。 fatal?: boolean; /// 自动修复信息。 fix: Fix; /// 错误消息文本。 message: string; /// 产生该消息的规则 ID;不适用时为 `null`。 ruleId: string | null; /// 消息的严重级别。 severity: 0 | 1 | 2; /// 建议(suggestion)信息。 suggestions?: Suggestion[]; }; type Fix = { range: [number, number]; text: string; }; type Suggestion = { desc?: string; messageId?: string; fix: Fix; };

其中Fix.range是一对索引,指向代码中将被替换的连续文本区间的起止位置;Fix.text是要替换进该区间的文本。severity取值0(off)、1(warn)、2(error)。

从源码看处理器的真实调用链

文档描述的接口在仓库中有完整的实现支撑。核心类是 lib/services/processor-service.js 中的ProcessorService

// lib/services/processor-service.js preprocessSync(file, config) { const { processor } = config; ... blocks = processor.preprocess(file.rawBody, file.path); ... if (typeof blocks.then === "function") { throw new Error("Unsupported: Preprocessor returned a promise."); } ... files: blocks.map((block, i) => { // 兼容旧行为:块可以是纯字符串 if (typeof block === "string") { return block; } const filePath = path.join(file.path, `${i}_${block.filename}`); return new VFile(filePath, block.text, { physicalPath: file.physicalPath }); }), } postprocessSync(file, messages, config) { const { processor } = config; return processor.postprocess(messages, file.path); }

值得注意的实现细节:

  • preprocessfile.rawBody(原始文本)和file.path为参数同步调用;
  • 处理器必须是同步的——若返回 Promise,会直接抛出Unsupported: Preprocessor returned a promise.错误(对应测试 中的"should throw an error if the preprocessor returns a promise"用例);
  • 代码块既可以是{ filename, text }对象(现代写法,会被包装成VFile,虚拟路径为原文件路径 + 索引前缀 + 块文件名,例如foo.md下的第 0 块成为foo.md/0_block.js),也可以是纯字符串(旧版兼容写法,直接按 JS 字符串校验);
  • preprocess抛出异常时会被捕获并转换成fatal: true, severity: 2, ruleId: null的致命 lint 消息,消息前缀为Preprocessing error:,同时会剥离错误信息开头的line N:前缀(对应测试用例"should strip leading 'line N:' prefix");
  • postprocess的异常不会被捕获,会直接向上传播。

调用链:从verifyProcessorService

处理器在 lib/linter/linter.js 的_verifyWithFlatConfigArrayAndProcessor方法中被接入主流程:

  1. 当配置对象中存在config.processor时(linter.js#L1393-L1404),ESLint 取出preprocesspostprocesssupportsAutofix,并计算disableFixes = options.disableFixes || !supportsAutofix—— 这就是「未声明supportsAutofix: true时即使带--fix也不会自动修复」的底层原因;
  2. 调用processorService.preprocessSync(file, { processor })得到各代码块(linter.js#L898);
  3. 每个代码块通过filterCodeBlock过滤后分别独立 lint。默认的过滤规则是:
// lib/linter/linter.js#L909-L911 const filterCodeBlock = options.filterCodeBlock || (blockFilename => blockFilename.endsWith(".js"));

默认只 lint 虚拟文件名以.js结尾的代码块。这正是文档中强调「代码块文件名应包含扩展名」的原因——想 lint 其他扩展名的片段(如.jsx),需要在配置中增加匹配的files条目;

  1. 若代码块内容或扩展名与原始文件不一致,还会使用递归配置解析(configForRecursive)重新匹配更精确的配置(linter.js#L933-L951),这保证了 Markdown 内的.jsx块能命中你为**/*.jsx单独书写的 parserOptions;
  2. 最后调用processorService.postprocessSync合并所有块的消息并返回(linter.js#L963)。

在 tests/lib/services/processor-service.js 中,这些行为都有对应的单元测试覆盖,例如验证preprocess以原始文本与路径被调用、对象块被包装为带索引前缀的VFile、字符串块保持旧行为、以及错误消息的格式化逻辑。

为处理器启用自动修复(Autofix)

默认情况下,即使命令行带上--fix标志,ESLint 在使用自定义处理器时也不会执行自动修复。这是因为处理器产出的 lint 消息位置在「处理后的 JS 片段」内,直接应用修复会改错地方。要让 ESLint 支持带处理器的自动修复,需要额外做两步:

  1. postprocess中转换fix属性:所有可自动修复的问题都带有fix属性,其结构为:
{ range: [number, number], text: string }
  • range包含两个索引,指向「将被替换的连续文本区间」的起止位置;
  • text是要替换进该区间的文本。

初始消息列表中的fix.range指向的是处理后的 JavaScript 片段中的位置,postprocess必须将其换算为原始未处理文件中的位置(按片段在原始文件中的偏移量整体平移range),修复才能落到正确的位置上。

  1. 在处理器对象上添加supportsAutofix: true属性:如上文源码所示,supportsAutofixfalse(默认值)时,disableFixes会被置为true,即使传入--fix也不会应用任何修复。

注意:不打算支持自动修复的处理器可以省略supportsAutofix,此时 ESLint 仍然正常报告问题,只是跳过修复阶段。

一个插件的多种处理器与多扩展名支持

你可以在同一个插件里同时包含规则(rules)和多个自定义处理器,也可以在一个插件里放多个处理器。若要支持多种文件扩展名,把每个处理器都加入processors元素并指向同一对象即可:

const plugin = { processors: { ".md": processorImpl, // 处理 Markdown ".html": processorImpl, // 同一实现也处理 HTML }, // 也可以同时放规则 rules: { "my-rule": { /* ... */ }, }, };

多个处理器共用一个实现对象是允许的,因为处理器只负责「如何提取代码块」,对不同扩展名的差异完全可以收敛到同一个逻辑中。

meta对象的作用与两种使用场景

meta对象帮助 ESLint缓存使用处理器的配置,并提供更友好的调试信息。处理器相关的meta分为两层,都建议提供。

插件级meta对象

插件的 meta 对象 提供插件自身的信息(nameversion)。当你在配置中用字符串格式plugin-name/processor-name引用处理器时,ESLint 会自动使用插件meta为处理器生成名称,这是最常见的使用方式:

// eslint.config.js import { defineConfig } from "eslint/config"; import example from "eslint-plugin-example"; export default defineConfig([ { files: ["**/*.txt"], // 对文本文件应用处理器 plugins: { example, }, processor: "example/processor-name", }, // ... 其他配置 ]);

此例中处理器名就是"example/processor-name",该值会用于配置的序列化。

处理器级meta对象

每个处理器也可以声明自己的meta对象。当你在配置中直接把处理器对象传给processor时,ESLint 无法从插件名推断归属,就会用到处理器自身的meta

  • meta.name应匹配处理器名称;
  • meta.version应匹配该处理器所在 npm 包的版本号。

最省事的做法是从你的package.json中直接读取这些信息:

// eslint.config.js import { defineConfig } from "eslint/config"; import example from "eslint-plugin-example"; export default defineConfig([ { files: ["**/*.txt"], processor: example.processors["processor-name"], }, // ... 其他配置 ]);

这里直接指定example.processors["processor-name"]时,使用的是处理器自己的meta对象,它必须被定义,才能保证处理器不通过插件名引用时的正确解析。

为什么两层meta都需要

建议插件和每个处理器都提供各自的meta对象。这样,无论处理器在配置中以字符串还是对象形式被指定,依赖meta的功能(例如--print-config输出配置、--cache缓存结果)都能正常工作。

从源码看,这一约束在 lib/config/config.js 的解析逻辑中同样存在:当processor为字符串时,会通过splitPluginIdentifier拆分出插件名与处理器名,并从plugins[pluginName].processors[localProcessorName]中取出处理器对象(找不到时报错Key "processor": Could not find ...);当processor为对象时则直接使用对象本身。若对象缺少meta,序列化配置时会报Could not serialize processor object (missing 'meta' object).。而 lib/config/flat-config-schema.js#L432-L450 中的processorSchema也做了双保险校验:字符串必须是合法的插件成员名,对象则必须同时具备preprocesspostprocess方法,否则抛Object must have a preprocess() and a postprocess() method.

在配置文件中指定处理器

要在配置文件中使用插件提供的处理器,需要先导入插件并放入plugins键、指定命名空间,再通过processor键引用,例如:

// eslint.config.js import { defineConfig } from "eslint/config"; import example from "eslint-plugin-example"; export default defineConfig([ { files: ["**/*.txt"], plugins: { example, }, processor: "example/processor-name", }, ]);

更完整的实战配置可参考 docs/src/use/configure/plugins.md 中@eslint/markdown的用法:先用files: ["**/*.md"]让 Markdown 文件走markdown/markdown处理器;再为**/*.jsx增加一个配置对象以启用 JSX 解析,从而使 Markdown 内的 JSX 块也能按 JSX 规则校验;必要时还可以通过ignores: ["**/test.md/*.jsx"]忽略特定文件中的特定块。文档同时提醒:全局 ignores(global ignores)同样作用于命名代码块

配置中processor键的取值在 flat-config-schema.js 中被验证为「字符串或包含preprocess/postprocess方法的对象」,二者在 config.js 中会被归一化为同一个处理器对象供 linter.js 使用。

编写一个最小可用的自定义处理器

综合以上规范,下面是一个「从.txt文件中提取 JS 片段并 lint」的最小插件骨架(完整逻辑可对照本文各节逐步补全):

// my-processor-plugin.js export default { meta: { name: "eslint-plugin-myprocessor", version: "1.0.0", }, processors: { "extract-js": { meta: { name: "eslint-processor-extract-js", version: "1.0.0", }, preprocess(text, filename) { // 提取 text 中所有 ```js ... ``` 片段 const blocks = []; const regex = /```js\s*([\s\S]*?)```/gu; let match; let i = 0; while ((match = regex.exec(text)) !== null) { blocks.push({ text: match[1], filename: `${i}.js` }); i++; } return blocks; }, postprocess(messages, filename) { // 此处若要支持 --fix,还需按片段在原始文件中的 // 偏移量平移每条消息的 line/column 与 fix.range return [].concat(...messages); }, supportsAutofix: false, // 尚未实现位置映射,保持关闭 }, }, };

然后按前文方式在eslint.config.js中通过processor: "myprocessor/extract-js"processor: myProcessor.processors["extract-js"]接入即可。

小结

  • 接口三要素preprocess(拆块)、postprocess(聚合 + 位置映射)、supportsAutofix(可选,默认false);
  • 代码块命名决定能否被 lint:默认只有.js结尾的块会被过滤通过,其他扩展名需补充files配置;
  • 自动修复不是免费的:必须同时改写fix.range到原始文件坐标并声明supportsAutofix: true
  • meta双保险:插件级与处理器级meta分别支撑字符串引用与对象直接引用两种配置写法,保证--print-config--cache等依赖序列化的功能稳定可用;
  • 源码印证ProcessorService(processor-service.js)、linter 调用链(linter.js)与 schema 校验(flat-config-schema.js)完整实现了本文描述的所有行为,单元测试 为这些行为提供了可验证的依据。

掌握了自定义处理器,你就可以让 ESLint 的检查能力覆盖 Markdown 文档、HTML 模板乃至任何自定义格式文件中的 JavaScript,把统一的质量门禁延伸到项目的每一个角落。

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 0:06:12

京东 JoyAI 物理基座模型 PhysBrain 1.5 发布拆解:8B 参数如何通过人类学习范式拿下开源第一,媲美 GPT-6 Astra

2026年9月10日&#xff0c;京东探索研究院正式发布物理基座模型PhysBrain 1.5。这款8B参数的开源模型在58项真人盲评中拿下开源第一。对比豆包视频通话助手胜率77.6%&#xff0c;对比Gemini胜率87.9%。这是全球首个基于人类学习范式的通用物理智能基座模型。它用第一人称人类视…

作者头像 李华
网站建设 2026/9/10 23:59:14

Mongoose 8 升级迁移指南:从 7.x 到 8.x 的全面破坏性变更解析

Mongoose 8 升级迁移指南&#xff1a;从 7.x 到 8.x 的全面破坏性变更解析 【免费下载链接】mongoose MongoDB object modeling designed to work in an asynchronous environment. 项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose 从 Mongoose 7.x 升级到 …

作者头像 李华
网站建设 2026/9/10 23:59:09

SpringBoot构建美术馆数字化平台的技术实践

1. 项目背景与核心需求去年参与了一个美术馆的数字化改造项目&#xff0c;他们需要将线下展览搬到线上。最初考虑用WordPress搭建&#xff0c;但发现其扩展性和定制化能力无法满足艺术品的多维展示需求。最终我们选择了SpringBoot作为技术底座&#xff0c;开发了一套专门针对艺…

作者头像 李华
网站建设 2026/9/10 23:57:24

### IEEE 754 单精度浮点数阶码深度解析报告

在现代计算机科学与数值计算领域&#xff0c;IEEE 754 标准是浮点数运算的绝对基石。该标准由电气和电子工程师协会&#xff08;IEEE&#xff09;于1985年制定&#xff0c;旨在解决不同计算机架构之间浮点数表示与运算不一致的问题。无论是底层的微处理器架构&#xff08;如x86…

作者头像 李华
网站建设 2026/9/10 23:57:10

GPS北斗双模公交调度方案:从车载终端选型到到站预报的落地实践

我在公交站等车时经常会看那个电子站牌&#xff0c;上面写着"XX路还有3分钟进站"&#xff0c;结果等了8分钟车才到。刚开始我也吐槽电子站牌不准&#xff0c;后来跟公交运营的朋友聊深了才发现&#xff0c;问题不在站牌本身&#xff0c;而在于很多公交公司连自己调度…

作者头像 李华