OpenAIReact Native2023/06/21 0:00

Package Exports Support in React Native

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

React Native における Package Exports サポート(ベータ)

Key Points

  • metroでexportsを有効化
  • サブパスのカプセル化
  • 互換性のための警告

Summary

React Native 0.72 から Metro が package.json の "exports" をベータサポートします。これによりパッケージは公開サブパスの明示・条件付きエントリポイントの指定が可能になり、React Native や React Native for Web をターゲットにしたマルチプラットフォーム対応が容易になります。一方で一部のエッジケースでモジュール解決の挙動が変わるため、プロジェクト側で検証と必要な修正を行うことを推奨します。

Key Points

  • 有効化方法
    • Metro の設定で resolver.unstable_enablePackageExports: true を設定してベータを有効化。
    • オプション: unstable_conditionNames(既定: [require, import, react-native])、unstable_conditionsByPlatform(web の場合に browser を追加)
  • 動作と互換性
    • exports があるパッケージではその定義が最優先で参照される(既存の main より優先)。
    • マッチしたターゲットに対して Metro は sourceExts の拡張やプラットフォーム固有拡張を展開しない。
    • ただし互換性のため、exports に未定義のサブパスは従来の解決にフォールバックし、警告を出す(将来的に厳格モードを導入予定)。
  • 検証手順(開発者向け)
    • 変更前に依存関係解決結果を取得: yarn metro get-dependencies index.js --platform android --output before.txt
    • Metro 設定で unstable_enablePackageExports を有効化し、同コマンドで after.txt を作成して差分を確認。
  • テスト・開発ツール対応
    • Jest: React Native preset はデフォルトで exports をサポート。テストでは testEnvironmentOptions で条件を上書き可能。
    • TypeScript: 解決を一致させるには moduleResolution: "bundler"resolvePackageJsonImports: false を推奨。
  • パッケージ公開者向けの注意点
    • exports は任意だが、導入時は破壊的変更として扱うのを推奨(内部 API へのアクセスを制限できる)。
    • 新しい react-native 条件を導入。React Native for Web を想定する場合は browser を先に指定して順序を制御する。
    • android/ios 条件は導入せず、プラットフォーム固有分岐は従来の手法(Platform.select 等)を推奨。

Practical next steps for engineers

  • アプリ側: ベータを有効化して yarn metro get-dependencies で差分を確認し、警告が出た箇所を修正または依存パッケージの公開者に相談する。
  • ライブラリ側: exports を導入する場合は公開手順をドキュメント化し、既存ユーザー向けに破壊的変更として扱う。
  • CI/テスト: Jest と TypeScript の設定を見直してビルド・テストで解決が変わらないことを確認する。

References

  • Metro ドキュメントと RFC を参照し、移行ガイドに従ってください。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

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

Package Exports Support in React Native(React NativeにおけるPackage Exportsサポート)

React Native 0.72のリリースに伴い、Metro(JavaScriptビルドツール)は package.json の "exports" フィールドのベータサポートを含むようになりました。有効にすると、次の機能が追加されます。

  • React Nativeプロジェクトがより多くのnpmパッケージとそのまま動作するようになる
  • パッケージがReact Nativeをターゲットに自身のAPIを定義するための新しい機能
  • パッケージ解決に関するいくつかの破壊的変更(エッジケース)

本記事ではPackage Exportsの仕組みと、React Nativeアプリ開発者やパッケージメンテナにとってこれらの変更が何を意味するかを解説します。

Package Exportsとは?

Node.js 12.7.0で導入されたPackage Exportsは、npmパッケージがエントリポイント(外部からimport可能なパッケージのサブパスと、それが解決されるファイル)を指定するためのモダンな方法です。

"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"
  }
}

上のパッケージをアプリ側で異なるサブパスとして利用する例:

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"に定義されたサブパスのみが外部からimport可能になり、パッケージが公開APIをコントロールできます。
  • サブパスのエイリアス:パッケージはカスタムサブパスを定義して別のファイル位置にマッピングできます(サブパスパターンも可)。これにより内部ファイルを移動しても公開APIを維持できます。
  • 条件付きエクスポート:サブパスは環境に応じて異なるファイルに解決できます。たとえば "node"、"browser"、"react-native" のランタイムをターゲットにすることができ、従来の "browser" フィールドの仕様を置き換えます。

注意: "exports" の詳細な機能は Node.js Package Entry Points spec に記載されています。

