T-CREATOR

Yarn PnP 互換性チートシート:loader 設定・patch・packageExtensions の書き方

Yarn PnP 互換性チートシート:loader 設定・patch・packageExtensions の書き方

Yarn PnP(Plug'n'Play)は、従来の node_modules を使わない革新的なパッケージ管理方式です。しかし、一部のパッケージが PnP に対応していないことがあり、その際には互換性問題が発生します。本記事では、そんな問題を解決するための 3 つの主要な手法——loader 設定patchpackageExtensions——の具体的な書き方とユースケースを、初心者の方にもわかりやすく解説していきますね。

早見表:Yarn PnP 互換性対策の比較

以下の表で、各手法の特徴と使い分けを一目で把握できます。

#手法用途難易度適用範囲永続性
1loader 設定Node.js の module 解決をカスタマイズ★☆☆プロジェクト全体.yarnrc.yml に記載
2patchパッケージのソースコードを直接修正★★☆特定パッケージpatches/ ディレクトリで管理
3packageExtensions依存関係のメタデータを補完★☆☆特定パッケージ.yarnrc.yml に記載
#使用ケース推奨手法
1ESM/CommonJS の混在で loader が必要loader 設定
2パッケージのバグを修正したいpatch
3依存関係が package.json に記載されていないpackageExtensions
4TypeScript の型定義が不足しているpackageExtensions
5ネイティブモジュールの依存が欠けているpackageExtensions

背景

Yarn PnP の仕組み

Yarn PnP は、npm や従来の Yarn Classic が採用していた node_modules ディレクトリを使わずに、パッケージの依存関係を .pnp.cjs ファイルで一元管理する仕組みです。この方式により、インストール速度の向上やディスク容量の削減が実現できます。

以下の図で、従来の node_modules 方式と PnP 方式の違いを見てみましょう。

mermaidflowchart TB
    subgraph traditional["従来の node_modules 方式"]
        app1["アプリケーション"] --> nm["node_modules/"]
        nm --> pkgA["package-a/"]
        nm --> pkgB["package-b/"]
        pkgA --> nmA["node_modules/<br/>依存パッケージ"]
        pkgB --> nmB["node_modules/<br/>依存パッケージ"]
    end

    subgraph pnp["Yarn PnP 方式"]
        app2["アプリケーション"] --> pnpFile[".pnp.cjs<br/>依存関係マップ"]
        pnpFile --> cache[".yarn/cache/<br/>パッケージ実体(zip)"]
        cache --> cachedA["package-a.zip"]
        cache --> cachedB["package-b.zip"]
    end

従来は各パッケージが独自の node_modules を持っていたため、同じパッケージが重複してインストールされることがありました。一方、PnP では全パッケージの実体が .yarn​/​cache​/​ に集約され、依存関係の情報だけが .pnp.cjs で管理されます。

PnP がもたらすメリット

Yarn PnP を採用することで、次のようなメリットが得られます。

  • 高速なインストール:ファイルのコピーが不要で、zip ファイルから直接読み込むため高速です
  • ディスク容量の削減:重複したパッケージが存在しないため、容量を大幅に節約できます
  • 厳密な依存解決:依存関係が明確になり、意図しない hoisting が発生しません

これらの利点により、モノレポや大規模プロジェクトでの採用が進んでいます。

課題

PnP 非対応パッケージの問題

Yarn PnP は素晴らしい技術ですが、すべてのパッケージが対応しているわけではありません。特に以下のような問題が発生することがあります。

  • 暗黙的な依存関係:package.json に記載されていない依存を暗黙的に使用している
  • ファイルシステムへの直接アクセスnode_modules の存在を前提としたコードがある
  • 動的 require:実行時に動的にモジュールを読み込むコードが PnP と相性が悪い
  • 型定義の不足:TypeScript の型定義が dependencies に含まれていない

以下の図で、PnP での互換性問題の発生フローを確認しましょう。

mermaidflowchart TD
    start["パッケージのインストール"] --> check{"PnP 互換性<br/>チェック"}
    check -->|OK| success["正常動作"]
    check -->|NG| problem["互換性問題発生"]

    problem --> type1["暗黙的依存の<br/>アクセスエラー"]
    problem --> type2["node_modules<br/>前提のコード"]
    problem --> type3["動的 require<br/>の失敗"]
    problem --> type4["型定義の<br/>欠落"]

    type1 --> solution["解決策の適用"]
    type2 --> solution
    type3 --> solution
    type4 --> solution

    solution --> fixed["問題解決"]

