Austin Birch

ClojureScript & React Native: bundling for release with advanced optimisations and source maps

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. 

A Hackers Expense Tracking (wip)

A quick and hacky way of tracking expenses.

tl;dr

  • Buy something
  • Send an email to your Gmail with a specific subject, and the transaction details in the body
  • Those emails are automatically forwarded to Zapier Email Parser
  • Transactions get extracted from the emails, and appended to a “transaction log” Google Sheet
  • Use that transaction log in other worksheets (e.g. income vs expenses)

First of all, why have I done this?! Because this is what hackers do! There are a lot of ready-made solutions for this stuff out there, with various strengths/weaknesses. The strength of this setup is that I can control those strengths and weaknesses.

Besides, I don’t really know what I want from this…yet. I think I want YNAB-like budgeting, but not exactly what they’ve done. The easiest way for me to figure out what I actually want is to start with something simple, and grow it around experience.

There are other systems I’d like to try too (like ledger), but I’m sharing this with somebody uncomfortable with the command line.

And the final reason for hacking something together using various tools: it’s fun!

How it works

Once I complete a purchase, I send emails to myself with a specific subject. A Gmail filter picks these up, hides them from my inbox, and forwards it to my Zapier Email Parser account. The Zapier Email Parser Mailbox is set up to parse amount, reason, and tags from the email. That parsed result gets appended to a “Transaction Log” Google Sheet, with some extra information (timestamp, from email, from name, etc).

Reporting

I’ve setup a spreadsheet that has per-month breakdowns of income, estimated expenses (on things like rent, bills, groceries etc), and actual expenses. This spreadsheet uses formula and scripts to categorise expenses (per-person, per-tag, etc) from the transaction log.

With that information available, I can make smarter financial decisions, and I can feel more comfortable about making them. Based on data, I can see when I need to change my behaviour. I can project finances into the future, and then simulate changes in financial situation.

This gives me lots more power than most personal finance software, as nearly all of the solutions only look at past results, and do not help you plan for the future. I’ve now got the ability to add “features” to help me budget for the future (as I said, I want something similar to YNAB, but I’m not sure exactly what yet).

Improvements

The email parsing needs improvement. It’s not always accurate, which results in me checking up on it (extra end-user work, not good). To fix this I could:

  • Use an email format that simpler for the parser to understand (That’s not good! Making the user do repetitive work that a computer should be doing.)
  • Look at different services for parsing the emails (Not sure I’ll find a good-enough one though)
  • Write less information in the email to make the parsing simpler (That’s not good! I want more data, not less.)
  • Use a more structured form of recording expenses (This is the most likely solution. Perhaps throwing together a simple iOS app that has fields/inputs for each field. This will definitely solve the parsing issue–no parsing required–but feels like more work. Could be easier to extend though.)

Next

I’m going to try and make improvements to the expense-recording aspect of this. I’m very happy with Google Sheets as a backend for now. It’s really flexible, I love having programmatic (through formulas and scripting) access to the raw data, and it’s great for collaboration (very important, as I’m sharing this with my girlfriend).

When I make some substantial changes I’ll write about it again. I’ll also write about if it’s a complete failure.

Notes

Alternative input

I really like that it doesn’t involve installing an App (just use my standard email client), but I might need something with a more structured input.

I did consider SMS an input method, however, it’s got roughly the same qualities/problems as email for this.

Zapier Email Parser Format

In case anybody else want’s to try a setup similar to mine, here’s email format I’m experimenting with:

Subject: _money

Body:

£5.60

Lunch - sandwich, fruit, popcorn

#lunch

The pains of UK online-banking

An article from two years ago that is (unfortunately) still pretty accurate: http://www.thinkui.co.uk/2013/01/lloyds-tsb-a-bad-ux/