In a React Native Hermes release build, why are two source maps composed into one, and what breaks if you skip it?
answer
- two compile steps, two maps
- packager map: bundle to source
- compiler map: bytecode to bundle
- compose-source-maps.js chains them
- hermesFlags must keep -output-source-map
basics
~20 sMetro's packager map links the bundle to your sources; hermesc's compiler map links bytecode to the bundle. Hermes crash frames are bytecode positions, so compose-source-maps.js chains the two; without that, frames resolve wrongly or not at all.
solid answer
~40 sA Hermes release build transforms code twice: Metro bundles the modules and can write a packager map from bundle to source, then hermesc compiles the bundle to bytecode and, with `-output-source-map`, writes a compiler map from bytecode to bundle. Crash frames are bytecode positions, so neither map alone reaches your source; `compose-source-maps.js` chains them into one map from bytecode to source. The Android Gradle plugin does this automatically when `hermesFlags` include `-output-source-map`, writing the result to `generated/sourcemaps/react/<variant>/`; the iOS script does it when `SOURCEMAP_FILE` is set. Skip it, by dropping that flag or archiving the `*.packager.map`, and `metro-symbolicate` gives unresolved or plausible-but-wrong frames.
go deeper
Know that a Hermes build compiles twice and that the map you use must cover both steps.
Explain the packager map, the compiler map and the composed map, and which one each platform writes to its output location.
Diagnose pipelines where composition silently didn't happen, from overridden hermesFlags to hand-rolled release scripts, and verify maps with a planted crash.
Require an end-to-end symbolication check in the release process, since a silently wrong map misleads crash triage for every build it covers.
## Two transformations, two maps A Hermes release build transforms your code twice, and each step can describe its own transformation: 1. **Metro** turns hundreds of source modules into one JavaScript bundle. With `--sourcemap-output` it writes the **packager map**: bundle position to original file, line, column and name. 2. **hermesc** compiles that bundle to bytecode. With `-output-source-map` it writes the **compiler map**: bytecode position to bundle position. A Hermes crash frame reports a **bytecode** position. Neither map alone gets from there to your source: the compiler map stops at the bundle, and the packager map expects bundle positions, not bytecode ones. **Composition** chains them into one map from bytecode to source. | Map | From | To | Written by | |---|---|---|---| | Packager | JavaScript bundle | Your source files | Metro | | Compiler | Hermes bytecode | JavaScript bundle | hermesc | | Composed | Hermes bytecode | Your source files | `compose-source-maps.js` | ## Who composes, and when React Native ships the script as `react-native/scripts/compose-source-maps.js`. Its usage line is: ```bash node node_modules/react-native/scripts/compose-source-maps.js \ packager.map compiler.map -o composed.map ``` The standard builds call it for you: - **Android**: the Gradle plugin's Hermes bundling task writes the packager map to `intermediates/sourcemaps/react/<variant>/index.android.bundle.packager.map`, moves hermesc's output to `...compiler.map`, and composes them into `generated/sourcemaps/react/<variant>/index.android.bundle.map`, but only when `hermesFlags` contains `-output-source-map`. - **iOS**: `react-native-xcode.sh` does the same when `SOURCEMAP_FILE` is set and Hermes is in use, writes the result to `SOURCEMAP_FILE`, and deletes both intermediates. The composed map carries Hermes-specific data (function offsets) that `metro-symbolicate` reads when it resolves Hermes frames. ## What breaks if you skip it Composition goes missing in a few realistic ways: - A custom `hermesFlags` list in `build.gradle` without `-output-source-map`: the plugin skips hermesc's map and the composition, so `generated/sourcemaps` stays empty and only the packager map exists. - A hand-written release script, for example in a brownfield app or custom CI, that runs Metro and hermesc directly and keeps only Metro's map. - Someone grabbing the `*.packager.map` from `intermediates` because it looks like "the" source map. In each case the frames either fail to resolve or, worse, resolve to **plausible but wrong** lines, because bytecode offsets are being read as bundle positions. A wrong symbolication is more expensive than none: it sends the investigation into innocent code. ## Signs you are holding the wrong map - The symbolicated top frame lands in code that cannot throw the reported error, such as a style object or an import line. - Every frame resolves to a handful of the same library files regardless of the crash. - Frames stay unresolved even though the map is clearly from the right release. - The map's file name ends in `.packager.map` or `.compiler.map`, which are the intermediates on Android. Each of these points first at composition, and only then at a version mismatch. ## How to check a pipeline 1. Build a release variant with a deliberate JavaScript crash behind a hidden debug gesture or a test flag. 2. Capture the stack from `adb logcat` or the iOS device log. 3. Run it through `metro-symbolicate` with the map your pipeline archived. 4. Confirm the top frame names the file and line you planted. If step 4 fails, look at which map was archived before anything else. ## Where this sits The interview point is not the script's name but the model: **every compilation step needs a map, and the maps must be chained**. The same idea applies whenever a tool adds a stage between your source and what runs. The Hermes bytecode format itself, and how the engine executes it, is a separate topic.
- How would you notice in a build that composition didn't happen on Android?The composed file at android/app/build/generated/sourcemaps/react/release/index.android.bundle.map would be missing, while intermediates/sourcemaps/react/release still holds a packager map. That pattern means hermesFlags lacks -output-source-map. A release pipeline should check that the composed file exists before archiving anything.
- Does a JavaScriptCore build need composition?No. Without Hermes there is no bytecode compilation step, so Metro's map already describes what runs; the Android plugin writes Metro's map straight to the output location and the iOS script writes it straight to SOURCEMAP_FILE. JSC is now a community package rather than part of core, so the Hermes flow is the normal one.
It is a book translated twice, English to French to Japanese, with a page-number concordance kept for each translation. To find where a Japanese page came from in the English original you need both concordances chained; the English-to-French one alone points you at the wrong page.
saying these in an interview costs you the question
- Metro's source map alone is enough for Hermes crashes.
- hermesc's map points bytecode straight to your source files.
- Composition only matters for iOS builds.
- A wrong map fails loudly, so you'll notice at once.
- Custom hermesFlags don't affect source map output.