Expo 40 から 48 へのアップグレードと、EAS build に移行した話

こんにちは。アソビューでフロントエンドエンジニアをしている白井です。

今回はアプリで使用している開発パッケージ Expo SDK のアップグレードと、EAS build 移行した際の対応について書いていきたいと思います。

目的と背景

アソビューでは、販売しているチケットのQRコードを施設側で読み取るためのアプリ「fast-in」を Expo を使用して開発しています。Expo を使うと Android/iOS アプリが React Native のコードで開発することができ、React を使っているフロントエンドエンジニアなら比較的簡単にアプリを構築することが可能です。

以前は Expo の Classic Build を使用して Android・iOS ビルドを行なっていましたが、2023年1月から Classic Build が廃止になり、新しいビルド方式である EAS build に移行しないとビルドが出来なくなりました。

当時「fast-in」では Expo 40 を使用していましたが、EAS build を使うには Expo 41 からでないと使用できず、また新しい OTA アップデートサービスの EAS Update は Expo 46 からでないと使えません。今回は 2023/03 時点の最新版である Expo 48 までアップグレードを行うことにしました。

Expo 40 → 48 へのアップグレード

基本的にはこちらの変更内容を参考にしつつ、必要な箇所を修正していきます。

当初 40 → 48 まで一気にアップグレードしたのですが、エラーを直してもアプリが正しく動作せず原因がはっきりしなかったため、バージョンを一つずつ上げていくことにしました。

Expo 41

Expo 41 では目立ったエラーが出なかったため、次へ進みます。

Expo 42

expo-updates が見つからないエラー

Android Bundling failed 2506ms
Unable to resolve module expo-updates from /**/*/node_modules/sentry-expo/build/sentry.js: expo-updates could not be found within the project.

If you are sure the module exists, try these steps:
 1. Clear watchman watches: watchman watch-del-all
 2. Delete node_modules and run yarn install
 3. Reset Metro's cache: yarn start --reset-cache
 4. Remove the cache: rm -rf /tmp/metro-*
  22 | exports.init = exports.Native = void 0;
  23 | const react_native_1 = require("react-native");
> 24 | const Updates = __importStar(require("expo-updates"));
     |                                       ^
  25 | const expo_constants_1 = __importStar(require("expo-constants"));
  26 | const Application = __importStar(require("expo-application"));
  27 | const integrations_1 = require("@sentry/integrations");

修正の手順が記載されていますが、手順通りにやっても解消されませんでした。そのため こちら を参考にして、削除された必要なパッケージをインストールして解消しました。

npx expo install expo-application expo-constants expo-device expo-updates

Expo 43

InstallationID の生成方法を変更する

端末を特定するIDとして Constants.installationId を使っていましたが、こちらが非推奨となりました。代わりに、Android は AndroidId、iOS は uuid を使うなどして対応する必要があります。

Constants.installationId has been deprecated in favor of generating and storing your own ID. Implement it using expo-application's androidId on Android and a storage API such as expo-secure-store on iOS and localStorage on the web. This API will be removed in SDK 44.

今回はどちらの OS も uuid を使ってローカルストレージに保存する方法を取りました。

// ENV_SETTINGS.appEnv には prod/stg などの環境を渡すようにしています。
const INSTALLATION_STORAGE_KEY = `YOUR_KEY_NAME_${ENV_SETTINGS.appEnv}`

const getInstallationId = async () => {
  const installationId = await SecureStore.getItemAsync(
    INSTALLATION_STORAGE_KEY
  )
  // ローカルストレージに ID があればそれを返す
  if (installationId) {
    return installationId
  } else {
  // ローカルストレージに ID がなければ生成して保存、生成したIDを返す
    const newInstallationId = uuidv4()
    await SecureStore.setItemAsync(INSTALLATION_STORAGE_KEY, newInstallationId)
    return newInstallationId
  }
}

export { getInstallationId }

expo-permission 非推奨により、パーミッションの取り方を変更する

expo-permission が非推奨となり、パーミッションは各パッケージから使える hooks から取得するようになりました。

expo-permissions is now deprecated — the functionality has been moved to other expo packages that directly use these permissions (e.g. expo-location, expo-camera). The package will be removed in the upcoming releases.

今回は expo-camera で発生していたので、書き換えます。

// 不要なパッケージを削除
yarn remove expo-permissions

// expo-cli で expo-camera を追加
npx expo install expo-camera

hooks から吐き出されるパーミッションを取得

const [permission] = Camera.useCameraPermissions()

@types/react がみつからないエラー

このあたりから @types/react が見つからないエラーが発生。原因がはっきりとわかりませんでしたが、こちら を参考に @types/react@17.0.38 を入れたところ解消しました。ただ、後続のバージョンでまたエラーが出始めるのと、バージョンを上げていくと最終的にこの指定は不要になるので、一時的なものです。

✔ It looks like you're trying to use TypeScript but don't have the required dependencies
installed. Would you like to install @types/react?

Expo 44 / Expo 45

このバージョンから、使っていた Expo Go で起動ができない(Expo Go は三世代前の Expo SDK のアプリしか起動することができず、インストール済みの Expo Go が古かった)こと、このバージョンではビルドエラーは発生しなかったこともあり、起動確認は行わずに次のバージョンに進みました。

Expo 46

ここで React と Typescript のバージョンが上がります。

expo-cli の削除

ローカル起動やパッケージアップグレードを行なっていた expo-cli が非推奨となり、代わりに expo パッケージに内包されるようになります。

yarn remove expo-cli

