npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-ota-guard

v0.1.1

Published

Answers one question before you publish an Expo OTA update: is this change safe to ship over-the-air, and to which live runtimes?

Readme

expo-ota-guard

Is this change safe to ship over the air, and to which live runtime?

A zero-config CLI that reads your pending changes and tells you, before you run eas update, whether the update will actually reach your users or quietly fail to.

npx expo-ota-guard
Inspected 26 changed files since 0c5126d~1

Native-affecting changes
  - package.json:@stripe/stripe-react-native [dependency-added]
    @stripe/[email protected] was added; @stripe/stripe-react-native ships
    native code: android/, ios/, podspec, config-plugin
    hint: This needs a new native build before the change can reach users.
  - app.json:plugins [config-native-field]
    plugins changed: config plugins run during prebuild and mutate the native projects
    hint: Run a new build (eas build) before shipping this; an OTA cannot change it.
  - android/app/build.gradle [native-directory]
    lives in a native project directory, which is compiled into the binary

OTA-safe changes (22)
  - src/services/videoTrim.ts
    is a source or asset file that ships inside the JS bundle
  ...

Runtime targeting
  - This update would target runtime 1.0.7

 UNSAFE TO OTA  This change alters the native layer. Ship a new build; an OTA would not deliver it.
7 native-affecting, 0 unclear, 22 OTA-safe

Why this exists

Three failure modes cost real teams real days. This tool exists to catch each one.

1. expo-updates fails silently when an OTA lands on a mismatched runtime. You add a native dependency, change a config plugin, or edit a native field in app.json, then publish an OTA. The JS ships. The native code does not. The app crashes on the new code path, or the feature quietly no-ops, and nothing in the publish output warned you. expo-ota-guard classifies every pending change and refuses to call the update safe when the native layer moved.

2. Nobody knows which runtimeVersion is actually live. eas build:list only shows builds EAS made. Builds produced with eas build --local or in your own CI never appear there, so the list you trust is missing the binaries your users are actually running. eas update:list is the only source of truth, because every published update records the runtime it targeted. expo-ota-guard runtimes reads the truthful source and says so out loud.

3. Your fix looks broken because you tested a stale bundle. With updates.fallbackToCacheTimeout: 0 and checkAutomatically: ON_LOAD (the common setup), the app launches the cached bundle immediately, downloads the new one in the background, and only runs it on the next cold start. So the first relaunch after publishing still shows the old code. Developers conclude the fix did not work, and ship another one. expo-ota-guard doctor names this behaviour explicitly for your config.

Quickstart

# Should I OTA what I have right now?
npx expo-ota-guard

# Everything since main, not just the working tree
npx expo-ota-guard --since origin/main

# Which runtimes are actually live?
npx expo-ota-guard runtimes

# Audit the updates config for known traps
npx expo-ota-guard doctor

# Gate CI before "eas update" (exits 1 when unsafe)
npx expo-ota-guard --ci

Commands

| Command | What it does | |---|---| | (default) | Full verdict: classifies changes and reports live runtimes from EAS | | check | Classification only, no network, no eas-cli | | runtimes | Which runtimeVersions are live per branch, channel and platform | | doctor | Audits expo-updates config for the traps above |

Options

| Flag | Meaning | |---|---| | --since <ref> | Classify <ref>..HEAD plus uncommitted work (default: working tree vs HEAD) | | --branch <name> | Limit runtime discovery to one EAS branch | | --cwd <dir> | Project directory (default: current directory) | | --ci | JSON output, exit 1 when the change is not OTA-safe | | --no-eas | Skip eas-cli entirely (offline, faster) | | --no-color | Disable coloured output |

Exit codes

| Code | Meaning | |---|---| | 0 | Safe to OTA | | 1 | Unsafe, or needs review, or doctor found an error | | 2 | Usage error, or no Expo project / git repo found |

What counts as native-affecting

The classifier is the core of the tool, and it explains every call it makes rather than just flagging a file.

Dependencies. Each added, removed or upgraded entry in dependencies is looked up in your node_modules and judged by what is actually installed: an android/ or ios/ directory, a .podspec, a build.gradle, an expo-module.config.json, a config plugin (app.plugin.js or an expo.plugin manifest field), or a cpp/ directory. devDependencies and peerDependencies are treated as JS-only, because they never ship in the bundle. Removing a native package is also flagged: the installed binary still links it until you rebuild.

App config. Changes to app.json / app.config.json are diffed field by field. plugins, android.*, ios.*, runtimeVersion, newArchEnabled, jsEngine, icon, splash, permissions, entitlements and the whole updates.* block are native. extra, version, web and other metadata are JS-only. Unrecognised fields are reported as needing review rather than silently assumed safe.

Native directories. Anything under android/ or ios/ is native, full stop.

Everything else (source files, assets, lockfiles) ships in the bundle and is safe.

CI usage

- name: Guard the OTA
  run: npx expo-ota-guard --ci --since ${{ github.event.before }}

- name: Publish update
  run: eas update --branch production --auto

The --ci run exits non-zero on an unsafe change, so the publish step never runs when the change needed a build instead.

Limitations

Worth knowing before you trust it blindly:

  • Dynamic configs are not executed. app.config.js / app.config.ts are reported as "needs review" rather than diffed, because knowing their output means running your code. Compare npx expo config --type public before and after if you need certainty.
  • Nativeness is judged from node_modules. If dependencies are not installed, the tool falls back to a curated list of well-known native packages and marks anything else as unclear rather than guessing.
  • A package can be native without looking native. A pure-JS package that reaches into a native module installed by something else will read as safe. Rare, but possible.
  • runtimes needs an authenticated eas-cli. Without it, runtime discovery is skipped and the classification still runs. Use --no-eas to skip it deliberately.
  • Channel and branch mismatches are reported, not diagnosed. runtimes shows which channel serves which branch, so you can see when a channel named production actually points at a branch named main. It does not yet warn you that publishing to a same-named branch would reach nobody. Read the channel column before you publish.
  • The nativeVersion runtime policy resolves per platform at build time, so a single target runtime cannot be reported for it statically.
  • This is a static analyser, not a build. It reasons about what changed, not about whether your code is correct.

Design notes

  • No network calls of its own. The only outbound work is shelling out to git and eas-cli.
  • No telemetry, no analytics, no phoning home.
  • Ships as plain Node ESM and runs on Node >= 18 with a single runtime dependency (picocolors).
  • The classifier is pure and table-driven: it takes file contents in and returns findings out, which is why it is covered by fixture-based unit tests rather than integration mocks.

Development

bun install
bun test          # table-driven tests, including real eas-cli payload fixtures
bun run typecheck
bun run build

License

MIT