React Japan light logo
article hero
Steven Sacks
スティーブン・サックス

ESLint - 基盤

以下はESLint、Prettier、Stylelint、およびeditorconfigに関する私の推奨事項です。

ESLint v8.57.0

まず第一に、ESLint v9が利用可能であるにもかかわらず、ほとんどのプラグインがv9と互換性がないため、互換性のあるプラグインが増えるまで、v8.57.0を使用することを強くお勧めします。

プラグイン

これらは、あなたのESLintセットアップの基盤となるルールセットを持つプラグインです(一部カスタマイズされています)。

こちらが.eslintrc.cjsファイルでの私の推奨設定です。ルールの設定を必要に応じて変更することもできますが、これらのルールはすべて何らかの値に設定されているべきだと考えています。このセットアップはjsおよびtsファイル(cjsmjsなども含む)をサポートしています。この記事のためのコメントも追加してありますが、削除しても構いません。

const shared = {
  extends: [
    'eslint:recommended',
    'plugin:eslint-comments/recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:sonarjs/recommended-legacy',
    'plugin:tailwindcss/recommended',
    'plugin:unicorn/recommended',
    'prettier',
  ],
  plugins: [
    'react',
    'sonarjs',
    'unicorn',
    'tailwindcss', // tailwindを使っている場合
    'you-dont-need-lodash-underscore', // lodashを使っている場合
  ],
  rules: [
    'consistent-return': 'off',
    curly: ['error', 'all'],
    
    'eslint-comments/disable-enable-pair': 'off',
    'eslint-comments/no-unused-disable': 'error',
    
    'import/extensions': 'off',
    'import/no-anonymous-default-export': [
      'error',
      {
        allowArray: true,
        allowLiteral: true,
        allowObject: true,
      },
    ],
    'import/order': 'off', // 他のpluginにより処理される
    'import/prefer-default-export': 'off',
    
    'jsx-a11y/anchor-is-valid': [
      'error',
      {
        components: ['Link'], // RemixやNextなどを使っている場合
        specialLink: ['to'],
      },
    ],
    
    'max-params': ['error'], // デフォルトは3です
    'padding-line-between-statements': [
      'error',
      {
        blankLine: 'always',
        next: ['block-like', 'export', 'return'],
        prev: '*',
      },
    ],
    quotes: [
      'error',
      'single',
      {
        allowTemplateLiterals: false,
        avoidEscape: true,
      },
    ],
    
    'react/boolean-prop-naming': [
      'error',
      {
        propTypeNames: ['bool', 'mutuallyExclusiveTrueProps'],
        rule: '^((is|has|can|show|hide|no)[A-Z]([A-Za-z0-9]?)+|(show|hide|disabled|required))',
      },
    ],
    'react/function-component-definition': 'off',
    'react/jsx-boolean-value': ['error', 'always'],
    'react/jsx-filename-extension': ['error', {extensions: ['.tsx']}],
    'react/jsx-newline': ['error', {prevent: true}],
    'react/jsx-props-no-spreading': 'off',
    'react/prop-types': 'off', // propsにはTypeScriptを使いましょう
    'react/require-default-props': 'off',
    
    'spaced-comment': 'off', // 論理としてはいいのですが、実際には使いにくいです
    
    // prettier-plugin-tailwindcssによって処理されます
    'tailwindcss/classnames-order': 'off',
    
    'unicorn/consistent-destructuring': 'error',
 
    // これらはReactとの互換性のためにoffであるべきものです
    // このうちのいくつかは私の意見として問題になるものです
    'unicorn/new-for-builtins': 'off',
    'unicorn/no-array-callback-reference': 'off',
    'unicorn/no-array-for-each': 'off',
    'unicorn/no-array-reduce': 'off',
    'unicorn/no-null': 'off',
    'unicorn/no-useless-undefined': 'off',
    'unicorn/prefer-export-from': 'off',
    'unicorn/prefer-set-has': 'off',
    'unicorn/prefer-switch': 'off',
    'unicorn/prefer-ternary': 'off',
    
    'unicorn/prevent-abbreviations': [
      'error',
      {
        // これらは私の趣向です
        ignore: [
          'acc',
          'ctx',
          'e2e',
          'env',
          'obj',
          'prev',
          'req',
          'res',
          /args/i,
          /fn/i,
          /params/i,
          /props/i,
          /ref/i,
          /utils/i,
        ],
      },
    ],
    
    'unicorn/text-encoding-identifier-case': 'off',
  ],
};
 
