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

react-native-flavor

v0.1.0

Published

Generate multiple flavors/environments for React Native (Expo CLI) — Android productFlavors + iOS schemes/xcconfig + a JS runtime layer, from a single config file.

Readme

react-native-flavor

Generate multiple flavors / environments for a React Native (Expo CLI) app from a single config file. Declare your flavors once, run one command, and react-native-flavor generates all the native plumbing:

  • AndroidproductFlavors, flavorDimensions, typed buildConfigField (String/int/boolean/…), per-flavor applicationId, app_name, manifestPlaceholders, resValue, customConfig (verbatim escape-hatch), global app-level resValues/buildConfigFields, and Firebase google-services.json.
  • iOS — per-flavor .xcconfig, Debug/Release (and opt-in Profile) build configurations cloned per flavor, shared schemes (MyApp-<flavor>), Info.plist keys, per-target variables, custom #includes, global/per-flavor buildSettings, a Firebase copy build phase, and the Podfile config mapping.
  • Icons — per-flavor launcher / app icons: Android legacy mipmaps and adaptive icons (API 26+, incl. explicit foreground/background/monochrome layers) in each flavor source set, plus an AppIcon-<flavor> set in the iOS asset catalog.
  • JS/TS runtimesrc/flavors.ts with Flavor.current, plus a native bridge (RNFlavor) so the running flavor is readable from JavaScript.
  • DXpackage.json run scripts and .vscode/launch.json entries.

It is idempotent (re-run as often as you like) and works in two modes: write straight into ios/+android/ (bare), or as an Expo config plugin that re-applies on every expo prebuild.

⚠️ Currently targets Expo CLI projects (managed with CNG, or bare).

Install

npm install --save-dev react-native-flavor
# or
yarn add -D react-native-flavor

1. Declare flavors (quick start)

Create flavors.yaml at your project root (flavors.json or a reactNativeFlavor block in package.json also work):

app:
  android:
    flavorDimensions: "env"          # name of the productFlavors dimension
  ios:
    buildConfigurations: [Debug, Release]

flavors:
  production:
    app: { name: "MyApp" }
    android: { applicationId: "com.example.myapp" }
    ios:     { bundleId: "com.example.myapp" }

  uat:
    app: { name: "MyApp (uat)" }
    android: { applicationId: "com.example.myapp.uat" }
    ios:     { bundleId: "com.example.myapp.uat" }

That is the minimum. Every other field below is optional. See example/flavors.yaml for a fully-loaded example.

2. Configuration reference

The config has two parts: a top-level app block (project-wide defaults) and a flavors map (one entry per environment). Paths in the config are resolved relative to the config file (e.g. flavors.yaml).

2.1 Top-level app

| Field | Type | Default | What it does | |---|---|---|---| | app.android.flavorDimensions | string | "flavor-type" | The dimension all flavors belong to. Emits flavorDimensions "<name>" and dimension "<name>" inside each flavor. | | app.android.buildConfigField | boolean | true | When true, enables buildFeatures { buildConfig true } (required by AGP 8) and generates a BuildConfig.FLAVOR string for every flavor. Set false to skip the FLAVOR field. | | app.android.resValues | map<"type/name", string> | {} | Global resValues applied to every flavor (a per-flavor resValues entry wins on key collision). | | app.android.buildConfigFields | map | {} | Global buildConfigFields applied to every flavor (per-flavor wins). Same value syntax as the per-flavor field. | | app.ios.buildConfigurations | string[] | [Debug, Release] | Base Xcode configurations cloned into <Base>-<flavor> (e.g. Debug-uat). Add Profile for the 3-mode Flutter layout — it has no base in RN/Expo so it's cloned from Release automatically. | | app.ios.buildSettings | map<string,string> | {} | Global Xcode build settings merged into every flavor's build configs (e.g. DEVELOPMENT_TEAM). | | app.ios.includes | list | [] | Global xcconfig #includes added to every flavor's per-mode wrapper. String or { value, target, optional }. | | app.ios.projectName | string | auto-detected | Override the .xcodeproj / scheme base name. Set this when ios/ contains more than one .xcodeproj (e.g. a stale folder after a rename). | | runtime | "native-module" | "react-native-config" | "native-module" | Runtime backend that exposes the flavor to JS. Only native-module is implemented today. |

2.2 Per-flavor: app and the shared icon

