Makefiles and JavaScript

Published 07 Feb 2019 on Adhityaa's Blog

When all you have is a hammer, everything looks like a nail. But if that hammer is absurdly multi-purposed, there’s nothing wrong with this approach. In case you haven’t figured it out yet, Makefiles and JavaScript are the proverbial hammer and nail in this post.

A standard *nix environment has an extremely rich set of tools that are often overlooked in favour of new, shiny alternatives such as Gulp or Webpack. Nothing I’m going to say in this post is really revolutionary in any way; in fact, using Makefiles to build JavaScript code has probably been done for ages. I’m just documenting my favourite and painless way to build JavaScript files in the standard *nix environment.

Before I say anything, however, I’d like to mention that tools like Gulp and Webpack aren’t entirely unnecessary. make comes with its own set of quirks that are often criticised – you must use tabs, filenames can’t contain spaces, and some difficult-to-remember symbols like $^ and $@. Tools like Gulp probably have pretty, colourful outputs too. But if your team is comfortable with standard *nix tools, you might find Makefiles for JavaScript to be simple and clean.

On the structure of JavaScript files

I like to build JavaScript code as a concatenation of smaller, logically separate .js files. I think of them as #include directives. For example, we may have a dashboard that has loosely coupled components that we want broken down into separate files to make the code more manageable. But I want to deliver just a single .js file to the HTML. My solution is to just concatenate all the individual .js files together.

But wait, wouldn’t that result in variable conflicts? Not if we properly encapsulate our JavaScript code as an anonymous function:

(function (global) {

  function foo() {
    // ...
  }

  function bar() {
    // ...
  }

  /**
   * I don't want to export foo because its use is contained to within this
   * file, but I want to use bar elsewhere outside this file. Think of foo as a
   * static function in C.
   */
  global.baz = {
    bar: bar,
  }

} (window));

Now we can call window.baz.bar() from another file or the HTML. We can also safely concatenate JavaScript files since everything is nicely encapsulated.

Declaring dependencies and concatenating files

So, back to our topic: let’s say we want to generate a foo.js that is supposed to be the concatenation of a few files. Here’s how I’d declare the dependencies in the Makefile:

# these are the files we want to build
FILES_TO_BUILD = foo.js

# foo.js is supposed to be a concatenation of these files:
foo.js = common.js foo-utils.js foo-http.js

Let’s define our Makefile targets to do the heavy lifting.

# an implicit target that is built when make is invoked without any arguments
js: ${FILES_TO_BUILD}

.SECONDEXPANSION:
${FILES_TO_BUILD}: %: $$(%)
	cat $^ >$@

The js target is defined to depend on targets for each build file we want. In our case, we’re only building one file (foo.js), but if you had multiple files to build, there would be a target for each of them and js would depend on all of them. When you invoke make, the js target is built, which in turn invokes the target for each file you’ve declared in FILES_TO_BUILD.

SECONDEXPANSION is a special target which tells make to not expand $$ variables in the first iteration. After the first expansion (which expands ${FILES_TO_BUILD} to create a target for each file we need to build), the target looks like:

foo.js: %: $(%)
	cat $^ >$@

Since we have only one file to build (foo.js), the % wildcard simply expands to foo.js. If we had more files to build, each of them would be built in a separate target as they are all dependencies of the js target, which is the implicit target we’re trying to build.

Anyway, the second expansion of $$(%) becomes $(foo.js), which expands to the three dependencies of foo.js in our example. This simply tells make to build foo.js only when the defined dependencies change. And when they do, each dependency (denoted by $^) is concatenated and the output is redirected into the target file (denoted by $@).

And that’s it! You can use fancier concatenation scripts that prepend each file’s contents with a comment of the file name to help navigate the built output. Or minify the built file for production. Or include source maps. Or all of these.

An example Makefile

Here’s an example Makefile summarising everything:

FILES_TO_BUILD = foo.js bar.js baz.js

# dependency list
foo.js = common.js foo-utils.js foo-http.js
bar.js = common.js bar-config.js bar-api.js
baz.js = baz-utils.js baz-auth.js

.SECONDEXPANSION:
${FILES_TO_BUILD}: %: $$(%)
	cat $^ >$@

Here’s an example Makefile I used in Commento to build the entire frontend (HTML, CSS, JS), in case you were wodering what this looks like in a larger, non-hello-world project.

Anyway, I’ll stop rambling on now. Let me know if you have any suggestions to improve on this design in the comments below.