これらの機能は既存のReact Nativeの概念(プラットフォーム固有の拡張など)と重複する部分があり、また "exports" はnpmエコシステム上である程度定着しているため、実装が開発者のニーズを満たすかどうかを確認するためにコミュニティと協議を行いました(PR、最終RFC参照)。

アプリ開発者向け

Package Exportsは現在ベータで有効にできます。FirebaseやStorybookのようにPackage Exportsの機能に依存するパッケージへのimportは、これで設計どおりに動作するはずです。Metroを使うReact Native for Webプロジェクトは、"browser" 条件付きエクスポートをそのまま使えるようになり、従来のワークアラウンドが不要になります。

Package Exportsを有効にすると、特定のプロジェクトに影響するいくつかのエッジケースな破壊的変更が導入されます。これらは今日テストできます。将来のReact Nativeのリリースでは、Package Exportsがデフォルトで有効になる予定です。

これまでの経緯では、React Nativeアプリ側が一部パッケージの "exports" への移行の足かせになっていました(あるいは我々の "react-native" ルートフィールドの逃げ道を使っていました)。Metroがこれらの機能をサポートすることでエコシステムの前進が可能になります。

Package Exportsを有効にする(ベータ)

アプリの metro.config.js で resolver.unstable_enablePackageExports オプションを有効にすることでPackage Exportsを利用できます。

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 に次を設定することで解決挙動を合わせられます:

{
  "compilerOptions": {
    "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 を実行)

    対応範囲を広げるため、アプリで使っている他のプラットフォーム(例: ios, web)についても --platform を置換してください。

  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を仕様準拠で実装しました(そのためいくつかの破壊的変更が必要になりました)が、既存のimportを持つアプリが段階的に移行できるよう互換性も確保しています。主な破壊的変更は次の通りです。

  • パッケージが "exports" を提供している場合、他の package.json フィールドよりも先に "exports" が参照され、マッチしたサブパスのターゲットが直接使われます。
  • Metroは import 指定子に対して sourceExts を展開しません。
  • Metroはターゲットファイルに対してプラットフォーム固有の拡張を解決しません。

詳細は Metroのドキュメントにあるすべての破壊的変更を参照してください。

パッケージカプセル化は寛容に扱われる

Metroが "exports" に列挙されていないサブパスに遭遇した場合、互換性を保つためにレガシー解決へフォールバックします。これは既存のReact Nativeプロジェクトで以前許容されていたimportによるユーザーの摩擦を減らすための互換性機能です。

エラーを投げる代わりに、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" を使っている場合

パッケージが既に "exports" を "react-native" ルートフィールドと併用している場合、上で述べたユーザーに対する破壊的変更を考慮してください。Metroでこの機能を有効にしているユーザーにとっては、モジュール解決時に "exports" が最初に評価されます。

実務上の主な違いは、アプリ内でアクセス不能なサブパス("exports" に含まれていない)がある場合に警告が出る点になると予想しています。

"exports" への移行

packageに "exports" を追加するのは完全に任意です。"exports" を使っていないパッケージについては既存の解決機能は同じように動作し続け、現在の挙動を削除する計画はありません。とはいえ、"exports" の新機能はReact Nativeパッケージメンテナにとって有用な機能群を提供します。

  • パッケージAPIの厳格化:公開サブパスでモジュールAPIを正式に定義する良い機会です。これにより内部APIへのアクセスが防がれ、バグの表面積が減ります。
  • 条件付きエクスポート:パッケージがReact Native for Web(つまり "react-native" と "browser")をターゲットにする場合、これらの条件の解決順序を制御できます(次の見出し参照)。

"exports" を導入する場合、破壊的変更としてリリースすることを推奨します。プラットフォーム固有の拡張の置き換え方法などを含む移行ガイドはMetroのドキュメントに用意しています。

注意: Metroの実装の寛容な振る舞いに依存しないでください。Metroは後方互換性を持ちますが、パッケージは仕様で文書化され他のツールで厳密に実装されているように "exports" を扱うべきです。

新しい "react-native" 条件

条件付きエクスポートで使うためのコミュニティ条件として "react-native" を導入しました。これは "node" や "deno" のような既存のランタイムと並んで、React Nativeフレームワークを表します(RFC参照)。

  • Community Conditions Definitions — "react-native"
    • React Nativeフレームワーク(すべてのプラットフォーム)でマッチします。
    • React Native for Web をターゲットにするには、この条件の前に "browser" を指定する必要があります。
    • これは従来の "react-native" ルートフィールドを置き換えます。

例:

"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 に大きな感謝を送ります。