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

@touqeerraza/rn-publisher

v0.0.3

Published

Interactive CLI to publish React Native apps to Firebase App Distribution, Google Play, and TestFlight.

Readme

@touqeerraza/rn-publisher

Interactive CLI to publish a React Native app to Firebase App Distribution, Google Play, and TestFlight.

It assumes a standard React Native layout (android/, ios/, release Gradle tasks, Xcode workspace). If your project uses custom flavors or paths, edit the generated config.

Requirements

  • Node.js 20+
  • A React Native app with Android and/or iOS native projects
  • Tools for the destinations you use:

| Destination | Tool | | ----------- | ---- | | Firebase App Distribution | Firebase CLI (firebase login) | | Google Play | fastlane (supply) | | TestFlight | macOS + Xcode (xcodebuild, xcrun altool) |

Install

npm install -D @touqeerraza/rn-publisher
# or
yarn add -D @touqeerraza/rn-publisher

Optional script in package.json:

{
  "scripts": {
    "publish:app": "rn-publisher"
  }
}

Quick start

From your React Native project root:

npx rn-publisher init
npx rn-publisher doctor
npx rn-publisher

What init creates

| File | Tracked? | Purpose | | ---- | -------- | ------- | | rn-publisher.config.js | yes | Build tasks, artifact paths, Xcode settings | | publish-secrets/keys.json | no | Destination credentials and IDs | | publish-secrets/README.md | no | What to put in the secrets folder | | .gitignore entries | yes | Ignores the secrets folder and release markers |

init always creates ./publish-secrets (gitignored). Change secretsDir / keysFile in the config if you need a different path.

It also fills values it can detect from a normal RN app:

  • Android applicationId → play.packageName
  • iOS bundle identifier → apple.bundleId
  • ios/*.xcworkspace → ios.workspace and ios.scheme
  • android/app/build.gradle or .kts → android.versionCodeFile

You still need to add Firebase / Play / Apple credentials and an iOS ExportOptions plist.

Default config

Generated config matches the usual RN release layout:

/** @type {import('@touqeerraza/rn-publisher').Config} */
module.exports = {
  secretsDir: "./publish-secrets",
  keysFile: "./publish-secrets/keys.json",
  environments: {
    prod: {
      label: "Production",
      android: {
        assembleTask: "assembleRelease",
        bundleTask: "bundleRelease",
        apkPath: "android/app/build/outputs/apk/release/app-release.apk",
        aabPath: "android/app/build/outputs/bundle/release/app-release.aab",
        versionCodeFile: "android/app/build.gradle",
      },
      ios: {
        workspace: "ios/MyApp.xcworkspace",
        scheme: "MyApp",
        configuration: "Release",
        exportOptionsPlist: "ios/ExportOptions-Prod.plist",
        projectDir: "ios",
      },
    },
  },
  platforms: {
    firebase: { enabled: true },
    play: { enabled: true },
    ios: { enabled: true },
  },
};

| Field | Meaning | | ----- | ------- | | secretsDir | Folder for Play JSON and Apple .p8 files | | keysFile | Path to destination credentials JSON | | environments | One or more publish targets (menus use these keys) | | android.assembleTask / bundleTask | Gradle tasks for APK / AAB | | android.apkPath / aabPath | Expected outputs after those tasks | | android.versionCodeFile | Gradle file that holds versionName / versionCode | | android.keystoreProperties | Optional existence check for doctor only | | ios.workspace / scheme / configuration | Archive settings | | ios.exportOptionsPlist | Plist for -exportArchive (method = app-store) | | ios.projectDir | Folder with the .xcodeproj (usually ios) | | platforms.*.enabled | Hide a destination from the menu |

Add more environments by copying the prod block and matching keys in keys.json.

Keys file

Credentials live in publish-secrets/keys.json. Environment names must match environments in the config.