このような問題に直面した時、適切な解決策を選択することが重要になります。

典型的なエラーメッセージ

PnP の互換性問題に遭遇すると、次のようなエラーメッセージが表示されることがあります。

typescript// Error 1: モジュールが見つからないエラー
Error: Cannot find module 'some-package'
Require stack:
- /path/to/your/project/.pnp.cjs
typescript// Error 2: 暗黙的依存のエラー
Error: Your application tried to access package-name,
but it isn't declared in your dependencies
typescript// Error 3: TypeScript の型定義エラー
error TS2307: Cannot find module '@types/node' or
its corresponding type declarations.

これらのエラーは、パッケージが PnP の仕組みに対応していないことを示しています。次のセクションでは、これらの問題を解決する具体的な手法を見ていきましょう。

解決策

Yarn PnP の互換性問題には、主に 3 つの解決策があります。それぞれの手法を詳しく解説します。

解決策の選択フロー

まず、どの手法を使うべきか判断するためのフローチャートをご覧ください。

mermaidflowchart TD
    start["互換性問題が発生"] --> question1{"loader の<br/>カスタマイズが<br/>必要?"}

    question1 -->|Yes| useLoader["loader 設定を使用"]
    question1 -->|No| question2{"パッケージの<br/>コードを<br/>修正したい?"}

    question2 -->|Yes| usePatch["patch を使用"]
    question2 -->|No| question3{"依存関係の<br/>メタデータを<br/>補完したい?"}

    question3 -->|Yes| useExtensions["packageExtensions<br/>を使用"]
    question3 -->|No| useNodeModules["nodeLinker: node-modules<br/>へフォールバック"]

    useLoader --> apply["設定を適用"]
    usePatch --> apply
    useExtensions --> apply
    useNodeModules --> apply

それでは、各手法の具体的な書き方を見ていきます。

loader 設定の書き方

loader 設定とは

loader 設定は、Node.js の module 解決メカニズムをカスタマイズするための機能です。.yarnrc.yml ファイルに記載することで、特定の loader を有効化できます。

主に、ESM(ECMAScript Modules)と CommonJS の混在環境や、特殊な module 解決が必要な場合に使用します。

基本的な loader 設定

.yarnrc.yml ファイルを開いて、以下のように loader を設定します。

yaml# .yarnrc.yml

# PnP を有効化(デフォルト)
nodeLinker: pnp

# loader の設定
pnpMode: loose

pnpMode には以下のオプションがあります。

#モード説明使用ケース
1strict厳密モード。すべての依存を明示する必要がある推奨。依存関係を明確にしたい場合
2loose緩いモード。暗黙的な依存も許可レガシーパッケージとの互換性が必要な場合

ESM loader の設定

Next.js や Nuxt.js など、ESM をサポートするフレームワークでは、ESM loader を設定することがあります。

yaml# .yarnrc.yml

nodeLinker: pnp

# ESM loader を使用する場合
pnpEnableEsmLoader: true

この設定により、--loader オプションを自動的に適用できます。

TypeScript との連携

TypeScript プロジェクトで PnP を使う場合、SDK のインストールと設定が必要です。

bash# TypeScript SDK のインストール
yarn dlx @yarnpkg/sdks vscode

この コマンドを実行すると、.yarn​/​sdks​/​ ディレクトリに TypeScript や ESLint の SDK が生成されます。

yaml# .yarnrc.yml

# TypeScript との連携設定
pnpEnableInlining: false

pnpEnableInlining: false を設定することで、.pnp.cjs ファイルのサイズを小さく保てます。

カスタム loader の指定

プロジェクト固有の loader が必要な場合は、package.json のスクリプトで指定できます。

json{
  "scripts": {
    "start": "node --loader ./custom-loader.mjs app.js"
  }
}

ただし、この方法は PnP 固有ではなく、Node.js 全般の機能です。

patch の書き方

patch とは

patch は、パッケージのソースコードを直接修正するための機能です。Yarn には組み込みの yarn patch コマンドがあり、パッケージにバグがある場合や、PnP 対応のための小さな修正が必要な場合に使用します。

修正内容は patches​/​ ディレクトリに保存され、yarn install 時に自動的に適用されます。

