SDK 58 の変更履歴を開いて、最初に感じたのは「長い」ということでした。
削除されるもの、挙動が変わるもの、非推奨になるもの。それぞれが別の節に分かれていて、読み終えたころには、最初の節に何が書いてあったかが曖昧になっております。読んで理解したつもりでも、いざ自分のプロジェクトに当てはめると、どこが該当するのかは別の作業として残ります。
そこで、読む順番を逆にしてみました。変更履歴から「検索できる形のもの」だけを先に拾い出して、自分のプロジェクトを走査する小さなスクリプトにします。読んで判断するのは、その一覧が出てからです。
Expo SDK 58 は 2026 年 10 月 15 日に公開されました(expo.dev/changelog の掲載による)。React Native は 0.86 から 0.88 へ上がります。Rork から書き出したプロジェクトも中身は普通の Expo プロジェクトですが、Rork 側が SDK 58 にいつ対応するかは、私の確認した範囲では案内がありません。ここで書くのは、書き出したあとのリポジトリを自分で上げる場合の話です。
先に、何を拾うかを三つに分けます
変更履歴の項目を、そのまま並べても点検にはなりません。点検の優先度で三つに分けました。
- REMOVED(消える): 残っているとビルドか型検査が止まります。最優先です
- BEHAVIOR(挙動が変わる): 動きはするが結果が変わります。意図と合っているかを人間が見る必要があります
- DEPRECATED(非推奨): 今は動きます。上げる当日ではなく、次の機会に直せば足ります
変更履歴の記載から、検索の対象にしたのは次の項目です。
| 区分 | 対象 | 直し先 |
|---|---|---|
| REMOVED | InteractionManager/Touchable のルート export/NativeMethods 型/Modal の animated prop | Pressable・instance 型・animationType など |
| REMOVED | react-native/Libraries/* への deep import(Strict TypeScript API が既定) | 公開 API からの import |
| REMOVED | expo-sqlite の syncLibSQL/libSQLOptions | libSQL 対応そのものが削除 |
| BEHAVIOR | File.write() が非同期に(expo-file-system) | await するか writeSync() |
| BEHAVIOR | expo-notifications がフォアグラウンドでも表示される | 意図した挙動かを確認 |
| BEHAVIOR | NODE_ENV=test の expo start が .env.development を読む | テスト用の env の置き場を見直す |
| DEPRECATED | File.md5/ImageBackground/withLayoutContext | File.digest()/View+Image/integrateWithRouter |
StatusBar の非推奨 prop も削除対象と書かれていますが、どの prop かまでは私は特定できておりません。スクリプトには一例として networkActivityIndicatorVisible を入れてあり、該当するかどうかは公式の一覧で確かめる前提にしています。
検査スクリプト全文
Node.js だけで動きます(SDK 58 は Node 22.13 以上、24.3 以上、または 26 以降が前提です。奇数の 23・25 は対象外と書かれています)。依存パッケージはありません。
// audit-sdk58.mjs — SDK 58 へ上げる前に、プロジェクト内の「消える・変わる」API を一覧にする
// 使い方: node audit-sdk58.mjs [プロジェクトのパス]
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import { join, relative, extname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const SELF = fileURLToPath(import.meta.url); // 自分自身は検査対象から外す
const root = process.argv[2] ?? '.';
const SKIP = new Set(['node_modules', '.git', 'ios', 'android', '.expo', 'dist']);
const EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs']);
// level: REMOVED=消える/BEHAVIOR=挙動が変わる/DEPRECATED=非推奨(今は動く)
const RULES = [
{ id: 'rn-interactionmanager', level: 'REMOVED', re: /\bInteractionManager\b/, why: 'RN から削除。requestAnimationFrame か、描画後に走らせたい処理を useEffect 側へ' },
{ id: 'rn-touchable-root', level: 'REMOVED', re: /import\s*\{[^}]*\bTouchable\b[^}]*\}\s*from\s*['"]react-native['"]/, why: 'Touchable のルート export が削除。Pressable へ' },
{ id: 'rn-nativemethods', level: 'REMOVED', re: /\bNativeMethods\b/, why: 'NativeMethods 型が削除。ViewInstance など instance 型へ' },
{ id: 'rn-modal-animated', level: 'REMOVED', re: /<Modal\b[^>]*\banimated\b/, why: 'Modal の animated prop が削除。animationType を使う' },
{ id: 'rn-deep-import', level: 'REMOVED', re: /from\s*['"]react-native\/Libraries\//, why: 'Strict TypeScript API が既定。Libraries/* の deep import は型エラー' },
{ id: 'rn-statusbar-ios-props', level: 'REMOVED', re: /<StatusBar\b[^>]*\bnetworkActivityIndicatorVisible\b/, why: 'StatusBar の非推奨 prop の一部が削除対象。この prop が該当するかは公式の変更履歴で確認(ここは例)' },
{ id: 'fs-file-write', level: 'BEHAVIOR', re: /\.write\(/, why: 'expo-file-system の File.write() は非同期に。同期のままなら writeSync()(File 以外の .write も拾うので目視)' },
{ id: 'fs-md5', level: 'DEPRECATED', re: /\.md5\b/, why: 'File.md5 は非推奨。File.digest() へ' },
{ id: 'sqlite-libsql', level: 'REMOVED', re: /\b(syncLibSQL|libSQLOptions|useLibSQL)\b/, why: 'expo-sqlite の libSQL 対応が削除' },
{ id: 'rn-imagebackground', level: 'DEPRECATED', re: /\bImageBackground\b/, why: '非推奨。View + 絶対配置の Image へ' },
{ id: 'router-withlayoutcontext', level: 'DEPRECATED', re: /\bwithLayoutContext\b/, why: '非推奨。integrateWithRouter へ' },
{ id: 'notif-foreground', level: 'BEHAVIOR', re: /from\s*['"]expo-notifications['"]/, why: 'フォアグラウンド通知が既定で表示される。意図した挙動か確認' },
{ id: 'nodeenv-test', level: 'BEHAVIOR', re: /NODE_ENV=test/, why: 'NODE_ENV=test で expo start すると .env.development を読む' },
];
function* walk(dir) {
for (const name of readdirSync(dir)) {
if (SKIP.has(name)) continue;
const p = join(dir, name);
const st = statSync(p);
if (resolve(p) === SELF) continue;
if (st.isDirectory()) yield* walk(p);
else if (EXT.has(extname(name)) || name === 'package.json') yield p;
}
}
const hits = [];
for (const file of walk(root)) {
const lines = readFileSync(file, 'utf8').split('\n');
lines.forEach((line, i) => {
for (const r of RULES) if (r.re.test(line)) hits.push({ r, file: relative(root, file), line: i + 1, text: line.trim() });
});
}
// ネイティブ側の確認(CNG でない場合に効く)
const notes = [];
if (existsSync(join(root, 'ios'))) notes.push('ios/ があります。CNG なら削除して再生成、手書き管理なら AppDelegate のシーン対応(SceneDelegate.swift)を確認');
if (existsSync(join(root, 'android'))) notes.push('android/ があります。R8 が release で既定オンになるため keep ルールを確認');
const order = { REMOVED: 0, BEHAVIOR: 1, DEPRECATED: 2 };
hits.sort((a, b) => order[a.r.level] - order[b.r.level] || a.file.localeCompare(b.file));
for (const h of hits) console.log(`[${h.r.level}] ${h.file}:${h.line} ${h.r.id}\n ${h.text}\n → ${h.r.why}`);
const c = (l) => hits.filter((h) => h.r.level === l).length;
console.log(`\n集計: REMOVED ${c('REMOVED')} / BEHAVIOR ${c('BEHAVIOR')} / DEPRECATED ${c('DEPRECATED')}`);
notes.forEach((n) => console.log('NOTE: ' + n));
process.exit(c('REMOVED') > 0 ? 1 : 0);なぜこう書くかを三点だけ補足します。
- ルールを配列にして、
whyに直し先まで書いておきます。 出力を読んだ時点で次の行動が決まるようにするためです。ルールを足すのも一行で済みます - 終了コードを REMOVED の有無で変えています。 後から CI に載せるとき、REMOVED がゼロでなければ止める、という使い方がそのままできます
- ios/ と android/ の存在を最後に注記します。 ネイティブ側は文字列検索では見えないので、「ここは別の確認が要ります」と出すだけに留めました