{
  "prod": {
    "firebase": {
      "appId": "1:1234567890:android:abcdef",
      "groups": "testers"
    },
    "play": {
      "packageName": "com.example.app",
      "serviceAccountJson": "./play-service-account.json",
      "closedTrack": "alpha",
      "releaseStatus": "draft"
    },
    "apple": {
      "bundleId": "com.example.app",
      "apiKeyId": "XXXXXXXXXX",
      "apiIssuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "apiKeyP8": "./AuthKey_XXXXXXXXXX.p8"
    }
  }
}

| Key | Used for | | --- | -------- | | firebase.appId | Firebase Android app id | | firebase.groups | App Distribution tester group(s) | | play.packageName | Play Console application id | | play.serviceAccountJson | Service-account JSON under secretsDir | | play.closedTrack | Closed-testing track name (default alpha) | | play.releaseStatus | draft (default) or completed — Play release status for uploads | | apple.bundleId | iOS bundle id | | apple.apiKeyId / apiIssuerId / apiKeyP8 | App Store Connect API key (preferred) | | apple.uploadEmail / appSpecificPassword | Alternative to API key |

Relative paths in keys resolve against secretsDir.

For Apple: if any API key field is set, all three are required. Otherwise both uploadEmail and appSpecificPassword are required.

How publish works

npx rn-publisher

Interactive flow:

  1. Shows which destinations are available on this machine
  2. Pick an environment
  3. Pick destinations (Firebase / Play / TestFlight)
  4. Pick a Play track if Play is selected
  5. Edit or accept release notes
  6. Enter version name and version code separately for each selected destination
  7. Confirm, then build and upload

What runs per destination:

| Destination | Build | Upload | | ----------- | ----- | ------ | | Firebase | assembleTask → APK | firebase appdistribution:distribute | | Google Play | bundleTask → AAB | fastlane supply (notes via changelog metadata) | | TestFlight | xcodebuild archive + export | xcrun altool |

Versioning

Publish asks per destination before building (not one shared pair for everything):

| Destination | Prompt | Written to project? | | ----------- | ------ | ------------------- | | Firebase | Version name + code (defaults to current) | No — applied only for that APK build, then restored | | Google Play | Version name + code (default code: current + 1) | Yes — versionName / versionCode in Gradle | | TestFlight | Version name + build (default build: current + 1) | Yes — MARKETING_VERSION / CURRENT_PROJECT_VERSION |

Version name must look like 1.0 or 1.2.3.

iOS: Info.plist (app and every extension) should use $(MARKETING_VERSION) and $(CURRENT_PROJECT_VERSION), not hardcoded numbers. Otherwise the IPA keeps the old build and App Store Connect / extensions can reject the upload.

If Google Play or TestFlight fails before any store upload succeeds, those version file changes are reverted. Firebase never leaves version edits in the repo. If a store upload succeeds, Play/iOS changes are kept.

Destinations

Firebase App Distribution

1. App ID and tester group

  1. Open the Firebase Console → select your project
  2. Project settings (gear) → General → Your apps → select the Android app → copy App ID (1:…:android:…)
  3. App Distribution → Testers & groups → create or select a group → copy the group alias next to the group name

2. Fill keys.json

| Key | Value | | --- | ----- | | firebase.appId | Android App ID from Project settings | | firebase.groups | Tester group alias from App Distribution |

3. Sign in on the publishing machine

firebase login

Use an account that can distribute builds for that Firebase project.

Google Play

1. Create a service account key

  1. Open the Google Cloud Console → select the project linked to Play
  2. IAM & Admin → Service Accounts → Create service account
  3. On the new account, open ⋮ → Manage keys → Add key → Create new key (JSON)
  4. Place the downloaded JSON file in publish-secrets/
  5. Copy the service account email (e.g. [email protected])

2. Invite the service account in Play Console

  1. Open Google Play Console → Users and permissions → Invite user
  2. Paste the service account email, select the app, and grant Releases and Store preferences (or equivalent release access)
  3. Accept the invite so the account can upload builds

3. Fill keys.json