yarn patch の基本的な使い方

まず、修正したいパッケージを patch モードで開きます。

bash# パッケージを patch モードで開く
yarn patch some-package

このコマンドを実行すると、一時ディレクトリにパッケージが展開され、パスが表示されます。

bash# 出力例
➤ YN0000: Package some-package@npm:1.2.3 got extracted to:
➤ YN0000: /private/var/folders/.../some-package-1234567
➤ YN0000:
➤ YN0000: Run yarn patch-commit -s /private/var/folders/.../some-package-1234567
➤ YN0000: to commit the changes

ファイルの修正

表示されたディレクトリに移動して、必要なファイルを修正します。

bash# 一時ディレクトリに移動
cd /private/var/folders/.../some-package-1234567

# ファイルを編集(例:index.js)
vim index.js

例えば、以下のような修正を行います。

javascript// 修正前
const fs = require('fs');
const path = require('path');

// node_modules を前提としたコード(PnP では動作しない)
const pkgPath = path.join(__dirname, '../package.json');
javascript// 修正後
const fs = require('fs');
const path = require('path');

// PnP 対応のコード
const pkgPath = require.resolve('../package.json');

patch のコミット

修正が完了したら、変更をコミットします。

bash# patch をコミット(先ほど表示されたパスを指定)
yarn patch-commit -s /private/var/folders/.../some-package-1234567

実行すると、patches​/​ ディレクトリに patch ファイルが生成されます。

bash# 生成された patch ファイルの例
patches/some-package-npm-1.2.3-a1b2c3d4.patch

package.json への自動記載

yarn patch-commit を実行すると、package.json の resolutions フィールドに自動的に記載されます。

json{
  "resolutions": {
    "some-package@npm:1.2.3": "patch:some-package@npm%3A1.2.3#./.yarn/patches/some-package-npm-1.2.3-a1b2c3d4.patch"
  }
}

これにより、次回以降の yarn install で自動的に patch が適用されます。

patch ファイルの確認

生成された patch ファイルの中身は、unified diff 形式で保存されています。

diffdiff --git a/index.js b/index.js
index 1234567..abcdefg 100644
--- a/index.js
+++ b/index.js
@@ -10,7 +10,7 @@
 const fs = require('fs');
 const path = require('path');

