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]
- 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 cssjs:
npx rollup -ccss:
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 theWATCH
variable to--watch
, then will run thebuild
target as a prereq (meaning of course it will run thejs
andcss
tasks) — this means thejs
task would be run asnpx rollup -c --watch
- running
make build
would just runjs
andcss
tasks, meaningWATCH
won’t be set to anything — thus thejs
task would be run asnpx rollup -c
- we can use
WATCH
in our Make recipes, but Rollup and Sass won’t know aboutWATCH
as an environment variable
dev: WATCH = --watch
dev: buildbuild: js cssjs:
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 ithelp:
@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 dateyarn:
yarn
# running 'make dev' will first run yarn
# then start Rollup and Sass in watch modedev: 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 buildprod: 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 exampleclean:
-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