| Field | Type | Required | What it does | |---|---|---|---| | <flavor>.app.name | string | ✅ | The app's display name for this flavor. Android: emitted as resValue "string", "app_name", ...; iOS: used as the default DISPLAY_NAME (→ CFBundleDisplayName). | | <flavor>.icon | string (path) | — | Square source icon (≥ 1024×1024 PNG) applied to both platforms. Override per platform with android.icon / ios.icon. See §3. |

2.3 Per-flavor: android

| Field | Type | Notes | |---|---|---| | applicationId | string | The full Android package id for this flavor. Use this or applicationIdSuffix, not both. | | applicationIdSuffix | string | Appended to the base applicationId from app.json. Mutually exclusive with applicationId. | | versionNameSuffix | string | Appended to the version name (e.g. -uat). | | manifestPlaceholders | map<string,string> | Emitted as manifestPlaceholders = [key: "value", ...]. Reference them in AndroidManifest.xml as ${key}. | | buildConfigFields | map | Shorthand KEY: "value"buildConfigField "String", "KEY", "\"value\"" (auto-escaped; surrounding quotes are stripped first). For a non-string field use the object form KEY: { type: int\|long\|double\|float\|boolean\|char\|String, value: … } → e.g. MAX: { type: int, value: 5 } becomes buildConfigField "int", "MAX", "5". Read in Kotlin/Java as BuildConfig.KEY. | | resValues | map<"type/name", string> | Each entry becomes resValue "type", "name", "value" (e.g. string/build_env: "uat"). string/app_name is added automatically from app.name; you can override it here. | | customConfig | map<string, string|number> | Arbitrary lines injected verbatim into the productFlavor block as key value — the escape-hatch for anything not modeled above (versionCode: 42, signingConfig: signingConfigs.release, minSdkVersion: 24, …). String values that need Groovy quotes must include them: versionNameSuffix: '"-uat"'. | | firebase.config | string (path) | Per-flavor google-services.json. See §4. | | icon | string (path) | Android-only icon source (overrides the shared icon). | | adaptiveIcon | { foreground, background?, monochrome? } | Explicit adaptive-icon layers (overrides the icon-derived adaptive layers). foreground/monochrome are image paths (authored at full 108dp canvas); background is a hex color or an image path. monochrome enables Android 13+ themed icons. | | iconBackground | string (hex color) | Adaptive-icon background when deriving layers from a single icon (default #FFFFFF). |

2.4 Per-flavor: ios

| Field | Type | Required | Notes | |---|---|---|---| | bundleId | string | ✅ | PRODUCT_BUNDLE_IDENTIFIER for this flavor. Written into both the .xcconfig and the build settings so it always wins. | | variables | map | — | Keys written into the flavor's xcconfig (KEY = value). Shorthand KEY: "value" applies to every build mode (lands in <flavor>.xcconfig). The object form KEY: { value, target } with targetdebug/profile/release scopes the variable to that mode only (lands in <flavor>.<mode>.xcconfig). DISPLAY_NAME (defaults to app.name) and FLAVOR (defaults to the flavor id) are added automatically. // is escaped to /$()/. Expose to native via $(KEY) in Info.plist. | | buildSettings | map<string,string> | — | Extra Xcode build settings merged into this flavor's build configs (project + target level), e.g. OTHER_SWIFT_FLAGS. Merged after app.ios.buildSettings; bundleId/app-icon still win. | | includes | list | — | Custom xcconfig #includes for this flavor. String or { value, target, optional }optional: true emits #include?; target scopes it to one mode. | | firebase.config | string (path) | — | Per-flavor GoogleService-Info.plist. See §4. | | icon | string (path) | — | iOS-only icon source (overrides the shared icon). |

Where do these values surface in JS? src/flavors.ts exposes Flavor.config.variables, which merges android.buildConfigFields and ios.variables (except DISPLAY_NAME/FLAVOR) at generation time — so Flavor.config.variables.API_BASE_URL works on both platforms. See §7.

2.5 Validation rules

  • Flavor ids must match [a-z][a-z0-9]* and may not be debug, release, main, androidtest, test, profile (they collide with Android buildTypes / source sets).
  • Each flavor needs app.name, an android.applicationId or android.applicationIdSuffix, and an ios.bundleId.
  • applicationId / bundleId must be unique across flavors (so multiple flavors can be installed side-by-side).

2.6 Full annotated example

app:
  android:
    flavorDimensions: "env"          # default: "flavor-type"
    buildConfigField: true           # generate BuildConfig.FLAVOR (default true)
  ios:
    buildConfigurations: [Debug, Release]
    projectName: "MyApp"             # optional: only needed if ios/ has >1 .xcodeproj

flavors:
  production:
    app: { name: "MyApp" }
    icon: "assets/icons/production.png"
    android:
      applicationId: "com.example.myapp"
      versionNameSuffix: ""
      iconBackground: "#1E66F5"
      manifestPlaceholders:
        deepLinkScheme: "myapp"
      buildConfigFields:
        API_BASE_URL: '"https://api.example.com"'
        ENABLE_LOGS: "false"
      resValues:
        string/build_env: "production"
      firebase:
        config: "firebase/production/google-services.json"
    ios:
      bundleId: "com.example.myapp"
      variables:
        DISPLAY_NAME: "MyApp"
        API_BASE_URL: "https://api.example.com"
      firebase:
        config: "firebase/production/GoogleService-Info.plist"

  uat:
    app: { name: "MyApp (uat)" }
    icon: "assets/icons/uat.png"
    android:
      applicationId: "com.example.myapp.uat"
      versionNameSuffix: "-uat"
      iconBackground: "#F5A623"
      buildConfigFields:
        API_BASE_URL: '"https://uat.example.com"'
        ENABLE_LOGS: "true"
      resValues:
        string/build_env: "uat"
    ios:
      bundleId: "com.example.myapp.uat"
      variables:
        DISPLAY_NAME: "MyApp (uat)"
        API_BASE_URL: "https://uat.example.com"

3. Per-flavor icons

Point a flavor at a square source image (≥ 1024×1024 PNG recommended). It is resized into every density bucket — you don't pre-cut sizes:

flavors:
  uat:
    app: { name: "MyApp (uat)" }
    android: { applicationId: "com.example.myapp.uat" }
    ios:     { bundleId: "com.example.myapp.uat" }
    icon: "assets/icons/uat.png"        # applies to both platforms
    # or override per platform / set the adaptive background:
    # android: { applicationId: "...", icon: "assets/icons/uat-android.png", iconBackground: "#0A84FF" }
    # ios:     { bundleId: "...",       icon: "assets/icons/uat-ios.png" }

What gets generated for a flavor with an icon:

  • Android — full launcher set in android/app/src/<flavor>/res/: mipmap-*/ic_launcher.png (+ _round) at all five densities for API < 26, and an adaptive icon (mipmap-anydpi-v26/ic_launcher.xml with generated ic_launcher_foreground / ic_launcher_background layers) so the override also wins on API 26+. iconBackground (default #FFFFFF) is the adaptive backdrop. Gradle merges these over src/main/res automatically — no gradle edits.
  • iOSAppIcon-<flavor>.appiconset (single 1024² universal image, the modern Xcode format) in your asset catalog; the flavor's .xcconfig and build settings select it via ASSETCATALOG_COMPILER_APPICON_NAME. Xcode's actool generates every required size from the single source at build time.

Flavors without an icon keep the project's default launcher/app icon. Image resizing uses the pure-JS jimp-compact (no native binaries) — it ships as a dependency, so icons work out of the box.

After an iOS icon change, Xcode may cache the old asset catalog. If the icon doesn't update, delete the build cache and the app, then rebuild: rm -rf ~/Library/Developer/Xcode/DerivedData/<App>-* and uninstall the app from the simulator.

4. Firebase

Each flavor is a separate Firebase app (one per applicationId / bundleId), so it gets its own config file. Download them from the Firebase console and point each flavor at its files:

flavors:
  production:
    android:
      applicationId: "com.example.myapp"
      firebase: { config: "firebase/production/google-services.json" }
    ios:
      bundleId: "com.example.myapp"
      firebase: { config: "firebase/production/GoogleService-Info.plist" }

  uat:
    android:
      applicationId: "com.example.myapp.uat"
      firebase: { config: "firebase/uat/google-services.json" }
    ios:
      bundleId: "com.example.myapp.uat"
      firebase: { config: "firebase/uat/GoogleService-Info.plist" }

What the generator does (paths are relative to the config file):

  • Android — copies each google-services.json into the flavor source set android/app/src/<flavor>/google-services.json. The Google Services Gradle plugin then picks the correct file per variant automatically — no gradle edits, no manual swapping.
  • iOS — copies each GoogleService-Info.plist into ios/flavors/firebase/<flavor>/GoogleService-Info.plist and adds a Run Script build phase (react-native-flavor: Copy Firebase config) that copies the right plist into the built .app at compile time, keyed on $(CONFIGURATION) (e.g. config Debug-uat matches the uat flavor). If a configuration doesn't match, the first flavor's plist is used as a fallback.

You still need the Firebase SDK set up the usual way — e.g. @react-native-firebase/app, and on Android the com.google.gms.google-services Gradle plugin applied. react-native-flavor only places the per-flavor config files in the right spot; it does not install Firebase for you.

Tip: keep the config files out of version control if your policy requires it, and commit only *.example placeholders.

5. Generate

Mode A — bare (recommended for simplicity)

Make sure native folders exist (npx expo prebuild), then:

npx react-native-flavor          # or the short alias: npx rn-flavor
cd ios && pod install && cd ..   # new build configs need a Pods refresh

Commit the generated ios/ + android/ changes and stop running expo prebuild --clean (it would wipe native patches).

Mode B — Expo config plugin (keeps Continuous Native Generation)

Register the plugin so prebuild re-applies everything:

// app.config.js
module.exports = {
  // ...
  plugins: [
    ['react-native-flavor', { /* config: 'flavors.yaml' */ }],
  ],
};
npx expo prebuild --clean
cd ios && pod install && cd ..

Keep your app.json / app.config.js android.package and ios.bundleIdentifier set to the base id (your production/default id). react-native-flavor layers the per-flavor ids on top; the base id must stay first so Expo resolves the right launch target.

6. Build & run a flavor

No re-generation needed between flavors — just pick the variant/scheme. Convenience scripts are added to package.json:

npm run android:uat     # expo run:android --variant uatDebug --app-id com.example.myapp.uat
npm run ios:uat         # expo run:ios --scheme MyApp-uat --configuration Debug-uat

(--app-id is added for flavors with an explicit applicationId so Expo launches the correct app instead of the base package.)

Release builds:

cd android && ./gradlew assembleUatRelease   # or bundleUatRelease
# iOS: open Xcode, pick scheme "MyApp-uat", Product ▸ Archive

7. Read the flavor at runtime

import { Flavor } from './src/flavors';

if (Flavor.is('uat')) showDebugBanner();
const apiUrl = Flavor.config.variables.API_BASE_URL;
console.log(Flavor.current); // 'uat'

Flavor.current is read from native (BuildConfig.FLAVOR on Android, the Flavor key in Info.plist on iOS) via the generated RNFlavor native module. Flavor.config.variables is a compile-time merge of android.buildConfigFields and ios.variables, so the same keys are available on both platforms.

Android registration: react-native-flavor tries to register RNFlavorPackage() in your MainApplication. If it cannot find the insertion point it prints a warning — add packages.add(RNFlavorPackage()) to getPackages() manually. iOS auto-registers via RCT_EXPORT_MODULE.

CLI

react-native-flavor [options]      # alias: rn-flavor

  -c, --config <path>      config path (default: auto-detect)
  -p, --processors <list>  run a subset, comma-separated (e.g. android:gradle,js:flavors)
      --platforms <list>   android,ios (default: both)
      --dry-run            print what would change, write nothing
  -f, --force              rebuild managed blocks even if present
  -v, --verbose

Processors run in this order:

android:gradle  android:manifest  android:firebase  native:bridge
ios:xcconfig    ios:buildConfigs  ios:schemes  ios:plist  ios:podfile
icons           js:flavors        scripts:package   ide:vscode

How idempotency works

  • Text files (build.gradle, Podfile) are wrapped in react-native-flavor:start … react-native-flavor:end marker blocks — re-running replaces only the block. (The Gradle block is placed after defaultConfig so Expo resolves the base applicationId correctly.)
  • project.pbxproj is edited through the xcode library; configs/schemes already present (by name) are left alone (use --force to rebuild them).
  • Generated files (src/flavors.ts, .xcconfig, native bridge, schemes, icons) are rewritten wholesale with a GENERATED — do not edit header.
  • A .rn-flavor/lockfile.json records the config hash and generated files.

Programmatic API

const { generate, loadFlavorConfig } = require('react-native-flavor');
await generate({ dryRun: true });

// Pure transforms (used by both the CLI and the Expo plugin):
const core = require('react-native-flavor/core');
const next = core.injectProductFlavors(buildGradleContents, config);

Roadmap

  • [x] Per-flavor launcher / app icons (Android mipmaps + adaptive, iOS asset catalogs)
  • [ ] react-native-config runtime backend option
  • [ ] Per-flavor launch screens (iOS)
  • [ ] React Native CLI (non-Expo) support

License

MIT