🩺

ESLint の Legacy Config と Flat Config における Plugin 構造の違いと両対応 Plugin の構造

2024/04/26に公開

概要

ESLint v9 への対応を進めていると、いくつか Flat Config に対応していない Plugin や Config に遭遇することがあります。その際に、Legacy Config のみ対応している Plugin と、Flat Config も対応している Plugin の両方の構造の違いを知っておくと、Flat Config に向けた対応をすすめる上で便利だったため、その違いを俯瞰するための資料としてまとめました。

前提

  • 以下で例示する Plugin は、パッケージ名を eslint-plugin-example として、いくつかのカスタムルールと、カスタム Processor を持つものを想定しています。
  • Plugin の実装方法や API の解説はしない。

Legacy Config Plugin の構造

参照したドキュメント
https://eslint.org/docs/v8.x/extend/plugins

Plugin のコード例

const plugin = {
  meta: {
    name: 'eslint-plugin-example',
    version: '1.0.0'
  },
  configs: {
    recommended: {
      // この plugin を利用する設定。パブリッシュ時のパッケージ名に依存している
      plugins: ['example'], 
      rules: {
        "example/my-rule": "error" 
      }
    }
  },
  rules: {
    "my-rule": myRule
  },
  processors: {
    markdown: myMarkdownProcessor
  }
}

module.exports = plugin

Plugin の利用例

Legacy Config の場合は、 pluginextends で指定する名称は、命名規則に基づいて、node_modules から解決されるようになっています。

Plugin は利用するが、Shareable Config は利用しない場合

modules.exports = {
  plugins: ["example"],

  // Plugin で利用可能になった Custom Rule の利用
  rules: {
    // plugin の名称にあわせて、custom rule の prefix が予め決まっている
    // 例) `eslint-plugin-sample` の場合は、`sample/` になる
    "example/my-rule": "warn",
  },

  // Plugin で利用可能になった Processor の利用
  processor: "example/my-processor",
}

Shareable Config を利用する場合

パッケージ名によって、extends で指定できる名称の省略方法が異なる。

パッケージ名が eslint-plugin- から始まる場合
modules.exports = {
  extends: ["plugins:example/recommended"],
}
パッケージ名が eslint-config- から始まる場合
modules.exports = {
  extends: ["example/recommended"],
}

Flat Config Plugin の構造

参照したドキュメント
https://eslint.org/docs/latest/extend/plugins

Plugin のコード例

const plugin = {
  // デバッグや、キャッシュに利用される
  meta: {
    name: 'eslint-plugin-example',
    version: '1.0.0'
  },

  // Shareable Config の登録先
  configs: {},

  // Custom Rule
  rules: {
    "my-rule": myRule
  },
  processors: {
    hoge: myMarkdownProcessor
  }
};

// plugins フィールドで plugin object を参照する必要があるため、
// 以下のようにして Config を登録
// plugin.configs に登録せずに、別モジュールとして提供してもよい。
Object.assign(plugin.configs, {
  recommended: [
    {
      plugins: { example: plugin }
      rules: {
        "example/my-rule": "error" 
      }
    },
  ]
});

module.exports = plugin;

Plugin の利用例

Flat Config の プラグインを自分で(ESLing 側が)解決しない という特徴どおり、eslint.config.js にて、利用する Plugin を読み込んで利用する必要があります。個人的には、Legacy Config に比べると明示的で分かりやすくなったと感じています。

Plugin のみを利用

const examplePlugin = require("eslint-plugin-example");

module.exports = [
  {
    // Plugin の利用
    plugins: {
      example: examplePlugin,
    },

    // Plugin で利用可能になった Custom Rule の利用
    // `example/` の部分は、`plugins.example` と対応しており、
    // `<prefix>/<rule>` の形式で指定する
    // 例) `plugins.sample` としたなら、`sample/` となる
    rules: {
      "example/my-rule": "warn",
    },

    // Plugin で利用可能になった Processor の利用
    // `rules` と同じ規則の prefix を `<prefix>/<processor>` の形式で指定する
    processor: "example/my-processor",
  }
];

Shareable Config を利用

const examplePlugin = require("eslint-plugin-example");

module.exports = [
  examplePlugin.configs.recommended, 
  {
    // 追加の設定
  }
];

両者の構造見ての雑感

configs 以外は一緒

Flat Config では、processor のキーに拡張子 (.md など)が利用できないという仕様の点での違いはありつつも、構造としては configs 以外は同じであることが分かります。
実際、 @types/eslintPlugin をみると configs の型が ConfigDataFlatConfig の Union 型の Record になっています。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/eslint/index.d.ts#L1375-L1380

ものによっては、FlatCompat を使わずに Flat Config に移行できそう

Flat Config も rulesprocessor の構文は同じため、例えば、eslint-plugin-react-hooksのように、Sharable Config が pluginsrules のみの単純なものの場合は、
https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/index.js#L13-L26

以下のように、plugin の指定と、config の指定を分割して行うだけで済む事がわかります。

const pluginReactHooks = require('eslint-plugin-react-hooks')

module.exports = [
  {
    plugins: {
      // configs.recommended.rules で指定されている rule の
      // prefix が `react-hooks` になっているため
      'react-hooks': pluginReactHooks
    },
  },
  {
    rules: {
      ...pluginReactHooks.configs.recommended.rules
    }
  }
]

オマケ

両対応 Plugin の構造

configs 以外は基本的に同じ構造をしているため、公開されている Plugin では以下の2パターンをみかけます。

  • configs に Legacy と Flat の両方の Shareable Config を入れているパターン
  • main では、Legacy Config の構造をとり、別ファイルで Flat Config の Shareable Config を export しているパターン

以下は、1つめの「configs に Legacy と Flat の両方の Shareable Config を入れているパターン」を例示したものになります。

const plugin = {
  // デバッグや、キャッシュに利用される
  meta: {
    name: 'eslint-plugin-example',
    version: '1.0.0'
  },
  
  // Custom Rule
  rules: {
    "my-rule": myRule
  },
  
  processors: {
    hoge: myMarkdownProcessor
  }
};

const legacyConfigs = {
  recommended: {
    plugins: ['example']
      rules: {
        "example/my-rule": "error" 
      }
    }
  }
}

const flatConfigs = {
  'flat/recommended': {
    plugins: {
      example: plugin
    },
    rules: plugin.configs.recommended.rules
  }
}

module.exports = {
  ...plugin,
  configs: {
    ...legacyConfigs,
    ...flatConfigs,
  }
};

資料

株式会社ゆめみ

Discussion