Skip to content
Back to skills

Expo Sdk Upgrade

ASecurity

Moves an Expo app to the next SDK and Xcode/Android toolchain in one proven commit - expo install --fix, overrides, breaking changes. Use when an SDK, Xcode or targetSdk upgrade is due. Not for one package (dependency-management).

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 18, 2026
developmenttypescriptgoreactnodetestinggitapi

Works with

  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill expo-sdk-upgrade --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Expo Sdk Upgrade?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Expo Sdk Upgrade
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-expo-sdk-upgrade/badge)](https://www.skillsdirectory.com/skills/zakariaf-expo-sdk-upgrade)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: expo-sdk-upgrade
description: Moves an Expo app to the next SDK and Xcode/Android toolchain in one proven commit - expo install --fix, overrides, breaking changes. Use when an SDK, Xcode or targetSdk upgrade is due. Not for one package (dependency-management).
---

# Expo SDK upgrade

Moves the app (every app and shared package, when multi-package) to the next Expo SDK, or the build
toolchain to another Xcode or Android level, in one proven step: only when the new SDK is stable and
old enough or a store deadline forces it, with every package, tool, override, code migration and
toolchain change in one commit, and two checkers that prove when a move is due and that the repo sits
on one SDK line.

## Rules that must hold

1. **Move only when the trigger holds:** npm `latest` (or the target's `sdk-<n>` tag) is the new SDK
   and at least the repo's release age old (`.npmrc` `min-release-age`, 7 days at least, measured to
   the minute as npm measures it), and so is every package the repo takes from its module map plus
   the `@react-native/*` tools at the new React Native version (`check-sdk-trigger.mjs` reports
   `upgrade-due`), and on a branch the whole repo passes the gate, iOS and Android release builds,
   E2E and the visual baselines. `RESULT: PASS` from the trigger means no move is due (stay). Never a
   preview, beta, release candidate or `next` release, never a release-age exclude for an SDK. *Why:*
   previews break things, and the first patch releases fix what `.0` broke; an SDK is never urgent
   enough for an exception.
2. **One SDK at a time, unless Expo's notes say to skip one; never an expo release the SDK's advisory
   rules out.** Apps on SDK 55 go straight to SDK 57 and no app installs expo 57.0.8 or older (a
   Hermes V1 memory regression with Worklets and Reanimated, in SDK 56 too); the SDK lines hold this <!-- stack-pin-ignore -->
   as `skipFrom` and `minExpo`, the trigger picks the target from them and `expo-min` fails a release
   below the floor. *Why:* skipping changes nothing else in the move, and the regression ships to
   every user.
3. **Move the whole repo in one commit, on a branch.** *Why:* Expo supports one React Native version
   per repo, and a half-moved tree can be neither tested nor rolled back cleanly.
4. **Expo-managed packages move only with `npx expo install expo@~<version>` then
   `npx expo install --fix`** (in every app when multi-package). Never hand-edit an Expo-managed
   specifier or install `react-native`, React, Reanimated, Worklets, Skia or Gesture Handler by hand.
   *Why:* Expo tests one coherent native set per SDK, and `--fix` writes exactly that set.
5. **Everything that follows the SDK moves in the same commit:** the root tools in one
   `npm install -D` (`jest-expo`, `eslint-config-expo`, `@react-native/jest-preset`,
   `@react-native/eslint-plugin`, `@types/react`, `test-renderer`, and `jest`, `babel-jest`,
   `@types/jest` when the SDK's jest-expo peers on a new Jest line), every root `overrides` key at the
   version the app installs, `allowScripts`, the code migrations, eas.json images that name an SDK,
   Android SDK pins that fell behind, and the app's `contracts/dependency-policy.json` rows. *Why:*
   each one alone breaks the install (`ERESOLVE`), the tree (duplicate native modules) or a gate.
6. **Tools the SDK does not move stay:** TypeScript (6.0 on SDKs 56 to 58), ESLint, Prettier and
   everything outside Expo's map keep their own held-back triggers (dependency-management). A tool
   ahead of the SDK's line fails `held-back`; one behind a line the SDK needs fails `sdk-tooling`.
   *Why:* two moves in one commit cannot be debugged.
7. **Readiness first:** run `check-sdk-alignment.mjs --target-sdk <next>` before the branch and fix
   every `FAIL` (React Native or Jest APIs the next SDK removes, app config keys its schema dropped,
   an Xcode too old) on the current SDK in its own commit; the `todo` lines (config plugin imports,
   Jest config, builder gestures, image aliases, pins) change only in the move commit. *Why:* the
   replacements already work on the old SDK, so the move commit stays about the move.
8. **When the SDK moves a library to a new API generation, migrate every user of it as a whole** in
   the move commit (Gesture Handler 2 builder to 3 hooks: every component that builds gestures).
   *Why:* hook and builder gestures cannot be related to each other, so a component that mixes them
   loses the exclusive or simultaneous relations it depends on.
9. **The toolchain follows the SDK and is chosen by version.** Xcode through `DEVELOPER_DIR` on a Mac
   (never `sudo xcode-select`) or an EAS image (`auto`, `sdk-<n>` or an exact name, never `latest`),
   recorded in one place the checkers read; an Xcode major newer than the SDK supports gets that SDK's
   extra step first (SDK 57 with Xcode 27 needs `ios.enableSceneSupport`, removed again on SDK 58;
   SDKs 55 and 56 have no such option and stay on Xcode 26); Android compileSdk, targetSdk and minSdk
   and the iOS deployment target follow the SDK, and an `expo-build-properties` pin never stays behind
   them. *Why:* a floating image or a stale pin changes the binary without a commit, a deployment
   target below the SDK's pods fails `pod install`, and an iOS 27 SDK build on the application life
   cycle does not launch correctly on iOS 27.
10. **Store deadlines are triggers too:** an App Store minimum Xcode or a Google Play minimum
    targetSdk in effect that the app does not meet fails `store-deadline` and is fixed by a toolchain
    or SDK move. *Why:* the store rejects the next upload regardless of how stable the app is.
11. **Never loosen a gate or re-accept a golden or screenshot to get green.** Every diff is explained
    by the change list and committed with a `Gate-Change:` trailer. *Why:* a silent visual or
    behavioural regression ships to every user at once.
12. **Read the new versions' release notes, versioned docs and published packages, never memory.**
    Put every new finding in the move report; when this skill's entry for the new SDK is missing or
    pre-release, write the app's own `contracts/expo-sdk-lines.json` entry from the stable release.
    *Why:* SDK facts change between previews (SDK 58's jest-expo switched to Jest 30 while SDK 58 was
    still a pre-release).
13. **Stop and ask before the branch (one message with a default: start on the branch) and wait for
    an explicit go before merging, pushing, EAS builds or uploads** (silence is never a go: the
    branch waits unmerged). Installing Xcode, licences, simulator runtimes, JDKs and `sudo` are human
    steps.

## Workflow

1. Read [references/upgrade-policy.md](references/upgrade-policy.md): the current and next line,
   the trigger, store deadlines, what moves together, what never does.
2. Check the trigger: `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-trigger.mjs . --online
   --save-facts reports/sdk-trigger.json` (offline: `--facts <saved file>`; with neither it exits 2).
   Its `sdk` line names the target (the next SDK, or the one Expo says to move to: a `note` line
   says so). `RESULT: PASS` means no move is due: its `wait` lines (`target-stable`, `target-age`,
   `package-age`, `package-missing`) say why and from which moment it can change; stop and, if asked,
   report "not yet" with that moment (message 1 of [examples/sdk-move-report.md](examples/sdk-move-report.md)).
   `FAIL [upgrade-due]`: the move is due; keep its `plan` lines and continue. `FAIL [store-deadline]`:
   a toolchain move is due; go to step 6.
3. Start green (the project gate, clean `git status`, and the plain
   `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs .` passing: the readiness scan checks
   imports only against the target SDK's line), then scan readiness:
   `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs . --target-sdk <target>` (58 from SDK
   57). Fix every `FAIL` on the current SDK in its own commit; the `todo` lines are the move commit's
   work list.
4. Tell the developer (message 2 of the example) and continue unless told to wait.
5. Read [references/sdk-58-changes.md](references/sdk-58-changes.md) in full (or the release notes
   of the target SDK, for 57 and later ones), then follow [references/runbook.md](references/runbook.md) steps 5 to 18: branch,
   `npx expo install`, root tools in one command, overrides, approvals, code migrations (config plugin
   imports, Jest resolver and Jest 30, Gesture Handler 3, scene-support removal), EAS images, Android
   and iOS pins, the dependency policy rows and, when needed, the app's SDK line entry
   ([references/sdk-lines.md](references/sdk-lines.md)).
6. For a toolchain-only change (a new Xcode, an EAS image, an Android target, a store deadline) read
   [references/toolchain.md](references/toolchain.md). SDK 57 with Xcode 27: copy
   `templates/scripts/config/scene-support.ts` and its test to `scripts/config/`, import it in
   `app.config.ts` with the extension, `import { addSceneSupport } from './scripts/config/scene-support.ts';`
   (Expo loads `app.config.ts` through Node, which needs the `.ts`; without it `npx expo config`
   fails with "Cannot find module"), wrap the plugin list with `addSceneSupport([...])`, then record
   the new Xcode.
7. Verification loop (runbook "Verification loop"): `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs .`
   until `RESULT: PASS` (each `FAIL` names the file, the rule and the fix), `npx expo install --check`,
   `npx expo-doctor` (an `install.exclude` entry only for a stale Expo suggestion, with its reason:
   runbook failure table), `npm ls react react-native react-native-reanimated react-native-worklets`, the
   gate, then a clean prebuild with Release builds on a named iOS simulator and a named Android
   emulator (or EAS preview builds), E2E and the visual baselines (hand-offs below); open every diff.
8. Commit the move as one commit with its `Gate-Change:` trailer (runbook "Commit, report, merge"),
   report with message 3 of the example, and merge or push only on the developer's go.

## Definition of done

- [ ] Before the branch, `check-sdk-trigger.mjs` reported `upgrade-due` (or `store-deadline` for a
      toolchain move) and its facts are saved in `reports/`; after the move
      `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-trigger.mjs . --online` prints `RESULT: PASS` again.
- [ ] Every app declares the new `expo` and exactly the new module map's specifiers;
      `npx expo install --check` and `npx expo-doctor` pass (in every app when multi-package);
      `npm ls` shows one version of `react`, `react-native`, `react-native-reanimated` and
      `react-native-worklets`.
- [ ] Root tools, overrides, `allowScripts`, eas.json images, Android and iOS pins and the dependency
      policy rows moved in the same commit; TypeScript, ESLint and the other held-back tools did not.
- [ ] Config plugin imports, the Jest config, gestures and scene support match the new SDK; the gate
      is green.
- [ ] Release builds launched on a named iOS simulator and a named Android emulator (or EAS preview
      builds), E2E passed, and every golden or screenshot diff was looked at and explained.
- [ ] The developer got the report; nothing was merged, pushed, built on EAS or uploaded without an
      explicit go (silence is not one: the branch waits unmerged).
- [ ] Expected SKIP lines only: `SKIP eas.json [xcode] not yet created (eas-build-and-submit creates
      it)` while the app has neither eas.json nor an Xcode pin, and `SKIP jest.config.js [jest-config]
      not yet created (unit-and-component-tests creates it)` while it has neither a `jest.config.*` file
      nor a package.json `"jest"` key (on an SDK with Jest config lines). `note` lines are information.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs .` prints `RESULT: PASS`

## Anti-patterns

- **Adopting a preview, beta, release candidate or `next` release,** or adding a
  `min-release-age-exclude` to get an SDK early: wait for the trigger.
- **Moving one part first "to try it"** (one app, one package, the tools before the SDK), or merging a
  half-moved repo: the whole move lives on one branch until it is green.
- **Installing `jest-expo` alone**, then `--legacy-peer-deps` to silence `ERESOLVE`: install the root
  tools in one command, Jest included when the SDK's jest-expo peers on a new line.
- **`npm dedupe` or deleting `node_modules` against duplicate native modules:** set the overrides and
  `npm install`; expo-doctor must show no duplicates.
- **Switching config plugins to `expo/config-plugins` before the move:** on SDK 57 and older, Node
  ESM (a plugin module referenced as `./plugins/x.ts`, scripts under `"type": "module"`) cannot
  resolve the bare specifier; `app.config.ts` itself, which Expo loads as CommonJS, accepts both, but
  keeps the house `.js` form (a `note` line). Keeping `expo/config-plugins.js` after the move breaks
  too, in every file that imports it, tests and test helpers included: SDK 58 resolves it to
  `config-plugins.js.js` (Node, Jest) and tsc fails with TS2307.
- **Mocking Reanimated to quiet Jest:** the mock loads the native initializers too; use the Reanimated
  resolver plus the asset-registry mapping.
- **Mixing builder and hook gestures** during the move: migrate each component completely.
- **`jest -u` over every golden, or a new screenshot baseline without looking:** open each diff;
  accept only what the change list explains.
- **`"image": "latest"` in eas.json, or an `sdk-<n>` alias left from the old SDK:** use `auto`,
  `sdk-<n>` of the current SDK or an exact image.
- **Selecting Xcode 27 on SDK 57 without scene support, or with `sudo xcode-select`.**
- **Leaving an `expo-build-properties` `compileSdkVersion`/`targetSdkVersion` or `ios.deploymentTarget`
  pin from an older SDK**, or keeping `newArchEnabled` and `android.edgeToEdgeEnabled` in the app config.
- **Moving an SDK 55 app to 56, or installing expo 57.0.8 or older:** go to the newest SDK 57 release.
- **Taking TypeScript 7, ESLint 10, a Node or npm move, or any held-back major along with the SDK.**

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/upgrade-policy.md](references/upgrade-policy.md) | Current and next SDK line, the three-part trigger, store deadlines, what moves together and what never does, toolchain policy in short, human steps, fact-check commands | Workflow step 1, and at session starts in an SDK window |
| [references/runbook.md](references/runbook.md) | The ordered steps with commands and expected output, the iOS and Android verification loop, commit, failure table, rollback, when multi-package, other package managers (Yarn, pnpm, Bun) | Workflow steps 5, 7 and 8, and when the repo has no package-lock.json |
| [references/sdk-58-changes.md](references/sdk-58-changes.md) | Every 57 to 58 change with its error text and fix: packages, root tools, overrides, config plugins, Jest resolver, Jest 30, Gesture Handler 3, Reanimated, Skia, React 19.3, React Native 0.88, Android AGP 9, iOS | Workflow step 5, and for each failing gate during the move |
| [references/toolchain.md](references/toolchain.md) | Where the Xcode version is recorded, Xcode selection, EAS images, scene support for SDK 57 with Xcode 27, Android SDK levels, AGP and JDK, store deadlines | Workflow step 6, and on a `scene-support`, `xcode`, `eas-image`, `android-sdk` or `store-deadline` failure |
| [references/sdk-lines.md](references/sdk-lines.md) | Every key of an SDK entry and the rule that reads it, the app's `contracts/expo-sdk-lines.json`, `rn-skills.config.json` keys, refreshing the data at each SDK release | Before writing an SDK entry; on `sdk-unknown` or `sdk-lines-contract` |
| [examples/sdk-move-report.md](examples/sdk-move-report.md) | The three developer messages (not yet, ready, done), the evidence list and the move commit, for a Tasks app | Workflow steps 2, 4 and 8 |
| `examples/expo-sdk-lines.json` | A complete repo-owned SDK entry (the SDK 58 snapshot) to start `contracts/expo-sdk-lines.json` from | When this skill lacks the app's new SDK |
| `templates/scripts/config/scene-support.ts` | `addSceneSupport(plugins)`: merges `ios.enableSceneSupport` into the `expo-build-properties` entry (tested) | Workflow step 6 (SDK 57 with Xcode 27) |
| `templates/scripts/config/scene-support.test.ts` | Its Jest tests (4 cases) | Copied with the template |
| `assets/sdk-lines.json` | Per-SDK expectations: React, test-renderer, @types/react, TypeScript and Jest lines, Gesture Handler major, config-plugin import, Jest config lines, Xcode and scene support, Android levels, EAS images, removed APIs and config keys | Read by the scripts |
| `assets/sdk-lines.schema.json` | The shape of an SDK entry (the skill's table and the app's contract) | When writing an entry |
| `assets/store-requirements.json` | App Store minimum Xcode and Google Play minimum targetSdk, with dates and sources | Read by `check-sdk-trigger.mjs`; add new store rules |
| `assets/expo-sdk-57-module-map.json` | Expo 57.0.27's `bundledNativeModules.json` (synced from the library, shared with dependency-management; do not edit here) | Read by the scripts when `node_modules` is absent |
| `assets/expo-sdk-58-next-module-map.json` | Expo 58.0.3's map (npm `next`, 2026-10-04); replaced by the stable map at the move | Read by the scripts when `node_modules` is absent |
| `assets/shared.json` | Declares the shared files copied into this skill | When adding a shared file |
| `scripts/check-sdk-trigger.mjs` | Is an SDK or toolchain move due? From npm facts (`--online` or `--facts`): `RESULT: PASS` = no move due, with `wait` reasons and dates; `FAIL [upgrade-due]` with this repo's plan; `FAIL [store-deadline]`; exit 2 without facts | Workflow step 2, at session starts in an SDK window, and after the move |
| `scripts/check-sdk-alignment.mjs` | Proves the repo sits on one SDK line (`--help` lists every rule); `--target-sdk` adds the readiness scan | Workflow steps 3 and 7, and after any SDK or toolchain change |
| `scripts/lib/sdk.mjs` | Helpers: the repo (single app or workspaces), SDK lines and the app's contract, module maps, code files, eas.json, Xcode sources, Android pins, versions and dates | Read only to change a script |
| `scripts/selftest.mjs` | Proves both scripts on five overlay suites: SDK 57, readiness for 58, SDK 58, multi-package, and the trigger on saved facts (one planted bug per `tests/fixtures/*/bad-*` case; pass cases pin SKIP, NOT APPLICABLE, notice and wait lines) | After changing a script, an asset, a template or a fixture |
| `scripts/check-lib.mjs` | Shared script helper, synced from the library (do not edit here) | Never by hand |
| `tests/bases/` | Base trees: a single app on SDK 57, the SDK 58 overlay, two apps and a shared package, saved trigger facts | When adding a case |
| `tests/fixtures/` | One folder per suite (`SUITE.json`) and per case (`EXPECT.txt`, `mutation.json`, `ARGS.txt`) | When adding a rule |

## Related skills

- `dependency-management` - single-package changes, the policy rows the move updates, held-back majors, the Node move.
- `unit-and-component-tests` - the Jest config, setup file and mocks the move touches (Jest 30).
- `golden-and-snapshot-tests` - reviewing and re-accepting goldens after a Skia or React Native change.
- `run-goldens-rebaseline` - the guarded re-accept of every golden when Jest 30 rewrites the snapshot headers.
- `native-modules-and-new-architecture` - rechecking `contracts/native-libraries.json` after the move.
- `ios-simulator-build`, `android-emulator-build` - clean prebuild, Release builds on named devices, Xcode selection.
- `eas-build-and-submit` - eas.json profiles and images, EAS builds.
- `e2e-maestro` - E2E flows and the screenshot matrix on the branch.
- `expo-prebuild-and-config-plugins` - local config plugins, `expo-build-properties`, AGP-sensitive plugins.
- `canvas-gestures-and-hit-testing`, `motion-and-animation`, `skia-canvas-rendering` - gesture, Reanimated and Skia code the change list touches.
- `ota-updates` - runtime versions across an SDK move.
- `quality-gates`, `git-commits-and-reporting` - the gate, the `Gate-Change:` trailer, the move commit and report.
- Expo's `expo-upgrade` skill - Expo's per-SDK migration guides (React 19, expo-av, React Navigation imports).

Files in this skill

  • SKILL.md19.9 KB
  • assets/expo-sdk-57-module-map.json4.2 KB
  • assets/expo-sdk-58-next-module-map.json4.6 KB
  • assets/sdk-lines.json13.1 KB
  • assets/sdk-lines.schema.json8.6 KB
  • assets/shared.json162 B
  • assets/store-requirements.json1 KB
  • examples/expo-sdk-lines.json6 KB
  • examples/sdk-move-report.md4.5 KB
  • references/runbook.md14.5 KB
  • references/sdk-58-changes.md19.3 KB
  • references/sdk-lines.md7.4 KB
  • references/toolchain.md10.4 KB
  • references/upgrade-policy.md9.8 KB
  • scripts/check-sdk-alignment.mjs34.7 KB
  • scripts/check-sdk-trigger.mjs21.6 KB
  • scripts/lib/sdk.mjs31.4 KB
  • scripts/selftest.mjs4.6 KB
  • templates/scripts/config/scene-support.test.ts1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.