T-CREATOR

Yarn PnP で「モジュールが見つからない」時の解決大全:packageExtensions/patch で対処

Yarn PnP で「モジュールが見つからない」時の解決大全:packageExtensions/patch で対処

Yarn PnP(Plug'n'Play)は、従来の node_modules を使わずにパッケージを管理する革新的な仕組みです。しかし、依存関係の解決方法が根本的に異なるため、「モジュールが見つからない」というエラーに遭遇することがあります。

本記事では、Yarn PnP で発生する典型的なモジュール解決エラーと、packageExtensionspatch といった強力な解決策を詳しく解説します。エラーメッセージの読み方から具体的な対処法まで、実践的な知識を身につけましょう。

背景

Yarn PnP の仕組み

Yarn PnP は、Node.js のパッケージ解決システムを効率化するために設計されました。従来の node_modules フォルダを使わず、.pnp.cjs というファイルでパッケージの位置情報を一元管理します。

mermaidflowchart LR
  install["yarn install 実行"] --> generate[".pnp.cjs 生成"]
  generate --> cache["グローバルキャッシュ<br />に依存パッケージ保存"]
  cache --> map["パッケージマップ<br />作成"]

  app["アプリケーション"] --> requireNode["モジュール読み込み"]
  requireNode --> pnp[".pnp.cjs 参照"]
  pnp --> resolve["キャッシュから<br />パッケージ解決"]
  resolve --> load["モジュール読み込み"]

  style generate fill:#e1f5ff
  style pnp fill:#e1f5ff
  style cache fill:#fff4e1

図で理解できる要点:

  • .pnp.cjs が依存関係のマップとして機能します
  • 実際のパッケージはグローバルキャッシュに保存されます
  • モジュール読み込み時は .pnp.cjs を経由してキャッシュを参照します

この仕組みにより、インストール速度が大幅に向上し、ディスク容量も節約できますね。また、依存関係が厳格に管理されるため、予期しない依存パッケージへのアクセスを防げます。

従来の node_modules との違い

従来の node_modules では、各パッケージが自身の node_modules フォルダを持ち、再帰的に依存関係を解決していました。この方式には以下の課題がありました。

#項目node_modulesYarn PnP
1ディスク容量大量のファイルで肥大化キャッシュ共有で削減
2インストール速度ファイルコピーで遅いリンク作成で高速
3依存関係の厳格性暗黙の依存が可能明示的な依存のみ
4パッケージ検索ディレクトリを上に辿るマップで即座に解決

Yarn PnP は、これらの課題を解決するために設計されました。しかし、依存関係の厳格性が高まったことで、従来は問題にならなかったコードがエラーになることもあります。

厳格な依存関係管理のメリット

Yarn PnP の厳格な依存関係管理は、一見すると制約に思えるかもしれません。しかし、この厳格性には重要なメリットがあります。

まず、依存関係の透明性が向上するでしょう。package.json に記載されていない依存パッケージは使用できないため、プロジェクトの依存関係が明確になります。

次に、セキュリティリスクの軽減も期待できます。暗黙の依存を許さないことで、意図しないパッケージへのアクセスを防ぎます。

さらに、バージョン競合の早期発見も可能です。依存関係の問題が開発時に明確になり、本番環境での予期しないエラーを減らせます。

課題

典型的なエラーパターン

Yarn PnP を導入すると、以下のようなエラーメッセージに遭遇することがあります。これらは、従来の node_modules では問題にならなかったコードで発生します。

パターン 1:暗黙の依存エラー

最も一般的なエラーは、直接インストールしていないパッケージを使おうとしたときに発生します。

typescript// エラーコード:Error [ERR_MODULE_NOT_FOUND]
Error: Cannot find module 'lodash'
Require stack:
- /path/to/your/project/src/utils.js

