壁紙アプリのリポジトリを開いて ios/Podfile の末尾を眺めていたら、自分で書き足した3行が残っていました。いつ足したのか、正直なところ思い出せません。
Expo SDK 57 のリリースノートに目を通したのは、その直後のことでした。目立つ見出しは React Native 0.86 への追随なのですが、実際に手元の作業を変えるのは「その他のハイライト」に一行だけ置かれた項目のほうでした。expo prebuild が、既定でネイティブディレクトリを消してから作り直すようになったのです。
思い出せない3行の行き先を、上げる前に決めておく必要が出てきました。
SDK 57 で本当に効くのは prebuild の一行
Expo SDK 57 のチェンジログを素直に読むと、これは意図して小さくまとめられたリリースです。React Native が 0.85 から 0.86 へ上がり、React は SDK 56 と同じ 19.2 で据え置かれています。Expo チーム自身が「これまでで最も簡単なアップグレード」を目指したと書いています。
とはいえ、0.85 と 0.86 の間には 1,552 ファイルに触れる 601 のコミットがあります。破壊的変更がない想定であっても、取り込むかどうかは意識して決めるべきだ、というのが SDK を分けた理由として説明されています。
| 変更点 | 手元の作業に効く度合い |
|---|---|
| React Native 0.85 → 0.86(React は 19.2 据え置き) | Android の edge-to-edge 修正や描画まわりの改善。見た目の詰めに効く |
expo prebuild が既定で android / ios を消して再生成 | ネイティブ側に手を入れているなら最優先。--no-clean で従来の挙動 |
expo@57.0.9 で Hermes V1 のメモリ回帰が解消 | Reanimated や Worklets を使うアプリは実質必須 |
| 開発時の起動が遅くなる回帰(未解決) | 本番アプリには影響なし。開発の体感だけ |
expo-image に writeToCacheAsync / readFromCacheAsync | 画像キャッシュを先回りで埋めたいときに使える |
3行目は見落とさないでください。SDK 57 の初期リリースは SDK 56 から Hermes V1 のメモリ回帰を引き継いでいて、react-native-worklets や react-native-reanimated を読み込むアプリでメモリ使用量が大きく増える場合がありました。8月13日に出た expo@57.0.9 が React Native 0.86.2 を取り込んで解消しています。
Rork が生成するアプリは、画面遷移やジェスチャの実装でほぼ確実に Reanimated 系に触れています。上げるなら 57.0.9 以上、と最初に決めてしまうのが安全でした。
消えるものを、上げる前に見る
--clean が既定になったことの本質は、挙動の変更というより前提の明文化だと私は受け取りました。Continuous Native Generation は元々「ネイティブディレクトリは生成物であって、原本ではない」という約束の上に立っています。手で編集した瞬間、その約束は破れています。
破れていたことに気づくのが、アップグレードの直後だと痛い。だから上げる前に見ます。
作業用のブランチで、いまのディレクトリの控えを取ってから再生成し、差分だけを読む手順です。
# 1. 作業ブランチを切る
git switch -c chore/sdk57-prebuild-audit
# 2. 現状の ios/ を控える(.gitignore している場合でも比較できる)
cp -R ios ../ios-before-sdk57
cp -R android ../android-before-sdk57
# 3. 現行 SDK のまま、既定の挙動で作り直す
npx expo prebuild --clean
# 4. 差分だけを読む(生成物どうしの比較なので、残るのは自分の痕跡)
diff -ru ../ios-before-sdk57/Podfile ios/Podfile
diff -rq ../ios-before-sdk57 ios | grep -v "Pods\|build\|\.xcworkspace"
diff -ru ../android-before-sdk57/app/src/main/AndroidManifest.xml \
android/app/src/main/AndroidManifest.xmlPods と build を除外しているのは、この2つが依存解決とビルドの副産物で、読んでも判断材料にならないためです。ここを外さないと差分が数千行になり、肝心の3行が埋もれます。
出てきた差分は、3つに仕分けます。
- 自分が手で入れた変更 — 移し替えの対象です。ここが本題になります
- ライブラリの autolinking が入れたもの — 再生成でも同じものが入ります。放置して構いません
- Expo 側のテンプレート更新 — 上げれば新しいほうが入ります。むしろ取り込みたい差分です
私の場合、1に該当したのは Podfile の3行と、Info.plist に足していた写真ライブラリの用途説明でした。どちらも「その場で通したくて手で書いた」類のもので、config plugin に移すべきだったものです。
手編集を config plugin に移す
移し替えは、消える前提のファイルを直接書くのをやめて、生成のたびに同じ変更が入る形に置き換える作業です。プロジェクト直下に plugins/with-native-tweaks.js を1つ置くところから始めました。
// plugins/with-native-tweaks.js
const fs = require('fs');
const path = require('path');
const {
withInfoPlist,
withAndroidManifest,
withDangerousMod,
AndroidConfig,
} = require('expo/config-plugins');
// (1) Info.plist の用途説明を宣言で入れる
const withPhotoLibraryUsage = (config, { message }) =>
withInfoPlist(config, (config) => {
config.modResults.NSPhotoLibraryAddUsageDescription = message;
return config;
});
// (2) AndroidManifest の属性を宣言で入れる
const withManifestAttribute = (config, { name, value }) =>
withAndroidManifest(config, (config) => {
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
app.$[name] = value;
return config;
});
// (3) Podfile への追記だけは、ファイルを直接触るしかない
const withPodfileSnippet = (config, { snippet }) =>
withDangerousMod(config, [
'ios',
async (config) => {
const podfile = path.join(config.modRequest.platformProjectRoot, 'Podfile');
const contents = fs.readFileSync(podfile, 'utf8');
// 二重追記を防ぐ。prebuild は何度も走るため、冪等性が要ります
if (contents.includes(snippet.trim())) {
return config;
}
fs.writeFileSync(podfile, `${contents}\n${snippet}\n`, 'utf8');
return config;
},
]);
module.exports = (config, props = {}) => {
config = withPhotoLibraryUsage(config, {
message: props.photoLibraryMessage ?? '保存した壁紙を写真に書き出すために使用します',
});
config = withManifestAttribute(config, {
name: 'android:largeHeap',
value: 'true',
});
config = withPodfileSnippet(config, {
snippet: props.podfileSnippet ?? '',
});
return config;
};app.config.ts 側で読み込みます。
export default {
name: 'Wallpaper',
slug: 'wallpaper',
plugins: [
[
'./plugins/with-native-tweaks',
{
photoLibraryMessage: '保存した壁紙を写真に書き出すために使用します',
podfileSnippet: "post_install do |installer|\n # プロジェクト固有の後処理\nend",
},
],
],
};書いていて意外だったのは、withDangerousMod の中の冪等チェックが思ったより重要だった点です。prebuild は既定で消してから作り直すため、素直に考えれば毎回まっさらな Podfile に1回だけ追記されるはずです。ところが --no-clean を混ぜて走らせたり、EAS Build 側の環境と手元で挙動が分かれたりすると、同じ行が二度入ることがあります。追記系のプラグインは、必ず「既にあるなら何もしない」を先に書いてください。
withInfoPlist と withAndroidManifest は上書き代入なので、この心配がありません。直接ファイルを触る withDangerousMod は最後の手段として扱い、宣言で書けるものは宣言で書くほうが結果的に安全でした。
移し終えたら、もう一度 npx expo prebuild --clean を走らせて、控えとの差分から自分の痕跡が消えていることを確認します。差分が autolinking とテンプレート更新だけになれば、移行は完了です。
アップグレードの実行順
洗い出しと移し替えが済んでから、ようやく本体を上げます。順序を守るだけで、原因の切り分けがずいぶん楽になりました。
- 依存を SDK 57 に揃える —
npx expo install expo@^57.0.0 --fix - バージョンを目で確認する —
npm ls expoで57.0.9以上になっているかを見ます。初期リリースのままだと、先ほどのメモリ回帰を踏みます - 健康診断を通す —
npx expo-doctor@latest - 旧 SDK 向けに生成した
androidとiosを消す — 残しておく理由がありません。次のビルドで作り直されます - 作り直す —
npx expo prebuild。ここで config plugin の効きを確認します - 開発ビルドを作り直す —
expo-dev-clientを使っているなら、古い開発ビルドは SDK 57 のバンドルを読めません - 実機で確認する — Reanimated を使っている画面のメモリ挙動を、上げる前と比べて見ておくと安心です
3の expo-doctor は、飛ばしたくなりますが飛ばさないでください。依存のバージョン不整合を、実機ビルドの失敗より先に教えてくれます。
なお、開発時の起動が遅くなる回帰は執筆時点で未解決です。本番アプリには影響しないと明記されているので、開発中に「重くなった気がする」と感じても、それが原因の可能性があります。自分のコードを疑い始める前に、既知の回帰を思い出してください。
--no-clean をいつ使うか
逃げ道は用意されています。npx expo prebuild --no-clean で、従来どおり既存のフォルダに変更を当てる挙動に戻せます。
ただ、私はこれを恒久的な運用にはしないと決めました。理由は自分の構成にあります。
壁紙アプリは1つのコードベースから複数本を出し分けていて、app.config.ts と EAS のビルドプロファイルで名前もアイコンも識別子も切り替えています。この構成では ios/ を1つ持ち続けること自体に意味がありません。どのアプリ向けの ios/ なのかが定まらないからです。詳しい組み方はapp.config.ts と EAS で組むホワイトラベル構成にまとめています。
--no-clean が向くのは、移行の途中で「今日中にビルドを通したい」という一時的な場面だと考えています。手編集が残っている状態でアップグレード当日を迎えてしまったとき、その日を凌ぐための道具です。凌いだあとで、消える前提の場所から config plugin へ移す。順序が逆になっただけで、やることは変わりません。
Rork で生成したプロジェクトは、コードを自分で所有できるぶん、手で直す誘惑が常にあります。私も何度も手で直してきました。それ自体は悪いことではないと思っています。ただ、直した場所が生成物なのかどうかだけは、直したその日に判断しておいたほうが、後の自分が楽になります。
今日できる一歩は、アップグレードそのものではなく、prebuild --clean の差分を一度取っておくことです。5分で終わりますし、そこに何も出てこなければ、SDK 57 へのアップグレードは本当に「これまでで最も簡単」なものになります。
私自身まだ移行の途中で、Android 側の差分はこれから読むところです。同じ地点にいる方の手がかりになれば嬉しく思います。お読みいただきありがとうございました。