| Key | Value | | --- | ----- | | play.packageName | Android application id (e.g. com.example.app) | | play.serviceAccountJson | Path/name of the JSON key under publish-secrets/ (e.g. ./play-service-account.json) | | play.releaseStatus | draft while the app is draft in Play Console; completed once completed releases are allowed | | play.closedTrack | Optional; defaults to alpha for closed testing |

TestFlight

1. Create an App Store Connect API key

  1. App Store Connect → Users and Access → Integrations → App Store Connect API
  2. Click Generate API Key (or +), name it, and give it at least App Manager access
  3. Copy the Key ID and the page’s Issuer ID
  4. Download the .p8 file once (named AuthKey_<KEY_ID>.p8) — Apple only shows the download once
  5. Place that file in publish-secrets/
  6. In keys.json, set:

| Key | Value | | --- | ----- | | apple.apiKeyId | Key ID from App Store Connect | | apple.apiIssuerId | Issuer ID from the same Integrations page | | apple.apiKeyP8 | Path/name of the .p8 file under publish-secrets/ (e.g. ./AuthKey_XXXXXXXXXX.p8) | | apple.bundleId | iOS bundle identifier (e.g. com.example.app) |

2. ExportOptions plist

  1. Create a plist under ios/ (e.g. ios/ExportOptions-Prod.plist) with method = app-store and your Apple Team ID
  2. Set ios.exportOptionsPlist in rn-publisher.config.js to that path

Example plist (ios/ExportOptions-Prod.plist):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>method</key>
	<string>app-store</string>
	<key>teamID</key>
	<string>YOUR_TEAM_ID</string>
	<key>uploadSymbols</key>
	<true/>
	<key>signingStyle</key>
	<string>automatic</string>
</dict>
</plist>

Team ID: Apple Developer → Membership details → Team ID.

3. Versioning (required for uploads)

  1. In the app target’s Info.plist, use $(MARKETING_VERSION) and $(CURRENT_PROJECT_VERSION) — not hardcoded numbers
  2. Ensure the app and any extensions (e.g. notification service) share the same build number via Build Settings / $(CURRENT_PROJECT_VERSION)
  3. App Store Connect rejects duplicate CFBundleVersion; bump the version code for each upload

Publishing requires macOS with Xcode. Run cd ios && pod install so ios.workspace resolves.

CI / flags

npx rn-publisher publish \
  --env prod \
  --platforms firebase,play \
  --track closed \
  --version-name 1.2.3 \
  --version-code 42 \
  --yes \
  --commit

| Flag | Meaning | | ---- | ------- | | --env | Environment key from config | | --platforms | firebase, play, ios (comma-separated) | | --track | Play: closed, open, or production | | --notes | Release notes (skips git-generated notes) | | --version-name | Default version name for each destination prompt (CI: applied per selected destination) | | --version-code | Default Android versionCode / iOS build number (interactive Play/iOS default: current + 1; Firebase / CI without flags: current) | | --yes / -y | Skip confirm | | --commit | Commit version file changes as chore: release <env> |

Non-interactive runs must pass the required flags. Without --version-name / --version-code, CI keeps the current project versions.

Doctor

npx rn-publisher doctor
npx rn-publisher doctor --env prod

Checks tools, config paths, and keys without publishing.

Troubleshooting

Failures include a suggestion. Common cases:

| Symptom | Fix | | ------- | --- | | Config not found | Run rn-publisher init in the app root | | Keys still placeholders | Fill publish-secrets/keys.json | | Play JSON missing | Put the file in secretsDir and set play.serviceAccountJson | | Firebase not logged in | firebase login | | fastlane missing | Install fastlane | | TestFlight unavailable | Use macOS with Xcode | | Workspace missing | cd ios && pod install, then check ios.workspace | | Archive / export failed | Check ios/build/archive-*.log or export-*.log | | TestFlight build already used | Use a higher version code; ASC rejects duplicate CFBundleVersion | | Extension version mismatch | Match app + .appex build numbers ($(CURRENT_PROJECT_VERSION)) | | Gradle signing failed | Fix Android signing in Gradle / CI |

License

MIT