Utilities to quickly add support of source-maps to your React Native project
As you probably already now, React Native minifies release code. It's perfect for bundle size and execution speed. But it can become a real pain to find production bug in such code.
For example error stack trace in your logging system can look like that:
0:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:947:1763
1:u@file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:137
2:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:891
...
And.... It's useless just to know about an error without a possibility to find it in the exact place of code. So we need source maps. By using source maps, we can map the code being executed to the original code source files, making fixing bug much, much easier.
To make it look like this:
0:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:947:1763
1:u@file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:137
2:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:891
...
Firstly I'd like to mention that React Native CLI has a flag to build source maps alongside with JS code bundle.
How can we do this?
For example, originally iOS builds use this packager script: ./node_modules/react-native/packager/react-native-xcode.sh Which call React Native CLI bundle command this way:
$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \
--entry-file "$ENTRY_FILE" \
--platform ios \
--dev $DEV \
--reset-cache \
--bundle-output "$BUNDLE_FILE" \
--assets-dest "$DEST"
We need to modify this script and add one line:
--sourcemap-output "$BUNDLE_FILE.map"
It can be done with simple patching of this file in npm postinstall script. Or creating custom bundle script.
For Android builds, add this line to the build.gradle under app module to make js bundle name same with iOS build:
project.ext.react = [
bundleAssetName: "main.jsbundle",
]
Then edit Android packager script: ./node_modules/react-native/react.gradle
and you need to add "--sourcemap-output" and bundle map file parameter to the build script,
def jsBundleMapFile = "${jsBundleFile}.map"
"--sourcemap-output", jsBundleMapFile
for example:
def jsBundleMapFile = "${jsBundleFile}.map"
def devEnabled = !targetName.toLowerCase().contains("release")
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", *nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, "--sourcemap-output", jsBundleMapFile, *extraPackagerArgs)
} else {
commandLine(*nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, "--sourcemap-output", jsBundleMapFile, *extraPackagerArgs)
}
This module was built to map minified sources to original JS files. And to solve our pain to find actual place where an error occurred.
npm i react-native-source-maps
import {initSourceMaps} from 'react-native-source-maps';
initSourceMaps({sourceMapBundle: "main.jsbundle.map"});
Example:
const stack = !__DEV__ && await getStackTrace(error);
if (stack) {
error.stack = stack;
}
log.error({message: 'Error (' + error.message + ')', error});
import log from "../log";
import {getStackTrace} from 'react-native-source-maps';
import ErrorUtils from "ErrorUtils";
(async function initUncaughtErrorHandler() {
const defaultGlobalHandler = ErrorUtils.getGlobalHandler();
ErrorUtils.setGlobalHandler(async(error, isFatal) => {
try {
if (!__DEV__) {
error.stack = await getStackTrace(error);
}
log.error({message: 'Uncaught error (' + error.message + ')', error, isFatal});
}
catch (error) {
log.error({message: 'Unable to setup global handler', error});
}
if (__DEV__ && defaultGlobalHandler) {
defaultGlobalHandler(error, isFatal);
}
});
})();
Property | Type | Description | Default value |
---|---|---|---|
sourceMapBundle |
string | source map bundle, for example "main.jsbundle.map" | undefined (Required) |
collapseInLine |
bool | Will collapse all stack trace in one line, otherwise return lines array | false |
projectPath |
string | project path to remove from files path index | undefined (Optional) |