Skip to content

Написание JS-плагинов

INFO

JS-плагины сейчас в alpha и активно развиваются.

API должны вести себя как в ESLint. Любые отличия — баг; сообщите об этом.

API, совместимый с ESLint

В Oxlint API плагинов совпадает с ESLint. См. документацию ESLint: создание плагина и пользовательские правила.

Простой плагин, ругающийся на файлы с более чем пятью объявлениями классов:

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) {
    // Define counter variable
    let classCount;

    return {
      before() {
        // Reset counter before traversing AST of each file
        classCount = 0;
      },
      // Same as before
      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. В ESLint create вызывается для каждого файла; createOnce — один раз. Подготовку на файл делайте в хуке before.
diff
-     let classCount = 0;
+     let classCount;

      return {
+       before() {
+         classCount = 0; // Reset counter
+       },
        ClassDeclaration(node) {
          classCount++;
          if (classCount === 6) {
            context.report({ message: "Too many classes", node });
          }
        },
      };

Что делает eslintCompatPlugin?

eslintCompatPlugin добавляет каждому правилу метод create, который делегирует в createOnce.

Плагин можно использовать и в Oxlint, и в ESLint.

  • В Oxlint получаете выгоду от более быстрого API createOnce.
  • В ESLint поведение как у классического create.

Публикуя плагин в NPM, добавьте @oxlint/plugins как runtime-зависимость (не dev).

Пропуск обхода AST

Если before возвращает false, правило пропускает файл.

js
// This rule does not run on files which start with a `// @skip-me` comment
const rule = {
  createOnce(context) {
    return {
      before() {
        if (context.sourceCode.text.startsWith("// @skip-me")) {
          return false;
        }
      },
      FunctionDeclaration(node) {
        // Do stuff
      },
    };
  },
};

Эквивалент в ESLint:

js
const rule = {
  create(context) {
    if (context.sourceCode.text.startsWith("// @skip-me")) {
      return {};
    }

    return {
      FunctionDeclaration(node) {
        // Do stuff
      },
    };
  },
};

Хук before

before выполняется до обхода AST.

ВАЖНО: для каждого файла вызов before не гарантирован.

Сейчас он вызывается, но позже планируется логика на стороне Rust: вызывать правило только если нужны интересующие его узлы AST — ради производительности.

В примере выше, если в файле нет FunctionDeclaration, правило для файла не запускается вообще, в том числе без вызова before.

Если код должен выполняться для каждого файла, используйте посетитель Program:

js
const rule = {
  createOnce(context) {
    return {
      Program(node) {
        // This always runs for every file, even if it
        // doesn't contain any `FunctionDeclaration`s
      },
      FunctionDeclaration(node) {
        /* do stuff */
      },
    };
  },
};

Хук after

Есть и хук after: один раз на файл после полного обхода AST (после Program:exit).

Используйте для освобождения ресурсов после обхода.

Если before вернул false, after не вызывается.

Как и для before, вызов after для каждого файла не гарантирован (см. выше).

Почему альтернативный API быстрее?

Коротко: сейчас — не быстрее. Но скоро будет.

Перед превью JS-плагинов был длительный R&D: найдены возможности оптимизации, прототипирован следующий уровень плагинов Oxlint с очень высокой производительностью.

Многие оптимизации ещё не в текущем релизе; в ближайшие месяцы их доведут и включат в Oxlint.

Альтернативный API рассчитан на эти оптимизации. Переходя на него сейчас, авторы плагинов позже получат существенный прирост скорости без правок кода — достаточно обновить версию oxlint.

Какие именно оптимизации?

Вернёмся к правилу «не больше пяти классов»:

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 интересны правилу. Тогда возможны две оптимизации:

  1. Не обходить AST в JS: при обходе в Rust собрать «указатели» на нужные узлы, передать список в JS — и прыгать сразу к ним, а не искать всё дерево.

  2. Если в AST нет подходящих узлов (в примере — объявлений классов), не вызывать JS для этого файла.

JS динамичен: create может что угодно вернуть. Приходится вызывать create, чтобы узнать, нужен ли вызов create.

С альтернативным API createOnce вызывается один раз — и поведение правила становится известно заранее, включая перечисленные оптимизации.

API create — не «плохое решение» ESLint; при связке Rust↔JS оно создаёт дополнительные сложности.

Дальше

Раздел Поддержка API — какие API ESLint доступны в плагинах Oxlint.