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

@saravana.devx/mapup

v0.4.3

Published

Stamps debug IDs into builds and uploads their sourcemaps so error stack traces resolve to original source. CLI + Vite plugin + Metro config wrapper + Gradle/Xcode hooks for React Native release builds.

Readme

mapup

Stamps a debug ID into each build and uploads its .map files to the telemetry backend, which stores them in the bucket so error stack traces resolve to original source. It has three entry points:

  • Vite plugin: stamps debug IDs and uploads at the end of vite build, then deletes the maps from dist/.
  • Metro wrapper (src/metro.cjs): stamps debug IDs into React Native / Expo bundles. A Gradle script (gradle/mapup.gradle) and an Xcode hook (bin/compose-source-maps.cjs) upload the map from every release build.
  • CLI (mapup inject / upload / react-native): for webpack or CI steps that run outside the bundler.

Projects never hold bucket credentials. Each project gets its own upload token, and the backend checks it on every request (POST /analytics/sourcemaps).

Debug IDs

A debug ID is a UUID derived from a bundle's content. The build writes it into the bundle's map ("debugId") and adds a one-line snippet to the bundle that registers it at runtime. When an error happens, the SDK sends the IDs of the bundles in the stack (debug_ids), and the backend picks the map by ID.

So the map always matches the exact build that crashed:

  • Forgetting to bump the version doesn't matter: a new build has new content, so a new ID.
  • Rebuilding the same code gives the same ID, so a re-upload just replaces it.
  • No git SHA or build counter is needed.

The release is still sent and stored. It labels errors on the dashboard, and maps without a debug ID (older builds, Hermes maps that lost it) are still found by release, as before.

How the snippet gets in:

  • Vite / any JS output on disk: prepended as a new first line, and the map's mappings gets one leading ; so every position still lines up.
  • Metro: shipped as a polyfill containing a 36-character placeholder, so it runs before any app module and is part of every map Metro builds (including the dev server's .map endpoint, which skips custom serializers). The serializer then swaps the placeholder for the real ID, which is also 36 characters, so nothing moves.

Setting up a project

Projects are named by slug: one slug per app, one key per project:

| project | app | version comes from | release | | --- | --- | --- | --- | | my-web-app | web app | its package.json | my-web-app-<version> | | my-mobile-app | mobile app | native version + build number (DeviceInfo.getVersion(), getBuildNumber()) | my-mobile-app-<version>+<build> |

The release starts with <project>-<version>. The SDK reports it on errors, and the upload uses the same string.

  1. Issue the project's key. The token is generated locally, printed once, and never stored by the tool:
    npx mapup token new --project telemetry
    The token goes to stdout. The SOURCEMAP_PROJECT_TOKENS entry (name:sha256) goes to stderr. Save the token in the project's CI secret as SOURCEMAP_UPLOAD_TOKEN. If it's lost, issue a new one.
  2. Add the entry to the telemetry backend's env and restart it. Entries can be comma-joined on one line or split one-per-line (either is accepted; one-per-line reads far better once there's more than one 64-char hash):
    # SOURCEMAP_PROJECT_TOKENS="telemetry:<hash>
    # my-web-app:<hash2>
    # my-mobile-app:<hash3>"
    Only hashes live in config, so a leaked env never leaks a usable token. The backend rejects names that aren't slugs (lowercase, digits, _, -, max 64). To rotate, issue a new token, put the new entry in place of the old one, and restart. To revoke, remove the entry and restart. token hash prints the hash of a token read from stdin, to check an entry.
  3. Install the package: npm install -D @saravana.devx/mapup.

Configuration

| variable | where | meaning | | --- | --- | --- | | SOURCEMAP_UPLOAD_URL | build/CI | telemetry backend base URL, e.g. https://telemetry.example.com | | SOURCEMAP_UPLOAD_TOKEN | build/CI secret | the project's upload token | | RELEASE | CLI only | default for --release. The Vite plugin takes release as an option instead. |

The token is read from the environment only, never a flag, so it stays out of shell history and ps output.

Vite plugin

vite.config.ts:

