React Native 0.72のリリースに伴い、私たちのJavaScriptビルドツールであるMetroに、package.jsonの"exports"フィールドのベータサポートが含まれるようになりました。有効にすると、以下の機能が追加されます:
- React Nativeプロジェクトがより多くのnpmパッケージとそのまま動作するようになります
- パッケージがAPIを定義し、React Nativeをターゲットにする新しい機能
- パッケージ解決における破壊的変更(エッジケースにおいて)
この投稿では、Package Exportsがどのように動作するか、そしてこれらの変更がReact Nativeアプリ開発者やパッケージメンテナーにとって何を意味するかを説明します。
Package Exportsとは?
Node.js 12.7.0で導入されたPackage Exportsは、npmパッケージがエントリーポイントを指定するための現代的なアプローチです。これは、外部からインポート可能なパッケージサブパスと、それらが解決すべきファイルのマッピングを定義します。
"exports"をサポートすることで、React Nativeプロジェクトがより広いJavaScriptエコシステム(現在約16.6kのパッケージで使用されている)とどのように連携するかが改善され、パッケージ作成者にマルチプラットフォームパッケージで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';
import { action } from '@storybook/addon-actions/preview';
import helpers from '@storybook/addon-actions/src/preset/addArgsHelpers';
Package Exportsの主要機能は以下の通りです:
- パッケージのカプセル化:"exports"で定義されたサブパスのみがパッケージ外部からインポート可能で、パッケージがパブリックAPIを制御できます
- サブパスエイリアス:パッケージは異なるファイル場所にマップするカスタムサブパスを定義できます(サブパスパターンを含む)。これにより、パブリックAPIを保持しながらファイルの再配置が可能です
- 条件付きエクスポート:サブパスは環境に応じて異なる基盤ファイルに解決される場合があります。例えば、"node"、"browser"、"react-native"ランタイムをターゲットにする場合("browser"フィールド仕様を置き換える)
注意
"exports"の全機能は、Node.js Package Entry Points仕様に詳述されています。
これらの機能は既存のReact Nativeの概念(プラットフォーム固有の拡張など)と重複し、"exports"がnpmエコシステムでしばらく稼働していたため、私たちの実装が開発者のニーズを満たすことを確認するためにReact Nativeコミュニティに働きかけました(PR、最終RFC)。
アプリ開発者向け
Package Exportsは現在ベータ版で有効にできます。Package Exports機能に依存するパッケージ(FirebaseやStorybookなど)に対するインポートが、設計通りに動作するようになります。
Metroを使用するReact Native for Webプロジェクトで、"browser"条件付きエクスポートを使用できるようになり、回避策の必要がなくなります。
Package Exportsを有効にすると、特定のプロジェクトに影響を与える可能性のあるエッジケースの破壊的変更がいくつか発生し、これらは今日テストできます。
将来のReact Nativeリリースでは、Package Exportsがデフォルトで有効になります。
鶏と卵の状況で、React Nativeアプリは以前、一部のパッケージが"exports"に移行する際の障害となっていたり、私たちの"react-native"ルートフィールドのエスケープハッチを使用していました。Metroでこれらの機能をサポートすることで、エコシステムが前進できるようになります。
Package Exportsの有効化(ベータ)
Package Exportsは、アプリのmetro.config.jsファイルでresolver.unstable_enablePackageExportsオプションを使用して有効にできます。
const config = {
resolver: {
unstable_enablePackageExports: true,
},
};
Metroは、条件付きエクスポートの動作を設定する2つの追加のresolverオプションを公開しています:
unstable_conditionNames — 条件付きエクスポートを解決する際にアサートする条件名のセット。デフォルトでは、['require', 'import', 'react-native']にマッチします
unstable_conditionsByPlatform — 指定されたプラットフォームターゲットに対して解決する際にアサートする追加の条件名。デフォルトでは、プラットフォームが'web'の場合に'browser'にマッチします
ヒント
React Native Jestプリセットを使用することを忘れないでください!JestはデフォルトでPackage Exportsをサポートしています。テストでは、testEnvironmentOptionsオプションを使用して解決されるcustomExportConditionsをオーバーライドできます。
TypeScriptを使用している場合、プロジェクトのtsconfig.json内でmoduleResolution: 'bundler'とresolvePackageJsonImports: falseを設定することで、解決動作をマッチさせることができます。
プロジェクトでの変更の検証
既存のプロジェクトでは、早期採用者がunstable_enablePackageExportsを有効にした後に解決の変更が発生するかどうかを確認するために、以下の手順に従うことをお勧めします。これは一度限りのプロセスです。変更がまったくない可能性が高いですが、開発者には確実性を持ってオプトインしてもらいたいと考えています。
💡 プロジェクトでの変更の検証
注意
Yarnを使用していない場合は、yarnをnpx(またはプロジェクトで使用される関連ツール)に置き換えてください。
-
すべての解決された依存関係を取得(変更前):
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 docsのすべての破壊的変更をご覧ください。
パッケージのカプセル化は寛容
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のデフォルトBehaviorに合わせるために、パッケージのカプセル化のストリクトモードを実装する予定です。したがって、ユーザーによって提起された場合は、すべての開発者がこれらの警告に対処することをお勧めします。
パッケージメンテナー向け(プレビュー)
情報
ロールアウト計画に従い、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にアクセスすることを防ぎ、バグの表面積を減らします
- 条件付きエクスポート:パッケージがReact Native for Web(つまり"react-native"と"browser")をターゲットにしている場合、これらの条件の解決順序をパッケージが制御できるようになりました(次の見出しを参照)
"exports"を導入することを決定した場合は、これを破壊的変更として行うことをお勧めします。プラットフォーム固有の拡張などの機能を置き換える方法を含む移行ガイドをMetro docsで準備しました。
注意
Metroの実装の寛容な動作に依存しないでください。Metroは後方互換性がありますが、パッケージは"exports"が仕様で文書化され、他のツールによって厳密に実装されている方法に従うべきです。
新しい"react-native"条件
条件付きエクスポートで使用するコミュニティ条件として"react-native"を導入しました。これは、"node"や"deno"などの他の認識されたランタイムと並んで、フレームワークとしてのReact Nativeを表します(RFC)。
情報
コミュニティ条件の定義 — "react-native"
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。
この機能の開発をサポートしてくれたMetaの@motiz88と@robhoganに心から感謝します。