Documentation Next.js

はじめに

Next.js App Routerプロジェクトでは、ESLintとPrettierを適切に設定することで、コード品質の維持とチーム間のスタイル統一を実現できます。本記事では、ESLint Flat Config形式での最新の設定方法を解説します。

ESLintとPrettierの役割

ツール役割
ESLintコード品質・問題検出未使用変数、型エラー、セキュリティ問題
Prettierコードフォーマットインデント、クォート、行の長さ

パッケージのインストール

# ESLint関連
npm install -D eslint @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks

# Prettier関連
npm install -D prettier eslint-config-prettier

# Next.js ESLint
npm install -D @next/eslint-plugin-next

# インポート順序
npm install -D eslint-plugin-import eslint-plugin-simple-import-sort

# Husky & lint-staged
npm install -D husky lint-staged

ESLint Flat Config設定

eslint.config.mjs

// eslint.config.mjs
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import nextPlugin from '@next/eslint-plugin-next';
import importPlugin from 'eslint-plugin-import';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import prettier from 'eslint-config-prettier';

export default tseslint.config(
  // グローバル無視パターン
  {
    ignores: [
      '.next/**',
      'node_modules/**',
      'out/**',
      'public/**',
      '*.config.js',
      '*.config.mjs',
    ],
  },

  // 基本設定
  js.configs.recommended,
  ...tseslint.configs.recommended,

  // React設定
  {
    files: ['**/*.{ts,tsx}'],
    plugins: {
      react,
      'react-hooks': reactHooks,
    },
    languageOptions: {
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      'react/react-in-jsx-scope': 'off',
      'react/prop-types': 'off',
      'react/no-unescaped-entities': 'off',
    },
  },

  // Next.js設定
  {
    files: ['**/*.{ts,tsx}'],
    plugins: {
      '@next/next': nextPlugin,
    },
    rules: {
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs['core-web-vitals'].rules,
    },
  },

  // インポート順序
  {
    files: ['**/*.{ts,tsx}'],
    plugins: {
      import: importPlugin,
      'simple-import-sort': simpleImportSort,
    },
    rules: {
      'simple-import-sort/imports': [
        'error',
        {
          groups: [
            // Node.js builtins
            ['^node:'],
            // React/Next.js
            ['^react', '^next'],
            // External packages
            ['^@?\\w'],
            // Internal aliases
            ['^@/'],
            // Parent imports
            ['^\\.\\.'],
            // Same folder imports
            ['^\\.'],
            // Style imports
            ['^.+\\.s?css$'],
          ],
        },
      ],
      'simple-import-sort/exports': 'error',
      'import/first': 'error',
      'import/newline-after-import': 'error',
      'import/no-duplicates': 'error',
    },
  },

  // TypeScript追加ルール
  {
    files: ['**/*.{ts,tsx}'],
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'error',
        {
          argsIgnorePattern: '^_',
          varsIgnorePattern: '^_',
        },
      ],
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/consistent-type-imports': [
        'error',
        {
          prefer: 'type-imports',
          fixStyle: 'inline-type-imports',
        },
      ],
      '@typescript-eslint/no-import-type-side-effects': 'error',
    },
  },

  // テストファイル
  {
    files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
    },
  },

  // Prettierとの競合回避(最後に配置)
  prettier
);

Prettier設定

.prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "useTabs": false,
  "bracketSpacing": true,
  "bracketSameLine": false,
  "arrowParens": "always",
  "endOfLine": "lf",
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.ts"
}

.prettierignore

.next
node_modules
out
public
pnpm-lock.yaml
package-lock.json
*.md

VSCode設定

.vscode/settings.json

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "eslint.useFlatConfig": true,
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

.vscode/extensions.json

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "bradlc.vscode-tailwindcss"
  ]
}

Husky & lint-staged設定

Huskyのセットアップ

npx husky init

.husky/pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

package.jsonにlint-staged設定

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "typecheck": "tsc --noEmit"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{js,jsx,mjs}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,css,scss,md}": [
      "prettier --write"
    ]
  }
}

GitHub Actions CI

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run ESLint
        run: pnpm lint

      - name: Check formatting
        run: pnpm format:check

      - name: TypeScript type check
        run: pnpm typecheck

      - name: Build
        run: pnpm build

カスタムESLintルール追加例

コンポーネント命名規則

// eslint.config.mjs に追加
{
  files: ['**/components/**/*.tsx'],
  rules: {
    // コンポーネントファイルはPascalCaseで命名
    'react/jsx-pascal-case': 'error',
  },
},

Server Components用ルール

// eslint.config.mjs に追加
{
  files: ['**/app/**/*.tsx'],
  rules: {
    // Server ComponentsでuseStateなどの使用を警告
    'react-hooks/rules-of-hooks': 'off',
  },
},
{
  files: ['**/components/**/*.tsx'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
  },
},

トラブルシューティング

ESLintとPrettierの競合

// eslint.config.mjs の最後に prettier を配置
import prettier from 'eslint-config-prettier';

export default tseslint.config(
  // ... 他の設定
  prettier // 必ず最後に配置
);

型インポートの自動修正

// tsconfig.json
{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

パフォーマンス最適化

// eslint.config.mjs
{
  languageOptions: {
    parserOptions: {
      project: true,
      tsconfigRootDir: import.meta.dirname,
    },
  },
},

まとめ

設定項目ファイル
ESLinteslint.config.mjs
Prettier.prettierrc
VSCode.vscode/settings.json
Git Hooks.husky/pre-commit
CI.github/workflows/ci.yml

参考文献

円