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 直下に置かないことで、.npmrc に public-hoist-pattern を足して再インストールすれば直る。以下、切り分けと原因、解決まで。
症状
.eslintrc.cjs は eslint-config-expo を継承しただけのごく普通の構成。
module.exports = {
extends: "expo",
rules: {
"no-console": "error",
// ...
},
};pnpm lint→Unexpected console statement (no-console)が出る ✅- VS Code → 同じファイルを開いても何も指摘されない ❌
CLI が通るので「設定は正しい」。なのにエディタだけ効かない、という気持ち悪い状態だった。
切り分け
まず順当に疑うところを潰していく。
- ESLint 拡張は入っているか → 入っている(
dbaeumer.vscode-eslint) - 設定は正しいか → CLI で動くので正しい
- モノレポでルートを開いているせいか → フロントの
app/を直接開いてもダメ
app/ を直接開けば、app/.eslintrc.cjs・app/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__.jseslint-config-expo が内部で使う eslint-plugin-expo が見つからず、ESLint サーバーが設定読み込みでコケていた。エディタ側はこれを画面に出さず黙るので「効いていない」ように見えていた、というわけ。
推測でググり続けるより、まずこのログを読むのが最短だった。
なぜ起きるのか
ポイントは2つ。
- pnpm は間接依存をフラットに展開しない。
npmやyarnは依存をできるだけnode_modules直下に平坦化(hoist)するが、pnpm は既定でそれをやらず、node_modules/.pnpm/配下にシンボリックリンクで厳格に配置する。直接依存だけがnode_modules直下に見える。 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 関連だけをルートへ持ち上げるよう、プロジェクトの .npmrc に public-hoist-pattern を追加する。
# .npmrc
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=@typescript-eslint/*
public-hoist-pattern[]=*prettier*そして再インストール。
pnpm installpublic-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-expoをnode_modules直下に置かず、エディタ拡張がルート基準で解決できずに設定読み込みで失敗していた - 決め手: エディタで諦めず
ESLint.log(~/Library/Application Support/Code/logs/.../dbaeumer.vscode-eslint/ESLint.log)を読む - 解決:
.npmrcにpublic-hoist-pattern[]=*eslint*などを足してpnpm install→ リロード - モノレポ: ルート開き時は
eslint.workingDirectoriesを設定
「CLI では動くのにエディタだけ動かない」系は、たいていツールの実行環境(cwd・モジュール解決)の違いが原因。まず拡張のログを読むのが遠回りに見えて一番早い。
参考ソース
関連記事

TanStack Query を Next.js App Router の静的エクスポート構成に入れる手順を、Providerの初期化(use client)→layoutラップ→共通fetch(lib/api.ts)→hooks(1ファイル1フック)→コンポーネントでの使い方、まで一気通貫でまとめます。

保存(Ctrl+S)のたびに未使用importを消し、importの並び順も自動で揃える設定を、ESLint flat config で作ります。eslint-plugin-unused-imports と simple-import-sort、VS Code の codeActionsOnSave、Prettier との併用・順序までまとめます。

create-next-app から Amplify Gen2 のバックエンド(Cognito/DynamoDB/Lambda)を追加し、Server Actions での SSR 書き込み、Lambda を API Gateway で REST 公開、Next.js API Route、Amplify Hosting の Git 連携デプロイまでを一気通貫でまとめます。