module.exports = {
  root: true,
  env: {
    browser: true,
    commonjs: true,
    es2024: true,
    node: true,
  },
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  ignorePatterns: ['node_modules', 'build'],
  extends: ['airbnb', ...shared.extends],
  plugins: shared.plugins,
  rules: shared.rules,
  overrides: [
    {
      files: ['**/*.ts?(x)'],
      extends: [
        'airbnb',
        'airbnb-typescript',
        'plugin:@typescript-eslint/recommended',
        'plugin:import/recommended',
        'plugin:import/typescript',
        ...shared.extends,
      ],
      parser: '@typescript-eslint/parser',
      parserOptions: {
        project: './tsconfig.json',
      },
      plugins: [
        ...shared.plugins,
        '@typescript-eslint',
        'import',
      ],
      settings: {
        'import/internal-regex': '^~/', // なぜチルダなのかについては下参照
        'import/resolver': {
          node: {
            extensions: ['.ts', '.tsx'],
          },
          typescript: {
            alwaysTryTypes: true,
          },
        },
        react: {
          pragma: 'React',
          version: 'detect',
        },
        tailwindcss: {
          callees: ['twJoin', 'twMerge'], // tailwind-merge
        },
      },
      rules: {
        ...shared.rules,
        'no-undef': 'off', // TypeScriptが処理します
        'no-unused-vars': 'off', // TypeScriptが処理します
        '@typescript-eslint/array-type': ['error', {default: 'generic'}],
        '@typescript-eslint/ban-ts-comment': 'off',
        '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
        '@typescript-eslint/method-signature-style': 'error',
        '@typescript-eslint/no-throw-literal': 'off',
        '@typescript-eslint/no-non-null-assertion': 'off',
        '@typescript-eslint/no-unnecessary-boolean-literal-compare': ['error'],
        '@typescript-eslint/no-unused-vars': 'error',
      },
    },
    {
      files: ['*.tsx', '**/hooks/*.ts?(x)'],
      rules: {
        // コンポーネントやフックで違反しやすいものです
        'sonarjs/cognitive-complexity': 'off',
      },
    },
    {
      files: ['src/?(components|pages|services|utils)/**/*.ts?(x)'],
      rules: {
        'max-lines': ['error', 200],
      },
    },
    {
      files: ['./*.ts'], // プロジェクトのルートにあるファイルのみ
      rules: {
        'global-require': 'off',
        'import/no-extraneous-dependencies': 'off',
        'import/no-unresolved': 'error',
        'import/prefer-default-export': 'off',
        'no-void': 'off',
        'unicorn/prefer-module': 'off',
        'unicorn/prevent-abbreviations': 'off',
      },
    },
    {
      files: ['**/*.d.ts'],
      rules: {
        '@typescript-eslint/consistent-type-definitions': 'off',
        '@typescript-eslint/method-signature-style': 'error',
        '@typescript-eslint/no-unused-vars': 'off',
      },
    },
  ],
};

詳細

