React Native におけるパッケージエクスポートのサポート
React Native 0.72のリリースに伴い、JavaScriptビルドツールであるMetroに、package.json
の"exports"
フィールドのベータサポートが追加されました。有効化されると、以下の機能が追加されます。
- React Nativeプロジェクトは、より多くのnpmパッケージとそのまま連携できるようになります。
- パッケージがAPIを定義し、React Nativeをターゲットにするための新しい機能
- パッケージ解決に関するいくつかの破壊的変更 (エッジケース)
この記事では、パッケージエクスポートの仕組みと、React Nativeアプリ開発者やパッケージメンテナーにとってこれらの変更が何を意味するのかについて説明します。
パッケージエクスポートとは?
Node.js 12.7.0で導入されたパッケージエクスポートは、npmパッケージが**エントリポイント**(外部からインポートできるパッケージサブパスと、それらが解決されるべきファイルのマッピング)を指定するための最新の pendekatan です。
"exports"
をサポートすることで、React Nativeプロジェクトがより広範なJavaScriptエコシステム(現在約16,600個のパッケージで使用されています)と連携しやすくなり、パッケージ作成者にはマルチプラットフォームパッケージがReact Nativeをターゲットにするための標準化された機能セットが提供されます。
"exports"
は、package.json
ファイル内で"main"
と併用、または代わりに使用できます。
{
"name": "@storybook/addon-actions",
"main": "./dist/index.js",
...
"exports": {
".": {
"node": "./dist/index.js",
"import": "./dist/index.mjs",
"default": "./dist/index.js"
},
"./preview": {
"import": "./dist/preview.mjs",
"default": "./dist/preview.js"
},
...
"./package.json": "./package.json"
}
}
以下は、@storybook/addon-actions
の異なるサブパスをインポートすることで、上記のパッケージを使用するアプリコードの例です。
import {action} from '@storybook/addon-actions';
// -> '@storybook/addon-actions/dist/index.js'
import {action} from '@storybook/addon-actions/preview';
// -> '@storybook/addon-actions/dist/preview.js'
import helpers from '@storybook/addon-actions/src/preset/addArgsHelpers';
// Inaccessible - not listed in "exports"!
パッケージエクスポートの主な機能は次のとおりです。
- **パッケージのカプセル化**:
"exports"
で定義されたサブパスのみをパッケージの外部からインポートできます。これにより、パッケージはパブリックAPIを制御できます。 - **サブパスエイリアス**: パッケージは、異なるファイルの場所(サブパスパターンを含む)にマッピングするカスタムサブパスを定義できます。これにより、パブリックAPIを維持しながらファイルを再配置できます。
- **条件付きエクスポート**: サブパスは、環境に応じて異なる基になるファイルに解決される場合があります。たとえば、
"node"
、"browser"
、または"react-native"
ランタイムをターゲットにする場合、"browser"
フィールド仕様を置き換えます。
"exports"
のすべての機能は、Node.jsパッケージエントリポイント仕様に詳述されています。
これらの機能は、既存のReact Nativeの概念(プラットフォーム固有の拡張子など)と重複しており、"exports"
はnpmエコシステムでしばらくの間公開されていたため、実装が開発者のニーズを満たせるように、React Nativeコミュニティに連絡を取りました(PR、最終RFC)。
アプリ開発者向け
パッケージエクスポートは、本日ベータ版で有効にすることができます。
- パッケージエクスポート機能に依存するパッケージ(**Firebase**や**Storybook**など)に対するインポートは、設計どおりに機能するはずです。
- Metroを使用するReact Native for Webプロジェクトでは、
"browser"
条件付きエクスポートを使用できるようになり、回避策の必要がなくなります。
パッケージエクスポートを有効にすると、特定のプロジェクトに影響を与える可能性のあるエッジケースの破壊的変更がいくつか発生します。これらは今日テストできます。
**将来のReact Nativeリリースでは、パッケージエクスポートがデフォルトで有効になります**。鶏と卵の状況では、React Nativeアプリは以前、一部のパッケージが"exports"
に移行するための保留状態でした。または、"react-native"
ルートフィールドエスケープハッチを使用していました。Metroでこれらの機能をサポートすることで、エコシステムが前進できるようになります。
パッケージエクスポートの有効化(ベータ版)
パッケージエクスポートは、アプリの**metro.config.js**ファイルで、resolver.unstable_enablePackageExports
オプションを使用して有効にできます。
const config = {
// ...
resolver: {
unstable_enablePackageExports: true,
},
};
Metroは、条件付きエクスポートの動作を構成する2つの追加のレゾルバーオプションを公開しています。
unstable_conditionNames
- 条件付きエクスポートを解決するときにアサートする条件名のセット。デフォルトでは、['require', 'import', 'react-native']
と一致します。unstable_conditionsByPlatform
- 特定のプラットフォームターゲットを解決するときにアサートする追加の条件名。デフォルトでは、プラットフォームが'web'
の場合、これは'browser'
と一致します。
**React NativeのJestプリセットを使用することを忘れないでください!** Jestには、デフォルトでパッケージエクスポートのサポートが含まれています。テストでは、testEnvironmentOptions
オプションを使用して、どのcustomExportConditions
が解決されるかをオーバーライドできます。
TypeScriptを使用している場合、プロジェクトの`tsconfig.json`内でmoduleResolution: 'bundler'
とresolvePackageJsonImports: false
を設定することで、解決の挙動を一致させることができます。
プロジェクトでの変更の検証
既存のプロジェクトの場合、アーリーアダプターは、`unstable_enablePackageExports`を有効にした後に解決の変更が発生するかどうかを確認するために、以下の手順に従うことをお勧めします。これは一度限りのプロセスです。変更が全くない可能性が高いですが、開発者には確実にオプトインしていただきたいと考えています。
💡 プロジェクトでの変更の検証
Yarnを使用していない場合は、`yarn`を`npx`(またはプロジェクトで使用されている関連ツール)に置き換えてください。
-
すべての解決済み依存関係を取得する(変更前)
# Replace index.js with your entry file if needed, such as App.js
yarn metro get-dependencies index.js --platform android --output before.txt- Expo CLI: プロジェクトに`metro.config.js`ファイルがない場合は、`npx expo customize metro.config.js`を実行します。
- 完全に網羅するには、`--platform android`をアプリで使用されている他のプラットフォーム(例:`ios`、`web`)に置き換えてください。
-
`metro.config.js`で`resolver.unstable_enablePackageExports`を有効にします。
-
すべての解決済み依存関係を取得する(変更後)
yarn metro get-dependencies index.js --platform android --output after.txt
-
比較!
diff before.txt after.txt
破壊的変更
私たちは、MetroにおけるPackage Exportsの実装を、仕様に準拠したもの(いくつかの破壊的変更が必要)にしつつ、それ以外は後方互換性を持たせる(既存のインポートを持つアプリが徐々に移行できるよう支援する)ことにしました。
重要な破壊的変更は、パッケージが`exports`を提供している場合、それが最初に(他の`package.json`フィールドの前に)参照され、一致するサブパスのターゲットが直接使用されることです。
- Metroは、インポート指定子に対して`sourceExts`を展開しません。
- Metroは、ターゲットファイルに対してプラットフォーム固有の拡張子を解決しません。
詳細については、Metroのドキュメントにあるすべての破壊的変更を参照してください。
パッケージのカプセル化は寛容です
Metroが`exports`にリストされていないサブパスに遭遇した場合、従来の解決方法にフォールバックします。これは、既存のReact Nativeプロジェクトで以前に許可されていたインポートのユーザーによる摩擦を軽減することを目的とした互換性機能です。
エラーをスローする代わりに、Metroは警告をログに記録します。
warn: You have imported the module "foo/private/fn.js" which is not listed in
the "exports" of "foo". Consider updating your call site or asking the package
maintainer(s) to expose this API.
私たちは、Nodeのデフォルトの動作に合わせて、将来パッケージのカプセル化に厳密モードを実装する予定です。そのため、ユーザーから警告が表示された場合は、すべての開発者がこれらの警告に対処することをお勧めします。
パッケージメンテナ向け(プレビュー)
私たちのロールアウトプランに従い、Package Exportsは今年の後半にリリースされる次のReact Nativeリリース(0.73)でほとんどのプロジェクトで有効になります。
`main`フィールドやその他の現在のパッケージ解決機能のサポートをすぐに削除する予定はありません。
Package Exportsは、パッケージの内部へのアクセスを制限し、React NativeとReact Native for Webをターゲットとするライブラリのためのより予測可能な機能を提供します。
現在`exports`を使用している場合
現在`react-native`ルートフィールドと一緒に`exports`を使用しているパッケージの場合、上記のユーザーに対する破壊的変更に注意してください。Metroでこの機能を有効にしているユーザーの場合、モジュール解決中に`exports`が最初に考慮されるようになります。
実際には、ユーザーにとっての主な変更点は、`exports`パッケージのカプセル化を尊重することで、アプリ内のアクセスできないサブパスが(警告を介して)強制されることになると予想されます。
`exports`への移行
パッケージに`exports`フィールドを追加することは完全に任意です。`exports`を使用しないパッケージの場合、既存のパッケージ解決機能は同じように動作します。また、この動作を削除する予定はありません。
私たちは、`exports`の新しい機能が、React Nativeパッケージメンテナにとって魅力的な機能セットを提供すると考えています。
- パッケージAPIの強化:これは、パッケージのモジュールAPIを見直す絶好の機会です。モジュールAPIは、エクスポートされたサブパスエイリアスを介して正式に定義できるようになりました。これにより、ユーザーが内部APIにアクセスするのを防ぎ、バグの発生源を減らすことができます。
- 条件付きエクスポート:パッケージがReact Native for Web(つまり、`react-native`と`browser`)をターゲットにしている場合、これらの条件の解決順序をパッケージが制御できるようになりました(次の見出しを参照)。
`exports`を導入する場合は、**破壊的変更として行うことをお勧めします**。Metroのドキュメントには、プラットフォーム固有の拡張子などの機能を置き換える方法を含む**移行ガイド**を用意しています。
**Metroの実装の寛容な動作に依存しないでください。** Metroは後方互換性がありますが、パッケージは仕様でドキュメント化され、他のツールによって厳密に実装されている`exports`の使用方法に従う必要があります。
新しい`react-native`条件
私たちは、`react-native`をコミュニティ条件(条件付きエクスポートで使用するため)として導入しました。これは、`node`や`deno`などの他の認識されているランタイムと並んで、フレームワークであるReact Nativeを表しています(RFC)。
React Nativeフレームワーク(すべてのプラットフォーム)と一致します。React Native for Webをターゲットにするには、この条件の前に "browser" を指定する必要があります。
これは、以前の`react-native`ルートフィールドを置き換えます。以前の解決の優先順位はプロジェクトによって決定され、React Native for Webを使用する場合に曖昧さが生じていました。`exports`では、*パッケージは条件付きエントリポイントの解決順序を具体的に定義します* — この曖昧さを解消します。
"exports": {
"browser": "./dist/index-browser.js",
"react-native": "./dist/index-react-native.js",
"default": "./dist/index.js"
}
他の既存のプラットフォーム選択方法が普及していること、およびこの動作がフレームワーク全体でどのように機能するかの複雑さから、`android`と`ios`の条件は導入しないことにしました。代わりに`Platform.select()` APIを使用してください。
将来:安定した`exports`、デフォルトで有効化
次のReact Nativeリリースでは、(計画されているパフォーマンスの作業とバグに対処した後)この機能の`unstable_`プレフィックスを削除し、デフォルトでPackage Exports解決を有効にすることを目指しています。
誰もが`exports`を使用できるようになれば、React Nativeコミュニティを前進させることができます。たとえば、React Nativeのコアパッケージを更新して、パブリックモジュールと内部モジュールをより適切に分離することができます。
謝辞
RFCにフィードバックを提供してくれたReact Nativeコミュニティのメンバーに感謝します:@SimenB、@tido64、@byCedric、@thymikee。