このエラーは、lodashpackage.jsondependenciesdevDependencies に記載されていない場合に発生するでしょう。従来の node_modules では、他のパッケージが lodash をインストールしていれば使えてしまいましたが、PnP では明示的な宣言が必要です。

パターン 2:ピア依存関係の未解決

パッケージが期待するピア依存関係が満たされていない場合も、エラーが発生します。

typescript// エラーコード:YN0060
YN0060: │ @testing-library/react@npm:13.4.0 lists a peer dependency on react (^18.0.0) but it is not provided by your application; this makes the require call ambiguous and unsound.

このエラーメッセージは、@testing-library​/​reactreact のバージョン 18 を期待しているのに、プロジェクトでインストールされていないことを示しています。

パターン 3:プラグインやローダーの解決失敗

ビルドツールやテストフレームワークのプラグインが見つからないエラーも頻出します。

typescript// エラーコード:Error: Cannot find module
Error: Cannot find module '@babel/plugin-transform-runtime'
  at Function._resolveFilename (node:internal/modules/cjs/loader:995:15)
  at resolve (.pnp.cjs:3582:46)

Babel や Webpack などのツールが、設定ファイルで指定されたプラグインを見つけられない状況ですね。

エラーが発生する根本原因

これらのエラーは、Yarn PnP の設計思想に起因します。PnP では、以下の原則が厳格に適用されるため、従来の緩やかな依存解決とのギャップが生まれます。

mermaidflowchart LR
  app["アプリケーション"] --> directDep["直接依存<br/>packageA"]
  directDep --> transitiveDep["間接依存<br/>packageB"]

  app -.->|"❌ アクセス不可"| transitiveDep
  directDep -->|"✓ アクセス可"| transitiveDep

  style app fill:#e1f5ff
  style directDep fill:#e8f5e9
  style transitiveDep fill:#fff4e1

図で理解できる要点:

  • アプリケーションは直接依存にのみアクセスできます
  • 間接依存(他のパッケージの依存)には直接アクセスできません
  • この制約により、依存関係が明確化されます

原因 1:暗黙の依存(Phantom Dependencies)

従来は、依存パッケージの依存(間接依存)も自由に使えました。しかし PnP では、package.json に明示的に記載された依存のみが使用可能です。

原因 2:不完全な package.json 宣言

一部のパッケージは、package.json で依存関係を正しく宣言していないことがあります。node_modules の緩やかな解決ではカバーされていましたが、PnP では問題が顕在化します。

原因 3:ツールの解決パスの違い

Babel や Webpack などのツールは、独自のモジュール解決ロジックを持っています。これらが PnP の解決メカニズムに対応していない場合、エラーが発生するでしょう。

トラブルシューティングの第一歩

エラーに遭遇したとき、まず確認すべきポイントを整理しましょう。

ステップ 1:エラーメッセージの確認

エラーメッセージには、解決のヒントが含まれています。特に以下の情報に注目してください。

  • 見つからないモジュール名
  • エラーコード(YN0060ERR_MODULE_NOT_FOUND など)
  • Require スタック(どのファイルから呼び出されたか)

ステップ 2:依存関係の確認

以下のコマンドで、パッケージの依存関係を確認できます。

bash# 特定パッケージの依存関係を表示
yarn why <package-name>

このコマンドは、指定したパッケージがなぜインストールされているのか、どこから依存されているのかを教えてくれます。

ステップ 3:PnP の解決ログを確認

詳細なログを有効にすることで、モジュール解決の過程を追跡できます。

bash# PnP の詳細ログを有効化
PNP_DEBUG_LEVEL=1 yarn <command>

これらの情報を組み合わせることで、エラーの原因を特定しやすくなりますね。

解決策

packageExtensions による依存関係の補完

packageExtensions は、既存パッケージの package.json を実質的に「上書き」する機能です。パッケージ自体を修正せずに、不足している依存関係を追加できます。

packageExtensions の基本構文

.yarnrc.yml ファイルに以下の形式で記述します。

