RORK LABJP
DEADLINE — Two days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeEXTENSION — The extension that keeps you shipping to all users until November 1 cannot be filed after the deadline passes, so the decision point is effectively todayCHECK — What matters is not the targetSdkVersion number itself but whether a build at that level still compiles and reaches submission. Run it end to end onceEXPO — expo@57.0.17 shipped on August 27, moving React Native to 0.86.3 and clearing both the Hermes V1 memory regression and the startup time regressionPREBUILD — expo prebuild clears and regenerates the native android and ios directories by default, so move hand-edited native changes into a config plugin firstRORK MAX — Rork Max generates native Swift, reaching AR and LiDAR, Dynamic Island, Live Activities, HealthKit, and Core ML — layers React Native struggles to touchDEADLINE — Two days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeEXTENSION — The extension that keeps you shipping to all users until November 1 cannot be filed after the deadline passes, so the decision point is effectively todayCHECK — What matters is not the targetSdkVersion number itself but whether a build at that level still compiles and reaches submission. Run it end to end onceEXPO — expo@57.0.17 shipped on August 27, moving React Native to 0.86.3 and clearing both the Hermes V1 memory regression and the startup time regressionPREBUILD — expo prebuild clears and regenerates the native android and ios directories by default, so move hand-edited native changes into a config plugin firstRORK MAX — Rork Max generates native Swift, reaching AR and LiDAR, Dynamic Island, Live Activities, HealthKit, and Core ML — layers React Native struggles to touch
Articles/Getting Started
Getting Started/2026-08-29Beginner

Nine Expo patches shipped in thirty days. Which one is your app actually running?

A tilde in package.json does not mean you are on the newest patch. The file that decides what actually gets installed is package-lock.json. Here is what npm did when I ran the checks myself.

Expo189npm3Versioning2Rork546Troubleshooting39

One of my apps had been sitting with a slow-startup review for a while. When I heard that Expo had shipped a patch fixing a startup-time regression, I opened package.json expecting a quick win.

It said "expo": "~57.0.9". A tilde, so patches come along automatically — that was my assumption. I rebuilt without changing anything.

Nothing improved. The build was still on 57.0.9, nine patches behind.

I knew the range syntax well enough to recite it, and had never once checked what it actually resolves to in a project that was already running. As an indie developer juggling several apps at once, an assumption like that can sit undisturbed for months. This is what I found when I ran the checks properly.

Nine patches in thirty days

Before anything else, it helps to know how fast patches arrive. The npm registry stores publish timestamps, so you can ask it directly.

npm view expo time --json

Here are the last ten 57.0.x releases, as of 29 August 2026.

VersionPublished (UTC)
57.0.92026-07-29 19:30
57.0.102026-08-04 07:40
57.0.112026-08-06 11:24
57.0.122026-08-10 15:43
57.0.132026-08-14 14:25
57.0.142026-08-17 10:48
57.0.152026-08-20 10:50
57.0.162026-08-24 07:56
57.0.172026-08-26 20:02
57.0.182026-08-28 10:48

Nine releases between 29 July and 28 August. Thirty days, so roughly one patch every three days.

The practical consequence is that a patch number you noted down a few days ago is quite likely no longer the newest one. If you carry a number over from a blog post, a changelog you skimmed last week, or your own notes, your starting assumption may already be stale. Ask the registry each time instead.

The range in package.json does not decide what you get

Next, what does each range style actually resolve to? I made an empty project with nothing but expo in dependencies, and asked npm to generate only the lockfile.

npm install --package-lock-only

Five different specifiers, same day, same registry.

Specifier in package.jsonResolved version
57.0.957.0.9
~57.0.957.0.18
^57.0.957.0.18
57.0.x57.0.18
"*" (unconstrained)57.0.18

Tilde and caret both landed on 57.0.18, and so did the wildcard forms. While no new minor has been published, the difference between ~ and ^ simply does not surface.

The two are not equivalent, though. A tilde allows patch updates within the minor you named, so ~57.0.9 will accept 57.0.18 but stop before 57.1.0. A caret allows minor updates too, so ^57.0.9 would happily take 57.1.0 or 57.4.2 once those exist. Right now no 57.1.x has been published, which is the only reason both rows read the same. The day the first new minor lands, a project on a caret starts pulling in changes the tilde project will not see, without anyone editing a file. That is worth knowing before you decide which one you want in a project you ship from.

