◉RORK LABJP
●EXPO — EAS Observe now records native crashes too (Oct 7). Check on SDK 57 at 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) were added to the model menu●RN 0.88 — Not out yet. The latest is rc.4 (Oct 7), and 0.87.1 is the current Latest●Q&A — A write-up lists four snags on the way to TestFlight with an Expo + Supabase app●ECOSYSTEM — With Shopify going back to native, one write-up explains why a solo developer stays on Expo●NEW — Before you write 'ship in a week', look at the breakdown of waiting days●EXPO — EAS Observe now records native crashes too (Oct 7). Check on SDK 57 at 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) were added to the model menu●RN 0.88 — Not out yet. The latest is rc.4 (Oct 7), and 0.87.1 is the current Latest●Q&A — A write-up lists four snags on the way to TestFlight with an Expo + Supabase app●ECOSYSTEM — With Shopify going back to native, one write-up explains why a solo developer stays on Expo●NEW — Before you write 'ship in a week', look at the breakdown of waiting days
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.

Expo214npm4VersioningRork580Troubleshooting41

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 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.

When you only want to know how far behind you are: npm outdated

Before deciding whether to upgrade, it helps to measure where you stand. The standard command for that is npm outdated.

npm outdated expo

The output has three columns: Current (what your lockfile holds), Wanted (the newest version your package.json range allows) and Latest (the newest on the registry). If Current and Wanted differ, you are behind inside your own range. If Wanted and Latest differ, something newer exists outside the range.

I used to glance at the Latest column alone and relax because no big release had appeared. The gap I should have been reading was between Current and Wanted, and that is where my nine patches were hiding. Reading all three columns together is a habit that would have saved me the month.

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-09-14
Rork No Longer Creates New Expo Projects: Keep Yours, or Rebuild It in SwiftUI?
You can no longer start an Expo project in Rork, but existing ones keep building and shipping. Here is how to decide whether to stay on Expo or rebuild in SwiftUI, and what quietly goes missing when you rebuild.
◈ Getting Started2026-09-08
Whether You Need Rork Max Is Answered by Your Permissions, Not the Price Page
When I cannot decide between standard Rork and Rork Max, I stop reading the price page. Instead I pull every OS capability my app actually asks for out of app.json and Info.plist, then sort them into what Expo covers, what a config plugin covers, and what truly needs native Swift.
◈ Getting Started2026-09-01
Starting with the EAS Observe free tier, and why Crashlytics stays installed
EAS Observe reached general availability on August 20. I looked into consolidating down to one monitoring service and found that native crashes are still out of scope. Here are the free-tier limits and the setup details that trip people up.
📚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