Find your node.cljs bugs with source maps

Have you ever gotten a JS stack trace from your CLJS code? They look something like this:

$ node hello_world.js
hello world

.../hello-world/hello_world.js:14006
  throw Error("fail!");
        ^
Error: fail!
    at Error (<anonymous>)
    at hello_world.core.fail (.../hello-world/hello_world.js:14006:9)
    at Function.hello_world.core._main (.../hello-world/hello_world.js:14009:32)
    at cljs.core.apply.b (.../hello-world/hello_world.js:6337:14)
    at cljs.core.apply.a (.../hello-world/hello_world.js:6383:18)
    at Object.<anonymous> (.../hello-world/hello_world.js:14014:17)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)

Line 14006? Really? Thanks.

Obviously the CLJS has been compiled into JS and node is giving us a stack trace with the JS file names and line numbers. Nothing really surprising here, but it's also not very helpful.

Source maps to the rescue

Fortunately the JS community has come up with source maps. Source maps are very much analogous to debugging symbols in compiled languages: they allow a debugger to link the executing code back to the original source code file and line number.

It looks like source maps were originally intended for helping with the debugging of minified client-side JS files. The idea is that the minifier creates a file that maps the minified symbols, file names, and line numbers to the original source file names and line numbers. The original file names and line numbers can then be displayed to the user, for example, by the browser's JS debugger.

The same mechanism can also be used for mapping JS source file locations to the original source code for languages that are compiled to JS, for example, CoffeeScript, TypeScript, and ClojureScript.

With source maps enabled in our project the above error looks like this:

$ node hello_world.js
hello world

.../hello-world/out/hello_world/core.cljs:6
  (throw (js/Error. "fail!"))
 ^
Error: fail!
    at Error (<anonymous>)
    at hello_world.core.fail (.../hello-world/out/hello_world/core.cljs:6:2)
    at Function.hello_world.core._main (.../hello-world/out/hello_world/core.cljs:11:3)
    at cljs.core.apply.b (.../hello-world/out/cljs/core.cljs:2571:17)
    at cljs.core.apply.a (.../hello-world/out/cljs/core.cljs:2599:12)
    at Object.<anonymous> (.../hello-world/out/hello_world/core.cljs:14:20)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)

The source references have now been translated back to CLJS source file names and line numbers!

How to enable source maps for your node.cljs project

To get the benefit of source maps in your project you have to do two things:

  • the ClojureScript compiler needs to emit a source map file alongside the generated JS
  • node needs to be told to use the generated source map to translate stack traces

Generate source maps for your CLJS

Turning on source maps generation in your project.cljs is just a matter of adding the following line to your :compiler map:

:source-map "<file name>.map"

The compiler includes a magic comment in the generated JS file, which tells node what file contains the source map, eg:

//# sourceMappingURL=hello_world.js.map

You can find it at the end of the generate JS file.

Enable source map support in node

Granting node the power of source maps is slightly more invoved. Basically node doesn't understand source maps out of the box, so you will need to install the node source map support library and initialize it in your CLJS code.

The library can be installed with npm:

npm install source-map-support

This will create a node_modules directory in your project and install the library in there. You can also install the library globally with:

npm install -g source-map-support

Note that unless you add the library to your source control repo, you will have to manually install the library whenever you checkout your repo. I'll write about using npm and package.json to declare package dependencies in the near future.

Finally, the library is initialized in your CLJS code with:

(.install (nodejs/require "source-map-support"))

An example project

To help you work out how all the bits fit together, here let's walk through all the steps with code samples.

Create a sample project

We use my nodecljs Leiningen template for creating and new node.cljs project:

lein new nodecljs hello-world

Install node source map support

npm install source-map-support

Edit project file

Add the :source-map configuration to project.clj. Edit the project file to look like this:

Edit CLJS source file

Initialize the node source map support library in code.cljs. Edit the source file to look like this:

Compile the project

Compile the CLJS code with lein-cljsbuild:

lein cljsbuild once

Run it!

Run the generated JS file with node:

node hello_world.js

You should now see a stack trace on your console with the source file names and line numbers translated to CLJS file names and line numbers. Result!

You'll also notice that hello_world.js.map has been generated next to hello_world.js.

Word of warning!

Source maps are great for debugging your code. Unfortunately they also come with quite a hefty compiler performance hit. The ClojureScript compiler compiles the above example project in roughly two seconds on my laptop with source maps disabled. With source maps enabled the same project takes over five seconds to compile.

Going from two seconds to five seconds doesn't sound like such a big deal, but when your project grows in size, the performance difference is also going to increase. And when you're used to interactive development, having to wait for the ClojureScript compiler gets really annoying really fast.

The simple answer for this issue is to disable source maps by default and enable them only when you need to decipher a stack trace.


Want to read more about ClojureScript on Node?
Sign up for our mailing list!

Unsubscribe at any time. No spam, ever.
comments powered by Disqus