-const pkgPath = path.join(__dirname, '../package.json');
+const pkgPath = require.resolve('../package.json');

 module.exports = {
   readPackage() {

この形式により、Git と同様に差分を確認できます。

patch の削除

patch が不要になった場合は、以下の手順で削除します。

bash# 1. patches/ ディレクトリから patch ファイルを削除
rm patches/some-package-npm-1.2.3-a1b2c3d4.patch
json// 2. package.json から resolutions エントリを削除
{
  "resolutions": {
    // この行を削除
    // "some-package@npm:1.2.3": "patch:..."
  }
}
bash# 3. 依存関係を再インストール
yarn install

これで patch の適用が解除されます。

packageExtensions の書き方

packageExtensions とは

packageExtensions は、パッケージのメタデータ(依存関係の情報)を補完するための機能です。.yarnrc.yml に記載することで、package.json に記載されていない依存関係を追加できます。

特に、暗黙的な依存関係や、型定義が不足している場合に有効です。

基本的な書き方

.yarnrc.yml ファイルに packageExtensions セクションを追加します。

yaml# .yarnrc.yml

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

* を使うことで、すべてのバージョンに適用できます。特定のバージョンのみに適用したい場合は、セマンティックバージョニングを使用します。

依存関係の追加

package.json に記載されていない依存を追加する例です。

yaml# .yarnrc.yml

packageExtensions:
  'react-native@*':
    dependencies:
      # React Native が暗黙的に依存している
      'metro-react-native-babel-preset': '*'

この設定により、react-native をインストールすると、自動的に metro-react-native-babel-preset もインストールされます。

peerDependencies の追加

プラグインシステムを持つパッケージでは、peerDependencies の記載漏れがあることがあります。

yaml# .yarnrc.yml

packageExtensions:
  'eslint-plugin-react@*':
    peerDependencies:
      # ESLint 本体への依存を明示
      'eslint': '^7.0.0 || ^8.0.0'

これにより、ESLint プラグインが正しく動作するようになります。

TypeScript 型定義の追加

TypeScript プロジェクトでよくある問題が、型定義パッケージの不足です。

yaml# .yarnrc.yml

packageExtensions:
  'express@*':
    dependencies:
      # Express の型定義を追加
      '@types/express': '*'
      '@types/node': '*'

この設定により、express をインストールすると、型定義も自動的にインストールされます。

devDependencies の追加

開発時のみ必要な依存を追加する場合は、devDependencies を使用します。

yaml# .yarnrc.yml

packageExtensions:
  'some-library@*':
    devDependencies:
      # テストツールを追加
      'jest': '^29.0.0'
      '@types/jest': '^29.0.0'

ただし、通常は開発依存関係は自分のプロジェクトの package.json に記載する方が適切です。

複数パッケージへの一括適用

類似のパッケージ群に同じ拡張を適用したい場合は、それぞれ記載します。

yaml# .yarnrc.yml

packageExtensions:
  'webpack@*':
    dependencies:
      'webpack-cli': '*'

  'webpack-dev-server@*':
    dependencies:
      'webpack': '*'
      'webpack-cli': '*'

バージョン範囲の指定

特定のバージョン範囲にのみ適用したい場合は、セマンティックバージョニングを使用します。

yaml# .yarnrc.yml

packageExtensions:
  # 1.x 系のみに適用
  'old-package@^1.0.0':
    dependencies:
      'legacy-dependency': '^1.0.0'

  # 2.x 系以降は適用しない(修正済みのため)

これにより、古いバージョンにのみ互換性対策を適用できます。

実際の設定例

実際のプロジェクトでよく使われる packageExtensions の例をご紹介します。

yaml# .yarnrc.yml

nodeLinker: pnp

packageExtensions:
  # React Native 関連
  'react-native@*':
    dependencies:
      'metro-react-native-babel-preset': '*'
      '@react-native-community/cli': '*'

  # Next.js 関連
  'next@*':
    dependencies:
      '@types/react': '*'
      '@types/node': '*'

  # テストツール関連
  '@testing-library/react@*':
    peerDependencies:
      'react': '*'
      'react-dom': '*'

  # ESLint プラグイン関連
  'eslint-plugin-import@*':
    peerDependencies:
      'eslint': '^7.0.0 || ^8.0.0 || ^9.0.0'

この設定により、多くの一般的な互換性問題を事前に回避できます。

具体例

ここでは、実際のプロジェクトでよく遭遇する互換性問題と、その解決方法を具体例で見ていきましょう。

例 1:Next.js プロジェクトでの型定義不足

Next.js プロジェクトを PnP で運用する場合、TypeScript の型定義が不足することがあります。

問題の発生

typescript// pages/index.tsx

// Error TS2307: Cannot find module '@types/react' or
// its corresponding type declarations.
import React from 'react';

packageExtensions による解決

.yarnrc.yml に型定義を追加します。

yaml# .yarnrc.yml

nodeLinker: pnp
pnpMode: strict

packageExtensions:
  'next@*':
    dependencies:
      '@types/react': '*'
      '@types/react-dom': '*'
      '@types/node': '*'

依存関係の再インストール

設定を反映するために、依存関係を再インストールします。

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

これで TypeScript の型定義エラーが解消されます。

例 2:React Native での Metro bundler の問題

React Native では、Metro bundler が暗黙的な依存を持っていることがあります。

問題の発生

bashError: Cannot find module 'metro-react-native-babel-preset'
Require stack:
- /path/to/project/babel.config.js

packageExtensions による解決

yaml# .yarnrc.yml

packageExtensions:
  'react-native@*':
    dependencies:
      'metro-react-native-babel-preset': '*'
      '@react-native-community/cli': '*'
      '@react-native-community/cli-platform-android': '*'
      '@react-native-community/cli-platform-ios': '*'

これにより、React Native と一緒に必要な Metro 関連パッケージがインストールされます。

例 3:ESLint プラグインの peerDependencies 問題

ESLint プラグインは、ESLint 本体を peerDependencies として要求しますが、記載が不十分な場合があります。

問題の発生

bashError: Your application tried to access eslint,
but it isn't declared in your dependencies

packageExtensions による解決

yaml# .yarnrc.yml

packageExtensions:
  'eslint-plugin-react@*':
    peerDependencies:
      'eslint': '^7.0.0 || ^8.0.0 || ^9.0.0'

  'eslint-plugin-import@*':
    peerDependencies:
      'eslint': '^7.0.0 || ^8.0.0 || ^9.0.0'

  '@typescript-eslint/eslint-plugin@*':
    peerDependencies:
      'eslint': '^7.0.0 || ^8.0.0 || ^9.0.0'
      'typescript': '>=4.0.0'

この設定により、ESLint プラグインが正しく動作するようになります。

例 4:レガシーパッケージの node_modules 参照修正

古いパッケージが node_modules を直接参照している場合、patch で修正できます。

問題の発生

javascript// old-package/lib/index.js

// node_modules を直接参照(PnP では動作しない)
const pkgPath = path.join(
  process.cwd(),
  'node_modules',
  'some-dep'
);

patch による解決

まず、パッケージを patch モードで開きます。

bashyarn patch old-package

表示されたパスに移動して、ファイルを修正します。

javascript// 修正前
const pkgPath = path.join(
  process.cwd(),
  'node_modules',
  'some-dep'
);
javascript// 修正後:require.resolve を使用
const pkgPath = require.resolve('some-dep');

変更をコミットします。

bashyarn patch-commit -s /path/to/temp/old-package-xxxxx

これで、PnP 環境でも正しく動作するようになります。

例 5:動的 require の問題

一部のパッケージは、実行時に動的にモジュールを読み込みますが、PnP では対応が必要です。

問題の発生

javascript// plugin-loader.js

// 動的 require(PnP では失敗する可能性がある)
const pluginName = getUserInput();
const plugin = require(pluginName);

patch による解決

bashyarn patch plugin-loader
javascript// 修正後:createRequire を使用
const { createRequire } = require('module');
const dynamicRequire = createRequire(__filename);

const pluginName = getUserInput();
const plugin = dynamicRequire(pluginName);
bashyarn patch-commit -s /path/to/temp/plugin-loader-xxxxx

createRequire を使用することで、PnP 環境でも動的 require が機能します。

例 6:monorepo での workspace 設定

monorepo では、workspace 間の依存関係を適切に管理する必要があります。

プロジェクト構成

plaintextmy-monorepo/
├── packages/
│   ├── app/
│   │   └── package.json
│   └── shared/
│       └── package.json
├── package.json
└── .yarnrc.yml

ルートの package.json

json{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

.yarnrc.yml の設定

yaml# .yarnrc.yml

nodeLinker: pnp
pnpMode: strict

# workspace 全体で共通の packageExtensions
packageExtensions:
  'shared@workspace:*':
    dependencies:
      '@types/node': '*'

この設定により、workspace 間でも PnP が正しく機能します。

例 7:Webpack との統合

Webpack を使用するプロジェクトでは、PnP plugin の設定が必要です。

webpack.config.js の設定

javascript// webpack.config.js

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

module.exports = {
  resolve: {
    // PnP で module を解決
    plugins: [PnpWebpackPlugin],
  },
  resolveLoader: {
    // PnP で loader を解決
    plugins: [PnpWebpackPlugin.moduleLoader(module)],
  },
};

必要なパッケージのインストール

bash# PnP Webpack plugin をインストール
yarn add -D pnp-webpack-plugin

これで、Webpack が PnP 環境で正しく動作します。

まとめ

Yarn PnP の互換性問題は、適切な手法を選択することで確実に解決できます。本記事で解説した 3 つの手法——loader 設定patchpackageExtensions——を使い分けることで、ほとんどの互換性問題に対処できるでしょう。

改めて、手法の使い分けを整理しておきます。

解決したい問題推奨手法設定場所
module 解決のカスタマイズloader 設定.yarnrc.yml
パッケージのバグ修正patchpatches/ ディレクトリ
依存関係の補完packageExtensions.yarnrc.yml
型定義の追加packageExtensions.yarnrc.yml
レガシーコードの修正patchpatches/ ディレクトリ

PnP は最初は戸惑うかもしれませんが、一度設定すれば高速で安定したパッケージ管理が実現できます。本記事の早見表と具体例を参考に、ぜひ PnP を活用してみてください。

トラブルシューティングの際は、まずエラーメッセージを確認し、上記のフローチャートに従って適切な手法を選択することをお勧めします。それでも解決しない場合は、nodeLinker: node-modules にフォールバックすることも選択肢の一つですが、可能な限り PnP の仕組みを活かした解決を目指しましょう。

関連リンク

;