naoki.dev

記事検索

タイトル・本文・タグから検索します

ホームに戻る
フロントエンド

pnpmで「CLIのlintは通るのにVS CodeのESLintだけ効かない」問題の原因と解決

目次

pnpm で作った Expo(SDK 52 / eslint-config-expo)のプロジェクトで、こんな状態にハマった。

  • ターミナルの pnpm lint(ESLint CLI)はちゃんとエラーを出す
  • なのに VS Code のエディタ上では console.log を書いても波線ひとつ出ない(ESLint が完全に沈黙)

結論から言うと、原因は pnpm が間接依存の ESLint プラグインを node_modules 直下に置かないことで、.npmrcpublic-hoist-pattern を足して再インストールすれば直る。以下、切り分けと原因、解決まで。

症状

.eslintrc.cjseslint-config-expo を継承しただけのごく普通の構成。

module.exports = {
  extends: "expo",
  rules: {
    "no-console": "error",
    // ...
  },
};
  • pnpm lintUnexpected console statement (no-console) が出る ✅
  • VS Code → 同じファイルを開いても何も指摘されない ❌

CLI が通るので「設定は正しい」。なのにエディタだけ効かない、という気持ち悪い状態だった。

切り分け

まず順当に疑うところを潰していく。

  • ESLint 拡張は入っているか → 入っている(dbaeumer.vscode-eslint
  • 設定は正しいか → CLI で動くので正しい
  • モノレポでルートを開いているせいか → フロントの app/直接開いてもダメ

app/ を直接開けば、app/.eslintrc.cjsapp/node_modules/eslint.vscode/settings.json が全部そろう理想状態のはず。それでも効かない。ここで「設定の場所の問題」ではなく「拡張の実行時に何かが失敗している」と当たりをつけた。

決め手はエディタ拡張のログ

VS Code の拡張は、出力パネル(表示 → 出力 → 「ESLint」)にログを吐く。そしてそれはファイルとしても残っている。macOS なら以下:

~/Library/Application\ Support/Code/logs/<日時>/window<N>/exthost/dbaeumer.vscode-eslint/ESLint.log

最新のものを開くと、はっきり書いてあった。

Failed to load plugin 'expo' declared in '.eslintrc.cjs » eslint-config-expo » ./utils/expo.js':
Cannot find module 'eslint-plugin-expo'
Require stack:
- /path/to/app/__placeholder__.js

eslint-config-expo が内部で使う eslint-plugin-expo が見つからず、ESLint サーバーが設定読み込みでコケていた。エディタ側はこれを画面に出さず黙るので「効いていない」ように見えていた、というわけ。

推測でググり続けるより、まずこのログを読むのが最短だった。

なぜ起きるのか

ポイントは2つ。

  1. pnpm は間接依存をフラットに展開しないnpmyarn は依存をできるだけ node_modules 直下に平坦化(hoist)するが、pnpm は既定でそれをやらず、node_modules/.pnpm/ 配下にシンボリックリンクで厳格に配置する。直接依存だけが node_modules 直下に見える。
  2. eslint-plugin-expoアプリの直接依存ではなく eslint-config-expo の依存(=間接依存)。なので app/node_modules/eslint-plugin-expo存在しない

実際に確認するとこうなる。

$ ls node_modules | grep eslint-plugin-expo
# 何も出ない(=直下に無い)

CLI が通るのは、ESLint 実行時の解決経路がプラグインを見つけられるから。一方 VS Code の ESLint 拡張は、プロジェクトルートに __placeholder__.js を置いてそこ基準でプラグインを解決しようとする。node_modules 直下に無い eslint-plugin-expo はここから辿れず、読み込みに失敗する。これが「CLI ○ / エディタ ×」の正体。

解決:.npmrc で hoist する

ESLint/Prettier 関連だけをルートへ持ち上げるよう、プロジェクトの .npmrcpublic-hoist-pattern を追加する。

# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=@typescript-eslint/*
public-hoist-pattern[]=*prettier*

そして再インストール。

pnpm install

public-hoist-pattern にマッチしたパッケージは node_modules 直下(public な位置)に hoist され、ルート基準で解決できるようになる。確認するとちゃんと出てくる。

$ ls node_modules | grep -E "eslint-plugin-expo|eslint-plugin-react"
eslint-plugin-expo
eslint-plugin-react

あとは VS Code をリロード(コマンドパレット → 「Developer: Reload Window」)すれば、ESLint サーバーが hoist 済みの node_modules を読み直し、エディタでも no-console などがちゃんと波線で怒るようになる。

hoist 設定を変えた直後の pnpm install は「node_modules を作り直すか?」と確認してくることがある。初回だけなので y でよい。

モノレポでリポジトリルートを開く場合

フロントを app/、ほかに .github/ などを持つモノレポで、リポジトリルートを開いて運用したいときは、ルートの .vscode/settings.json で ESLint の作業ディレクトリを教える。

{
  // app/ を作業ディレクトリとして扱い、そこの設定と node_modules で lint する
  "eslint.workingDirectories": ["./app"],
  // ESLint 8 系 + .eslintrc を使う(新しい拡張が flat config を優先して見失うのを防ぐ)
  "eslint.useFlatConfig": false
}

これが無いと、ルートには node_modules が無いため拡張が ESLint 本体を解決できず、やはり沈黙する。サブプロジェクトが増えたら配列に足していけばよい(例: ["./app", "./packages/api"])。

まとめ

  • 症状: pnpm lint(CLI)は通るのに、VS Code のエディタだけ ESLint が効かない
  • 原因: pnpm が間接依存の eslint-plugin-exponode_modules 直下に置かず、エディタ拡張がルート基準で解決できずに設定読み込みで失敗していた
  • 決め手: エディタで諦めず ESLint.log~/Library/Application Support/Code/logs/.../dbaeumer.vscode-eslint/ESLint.log)を読む
  • 解決: .npmrcpublic-hoist-pattern[]=*eslint* などを足して pnpm install → リロード
  • モノレポ: ルート開き時は eslint.workingDirectories を設定

「CLI では動くのにエディタだけ動かない」系は、たいていツールの実行環境(cwd・モジュール解決)の違いが原因。まず拡張のログを読むのが遠回りに見えて一番早い。

参考ソース

関連記事