编写 JS 插件
ESLint 兼容 API
Oxlint 提供与 ESLint 相同的插件 API。请参阅 ESLint 关于创建插件和自定义规则的文档。
一个简单的插件,标记包含超过 5 个类声明的文件:
// plugin.js
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};
const plugin = {
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
};
export default plugin;{
"jsPlugins": ["./plugin.js"],
"rules": {
"best-plugin-ever/max-classes": "error"
}
}import { defineConfig } from "oxlint";
export default defineConfig({
jsPlugins: ["./plugin.js"],
rules: {
"best-plugin-ever/max-classes": "error",
},
});替代 API
Oxlint 还提供了一个略有不同的替代 API,性能更高。
使用此 API 创建的规则仍与 ESLint 兼容(参见下方)。
与上面相同的规则,使用替代 API:
import { eslintCompatPlugin } from "@oxlint/plugins";
const rule = {
createOnce(context) {
// 定义计数器变量
let classCount;
return {
before() {
// 在遍历每个文件的 AST 之前重置计数器
classCount = 0;
},
// 与之前相同
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};
const plugin = eslintCompatPlugin({
meta: {
name: "best-plugin-ever",
},
rules: {
"max-classes": rule,
},
});
export default plugin;区别在于:
- 将插件对象包装在
eslintCompatPlugin(...)中。
- const plugin = {
+ const plugin = eslintCompatPlugin({- 使用
createOnce代替create。
- create(context) {
+ createOnce(context) {create(ESLint 的 API)会_为每个文件_重复调用,而createOnce只调用一次。 在before钩子中执行任何文件级别的设置。
- let classCount = 0;
+ let classCount;
return {
+ before() {
+ classCount = 0; // 重置计数器
+ },
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};eslintCompatPlugin 做什么?
eslintCompatPlugin 为插件中的每个规则添加一个 create 方法,该方法委托给 createOnce。
这意味着该插件可以在 Oxlint 或 ESLint 中使用。
- 在 Oxlint 中,它将从更快的
createOnceAPI 获得性能提升。 - 在 ESLint 中,它的工作方式与使用原始 ESLint
createAPI 编写的一样。
如果您要将插件发布到 NPM,请将 @oxlint/plugins 作为_运行时_依赖(而不是开发依赖)添加。
跳过 AST 遍历
从 before 钩子返回 false 会导致规则跳过此文件。
// 此规则不在以 `// @skip-me` 注释开头的文件上运行
const rule = {
createOnce(context) {
return {
before() {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return false;
}
},
FunctionDeclaration(node) {
// 做一些事情
},
};
},
};这相当于 ESLint 中的这种模式:
const rule = {
create(context) {
if (context.sourceCode.text.startsWith("// @skip-me")) {
return {};
}
return {
FunctionDeclaration(node) {
// 做一些事情
},
};
},
};before 钩子
before 钩子在访问 AST 之前运行。
重要:before 钩子不保证在每个文件上运行。
目前它会运行,但将来我们打算在 Rust 端添加逻辑来确定规则是否需要运行,这基于规则"感兴趣的"AST 节点以及 AST 包含的内容。这将通过跳过从 Rust 到 JS 的冗余调用来实现更好的性能。
在上面的示例中,如果文件不包含任何 FunctionDeclaration,在该文件上运行规则将被完全跳过,_包括_跳过 before 钩子。
如果您需要代码在每个文件上始终运行一次,请实现 Program 访问器:
const rule = {
createOnce(context) {
return {
Program(node) {
// 这对每个文件都会运行,即使它
// 不包含任何 `FunctionDeclaration`
},
FunctionDeclaration(node) {
/* 做一些事情 */
},
};
},
};after 钩子
还有一个 after 钩子。它对每个文件运行一次,_在_整个 AST 被遍历之后(在 Program:exit 之后)。
用它来清理在规则的 AST 遍历期间使用的任何昂贵资源。
如果 before 钩子返回 false 以跳过在该文件上运行规则,after 钩子也会被跳过。
与 before 钩子相同,after 钩子不保证在每个文件上运行(参见上方)。
为什么替代 API 更快?
简短回答:目前并不是。但它_很快就会是_。
在 JS 插件的初始技术预览版发布之前,我们经历了漫长的"研发"过程。我们发现了许多优化机会,并已经原型化了_下一版_的 Oxlint 插件,它具有_极其_好的性能。
其中许多优化不在当前版本中,但我们将在未来几个月内完善它们并将其整合到 Oxlint 中。
替代 API 旨在启用并利用这些优化。现在采用替代 API 的插件作者将看到他们的插件在未来"免费"获得显著的性能提升,只需升级 oxlint 版本,无需任何代码更改。
这些优化是什么?
回到上面"不超过 5 个类"的规则示例:
const rule = {
create(context) {
let classCount = 0;
return {
ClassDeclaration(node) {
classCount++;
if (classCount === 6) {
context.report({ message: "Too many classes", node });
}
},
};
},
};create 方法对每个文件调用一次,每次使用一个新的 context 对象。
为什么这是一个问题?
为了获得最大性能,理想情况下我们希望静态地知道规则"感兴趣的"AST 节点。有了这些信息,我们可以执行 2 个优化:
不在 JS 端遍历 AST。相反,在 Rust 端遍历 AST 时,编译一个指向相关 AST 节点的"指针"列表。将该列表发送到 JS,JS 可以"跳转"直接到相关 AST 节点,而不是搜索整个 AST。
如果 AST 不包含_任何_与规则感兴趣的节点匹配的 AST 节点(在上面的示例中,如果文件不包含类声明),则完全跳过为该文件调用 JS。
但 JS 是一种动态语言,create 可以做_任何事情_。它每次被调用时可能返回一个完全不同的访问器。所以我们必须调用 create 才能知道是否需要调用 create!
相比之下,使用替代 API,createOnce 只调用一次,然后我们就知道规则做什么。这使上述优化成为可能。
需要澄清的是,create API 并不是 ESLint 方面的糟糕设计决策。它只是在 Rust-JS 互操作介入后带来了一些困难。
下一步
请参阅 API 支持部分,了解 Oxlint 插件支持的 ESLint API。