注意点として、expo パッケージのアップグレード・パッケージ検査に使う expo upgradeexpo doctor はまだ expo-cli を使う必要があります。これらは各々の環境にインストールし、必要な時に実行するのが良さそうです。

Global Expo CLI is still required for expo upgrade  and expo doctor: these commands haven’t yet been migrated to standalone packages, they are up next. Invoke them with expo-cli upgrade and expo-cli doctor

Expo 47

withExpoRoot が削除されたためモジュールエラーが出ましたが、特に不要だったので削除して対応。他は特に問題ありませんでした。

src/index.tsx:5:30 - error TS2307: Cannot find module 'expo/build/launch/withExpoRoot.types' or its corresponding type declarations.

Expo 48

いよいよ最新のバージョンです。

app.json の entryPoint の移行

expo.entryPoint が削除されたため、ルートファイルの設定を変更する必要があります。

ConfigError: expo.entryPoint has been removed in favor of the main field in the package.json.

「package.json の main フィールドへ移行してください」とあるのですが、次の EAS build の migration の手順ではまさかの package.json の main はサポートしないとあります。

そのため、プロジェクトルートに index.js を配置しそこでルートコンポーネントを呼び出す方式に修正します。

import { registerRootComponent } from 'expo'
import { AppContainer } from './src/AppContainer'

registerRootComponent(AppContainer)

例では AppContainer を入れていますが、registerRootComponent の引数にルートコンポーネントを入れるようにします。

Classic Build から EAS build への移行

公式にドキュメントがあるので、こちらの手順に沿って必要なところを修正していくのが手っ取り早いです。修正箇所は各々異なるので、今回の対応で必要だった箇所を記載します。

eas.json にビルド設定を記載

ビルドのための設定を eas.json に記載していきます。ドキュメントを参考に簡単に設定します。

{
  "cli": {
    "version": "^3.8.1"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development",
      "ios": {
        "resourceClass": "m1-medium"
      }
    },
    "staging": {
      "distribution": "internal",
      "channel": "staging",
      "ios": {
        "resourceClass": "m1-medium"
      }
    },
    "production": {
      "channel": "production",
      "ios": {
        "resourceClass": "m1-medium"
      }
    }
  },
  "submit": {
    "production": {}
  }

Constants.manifest の置き換え

Constants.manifest が非推奨となったため、ここから取得している値は別のものに置き換える必要があります。今回は Constants.manifest?.releaseChannel などで使用していましたが、こちらは環境変数から取得するようにしました。環境変数については後述します。

expo-cli の --config オプションの廃止

ビルドする際に --config で設定ファイル app.json のファイルパスを指定していましたが、こちらが廃止になりました。代わりに設定ファイルとして app.config.js を配置します。

今回は書き方はこちらを参考に、app.config.js の中でオブジェクトを定義して返す方法を採用しました。次で設定を書いていきます。

app.config.js の設定と環境変数の定義

基本的には、app.json の内容をまるっと移します。そして、環境変数 EAS_BUILD_PROFILE で prodction、staging、development いずれかの環境なのかを判定し分岐をかけることで環境ごとに設定を変えることが可能です。

const PROFILE = process.env.EAS_BUILD_PROFILE ?? 'development'

const COMMON_CONFIGS = {
    ...共通フィールド
}

module.exports = () => {
  if (PROFILE === 'production') {
    return {
      ...COMMON_CONFIGS,
      ...productionのフィールド
   }
    } else if (PROFILE === 'staging') {
    return {
      ...COMMON_CONFIGS,
      ...stagingのフィールド
    }
  } else {
    return {
      ...COMMON_CONFIGS,
      ...developmentのフィールド
    }
  }

EAS_BUILD_PROFILE については、EAS build 時に渡される環境変数となっており、eas build コマンドの profile 引数で渡ってくるので、こちらを用いて分岐をかけることが可能です。

次にアプリ内で使用する環境変数の設定ですが、extra フィールドに設定していきます。

{
  ...,
    extra: {
      appEnv: PROFILE,
      hoge: '...',
    }
}

上記のようにして設定した値は、Constants.expoConfig?.extra から取得することが可能です。

Constants.expoConfig?.extra.appEnv
// -> production

.easignore で不要なファイルを無視する

EAS build を実行する際に、プロジェクトファイルすべてを圧縮してアップロードするようで、余計なファイルが含まれているとサイズが無駄に大きくなってしまいます。

必要なファイル以外は easignore で gitignore ライクに設定が可能なので、いらないファイルがある場合は設定しておきましょう。

ビルドの実行

ここまできたら、あとは公式ドキュメントのビルド設定とコマンドを実行すると EAS build サーバーにファイルがアップロードされ、ビルドが実行されます。

無料版だとビルドは月に30回制限、かつ混雑時はビルド実行までの待ち時間が長く1時間以上かかる場合もあるのでご注意ください(特に夜〜深夜は遅い印象です)。

まとめ

今回は Expo 40 から Expo 48 へのアップグレード、ビルド方式の変更ということで、ビルド・起動時の細かいエラーなども多く結構な作業になりました。また Expo Go アプリは三世代前までしかサポートしていない、かつ Expo SDK の新バージョンは3〜4ヶ月ごとにリリースされることを考えると、一年に一回は最新版へのアップグレードして行った方がよさそうです。

アソビューでは、より良いプロダクトを世の中に届けられるよう一緒に挑戦していくエンジニアを募集しています。カジュアル面談もやっていますので、気になった方はぜひエントリーください!お待ちしております。

www.asoview.com