VitePressプロジェクトにESLint 9.x Flat ConfigとStylelintを導入し、GitHub Actionsで自動化するフローを示す、親しみやすいちびキャラが描かれたテクニカルブログ用アイキャッチ画像。

ESLint 9.x Flat ConfigとStylelintをVitePressに導入する完全ガイド

VitePressで構築した静的サイトにESLint 9.xStylelintを導入しました。

ESLint 9.xから採用された新しいFlat Config形式を採用し、TypeScript/JavaScript/CSSのLint環境を整備しています。

また、GitHub ActionsでCI/CD自動Lintを実現し、コード品質の向上と一貫性のあるコードスタイルの維持を目指しました。

本記事では、導入から設定、つまづきポイントまで、実際のコードを交えて詳しく解説します。

こんな人におすすめ

この記事は、以下のような方におすすめです。

  • VitePressやVue.jsプロジェクトにLint環境を導入したい方
  • ESLint 9.xの新しいFlat Config形式を知りたい方
  • TypeScriptとJavaScriptが混在するプロジェクトでLint設定に悩んでいる方
  • GitHub Actionsで自動Lintを構築したい方
  • StylelintでCSSの品質管理を始めたい方

目次

ESLint設定の全体像

今回構築したLint環境の構成を図で示します。

flowchart TD
    subgraph ESLint
        A[eslint.config.js] --> B[JavaScript Files]
        A --> C[TypeScript Files]
        A --> D[Cloudflare Functions]

        B --> E[js.configs.recommended]
        C --> F[tseslint.configs.recommended]
        D --> G[Workers Globals]
    end

    subgraph Stylelint
        H[.stylelintrc.js] --> I[CSS Files]
        H --> J[Vue SFC]
    end

    subgraph CI/CD
        K[GitHub Actions] --> L[ESLint]
        K --> M[Stylelint]
        K --> N[Tests]
    end

ESLint 9.x Flat Configとは

ESLint 9.xから導入されたFlat Config形式eslint.config.js)を使用しました。

従来の.eslintrc.js形式と比較して、以下のメリットがあります。

特徴 従来形式(.eslintrc.js) Flat Config(eslint.config.js)
記述形式 JSON/YAML/JS JavaScript(ESM)のみ
ファイルタイプ別設定 overridesで記述 files配列で簡潔に
共有設定 extendsで継承 スプレッド構文で合成
TypeScript対応 別途設定が必要 ネイティブサポート向上

特に、filesプロパティでファイルタイプごとに異なる設定を適用できるため、TypeScriptとJavaScriptで別々のパーサーを使用する構成が実現しやすくなっています。

eslint.config.js(新規)

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import globals from 'globals';

export default [
  // Global ignores
  {
    ignores: [
      'node_modules/**',
      'docs/.vitepress/dist/**',
      'docs/.vitepress/cache/**',
      'dist/**',
      'coverage/**',
      '.wrangler/**',
      'build/**'
    ]
  },

  // Base JavaScript rules
  js.configs.recommended,

  // JavaScript files (.mjs, .js)
  {
    files: ['**/*.mjs', '**/*.js'],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: { ...globals.node }
    },
    rules: {
      'spaced-comment': ['error', 'always', { markers: ['/'] }],
      'no-console': 'off',
      'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
      'no-prototype-builtins': 'off'
    }
  },

  // TypeScript configuration
  ...tseslint.configs.recommended.map(config => ({
    ...config,
    files: ['**/*.ts', '**/*.mts']
  })),

  // TypeScript specific overrides
  {
    files: ['**/*.ts', '**/*.mts'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        project: null
      },
      globals: {
        ...globals.node,
        ...globals.browser
      }
    },
    rules: {
      'spaced-comment': ['error', 'always', { markers: ['/'] }],
      'no-console': 'off',
      '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/require-await': 'off',
      'no-prototype-builtins': 'off'
    }
  },

  // Cloudflare Pages Functions
  {
    files: ['functions/**/*.ts'],
    languageOptions: {
      globals: {
        ...globals.node,
        fetch: 'readonly',
        Request: 'readonly',
        Response: 'readonly',
        Headers: 'readonly'
      }
    },
    rules: { '@typescript-eslint/no-explicit-any': 'off' }
  }
];

TypeScript/JavaScript混在環境の設定