yaml# .yarnrc.yml
# packageExtensions の基本設定

packageExtensions:
  # 対象パッケージ名@バージョン範囲
  'package-name@*':
    # 追加する依存関係
    dependencies:
      'missing-dependency': '*'

package-name@** は、すべてのバージョンに適用することを意味します。特定のバージョンにのみ適用したい場合は、package-name@^1.0.0 のようにバージョン範囲を指定できますね。

実践例:react-scripts の依存関係補完

Create React App の react-scripts は、多くの依存パッケージを暗黙的に使用しています。これらを明示的に宣言しましょう。

yaml# .yarnrc.yml
# react-scripts に不足している依存関係を追加

packageExtensions:
  'react-scripts@*':
    dependencies:
      # Babel 関連の依存
      '@babel/plugin-proposal-private-property-in-object': '*'
      # PostCSS 関連の依存
      'postcss': '*'

この設定により、react-scripts が依存する Babel プラグインや PostCSS が正しく解決されるようになります。

設定後は、依存関係を再インストールして変更を適用します。

bash# 依存関係を再インストール
yarn install

packageExtensions の詳細オプション

より細かい制御が必要な場合は、以下のオプションも使用できます。

yaml# .yarnrc.yml
# packageExtensions の詳細設定

packageExtensions:
  'package-name@*':
    # 通常の依存関係
    dependencies:
      'dep1': '^1.0.0'
    # 開発用依存関係
    devDependencies:
      'dev-dep': '^2.0.0'
    # ピア依存関係
    peerDependencies:
      'react': '^18.0.0'
    # ピア依存関係のメタ情報
    peerDependenciesMeta:
      'react':
        optional: true

peerDependenciesMetaoptional: true を設定すると、そのピア依存関係が存在しなくても警告を抑制できますね。

patch プロトコルによるパッケージ修正

packageExtensions だけでは解決できない場合、パッケージのソースコードを直接修正する patch プロトコルが有効です。

patch の作成手順

パッチファイルの作成は、以下の手順で行います。

ステップ 1:修正対象のパッケージを特定

まず、どのパッケージを修正するかを決めます。エラーメッセージから特定できるでしょう。

bash# パッケージのキャッシュ場所を確認
yarn cache dir

ステップ 2:パッケージのコピーを作成

修正したいパッケージのコピーを作業ディレクトリに展開します。

bash# パッケージを展開(例:lodash)
yarn unplug lodash

このコマンドは、指定したパッケージをキャッシュから .yarn​/​unplugged ディレクトリにコピーします。

ステップ 3:ソースコードを修正

.yarn​/​unplugged 内のファイルを直接編集します。例えば、不足している依存のインポートを追加したり、モジュール解決のパスを修正したりできますね。

ステップ 4:パッチファイルを生成

修正が完了したら、変更差分をパッチファイルとして保存します。

bash# パッチファイルを生成
yarn patch-commit -s /path/to/.yarn/unplugged/lodash-xxx

このコマンドは、.yarn​/​patches ディレクトリにパッチファイルを作成し、.yarnrc.yml に設定を追記します。

patch の適用例

実際のパッチ適用例を見てみましょう。ここでは、古いパッケージの ESM 対応を追加します。

修正前のパッケージコード:

javascript// node_modules/old-package/index.js
// CommonJS のみの古いコード

module.exports = function oldFunction() {
  return 'legacy code';
};

ESM 対応を追加した修正版:

javascript// .yarn/unplugged/old-package-xxx/index.js
// ESM エクスポートを追加

module.exports = function oldFunction() {
  return 'legacy code';
};

// ESM 対応の追加
export default module.exports;

パッチを生成すると、.yarn​/​patches​/​old-package-npm-1.0.0-abc123.patch のようなファイルが作成されます。

生成されたパッチファイルの内容例:

