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)
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
: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:
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 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
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
: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 once
Run the generated JS file with node:
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
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.