Simple Front-end Builds With Makefiles

Until recently, I assumed Makefiles were something only C/C++ and systems programmers touched. Surely there are better tools around now, like Rake and Cake, Ant and Jake, Maven, Grunt, Broccoli, Gulp.

But the truth is Makefiles are not that complicated, and they are powerful. Anything you can do from the command line you can do with Makefiles. If you want to incorporate fancytemplateengine in your project, you don’t need to search for and download (or write!) a grunt-contrib-fancytemplateengine extension. Simply write out the CLI command as documented by fancytemplateengine.

Compiling SASS to CSS

Lets say we want to compile a SCSS file to CSS. Once SASS is installed, we can type sass --help from the command line to get documentation. Using the sass CLI tool is simple, so create a blank file Makefile and add the following lines:

site.css: site.scss
    sass site.scss > site.css

Then at the command line, run make and your scss will be built.

How is this any different to writing a shell script? Essentially, Makefiles are like shell scripts that know what needs to be run and what doesn’t, based on which files have changed. For large projects, this can speed up build time immensely.

Let’s break down the above example. The first line specifies the make ‘target’, in this case we are saying that we want the file site.css to exist, and that it depends on the file site.scss. If you run make again, you’ll get the message:

make: 'test.css' is up to date.

The second indented line specifies the command to be run. In this case, run the sass compiler on site.scss and output STDOUT to the file site.css.

More Complex Folder Structures

Chances are we’ll be building something more than just a css stylesheet, lets assume we have the following folder structure:

client
|--- src                         This folder contains the input to the build process
     |--- js
         |--- app.ts
         |--- models.ts
         |--- views.ts
     |--- css
         |--- client.scss
     |--- lib
         |--- jquery.min.js
         |--- bootstrap.min.css
     |--- img
         |--- logo.jpg
|--- build                       This folder contains the output and can be deleted
     |--- js
        |--- app.js
     |--- css
        |--- client.css
     |--- lib
        |--- jquery.min.js
        |--- bootstrap.min.css
     |--- img
         |--- logo.jpg

Now we need to make sure that the folder /client/build/css exists before we can output anything to it.

client/build/css/site.css: client/src/css/site.scss
    mkdir -p client/build/css
    sass client/src/css/site.scss > client/build/css/site.css

The -p option tells mkdir to create all intermediate directories and not give any error if the directory already exists.

Note: Directories created with the -p option are given full rwx permissions (ie, chmod -R 777) - something to be aware of if you are planning to directly serve those files.

Concatenating JavaScript with TypeScript

Even if you don’t intend to use the optional typing system, TypeScript is great for managing front-end JS concatenation by using the references system. What I love about TypeScript is that it gives you optional ES6 features for free, as well as optional type safety only when you need it, while still allowing you to write pure JavaScript if you want to.

Compiling TypeScript to Javascript is similar to SASS to CSS, except that in our example we have multiple .ts files. While the TypeScript compiler will take care of including the relevant files where they are requested, Make will also need to know that the final build depends upon these files.

tsfiles = $(shell find client/src/js -name  '*.ts')
client/build/js/app.js: $(tsfiles)
    mkdir -p client/build/js
    tsc --sourceMap --out client/build/js/app.js client/src/js/app.ts

The first line here creates a new variable tsfiles and fills it with the output of a shell command. In this case the shell command is searching for all files inside client/src/js with the .ts extension. Now if we change any of those files, re-running Make will detect that this target needs rebuilding.

Multiple Make Targets

Now that we have two make targets, simply running make will only run the first target in the file. What we need to do is create a new target which ‘requires’ the two targets we have already created. Let’s call this target ‘client’ and put the following at the top of the Makefile:

.PHONY: client
client: client/build/js/app.js client/build/css/app.css

There are no actions specific to the client target, so we don’t need to specify any commands. Make will automatically detect that both the .js and .css files are required and build those targets if they need to be updated.

The .PHONY target at the top lets Make know that this target is not actually outputting any files, so it will always run when requested.

Clean and Rebuild Targets

A common pattern in Makefiles is to add clean and rebuild targets to remove all generated code. This can come in handy when committing to source control or if you make a mistake in your Makefile. You’ll need to add the new targets to the .PHONY target as follows:

.PHONY: client clean rebuild

    ...

# Note this target does not have any dependencies
clean:
    rm -Rf client/build

rebuild: clean client

Now simply run make rebuild and the client/build directory will be cleared and built from scratch.

Copying Images and Other Assets Directly

Often you’ll have images and precompiled assets that you want to use directly in the frontend, in this case we’re using the minified jQuery and Bootstrap libraries. These should be copied across without any modifications - the easiest way to do this with the least overhead is to use the infinitely useful rsync tool included in most Linux installs.

COPYDIRS = lib img

    ...

.PHONY: client clean rebuild copy $(COPYDIRS)
client: copy client/build/js/app.js client/build/css/app.css

    ...

copy: $(COPYDIRS)
$(COPYDIRS):
    mkdir -p client/build
    rsync -rupE client/src/$@ client/build

Notice that we used a variable in the target name. This will be replaced with lib img and expanded into two targets, a lib target and an img target. When each of the targets are run, the $@ in the contents will be replaced with the name of the target. Since we have two directories (lib and img) that we want to copy, this saves us the work of rewriting the target twice.

The -rupE switches tell rsync to copy recursively, update only newer files, and preserve permissions and executability.

Final Comments

The full Makefile is available as a GitHub gist.

Makefiles do have some downsides. Although Make is available on most platforms, the Makefiles themselves are not quite as cross-platform as the JavaScript based front-end build configurations, since we’re relying on shell tools like rsync.

This article only touches the surface of what is possible with Makefiles. Using tools like Facebook’s Watchman, its possible to create a highly flexible watch target for automatically rebuilding upon changes. You might even want to trigger an application server restart, synchronise source with a backup server, or rebuild a Docker image. Anything that’s possible on the shell can be automated with a Makefile.

Further Reading