Skip to content

编写 JS 插件

INFO

JS 插件目前处于 alpha 阶段,仍在积极开发中。

所有 API 的行为应与 ESLint 相同。如果您发现任何行为差异,那是 bug——请报告它

ESLint 兼容 API

Oxlint 提供与 ESLint 相同的插件 API。请参阅 ESLint 关于创建插件自定义规则的文档。

一个简单的插件,标记包含超过 5 个类声明的文件:

js
// 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;
json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/max-classes": "error"
  }
}
ts
import { defineConfig } from "oxlint";

export default defineConfig({
  jsPlugins: ["./plugin.js"],
  rules: {
    "best-plugin-ever/max-classes": "error",
  },
});

替代 API

Oxlint 还提供了一个略有不同的替代 API,性能更高。

使用此 API 创建的规则仍与 ESLint 兼容(参见下方)。

与上面相同的规则,使用替代 API:

js
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;

区别在于:

  1. 将插件对象包装在 eslintCompatPlugin(...) 中。
diff
- const plugin = {
+ const plugin = eslintCompatPlugin({
  1. 使用 createOnce 代替 create
diff
-   create(context) {
+   createOnce(context) {
  1. create(ESLint 的 API)会_为每个文件_重复调用,而 createOnce 只调用一次。 在 before 钩子中执行任何文件级别的设置。
diff
-     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 中,它将从更快的 createOnce API 获得性能提升。
  • 在 ESLint 中,它的工作方式与使用原始 ESLint create API 编写的一样。

如果您要将插件发布到 NPM,请将 @oxlint/plugins 作为_运行时_依赖(而不是开发依赖)添加。

跳过 AST 遍历

before 钩子返回 false 会导致规则跳过此文件。

js
// 此规则不在以 `// @skip-me` 注释开头的文件上运行
const rule = {
  createOnce(context) {
    return {
      before() {
        if (context.sourceCode.text.startsWith("// @skip-me")) {
          return false;
        }
      },
      FunctionDeclaration(node) {
        // 做一些事情
      },
    };
  },
};

这相当于 ESLint 中的这种模式:

js
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 访问器:

js
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 个类"的规则示例:

js
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 个优化:

  1. 不在 JS 端遍历 AST。相反,在 Rust 端遍历 AST 时,编译一个指向相关 AST 节点的"指针"列表。将该列表发送到 JS,JS 可以"跳转"直接到相关 AST 节点,而不是搜索整个 AST。

  2. 如果 AST 不包含_任何_与规则感兴趣的节点匹配的 AST 节点(在上面的示例中,如果文件不包含类声明),则完全跳过为该文件调用 JS。

但 JS 是一种动态语言,create 可以做_任何事情_。它每次被调用时可能返回一个完全不同的访问器。所以我们必须调用 create 才能知道是否需要调用 create

相比之下,使用替代 API,createOnce 只调用一次,然后我们就知道规则做什么。这使上述优化成为可能。

需要澄清的是,create API 并不是 ESLint 方面的糟糕设计决策。它只是在 Rust-JS 互操作介入后带来了一些困难。

下一步

请参阅 API 支持部分,了解 Oxlint 插件支持的 ESLint API。