Makefiles for Frontend

An alternative to npm package scripts

While npm scripts are built-in and work well for a variety of situations, there are times when another task runner might make more sense.

My goal is not to suggest you always use Makefiles instead of npm scripts — but rather show possible advantages and provide an introduction. Doesn’t hurt to have one more tool in the toolbox, right?

Advantages

Makefiles can easily include documentation, or even self-document

Makefiles support inheritance — this means you can easily share a common set of commands across projects in your team — we even distribute our commands in an npm module!

Make can help consolidate any shell scripts you’re using

You can use npm-scripts and Make together, these aren’t mutually exclusive approaches

Make includes a dry run flag ( -n ), which is extremely helpful when creating more complex scripts

Quick intro

In this section we’ll do some comparisons between NPM scripts and what they’d look like when translated to Make. We’ll also quickly review the structure of a Make rule.

Basic rules

"scripts": {

"build": "rollup -c"

}

Here we have a typical npm script (in our package.json ) to run Rollup. We could run this with npm run build or yarn build .

build:

npx rollup -c

This same command would look like this 👆 in Make (in a file named Makefile ), and can be run with make build .

We do need to use a task-runner (either npx or yarn ) to actually run our commands, but things still work the same outside of that small change. [1]

Breaking it down

A single script in Make is called a rule, and rules have the following form:

target: prereq1 prereq2 prereqN

command1

command2

commandN

Note that the indentation for commands — must be a tab. [2]

[2] A target can have as many commands as needed, they are all run in a separate shell. [3]

Prereqs are often another target, but can be used to set up variables for your rule. Examples of these are both shown below.

Running multiple rules

To run multiple commands with npm package scripts, you might do something like this.

"scripts": {

"build": "npm run build:js && npm run build:css",

"build:js": "rollup -c",

"build:css": "sass main.scss dist/main.css"

}

The syntax for this in Make is shown below — note Make will automatically short-circuit any failures, so if the js command fails, css won’t run; the same behavior we’re getting above via our && .

build: js css js:

npx rollup -c css:

npx sass main.scss dist/main.css

Recipe-level variables

These can be used by Make, but not by any programs we run. They’re useful in cases where we’d just be adding another flag to an already existing command — as we often do when running in development vs. production environments.

In the example below:

running make dev will first set the WATCH variable to --watch , then will run the build target as a prereq (meaning of course it will run the js and css tasks) — this means the js task would be run as npx rollup -c --watch

will first set the variable to , then will run the target as a prereq (meaning of course it will run the and tasks) — this means the task would be run as running make build would just run js and css tasks, meaning WATCH won’t be set to anything — thus the js task would be run as npx rollup -c

would just run and tasks, meaning won’t be set to anything — thus the task would be run as we can use WATCH in our Make recipes, but Rollup and Sass won’t know about WATCH as an environment variable

dev: WATCH = --watch

dev: build build: js css js:

npx rollup -c $(WATCH) css:

npx sass $(WATCH) main.scss ./dist/main.css

If we do want our recipes to have the environment variable set, that’s an easy fix! We just add export like so:

prod: export NODE_ENV = production

prod: build

An example Makefile

You can also view a gist for this example instead.

# this tells Make to run 'make help' if the user runs 'make'

# without this, Make would use the first target as the default .DEFAULT_GOAL := help

# here we have a simple way of outputting documentation

# the @-sign tells Make to not output the command before running it





help: @ echo 'Available commands:' @ echo -e 'dev \t\t — \t run the development environment @ echo -e 'prod \t\t — \t build for production

# setting a command up so we can run yarn before any other command

# this way our dependencies will always be up to date yarn:

yarn

# running 'make dev' will first run yarn

# then start Rollup and Sass in watch mode dev: WATCH = --watch

dev: yarn build

# running 'make prod' will first run yarn

# then will clean up our old css build

# then will run Rollup and Sass for a production build prod: export NODE_ENV = production

prod: yarn clean build

# the '-' before rm below allows that command to fail

# even if this fails, make will continue running

# this is a contrived example clean:

-rm ./dist/main.css

build: js css

js:

npx rollup -c $(WATCH)

css:

npx sass $(WATCH) main.scss ./dist/main.css

Links

The Guardian (a British newspaper) uses Makefiles to manage their frontend commands

Here’s a post on how to set up self-documenting Makefiles

The official documentation for Make

Notes

[1] — We can directly run the node_modules/.bin files as well if this is preferred.

[2] — It actually doesn’t have to be a tab, but you should leave the Make-default alone. If you really want to change this, you can look into the RECIPEPREFIX variable.

[3] — The ONESHELL variable can work around this, but is generally advised against.

[*] — Yes, we should have set up PHONY

Photo by Sucrebrut on Unsplash