Skip to content

JS 插件

Oxlint 支持以 JavaScript 编写的插件——无论是自定义编写,还是来自 npm。

Oxlint 的插件 API 与 ESLint v9+ 兼容,因此大多数现有的 ESLint 插件在 Oxlint 中可开箱即用。

我们正在努力实现 所有 ESLint 的插件 API,很快 Oxlint 就能运行任意 ESLint 插件。

WARNING

JS 插件目前处于技术预览阶段,仍在持续开发中。 几乎所有的 ESLint 插件 API 已经实现(参见 下方)。

所有 API 的行为应与 ESLint 完全一致。如果发现任何行为差异, 那是一个缺陷——请 报告问题

使用 JS 插件

  1. .oxlintrc.json 配置文件的 jsPlugins 下添加插件路径。
  2. rules 下添加来自该插件的规则。

路径可以是任意有效的导入说明符,例如 ./plugin.jseslint-plugin-foo,或 @foo/eslint-plugin。 路径相对于配置文件自身进行解析。

json
// .oxlintrc.json
{
  "jsPlugins": ["./path/to/my-plugin.js", "eslint-plugin-whatever", "@foobar/eslint-plugin"],
  "rules": {
    "my-plugin/rule1": "error",
    "my-plugin/rule2": "warn",
    "whatever/rule1": "error",
    "whatever/rule2": "warn",
    "@foobar/rule1": "error"
  }
  // ... 其他配置 ...
}

插件别名

你还可以为插件定义一个不同的名称(别名)。这在以下情况下非常有用:

  • 默认插件名称与 Oxlint 内置插件名称冲突(例如:jsdoc、react 等)。
  • 默认插件名称过长。
  • 你想使用 Oxlint 原生支持的插件,但特定规则尚未在 Oxlint 的原生版本中实现。
json
{
  "jsPlugins": [
    // `jsdoc` 是保留名称,因为 Oxlint 原生支持它
    {
      "name": "jsdoc-js",
      "specifier": "eslint-plugin-jsdoc"
    },
    // 缩短名称
    {
      "name": "short",
      "specifier": "eslint-plugin-with-name-so-very-very-long"
    },
    // 列出不需要别名的插件,仅作为说明符
    "eslint-plugin-whatever"
  ],
  "rules": {
    "jsdoc-js/check-alignment": "error",
    "short/rule1": "error",
    "whatever/rule2": "error"
  }
}

编写 JS 插件

与 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: "太多类了", node });
        }
      },
    };
  },
};

const plugin = {
  meta: {
    name: "best-plugin-ever",
  },
  rules: {
    "max-classes": rule,
  },
};

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

替代 API

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

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

上面规则的替代实现方式:

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: "太多类了", 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: "太多类了", 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 层添加逻辑,根据规则“感兴趣”的节点类型以及实际存在的节点,判断是否需要运行规则。 这将通过避免从 Rust 到 JS 的冗余调用,实现更好的性能。

在上例中,如果某个文件不包含任何 FunctionDeclaration,则对该文件的规则执行将被完全跳过, 包括跳过 before 钩子

如果你需要确保代码在每个文件上都只运行一次,请改用 Program 访问器:

js
const rule = {
  createOnce(context) {
    return {
      Program(node) {
        // 此代码对每个文件都会执行,即使文件中不包含任何
        // `FunctionDeclaration` 也一样
      },
      FunctionDeclaration(node) {
        /* 执行操作 */
      },
    };
  },
};

after 钩子

还有一个 after 钩子。它在每个文件的整个 AST 遍历完成后运行(在 Program:exit 之后)。

可用于清理规则在遍历过程中使用的昂贵资源。

如果 before 钩子返回 false 以跳过文件上的规则执行,则 after 钩子也会被跳过。

before 钩子类似,after 钩子 也不保证在每个文件上都运行(参见 上方)。

为什么替代 API 更快?

简短回答:目前还不是。但 很快就会成为现实

在首次技术预览版发布前,我们经历了一段漫长的“研发”过程。我们已经识别出许多优化机会,并原型化了下一代 Oxlint 插件,其性能表现 极其优异

其中许多优化尚未包含在当前版本中,但我们将在接下来的几个月里逐步完善并集成到 Oxlint 中。

替代 API 的设计正是为了启用和利用这些优化。现在采用该 API 的插件作者,在未来只需升级 oxlint 版本(无需修改代码),即可免费获得显著的速度提升。

这些优化具体是什么?

回到前面“不超过 5 个类”的规则示例:

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "太多类了", node });
        }
      },
    };
  },
};

create 方法在每个文件上都会被调用一次,每次传入一个新的 context 对象。

为什么这是个问题?

为了达到最佳性能,理想情况是我们能静态地知道规则“关心”的哪些 AST 节点。有了这个信息,我们可以执行两项优化:

  1. 不要在 JS 侧遍历整个 AST。相反,在 Rust 侧遍历时,编译一份“相关节点”的指针列表。将该列表发送到 JS,JS 可直接“跳转”到相关节点,而不是在整个 AST 中搜索。
  2. 如果文件中没有任何规则感兴趣的节点(例如,文件中没有类声明),则完全跳过对该文件的 JS 调用。

但 JS 是一门动态语言,create 可能会做 任何事情。它可能每次调用时都返回一个完全不同的访问器。因此我们必须调用 create 才能确定是否需要调用!

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

需要强调的是,create API 并非 ESLint 设计上的失误。只是在引入 Rust-JS 互操作后带来了某些挑战。

API 支持

Oxlint 几乎支持 ESLint 的全部 API 表面功能:

  • AST 遍历。
  • AST 探索(node.parentcontext.sourceCode.getAncestors)。
  • 修复(Fixes)。
  • 规则选项。
  • 选择器(ESLint 文档)。
  • SourceCode API(如 context.sourceCode.getText(node))。
  • SourceCode 令牌 API(如 context.sourceCode.getTokens(node))。
  • 作用域分析。
  • 控制流分析(代码路径)。
  • 行内禁用指令(// oxlint-disable)。
  • 语言服务器(IDE)支持 + 建议(编辑器内诊断和快速修复)。

尚未支持:

  • 自定义文件格式和解析器(如 Svelte、Vue、Angular)。
  • 依赖于 TypeScript 类型感知的规则。

ESLint v9 及更早版本中已移除的 API 在大多数情况下将不会被实现。如果某个 ESLint 插件已不再维护且从未更新以适配 ESLint v9 的 API,你可能需要自行修改插件或寻找替代品。

我们将在未来几个月内逐步实现剩余功能,目标是支持 100% 的 ESLint 插件 API 表面功能。