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
- Move only when the trigger holds: npm
latest(or the target'ssdk-<n>tag) is the new SDK and at least the repo's release age old (.npmrcmin-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.mjsreportsupgrade-due), and on a branch the whole repo passes the gate, iOS and Android release builds, E2E and the visual baselines.RESULT: PASSfrom the trigger means no move is due (stay). Never a preview, beta, release candidate ornextrelease, never a release-age exclude for an SDK. Why: previews break things, and the first patch releases fix what.0broke; an SDK is never urgent enough for an exception. - 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
as
skipFromandminExpo, the trigger picks the target from them andexpo-minfails a release below the floor. Why: skipping changes nothing else in the move, and the regression ships to every user. - 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.
- Expo-managed packages move only with
npx expo install expo@~<version>thennpx expo install --fix(in every app when multi-package). Never hand-edit an Expo-managed specifier or installreact-native, React, Reanimated, Worklets, Skia or Gesture Handler by hand. Why: Expo tests one coherent native set per SDK, and--fixwrites exactly that set. - 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, andjest,babel-jest,@types/jestwhen the SDK's jest-expo peers on a new Jest line), every rootoverrideskey 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'scontracts/dependency-policy.jsonrows. Why: each one alone breaks the install (ERESOLVE), the tree (duplicate native modules) or a gate. - 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 failssdk-tooling. Why: two moves in one commit cannot be debugged. - Readiness first: run
check-sdk-alignment.mjs --target-sdk <next>before the branch and fix everyFAIL(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; thetodolines (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. - 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.
- The toolchain follows the SDK and is chosen by version. Xcode through
DEVELOPER_DIRon a Mac (neversudo xcode-select) or an EAS image (auto,sdk-<n>or an exact name, neverlatest), 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 needsios.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 anexpo-build-propertiespin 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 failspod install, and an iOS 27 SDK build on the application life cycle does not launch correctly on iOS 27. - 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-deadlineand is fixed by a toolchain or SDK move. Why: the store rejects the next upload regardless of how stable the app is. - 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. - 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.jsonentry 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). - 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
sudoare human steps.
Workflow
- Read references/upgrade-policy.md: the current and next line, the trigger, store deadlines, what moves together, what never does.
- 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). Itssdkline names the target (the next SDK, or the one Expo says to move to: anoteline says so).RESULT: PASSmeans no move is due: itswaitlines (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).FAIL [upgrade-due]: the move is due; keep itsplanlines and continue.FAIL [store-deadline]: a toolchain move is due; go to step 6. - Start green (the project gate, clean
git status, and the plainnode ${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 everyFAILon the current SDK in its own commit; thetodolines are the move commit's work list. - Tell the developer (message 2 of the example) and continue unless told to wait.
- Read 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 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). - For a toolchain-only change (a new Xcode, an EAS image, an Android target, a store deadline) read
references/toolchain.md. SDK 57 with Xcode 27: copy
templates/scripts/config/scene-support.tsand its test toscripts/config/, import it inapp.config.tswith the extension,import { addSceneSupport } from './scripts/config/scene-support.ts';(Expo loadsapp.config.tsthrough Node, which needs the.ts; without itnpx expo configfails with "Cannot find module"), wrap the plugin list withaddSceneSupport([...]), then record the new Xcode. - Verification loop (runbook "Verification loop"):
node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs .untilRESULT: PASS(eachFAILnames the file, the rule and the fix),npx expo install --check,npx expo-doctor(aninstall.excludeentry 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. - 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.mjsreportedupgrade-due(orstore-deadlinefor a toolchain move) and its facts are saved inreports/; after the movenode ${CLAUDE_SKILL_DIR}/scripts/check-sdk-trigger.mjs . --onlineprintsRESULT: PASSagain. - Every app declares the new
expoand exactly the new module map's specifiers;npx expo install --checkandnpx expo-doctorpass (in every app when multi-package);npm lsshows one version ofreact,react-native,react-native-reanimatedandreact-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, andSKIP jest.config.js [jest-config] not yet created (unit-and-component-tests creates it)while it has neither ajest.config.*file nor a package.json"jest"key (on an SDK with Jest config lines).notelines are information. -
node ${CLAUDE_SKILL_DIR}/scripts/check-sdk-alignment.mjs .printsRESULT: PASS
Anti-patterns
- Adopting a preview, beta, release candidate or
nextrelease, or adding amin-release-age-excludeto 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-expoalone, then--legacy-peer-depsto silenceERESOLVE: install the root tools in one command, Jest included when the SDK's jest-expo peers on a new line. npm dedupeor deletingnode_modulesagainst duplicate native modules: set the overrides andnpm install; expo-doctor must show no duplicates.- Switching config plugins to
expo/config-pluginsbefore 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.tsitself, which Expo loads as CommonJS, accepts both, but keeps the house.jsform (anoteline). Keepingexpo/config-plugins.jsafter the move breaks too, in every file that imports it, tests and test helpers included: SDK 58 resolves it toconfig-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 -uover 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 ansdk-<n>alias left from the old SDK: useauto,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-propertiescompileSdkVersion/targetSdkVersionorios.deploymentTargetpin from an older SDK, or keepingnewArchEnabledandandroid.edgeToEdgeEnabledin 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 | 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 | 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 | 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 | 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 | 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 | 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- recheckingcontracts/native-libraries.jsonafter 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, theGate-Change:trailer, the move commit and report.- Expo's
expo-upgradeskill - Expo's per-SDK migration guides (React 19, expo-av, React Navigation imports).