import sourcemapUploadPlugin from '@saravana.devx/mapup/vite';

plugins: [react(), sourcemapUploadPlugin({ platform: 'web', release: RELEASE })],
SOURCEMAP_UPLOAD_URL=http://localhost:8080 SOURCEMAP_UPLOAD_TOKEN=... npm run build

RELEASE is computed in vite.config.ts as <project>-<package.json version> (see "Setting up a project"), so the app sets it, not the environment.

Options: url, token, release (required, no env fallback in the app), platform (default web), deleteAfterUpload (default true), enabled (default true). url and token fall back to their env vars.

What happens during a build:

  • Config present: sets build.sourcemap = 'hidden' unless you set it yourself, stamps a debug ID into every bundle that has a map, uploads every map, then deletes them. A failed upload fails the build and deletes nothing.
  • Config missing, local build: logs a warning. No maps are generated, so none can be deployed by accident.
  • Config missing, CI set: fails the build, so a release can't ship without its maps.

React Native / Expo (Metro)

metro.config.js:

const { getDefaultConfig } = require('expo/metro-config');
const { withDebugIds } = require('@saravana.devx/mapup/metro');

module.exports = withDebugIds(getDefaultConfig(__dirname));

Bare React Native works the same way with @react-native/metro-config's config. Needs Node 20.19+ or 22.12+ (Expo's minimum), which can require() the ES module the wrapper uses.

Upload from the release build (APK, AAB, IPA)

Every release build uploads its own map, so a store build can't ship without one. Debug builds never upload.

Android (assembleRelease for an APK, bundleRelease for an AAB). In android/app/build.gradle, after apply plugin: "com.facebook.react":