このプロジェクトでは、TypeScript.ts, .mts)とJavaScript.js, .mjs)が混在しています。

つまづいた点

最初はtseslint.configs.recommendedをグローバルに適用していましたが、これにより.jsファイルでも@typescript-eslintルールが適用されてエラーが発生しました。

解決策

filesプロパティで.ts/.mtsファイルのみにTypeScript設定を適用するように修正しました。

flowchart LR
    A[ファイル検出] --> B{拡張子判定}
    B -->|.ts / .mts| C[TypeScript Parser]
    B -->|.js / .mjs| D[JavaScript Parser]
    C --> E[tseslint.rules]
    D --> F[js.configs.rules]

日本語コメントの保護

プロジェクトで日本語コメントを使用しているため、spaced-commentルールにmarkers: ['/']オプションを指定して、日本語コメントを許可しています。

これにより、// コメント形式の日本語コメントがエラーにならず、コードの可読性を維持できます。

Cloudflare Pages Functionsの対応

Cloudflare Workers環境では、fetch, Request, Response, Headersなどのグローバル変数が利用可能です。

これらをreadonlyとして設定することで、未使用変数警告を回避しています。

Stylelintの設定(VitePress対応)

StylelintでCSSのLint設定を作成しました。

VitePressのカスタムプロパティや疑似クラス/疑似要素に対応するため、一部のルールを無効化しています。

.stylelintrc.js(新規)

export default {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-standard-vue'
  ],

  rules: {
    // VitePress対応
    'custom-property-pattern': null,
    'alpha-value-notation': null,
    'color-function-notation': null,
    'selector-class-pattern': null,
    'selector-id-pattern': null,
    'declaration-block-no-shorthand-property-overrides': null,
    'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'deep', 'slotted'] }],
    'selector-pseudo-element-no-unknown': [true, { ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] }],
    'font-family-name-quotes': null,
    'keyframes-name-pattern': null,
    'unit-allowed-list': null,
    // 日本語コメント対応
    'comment-empty-line-before': null,
    'comment-whitespace-inside': null,
    'declaration-empty-line-before': null,
    // VitePressオーバーライド用
    'declaration-no-important': null,
    'no-duplicate-selectors': null,
    'max-empty-lines': 2
  },

  ignoreFiles: [
    'node_modules/**',
    'docs/.vitepress/dist/**',
    'coverage/**',
    '**/*.min.css'
  ]
};

GitHub Actions CI/CDパイプライン

GitHub ActionsでCI/CDパイプラインを作成しました。

プッシュ時とPR時に自動的にLintが実行されます。

CI/CDフロー

flowchart LR
    A[Push / PR] --> B[Checkout]
    B --> C[Setup Node.js]
    C --> D[npm ci]
    D --> E[ESLint]
    D --> F[Stylelint]
    D --> G[Tests]
    E --> H{全てPass?}
    F --> H
    G --> H
    H -->|Yes| I[✅ Success]
    H -->|No| J[❌ Fail]

.github/workflows/lint.yml(新規)

name: Lint

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

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint:js

      - name: Run Stylelint
        run: npm run lint:css

      - name: Run tests
        run: npm test

package.json(変更)

Lintスクリプトと依存パッケージを追加しました。

{
  "scripts": {
+    "lint": "npm run lint:js && npm run lint:css",
+    "lint:fix": "npm run lint:js:fix && npm run lint:css:fix",
+    "lint:js": "eslint .",
+    "lint:js:fix": "eslint . --fix",
+    "lint:css": "stylelint \"**/*.css\"",
+    "lint:css:fix": "stylelint \"**/*.css\" --fix"
  },
  "devDependencies": {
+    "@eslint/js": "^9.39.2",
+    "eslint": "^9.39.2",
+    "eslint-plugin-vue": "^10.6.2",
+    "globals": "^17.0.0",
+    "postcss": "^8.5.6",
+    "postcss-html": "^1.8.0",
+    "stylelint": "^16.26.1",
+    "stylelint-config-standard": "^39.0.1",
+    "stylelint-config-standard-vue": "^1.0.0",
+    "typescript-eslint": "^8.52.0"
  }
}

package-lock.jsonの重要性

GitHub Actionsのnpm cache機能を使用するには、package-lock.jsonがリポジトリに含まれている必要があります。

