ClaudeReact NativeJun 21, 2023, 12:00 AM

Package Exports Support in React Native

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

claudeenmodel: claude-sonnet-4-20250514

React Native 0.72 Introduces Beta Support for Package Exports in Metro

Key Points

  • Beta support for package.json exports field in Metro bundler
  • Improved compatibility with Firebase, Storybook and 16.6k+ npm packages
  • New react-native conditional export for better multiplatform targeting

Summary

React Native 0.72 introduces beta support for the package.json "exports" field in Metro, the JavaScript build tool. This feature enables better compatibility with npm packages and provides new capabilities for package authors to define APIs targeting React Native.

Key Points

  • Enhanced Package Compatibility: React Native projects now work with more npm packages out-of-the-box, including Firebase and Storybook
  • Package Encapsulation: Only subpaths defined in "exports" can be imported, giving packages control over their public API
  • Conditional Exports: Packages can resolve to different files based on environment ("node", "browser", "react-native")
  • New "react-native" Condition: Introduced as a community condition for conditional exports, replacing the previous "react-native" root field
  • Breaking Changes: When "exports" is provided, Metro consults it first and won't expand sourceExts or resolve platform-specific extensions against target files
  • Gradual Migration: Lenient package encapsulation falls back to legacy resolution with warnings instead of errors

Configuration

Enable in metro.config.js:

const config = {
  resolver: {
    unstable_enablePackageExports: true,
  },
};

Future Plans

Package Exports will be enabled by default in React Native 0.73, with the unstable_ prefix removed after addressing performance improvements and bug fixes.

Full Translation

Translations

A translation section that keeps the flow of the original article.

claudejamodel: claude-sonnet-4-20250514

React NativeにおけるPackage Exportsサポート

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';
// -> '@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';
// アクセス不可 - "exports"にリストされていません!

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(またはプロジェクトで使用される関連ツール)に置き換えてください。

  1. すべての解決された依存関係を取得(変更前):

    # 必要に応じてindex.jsをエントリーファイル(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)に置き換えてください。

  2. metro.config.jsでresolver.unstable_enablePackageExportsを有効にします。

  3. すべての解決された依存関係を取得(変更後):

    yarn metro get-dependencies index.js --platform android --output after.txt
    
  4. 比較!

    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に心から感謝します。