apply from: new File(["node", "--print", "require.resolve('@saravana.devx/mapup/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/gradle/mapup.gradle"

This adds mapupUpload<Variant>SourceMap for each release variant. It runs after React Native's createBundle<Variant>JsAndAssets and before assemble<Variant> (APK) and bundle<Variant> (AAB). It runs only after a successful bundle step, so a failed build never uploads the previous build's map. Run it alone with ./gradlew :app:mapupUploadReleaseSourceMap.

iOS (a Release build or Archive, so every IPA). Append to ios/.xcode.env:

# mapup: Release builds (every IPA) write the JS sourcemap and upload it.
# Skipped when mapup isn't installed, so the build never points at a missing script.
MAPUP_DIR="$("$NODE_BINARY" --print "require('path').dirname(require.resolve('@saravana.devx/mapup/package.json'))" 2>/dev/null || true)"
if [ -n "$PROJECT_DIR" ] && [ -f "$MAPUP_DIR/bin/compose-source-maps.cjs" ]; then
  export SOURCEMAP_FILE="$PROJECT_DIR/build/sourcemaps/main.jsbundle.map"
  mkdir -p "$(dirname "$SOURCEMAP_FILE")"
  export COMPOSE_SOURCEMAP_PATH="$MAPUP_DIR/bin/compose-source-maps.cjs"
fi

PROJECT_DIR is only set inside Xcode, so pod install skips the block.

react-native-xcode.sh only writes a map when SOURCEMAP_FILE is set. Then it calls COMPOSE_SOURCEMAP_PATH in place of React Native's compose script. mapup's version runs the original and then uploads. No Xcode build phase is added. If ios/ is regenerated by expo prebuild, add the block again.

What both do: React Native's Hermes step (compose-source-maps.js) drops "debugId" from the final map, so it is copied back from Metro's map first. Then the map is uploaded as index.android.bundle.map or main.jsbundle.map, which is the on-device bundle name plus .map.

Credentials. Never put them in the app's .env: react-native-config copies that file into the APK/IPA. Use one of these:

| | Android | iOS | | --- | --- | --- | | CI | env SOURCEMAP_UPLOAD_URL, SOURCEMAP_UPLOAD_TOKEN | same | | your machine | ~/.gradle/gradle.properties (user-level, not the project's) | ios/.xcode.env.local (gitignored) |

# ios/.xcode.env.local (macOS keychain, so the token is not in a file)
export SOURCEMAP_UPLOAD_URL=https://telemetry.example.com
export SOURCEMAP_UPLOAD_TOKEN="$(security find-generic-password -s mapup -a upload -w)"

Without them the build still succeeds and logs a warning. A failed upload, or even a crash inside mapup, also only warns, so mapup can never block a store build. Set SOURCEMAP_FAIL_ON_ERROR=1 in CI to fail the build instead, so a release can't ship without its map.

Release is <project>-<version>+<build>. On Android that is versionName and versionCode. On iOS it is CFBundleShortVersionString and CFBundleVersion from Info.plist. <project> is MAPUP_PROJECT (env or Gradle property). It defaults to the lowercased rootProject.name (Android) or PRODUCT_NAME (iOS). Pass the same string as release to the SDK's init(). The map is still found by its debug ID first.

Running next to Sentry (e.g. while migrating off it): wrap Sentry's config, withDebugIds(getSentryExpoConfig(__dirname)). If the bundle already has a debug ID, mapup uses that same ID, so both tools' maps match the bundle and both keep resolving. mapup looks in three places: the map's debugId, the //# debugId= comment, and Sentry's sentry-dbid-<id> snippet. Expo's serializer computes that ID for release exports, and Sentry only reuses Expo's. So mapup doesn't depend on Sentry: once Sentry is removed (switch the base to getDefaultConfig from expo/metro-config), it uses Expo's ID, or makes its own from the bundle's content.

To upload by hand from an existing build, run the same command the hooks run:

npx mapup react-native --platform android \
  --map android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
  --packager-map android/app/build/intermediates/sourcemaps/react/release/index.android.bundle.packager.map \
  --release "my-mobile-app-$VERSION+$BUILD"

CLI

npx mapup inject --dir dist                 # stamp debug IDs, before deploying
npx mapup upload --dir dist --platform web --release "$RELEASE" --delete
npx mapup upload --dir build --platform web --dry-run   # no network, no deletes

inject stamps every bundle under --dir that has a .map next to it as text (index-abc.js + index-abc.js.map). Bytecode bundles are skipped. Running it twice is a no-op. The Vite plugin and Metro wrapper do this for you.

upload flags: --dir (searched recursively), --platform (web|android|ios), --release (default $RELEASE), --delete, --dry-run.

Point --dir at the build output, not the project root. The search is recursive and would otherwise walk node_modules.

What the backend enforces

POST /analytics/sourcemaps: multipart form with platform, release and one or more file parts. Each request is checked before anything is written:

  • The token must match a project in SOURCEMAP_PROJECT_TOKENS (401 otherwise).
  • platform is web, android or ios. release is 1 to 128 characters of letters, digits, . _ + -.
  • Each file is a plain *.map name, at most 50 MiB, and a version 3 sourcemap. A debugId, if present, must be a UUID.
  • Up to 20 files per request. The request body is capped at 120 MiB. The batch is all-or-nothing on validation: one bad file writes nothing.
  • A map with a debug ID goes to deployed-map-files/<project>/<release>/<id>.map. Uploading it again replaces it. Each release has its own folder, so retiring a build is deleting that one prefix; a newer release that ships an unchanged chunk has its own copy. Maps uploaded earlier sit at debug-ids/<release>/<id>.map or flat at debug-ids/<id>.map, which the backend still reads.
  • A map without one goes to deployed-map-files/<project>/<platform>/<release>/<name>. If that file already exists with different content, it's a 409 and nothing is written. Identical content is accepted.

Safety rules

  • Maps include sourcesContent, which is your original source. Never serve them from a public path. The plugin and --delete remove them from build output.
  • Local files are deleted only after every batch succeeds.
  • Duplicate map names in one build are rejected.

Distribution

Published to npm as @saravana.devx/mapup. It has no runtime dependencies. Vite is an optional peer dependency, needed only for the /vite and /vite-dev plugins.

Subpath imports: @saravana.devx/mapup/vite, @saravana.devx/mapup/vite-dev, @saravana.devx/mapup/metro. The CLI is npx mapup.

Tests

npm test   # node:test, fake fetch; no network needed