.gitignoreから除外すると、npm ci実行時に「Dependencies lock file is not found」エラーが発生します。

ロックファイルは依存関係の再現性を保証するため、バージョン管理に含めることが推奨されます。

.gitignore(変更)

 # 依存関係
 node_modules/
-package-lock.json
 yarn.lock
 pnpm-lock.yaml

つまづきポイントと解決策

Lint警告の修正(複数ファイル)

未使用のインポートと変数を修正しました。

docs/.vitepress/config.mts:

- import { loadEnv } from 'vite'

docs/.vitepress/theme/index.ts:

- import { useRouter } from 'vitepress'

docs/.vitepress/theme/language-persistence.ts:

+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
  if ((window as any).__VITEPRESS__?.router) {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    (window as any).__VITEPRESS__.router.onAfterRouteChange?.((to: any) => {
+    (window as any).__VITEPRESS__.router.onAfterRouteChange?.((_to: any) => {
        updateLanguageCookie();
      });
    }

functions/tests/middleware.test.ts:

- import { describe, it, expect, beforeEach, vi } from 'vitest';
+ import { describe, it, expect } from 'vitest';

-      const acceptLanguage = 'en-US';
-      const country = 'JP';
+      const _acceptLanguage = 'en-US';
+      const _country = 'JP';

scripts/generate-roadmap.js:

-  for (const [key, status] of Object.entries(statuses)) {
+  for (const [_key, status] of Object.entries(statuses)) {

得られた効果

今回のLint導入によって、以下の効果が得られました。

定量的な効果

項目 導入前 導入後
未使用変数の検出 手動のみ 自動検出(12件修正)
CIでの品質チェック なし プッシュ時自動実行
コードスタイルの一貫性 個人依存 ルールベースで統一

定性的な効果

  • レビュー負荷の軽減: スタイル指摘が自動化され、本質的なコードレビューに集中できるようになりました
  • 新規参加者のオンボーディング: ルールが明文化されることで、チームメンバーがコードスタイルをすぐに理解できるようになりました
  • 心理的安全性の向上: CIで自動チェックされるため、スタイル指摘が個人的なものではなくなりました

よくある質問(FAQ)

ESLint 9.x Flat Configで、従来のextendsは使えますか?

いいえ、Flat Configではextendsは使用できません。

代わりに、設定オブジェクトをスプレッド構文(...)で合成します。

// 従来の書き方(Flat Configでは使えない)
module.exports = {
  extends: ['eslint:recommended']
};

// Flat Configでの書き方
import js from '@eslint/js';
export default [
  ...js.configs.recommended
];

TypeScriptとJavaScriptで異なるパーサーを指定するには?

filesプロパティで対象ファイルを絞り込み、それぞれの設定でlanguageOptions.parserを指定します。

export default [
  // JavaScript用
  {
    files: ['**/*.js'],
    languageOptions: { ecmaVersion: 'latest' }
  },
  // TypeScript用
  {
    files: ['**/*.ts'],
    languageOptions: { parser: tseslint.parser }
  }
];

日本語コメントがエラーになるのはなぜ?

spaced-commentルールが// 日本語のような形式を許可しないためです。

以下のオプションで解決できます。

'spaced-comment': ['error', 'always', { markers: ['/'] }]

GitHub Actionsでnpm ciが失敗するのはなぜ?

package-lock.jsonがリポジトリに含まれていない可能性があります。

.gitignoreからpackage-lock.jsonを削除し、コミットしてください。

StylelintでVueのSFCスタイルをLintするには?

stylelint-config-standard-vueをextendsに追加し、postcss-htmlをインストールします。

export default {
  extends: ['stylelint-config-standard', 'stylelint-config-standard-vue']
};

まとめ

VitePressサイトへのESLint 9.x Flat ConfigStylelint導入について解説しました。

Flat Config形式を活用することで、TypeScript/JavaScript混在環境でも柔軟な設定が可能です。

また、GitHub Actionsと組み合わせることで、コード品質を自動的に維持できる仕組みを構築できました。

特に印象的だったのは、TypeScriptとJavaScriptで別々のパーサーを適用する設定が、Flat Config形式のおかげでシンプルに記述できた点です。

これからLint環境を構築される方は、ぜひESLint 9.x Flat Config形式を試してみてください。