saif-prebuild-guard
v1.0.0
Published
Catches Expo config-plugin conflicts (missing packages, duplicate plugins, colliding native config keys) before you spend 10-20 minutes waiting for eas build to fail.
Maintainers
Readme
saif-prebuild-guard
Catches Expo config-plugin conflicts before you spend 10-20 minutes waiting for eas build to fail.
Created by Saif Ullah Abid.
The problem
Modern Expo apps use Continuous Native Generation (CNG) — instead of hand-editing native Android/iOS project files, you list "config plugins" (expo-camera, expo-notifications, @react-native-firebase/app, and dozens more) in app.json, and Expo generates the native project from them at build time.
This is powerful, but it has a real, common failure mode with no existing tooling built specifically around it: two plugins can silently modify the same native setting, a plugin can be listed but never actually installed, or the same plugin can accidentally end up listed twice with different configs. None of these show up in your editor. None of them show up in Expo Go. They only show up after a real eas build run — typically 10 to 20 minutes of cloud build time — fails, often with a native build error that doesn't obviously point back to the actual cause.
saif-prebuild-guard checks for these specific, real mistakes statically, in about a second, before you burn a build.
What it checks
Plugin listed but not installed — a plugin appears in
app.json'spluginsarray but isn't inpackage.json. Guaranteed to fail at prebuild time; this is the single most common version of this bug.The same plugin listed twice — Continuous Native Generation applies it twice, which can duplicate native permission entries or silently apply two different configurations depending on order.
Two different plugins that modify the same native setting — checked against a curated database of well-known Expo config plugins and the native keys they're documented to touch (permissions, Info.plist keys, Gradle build values, Firebase config file registration). Not a guaranteed failure, but a concrete, worth-checking-before-you-build risk.
Install
npm install --save-dev saif-prebuild-guardUsage
Run it from your Expo project root:
npx saif-prebuild-guardReal output, from the deliberately broken example project included in this repo:
Checked: app.json
6 plugin entries declared
error Plugin "expo-document-scanner-not-installed" is listed in your app config but is not in package.json. This will fail at prebuild/build time.
error Plugin "expo-build-properties" appears 2 times in your plugins array. Continuous Native Generation will apply it 2 times, which can duplicate native entries or apply conflicting configuration from each occurrence.
warning "expo-notifications" and "@react-native-firebase/messaging" both modify: android.permission.POST_NOTIFICATIONS, android.googleServicesFile. Verify their configuration doesn't conflict before building.
2 error(s), 1 warning(s)Exits with code 1 if any error-severity finding exists — drop it into CI, right before your eas build step:
- run: npx saif-prebuild-guard
- run: eas build --platform all --non-interactiveCatching this in a 1-second check instead of a 15-minute failed cloud build is the entire point.
Supported config formats
app.json— fully supported (plain JSON)app.config.js/app.config.ts— best-effort: literal plugin entries ("expo-camera",["expo-build-properties", {...}]) are extracted correctly; plugins added via computed/dynamic logic are skipped rather than guessed at, for the same reason described below.
The plugin database is a curated starting set, not a universal registry
This is the single most important honesty note in this README. saif-prebuild-guard ships with a hand-curated set of well-known Expo config plugins and their documented native effects — it is not an exhaustive database of every Expo config plugin that exists. An unlisted plugin is simply skipped by the "shared native key" check rather than assumed to be either safe or conflicting. The "not installed" and "duplicate plugin" checks work for any plugin regardless of whether it's in the database, since those two only need the plugin's name, not its documented effects.
The database lives in src/pluginDatabase.ts as a plain, readable list — extending it for a plugin you use that isn't covered yet is a two-minute edit and a genuinely useful contribution.
Proof it works
26 automated tests, including a full integration test that runs the tool against a deliberately broken example project and confirms it catches exactly the right violations, plus a clean project confirming zero false positives:
npm test# tests 26
# suites 8
# pass 26
# fail 0License
MIT
Built by Saif Ullah Abid