diffdiff --git a/index.js b/index.js
index 1234567..abcdefg 100644
--- a/index.js
+++ b/index.js
@@ -2,3 +2,6 @@ module.exports = function oldFunction() {
   return 'legacy code';
 };

+// ESM 対応の追加
+export default module.exports;
+

この差分ファイルは、次回の yarn install で自動的に適用されます。

patch の管理とメンテナンス

パッチファイルは .yarn​/​patches ディレクトリに保存され、Git で管理することが推奨されます。

bash# .gitignore の設定例
# Yarn の依存関係ファイルを Git で管理

# Yarn のファイルは含める
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks

# キャッシュとアンプラグは除外
.yarn/cache
.yarn/unplugged

パッチファイルをバージョン管理することで、チーム全体で同じ修正を共有できますね。

その他の解決テクニック

1. nodeLinker の設定変更

PnP が問題を引き起こす場合、一時的に従来の node_modules 方式に戻すこともできます。

yaml# .yarnrc.yml
# node_modules 方式に戻す設定

nodeLinker: node-modules

この設定により、Yarn は PnP ではなく従来の node_modules を生成するようになります。ただし、PnP のメリットは失われるため、最終手段として考えましょう。

設定を変更したら、依存関係を再インストールします。

bash# nodeLinker の変更を適用
yarn install

2. PnP のルーズモード

より緩やかな依存解決を許可する「ルーズモード」も用意されています。

yaml# .yarnrc.yml
# PnP のルーズモードを有効化

pnpMode: loose

ルーズモードでは、一部の暗黙の依存が許可されますが、厳格性は低下します。段階的な移行期間に有用でしょう。

3. エディタ・IDE の設定

Yarn PnP を使用する場合、エディタや IDE も PnP に対応させる必要があります。

VS Code の設定例:

bash# VS Code の SDK を生成
yarn dlx @yarnpkg/sdks vscode

このコマンドは、.yarn​/​sdks ディレクトリに VS Code 用の設定ファイルを生成します。TypeScript や ESLint が PnP を正しく認識できるようになりますね。

生成後、VS Code でワークスペース版の TypeScript を選択します:

  1. TypeScript ファイルを開く
  2. コマンドパレット(Cmd+Shift+P / Ctrl+Shift+P)を開く
  3. 「TypeScript: Select TypeScript Version」を選択
  4. 「Use Workspace Version」を選択

WebStorm などの JetBrains IDE は、自動的に PnP を検出して設定します。

具体例

ケーススタディ 1:Next.js プロジェクトでの PnP 導入

実際の Next.js プロジェクトで PnP を導入し、発生したエラーを解決していきます。

初期セットアップ

まず、既存の Next.js プロジェクトを Yarn PnP に移行します。

bash# Yarn のバージョンを確認(Berry が必要)
yarn set version stable

このコマンドは、最新の安定版 Yarn(Yarn Berry)をインストールします。.yarnrc.yml ファイルも自動生成されますね。

次に、PnP を有効化します。

yaml# .yarnrc.yml
# PnP を有効化する基本設定

nodeLinker: pnp
pnpMode: strict

依存関係をインストールし直します。

bash# PnP モードで依存関係を再インストール
yarn install

発生したエラーと対処

インストール後、開発サーバーを起動すると複数のエラーが発生しました。

エラー 1:Babel プラグインが見つからない

typescript// エラーコード:Error: Cannot find module
Error: Cannot find module '@babel/plugin-syntax-jsx'
  at Function._resolveFilename (node:internal/modules/cjs/loader:995:15)
  at .pnp.cjs:3582:46

Next.js が内部で使用する Babel プラグインが解決できていません。

解決策として、packageExtensions で依存関係を補完します。

yaml# .yarnrc.yml
# Next.js の Babel プラグインを補完

packageExtensions:
  'next@*':
    dependencies:
      '@babel/plugin-syntax-jsx': '*'
      '@babel/plugin-transform-react-jsx': '*'

エラー 2:ESLint の設定エラー

