Yarn PnP 互換性チートシート:loader 設定・patch・packageExtensions の書き方
Yarn PnP(Plug'n'Play)は、従来の node_modules を使わない革新的なパッケージ管理方式です。しかし、一部のパッケージが PnP に対応していないことがあり、その際には互換性問題が発生します。本記事では、そんな問題を解決するための 3 つの主要な手法——loader 設定、patch、packageExtensions——の具体的な書き方とユースケースを、初心者の方にもわかりやすく解説していきますね。
早見表:Yarn PnP 互換性対策の比較
以下の表で、各手法の特徴と使い分けを一目で把握できます。
| # | 手法 | 用途 | 難易度 | 適用範囲 | 永続性 |
|---|---|---|---|---|---|
| 1 | loader 設定 | Node.js の module 解決をカスタマイズ | ★☆☆ | プロジェクト全体 | .yarnrc.yml に記載 |
| 2 | patch | パッケージのソースコードを直接修正 | ★★☆ | 特定パッケージ | patches/ ディレクトリで管理 |
| 3 | packageExtensions | 依存関係のメタデータを補完 | ★☆☆ | 特定パッケージ | .yarnrc.yml に記載 |
| # | 使用ケース | 推奨手法 |
|---|---|---|
| 1 | ESM/CommonJS の混在で loader が必要 | loader 設定 |
| 2 | パッケージのバグを修正したい | patch |
| 3 | 依存関係が package.json に記載されていない | packageExtensions |
| 4 | TypeScript の型定義が不足している | 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 には以下のオプションがあります。
| # | モード | 説明 | 使用ケース |
|---|---|---|---|
| 1 | strict | 厳密モード。すべての依存を明示する必要がある | 推奨。依存関係を明確にしたい場合 |
| 2 | loose | 緩いモード。暗黙的な依存も許可 | レガシーパッケージとの互換性が必要な場合 |
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 設定、patch、packageExtensions——を使い分けることで、ほとんどの互換性問題に対処できるでしょう。
改めて、手法の使い分けを整理しておきます。
| 解決したい問題 | 推奨手法 | 設定場所 |
|---|---|---|
| module 解決のカスタマイズ | loader 設定 | .yarnrc.yml |
| パッケージのバグ修正 | patch | patches/ ディレクトリ |
| 依存関係の補完 | packageExtensions | .yarnrc.yml |
| 型定義の追加 | packageExtensions | .yarnrc.yml |
| レガシーコードの修正 | patch | patches/ ディレクトリ |
PnP は最初は戸惑うかもしれませんが、一度設定すれば高速で安定したパッケージ管理が実現できます。本記事の早見表と具体例を参考に、ぜひ PnP を活用してみてください。
トラブルシューティングの際は、まずエラーメッセージを確認し、上記のフローチャートに従って適切な手法を選択することをお勧めします。それでも解決しない場合は、nodeLinker: node-modules にフォールバックすることも選択肢の一つですが、可能な限り PnP の仕組みを活かした解決を目指しましょう。
関連リンク
articleYarn PnP 互換性チートシート:loader 設定・patch・packageExtensions の書き方
articleYarn を Classic から Berry に移行する手順:yarn set version の正しい使い方
articleYarn の歴史と進化:Classic(v1) から Berry(v2/v4) まで一気に把握
articleYarn 運用ベストプラクティス:lockfile 厳格化・frozen-lockfile・Bot 更新方針
articleYarn PnP で「モジュールが見つからない」時の解決大全:packageExtensions/patch で対処
articleYarn vs npm vs pnpm 徹底比較:速度・メモリ・ディスク・再現性を実測
articleYarn PnP 互換性チートシート:loader 設定・patch・packageExtensions の書き方
articleElectron IPC 設計チートシート:チャネル命名・型安全・エラーハンドリング定型
articleDocker セキュアイメージ設計:非 root・最小ベース・Capabilities 削減の実装指針
articleJotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
articleDevin による段階的リファクタリング設計:ストラングラーパターン適用ガイド
articleJest のカバレッジが 0% になる原因と対処:sourceMap/babel 設定の落とし穴
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来