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
lots of old hammers — because Make is ‘one more tool in the toolbox’

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 -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
  • 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
  • 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 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 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

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

--

--