But notice what this table is really measuring: what npm picks when it resolves from scratch. It says nothing about what happens when you run npm install in a project that already has a lockfile — which is every real project.

The lockfile is what decides

So I ran the same check starting from an existing lockfile.

  1. Pin "expo": "57.0.9" and generate the lockfile.
  2. Edit package.json only, changing the range to "expo": "~57.0.9".
  3. Run npm install with the lockfile still in place.

The output:

step 1, pinned lock:        57.0.9
step 2, install with lock:  57.0.9
step 3, install without:    57.0.18

Step 2 is the whole answer. Even with ~57.0.9 written in package.json, npm keeps 57.0.9 as long as the locked version still satisfies the range. It has no reason to move, so it doesn't.

Step 3 is the same package.json with the lockfile deleted first. Now 57.0.18 goes in. Which means that if package-lock.json is not committed to Git, your machine and your build server can quietly install different versions of Expo from identical source. It is the same shape of problem as filenames that look identical on your Mac and differ on the build server, which I wrote about in Sunset.png and sunset.png: one file on your Mac, two on the build server.

If you exported your code from Rork and started developing locally, check that package-lock.json made it into your first commit. If it lands in your ignore file, your dependency versions drift on every build. Write .gitignore Before You Run git init on an Exported Rork Project covers the setup.

npm ci will tell you when they disagree

A mismatch between package.json and the lockfile gets silently reconciled by npm install. When you want to hear about it, use npm ci.

Here is the real output with the lockfile on 57.0.9 and package.json asking for ~57.0.15:

npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync.
npm error Invalid: lock file's expo@57.0.9 does not satisfy expo@57.0.18

It stops. That refusal is exactly why build scripts and CI pipelines should use npm ci rather than npm install — failing at the point of disagreement costs far less than tracing a mystery difference days later.

When you just want to read the version currently locked, go straight to the file:

node -p "require('./package-lock.json').packages['node_modules/expo'].version"

Cloud build services follow the same rule. EAS Build installs from the lockfile in your repository, so a lockfile that has not moved since March produces a March build today, no matter what range sits above it in package.json. Nothing warns you, because from npm's point of view nothing is wrong.

If your project uses a different package manager the file name changes but the logic does not. Yarn writes yarn.lock, pnpm writes pnpm-lock.yaml, and both resolve from the lock first and the range second. Whichever one you have, that file is the honest record of your dependency versions, and package.json is closer to a statement of intent.

What to run when you do decide to upgrade

Upgrading is an explicit act. Name the version, and the lockfile updates along with it because the new specifier falls outside the locked range.

npm install expo@57.0.18
npx expo install --fix

The second line realigns the packages that Expo manages so they match the combination the SDK expects. Bumping expo alone and leaving everything else where it was gets you a set of versions nobody tested together.

One more thing if you keep native directories in your repository: running prebuild after an upgrade can wipe hand-edited native changes. Take an inventory before you upgrade rather than after — Find the native edits expo prebuild will erase before you upgrade to SDK 57 walks through it.

The ordering lesson generalises. Before you go hunting through your own code for a memory problem, confirm which versions you are actually running, which is the same argument I made in Count your worklets before you start deleting your own code.

Start here today

Close package.json and open package-lock.json instead. Read one line: the version under node_modules/expo. That single line tells you whether your app is at the front of the queue or nine patches behind it.

I never checked, and left a fixed regression sitting in one of my indie developer builds for a month. If this saves you that detour, it was worth writing down.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Getting Started2026-08-28
Sunset.png and sunset.png: one file on your Mac, two on the build server
An image that renders locally but breaks the cloud build. The cause was filename casing. Here is the reproduction, the git behavior behind it, and a small checker with measured results.
Getting Started2026-08-22
The names you can change, and the ones you can't — the 30 minutes before your first Rork release
An app has three names, and only the display name can be changed after release. Here is how I check the identifiers Rork fills in by default, using a dependency-free script, before I hit submit.
Getting Started2026-08-17
Write .gitignore Before You Run git init on an Exported Rork Project
Running git init on a freshly exported Rork project puts your signing keys and .env straight into history. I measured what gets committed when the ignore file comes first versus last, and which exclusion patterns actually work.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →