Introduction

JavaScript has provided us with a giant explosion of build tools, grunt, gulp, slush, broccoli and brunch just to name a few of the more popular ones, there is just way to many to name them all.

More or less these tools all depend on plugins to do even the simplest of tasks, from copying a file to creating an archive, you'll need a plugin for it.

While, in theory this leads to great flexibility, in reality we are just duplicating the unix ecosystem. This also quickly leads to your project having a big bundle of development dependencies, and the tasks you are doing are just run-of-the-mill ordinary copying, building, bundling and minification.

Introducing Make

Make is a pretty old tool, and for most developers coming from a native development background it is an old friend, but most web developers that don't come from a computer science background don't really seem to ever get introduced to it. However, that does not mean make is not a good tool, in-fact its far too underrated these days.

You have the entire unix ecosystem available to you, pipes, streams, utilities, it's all there, most of the tools you'll need to build are already on the system. If you are developing on a non-unix machine like Windows, then you should at least do yourself a favor and install a proper shell and a minimal bundle of tools like gow, perhaps a better terminal emulator to replace cmd.exe as-well, like cmder.

Make works by defining rules in a file typically called Makefile, no file extension, and do take note that capitalization matters. Rules are made up from targets and prerequisites, which can have shell commands executed upon them, which is usually referred to as the recipe.

In general a rule looks something like this:

targets: prerequisites
	recipe
# Do take note, that is a tab, not spaces.

So with that knowledge, lets do a simple example. We want to copy a file from input.txtwhich is the prerequisite to output.txtwhich is the target. Our recipe will be a simple command line utility.

output.txt: input.txt
  cp $< $@

Pretty simple right? This does a shell invocation of cp(1), with a couple of automatic variables, $@ holds the file name of the target, $< holds the name of the first prerequisite.

Lets move on to something more practical.

Compiling our Code

Coffeescript, Typescript, or modern JavaScript transpilers like babel are pretty common nowadays, so we'll compile our code from modern JavaScript to something that can be consumed today with babel as our first example.

To do this we will need to define a rule that does a one to one conversion between the files in our src directory, to the transpiled files we want in our lib directory.

# First we will assign a variable to our JavaScript compiler,
# babel in this case. While this is not strictly nesscescary it
# makes maintenance a little easier.
JC            = babel

# We'll enable loose transformations
JCFLAGS       = --loose

# Next we'll find all the source files in our source directory
SRC           = $(shell find src -name "*.js")

# And then do substitution to get strings that map one to one from
# the source directory to the library directory
LIB           = $(SRC:src/%.js=lib/%.js)

# Finally, we'll define our rule to convert from source to library
# Which is to invoke the compiler with the options and prerequisites
# we've already defined and output to the target name.
$(LIB): $(SRC)
  mkdir -p $(@D)
  $(JC) $(JCFLAGS) $< -o $@

It's all very similar to bash scripting, $(VARIABLE) does variable expansion and $(@D) is another automatic variable, it holds the directory part of the file name of the target.

Bundling our Code

Browserify, Webpack or just plain concatenation is also a rather standard thing to do if you are going to distribute your code for the browser environment. In order to do this we will need a rule that has a single target, takes all the sources files as prerequisites, feeding them through the bundler as the recipe.

# Like before, we will store our bundler in a variable
BUNDLE        = browserify

# Along with its invocation flags.
# This is an example of where using variables makes things easier
# We will want to pass the same flags to both babel and
# the babelify transform, since it is already a 
# variable there is no duplication.
BUNDLEFLAGS   = --transform babelify [$(JCFLAGS)]

# Next we'll define our bundle filename
DIST          = dist.js

# Finally we'll define the rule, again the goal is to pass all
# prerequisites into the bundler and output it to the target.
# We could have used the already compiled library files,
# but generally this would yield a slightly bigger file
# since compilers often generate helpers.
$(DIST): $(SRC)
  $(BUNDLE) $(BUNDLEFLAGS) -o $@ $^

Optimizing our Code

Minification and dead code removal is another common build step. In this case we'll just define a rule that takes the previously defined bundle target as a prerequisite and run it through uglify, we'll do some simple substitution to get our minified bundle name based on the non-minified bundle name.

DIST_MIN            = dist.min.js

# Using uglifyjs as our minifier
MIN                 = uglifyjs

# No flags, just yet at least.
MINFLAGS            = 

# This rule will be really simple.
# It's a single target with a single prerequisite passed through our optimizer
$(DIST_MIN): $(DIST)
  $(MIN) $(MINFLAGS) $< -o $@

Cleaning our build

Make also has a special target called .PHONY, the prerequisites of this special target are considered to be phony targets. Make will run the recipie of such a target unconditionally, regardless of whether a file with that name exists or what its last-modification time was, it's useful in cases where we want to run a script like task regardless of what files have been generated, for example if we need to clean our build.

clean:
  rm $(LIB)
  rm $(DIST)
  rm $(DIST_MIN)

Conclusion

Make has a bit of a undeserved reputation for being hard to understand, it is not that different from shell scripting. In my opinion the syntax is clear and concise, you get free interop with everything, it has streaming, it has pipes, it has a big ecosystem called unix, so with that said. Viva la revoluciĆ³n, write a makefile today!

Get the source for this article

Feel free to hate on make at me via @caspervonb on Twitter