ルールの詳細を調べ、設定を変更するかどうかはあなたにお任せします。以下はいくつかのルールについての私の持論です。

  • max-params - 関数型プログラミングでは、理想的には関数ごとにパラメーターを1つだけ持つべきです。実際には、これはやや制限が厳しすぎるかもしれませんが、3つのパラメーターの制限は、名前付きパラメーターオブジェクトに変換する前に合理的です。多すぎるパラメーターは、開発者に順序を覚えさせる必要があり、複数のオプションパラメーターも面倒です。シグネチャを調べられるかどうか、またはTypeScriptが助けてくれるかどうかは重要ではありません。実際には、この制限の方が良いです。Reactコンポーネントは、propsが単一のオブジェクトパラメーターであるため、これが実践的な良い例です。
  • padding-line-between-statements - このルールは、一貫したコードスタイルと可読性を保証します。これは「prettier」タイプのルールであり、それが行うことを受け入れるだけで、誰もが同じようにコードをフォーマットされ、すぐに慣れることができます。
  • quotes - すべての3種類の引用符(シングル、ダブル、バッククォート)に最適な引用符を保証します。
  • react/jsx-boolean-value - 明示的であることが良いし、propsを一貫した見た目にします。これを好まない人にとっても、保存時に修正されるため、明示的に書かなくても、保存時に明示的になり、したがって誰にとっても一貫性があります。
  • import/internal-regex - Remixは~チルダを使用し、私はこれが優れていると考えています。多くのインストールされたパッケージが@を使用しているため、~を使用することで、あなたのコードとnode_modulesからのコードを区別する明確な違いがあります。これはtsconfig.json内でも構成する必要があります(paths)。
  • @typescript-eslint/array-type - 好みに応じた設定を選択してください。arraygeneric の両方には妥当な議論があります。これは自動的に片方を強制し、一貫性が目標です。開発者は、より快適な方を書くことができるように、保存時に好ましい方に自動的に修正されます。
  • max-lines - コンポーネントが大きすぎる場合、パフォーマンス、可読性、および関心の分離のために、通常はそれらをより小さなコンポーネントに分割すべきサインです。例外はあるでしょう。その場合、開発者はファイル内でこのルールを無効にすることができ、コードレビュー中に議論することができます。私の経験では、ほとんどのコンポーネントファイルは200行未満ですが、これを250行に増やすか、デフォルトの300行を使用することもできます。
  • @typescript-eslint/consistent-type-definitions - 関数型プログラミングとイミュータビリティにはtypeを使用すべきです。interfaceはOOPとミュータビリティのためです。この記事 に良い説明があります。どちらを使用するかに関係なく、一貫して1つを使用することが望ましいです。サードパーティライブラリがinterfaceを要求する場合があるので、ルールは.d.tsファイルでは無効になっています。

.prettierrc

これは私のPrettierの設定です

{
  "bracketSpacing": false,
  "experimentalTernaries": true,
  "plugins": ["prettier-plugin-tailwindcss"], // tailwindを使っている場合
  "singleQuote": true,
  "tailwindFunctions": ["twJoin", "twMerge"], // tailwindを使っている場合
  "trailingComma": "es5"
}

bracket-spacing

一部の人はブラケットのスペースを好むかもしれません。私はコードを必要以上に広くすると思いますし、それが可読性を向上させるとは思いません。もしあなたの好みならば、デフォルト設定を使い続けてください。

experimentalTernaries

これについてはこちらで読むことができます。これらは最終的にデフォルトになる予定なので、早めに慣れるために有効にしています。Prettierチームと同意見で、これらの方が優れていると考えています。

plugins

Tailwindを使用している場合は、Tailwind Prettier Pluginを使用する必要があります。

single-quote

シングルクォートを入力するのは簡単です(2つのキーを同時に押す必要がありません)、文字列内でアポストロフィやダブルクォートを使用する頻度よりも、コーディング中にシングルクォートを入力する頻度の方が上回ります。シングルクォートは長い間標準とされており、私の経験では、誰もがこれをtrueに設定しています。

tailwindFunctions

Tailwindを使用している場合は、Tailwind Mergeを使用するべきです。clsxclassnamesの代わりに。

trailingComma

トレイリングコンマは素晴らしいです!ただし、関数のパラメータでは好きではありません。閉じ括弧とアローの前に見づらくなると思います const foo = (a, b, c,) =>。そのため、これを「es5」に設定しています。

.stylelintrc.json

こちらは私のStylelintの提案設定です。

{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-idiomatic-order",
    "stylelint-config-tailwindcss" // tailwindを使っている場合
  ],
  "ignoreFiles": ["build/**/*.css", "public/build/**/*.css"],
  "overrides": [
    {
      "files": ["**/*.module.css"],
      "rules": {
        // cssモジュールでキャメルケースの名前を強制する
        "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$"
      }
    }
  ],
  "plugins": ["stylelint-order"],
  "rules": {
    "no-descending-specificity": null,
    "selector-pseudo-class-no-unknown": [
      true,
      {
        "ignorePseudoClasses": ["global", "local"]
      }
    ]
  }
}

.editorconfig

IDEはプロジェクトのルートにこのファイルを追加することで自動的にあなたのルールに従います。これは私の提案設定です。

# editorconfig.org
 
root = true
 
# これらに変更を加えないことを推奨します
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
 
[*.md]
trim_trailing_whitespace = false
 

次の記事: ESLint - ソート

主な記事: ESLintの保存時の修正でワークフローを次のレベルに上げる方法