When bundling a large React Native & ClojureScript project for deployment, sometimes we can run into timeout and out-of-memory issues with the React Native packager. This happens because the output generated by the ClojureScript compiler can be large (relative to the files the React Native bundler expects), and the React Native packager (now metro-bundler) attempts to apply optimisations and transformations to that output.

Also, running the ClojureScript output through metro-bundler causes the source mapping information generated by the ClojureScript compiler to be lost. If we’re using advanced optimisations for our release build (to reduce the start-up time and memory usage of our deployed native app) we definitely want these source maps to be working (and ideally, integrated with whichever crash reporting service we are using).

We don’t really need the transforms/optimisations to be applied to our ClojureScript output either (Google Closure already does a great job there), but we do need metro-bundler to extract a list of dependencies (the JavaScript libraries our code requires) from our code so that it knows which libraries to add to the final bundle.

What follows next is a complete hack. Have fun!

Notes:

  • This is currently working for me on react-native@0.46.4/metro-bundler@0.7.8. Not tested on any other versions, though I was following a similar approach on an earlier version of React Native (before the packager split).
  • For me, following this approach causes source maps to be broken for the JavaScript code that my ClojureScript code depends on. I’d rather have my ClojureScript source correctly mapped though, so that’s okay enough.
  • When metro-bundler/React Native upgrade, this is all likely to break. However, this can be used a starting place to workaround breakage after upgrades. I had a similar setup for react-native@0.40.0; this is an upgraded version of that setup.
  • The hacks below get a bit filename/directory-specific. If you have a different project structure you’ll need to pay careful attention to get stuff working. For my project I have configured the ClojureScript compiler to output a release.ios.js file and a release.ios.js.map source map for release builds, with advanced optimisations enabled.
  • It might be that this doesn’t work for you. Sorry! I’m very interested in hearing any alternative approaches that you do have success with though.
  • What we really need is a proper solution to this, which is conceptually simple: we just need metro-bundler to skip transforming our ClojureScript output, to merge in our pre-created source maps, and still to extract the dependencies from our file.
  • Don’t judge; this is terrible.

Recovering ClojureScript source maps and skipping some babel transformations

The first thing we need to do is create a custom transformer we can supply to metro-bundler. This will let us skip some transforms and, most importantly, return the ClojureScript source maps we’ve already generated. This custom transformer is based off of one I found in the boot-react-native project, but updated for the newer metro-bundler dependency and with a different approach to inlining the ClojureScript sources for the final bundle source map. Save it somewhere in your repository.

Hacks to prevent metro-bundler from applying minification etc

The second thing we need to do is skip the constant folding and minification transforms that metro-bundler applies in addition to the transform step above, but only for our ClojureScript output. I’ve not spent enough time with the metro-bundler to figure out what a sensible pull request would be (i.e. one that’s applicable to other users too), so instead, our terrible hack1:

Save the above to the root of your repo (or somewhere else, and then update the relative paths in the script). Running this file will patch metro-bundler (within your node_modules directory) so that the constant folding and minification steps are skipped for our ClojureScript output. It will only perform the replacement once, so it’s okay to run it before each bundle (I automate this as part of my fastlane setup).

Now to package your React Native + ClojureScript project:

This should finish pretty quickly–or if you were experiencing out-of-memory/timeout issues, it should actually finish!–producing the final bundle and a source map for that. If you open the bundle source map (ios/main.jsbundle.map) you should find your original ClojureScript code in there. If all has gone well, you can use this file to map stack trace locations to your ClojureScript source code. Bundling the source code into the source map itself makes it easy to integrate with error tracking services (such as Sentry), because we now only have to upload/supply the one source map file.

You can test this source map is correct by using source-map, and running:

source-map resolve main.jsbundle.map {line-number} {column-number}

Where {line-number} & {column-number} come from a stack trace for the corresponding build. I usually add a developer-only method of triggering test exceptions so that I can get line/column numbers to check mappings for; this also has the benefit of being very similar to how exceptions will be generated & reported for users during normal use. You could also pull line/column numbers out of main.jsbundle, but it might be difficult to prove that the mapping is correct.

Issues/resources worth monitoring

  • The custom transformer from boot-react-native that I based my modifications on. And for discussion into some issues around that, look here.
  • Open issue on metro-bundler: Release optimization pass doesn’t respect input source maps?

    Depending on how this issue gets resolved, we might be able to drop our custom transformer. Ideally we’d like the ability to let metro-bundler know not to apply any transformations/minification to our single-file ClojureScript output, and to accept an existing source map for that file too.

  • In theory you can use a .babelrc file to ignore files. Unfortunately I’ve not been able to get this to work yet (still getting the out-of-memory/timeout issues). As far as I can tell, we’d still need to use a custom transformer to prevent the loss of our source mapping information, even if we could skip the transformations.

  1. The lines we actually want to patch are here and here, but as metro-bundler has a post-install compilation step we need to target the compiled output instead.