Customizing CoffeeScript

July 10, 2014 • hack

We have build some fairly large single-page apps using CoffeeScript. Naturally, we wanted to use some kind of module / dependency system to keep our code organized and not go crazy. We had decided on using RequireJS. In development we only need to recompile the one file we changed instead of bundling everything up. That’s so fast that I usually need more time to switch from my editor to browser window. And, in production we bundle all source files and minify that before we send it to the client.

A simple module looks like this:

define([
  "jquery"
  "lodash"
  "./base_class"
], ($, _, BaseClass) ->

  class AwesomeClass extends BaseClass
    print : ->
      console.log("Awesome class is cleared for take-off.")
)

One thing was very annoying, though. We had to add one level of indentation just to satisfy the module definition before writing any actual code. Also, if your module starts to have a bunch of dependencies there will be a stark visual mismatch between dependency array (vertical) and the parameter list (horizontal) in the module definition. That leads to nasty bugs where modules are named wrong.

So, we thought of an alternative syntax and customized the CoffeeScript compiler. We had to fork the compiler because it doesn’t have a plugin system. You can check it out on Github.

### define
jquery : $
lodash : _
./base_class : BaseClass
###

class AwesomeClass extends BaseClass
  print : ->
    console.log("Awesome class is cleared for take-off.")

If you have heard about compiler theory, you’ll probably know that a compilation process roughly has these steps: Tokenizing > Parsing > Optimization > Code Generation. After Parsing an abstract syntax tree (AST) has been constructed. That’s a tree-like representation of your code. CoffeeScript does some of it’s handy optimizations by applying transformations to the AST.

This is also the stage where our code comes in. We traverse the first level of the AST and look for comment nodes. As soon as we have found one with the define keyword at its start we stop and parse out the dependency definitions. Then we’ll create a new CallExpression node to wrap the whole module code in it. Because we execute our transformation on the AST we get CoffeeScript’s implicit return for free. See the code.

To maintain this fork we merge the changes from the original compiler every once in a while. Because we put our transformation logic into a separate file, we only had to change a few lines in the original code. That makes most merges pretty straight forward. Also, now we need to list our own compiler in all package.json dependency lists. But with a recent version of npm we can take advantage of the Github shortcuts, like this:

...
  "dependencies": {
    "coffee-script": "scalableminds/coffee-script",
...

Very similar to sweet.js, Frank van Viegen recently introduced a hygenic macro system for CoffeeScript called black coffee. Both might be very simple and sustainable ways to create custom language features in the JS world. We love these kind of workflow optimizations because it removes some of the boilerplate tasks and allows us to focus on the actual business code.

scalableminds/coffee-script

Title photo by Jeremy Keith

by Norman Rzepka


Related posts