typescript// エラーコード:YN0060
YN0060: │ eslint-config-next@npm:13.4.0 lists a peer dependency on eslint (^7.23.0 || ^8.0.0) but it is not provided

ESLint のピア依存関係が満たされていないエラーです。

package.json に ESLint を追加します。

json{
  "devDependencies": {
    "eslint": "^8.45.0"
  }
}

依存関係を追加したら、再インストールします。

bash# 依存関係を更新
yarn install

エラー 3:画像最適化ライブラリのエラー

typescript// エラーコード:Error [ERR_MODULE_NOT_FOUND]
Error: Cannot find module 'sharp'
Require stack:
- /path/to/project/node_modules/next/dist/server/image-optimizer.js

Next.js の画像最適化機能が、sharp パッケージを見つけられません。

sharp を明示的にインストールします。

bash# sharp を依存関係に追加
yarn add sharp

最終的な設定ファイル

すべての対処を適用した最終的な .yarnrc.yml は以下のようになります。

yaml# .yarnrc.yml
# Next.js プロジェクトの完全な PnP 設定

# PnP の基本設定
nodeLinker: pnp
pnpMode: strict

# エディタサポート
enableGlobalCache: true

# パッケージの依存関係補完
packageExtensions:
  'next@*':
    dependencies:
      '@babel/plugin-syntax-jsx': '*'
      '@babel/plugin-transform-react-jsx': '*'
      'sharp': '*'

  'eslint-config-next@*':
    peerDependencies:
      'eslint': '*'

この設定により、Next.js プロジェクトが PnP で正常に動作するようになりました。

ケーススタディ 2:Jest のテスト環境構築

テストフレームワークの Jest も、PnP での動作に調整が必要です。

Jest の PnP 対応設定

Jest で PnP を使用するには、専用の resolver が必要になります。

bash# Jest の PnP resolver をインストール
yarn add -D jest-pnp-resolver

jest-pnp-resolver は、Jest が PnP のモジュール解決を理解できるようにするパッケージです。

次に、Jest の設定ファイルに resolver を追加します。

javascript// jest.config.js
// Jest の PnP 対応設定

module.exports = {
  // PnP 用の resolver を指定
  resolver: 'jest-pnp-resolver',

  // テスト環境の設定
  testEnvironment: 'node',

  // ファイルの拡張子
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
};

トランスフォーム設定の追加

TypeScript や JSX を使用する場合、トランスフォーム設定も必要です。

bash# Babel の Jest トランスフォームをインストール
yarn add -D babel-jest @babel/preset-env @babel/preset-typescript

Jest 設定にトランスフォーム設定を追加します。

javascript// jest.config.js
// トランスフォーム設定を追加

module.exports = {
  resolver: 'jest-pnp-resolver',
  testEnvironment: 'node',

  // TypeScript/JSX のトランスフォーム
  transform: {
    '^.+\\.(ts|tsx|js|jsx)$': 'babel-jest',
  },

  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
};

モックの設定

PnP 環境では、モジュールのモックにも注意が必要です。

javascript// __tests__/example.test.js
// PnP 環境でのモック例

// PnP では require.resolve() が異なる挙動をする
jest.mock('axios', () => ({
  get: jest.fn(),
  post: jest.fn(),
}));

describe('API テスト', () => {
  it('データを取得できる', async () => {
    const axios = require('axios');
    axios.get.mockResolvedValue({ data: 'test' });

    // テストコード
    const result = await axios.get('/api/data');
    expect(result.data).toBe('test');
  });
});

モジュールのパスは、PnP のマップを通じて解決されるため、通常の node_modules とは異なる動作になることを理解しておきましょう。

ケーススタディ 3:Webpack のカスタムビルド

独自の Webpack 設定を使用するプロジェクトでも、PnP 対応が必要です。

Webpack の PnP プラグイン

Webpack で PnP を使用するには、専用のプラグインを導入します。

bash# PnP 用の Webpack プラグインをインストール
yarn add -D pnp-webpack-plugin

Webpack の設定ファイルでプラグインを有効化します。

javascript// webpack.config.js
// PnP プラグインのインポート

const PnpWebpackPlugin = require('pnp-webpack-plugin');

module.exports = {
  // 他の設定...
};

次に、resolver の設定を追加します。

javascript// webpack.config.js
// resolver の PnP 対応

const PnpWebpackPlugin = require('pnp-webpack-plugin');

module.exports = {
  resolve: {
    // PnP のモジュール解決プラグイン
    plugins: [PnpWebpackPlugin],
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
};

ローダーの resolver 設定

Webpack のローダーも PnP に対応させる必要があります。

javascript// webpack.config.js
// ローダーの resolver 設定

const PnpWebpackPlugin = require('pnp-webpack-plugin');

module.exports = {
  resolve: {
    plugins: [PnpWebpackPlugin],
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },

  // ローダーの resolver 設定
  resolveLoader: {
    plugins: [PnpWebpackPlugin.moduleLoader(module)],
  },
};

resolveLoader の設定により、babel-loaderstyle-loader などのローダーが PnP で正しく解決されるようになります。

完全な Webpack 設定例

TypeScript と React を使用するプロジェクトの完全な設定例です。

javascript// webpack.config.js
// PnP 対応の完全な Webpack 設定

const path = require('path');
const PnpWebpackPlugin = require('pnp-webpack-plugin');

module.exports = {
  // エントリーポイント
  entry: './src/index.tsx',

  // 出力設定
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },

  // モジュール解決の設定
  resolve: {
    plugins: [PnpWebpackPlugin],
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },

  // ローダーの解決設定
  resolveLoader: {
    plugins: [PnpWebpackPlugin.moduleLoader(module)],
  },
};

ローダーのルール設定も追加します。

javascript// webpack.config.js
// module.rules の設定

module.exports = {
  // 前述の設定...

  module: {
    rules: [
      {
        // TypeScript/JSX のトランスパイル
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env',
              '@babel/preset-react',
              '@babel/preset-typescript',
            ],
          },
        },
      },
      {
        // CSS の処理
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
};

この設定により、Webpack が PnP 環境で正常にビルドできるようになりますね。

まとめ

Yarn PnP は、モダンな JavaScript プロジェクトにおけるパッケージ管理を大きく改善する技術です。しかし、従来の node_modules との違いにより、「モジュールが見つからない」エラーに遭遇することがあります。

本記事で解説した解決策をまとめます:

#解決策適用ケース難易度
1packageExtensions依存関係の不足を補完★☆☆
2patch プロトコルソースコードレベルの修正★★☆
3nodeLinker 変更PnP を無効化★☆☆
4pnpMode ルーズ緩やかな解決を許可★☆☆
5ツール固有設定Jest/Webpack の対応★★★

重要なポイント:

まず、エラーメッセージを丁寧に読み、不足している依存関係を特定することが重要です。yarn why コマンドで依存関係の構造を理解しましょう。

次に、packageExtensions で依存関係を補完するのが最も簡単で安全な方法ですね。.yarnrc.yml に設定を追加するだけで、パッケージ本体を変更せずに問題を解決できます。

さらに、patch プロトコルは、より複雑な問題に対応できる強力なツールです。パッケージのソースコードを直接修正し、その変更を再利用可能な形で管理できます。

最後に、Jest や Webpack などのツールには、それぞれ PnP 専用の設定が必要です。公式のプラグインやドキュメントを活用することで、スムーズに移行できるでしょう。

Yarn PnP の厳格な依存関係管理は、最初は制約に感じるかもしれません。しかし、適切に対処することで、より堅牢で保守性の高いプロジェクトを構築できます。本記事の知識を活用して、PnP の恩恵を最大限に享受してください。

関連リンク