Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Using Make & Makefiles to Automate your Frontend Workflow
Make is a build automation tool that’s been used for decades to build software. Learn how you can leverage make to automate frontend web development
make is a Unix tool that’s been around since the mid 1970’s, and is still widely used today for automating software build processes.
make is the OG of build tools
The make tool is available for just about every platform you can imagine, and is installed with the XCode CLI Tools on the Mac, and WSL2 on Windows. Probably you have these installed already if you’re doing development.
This article describes how you can leverage make to automate your frontend web development via a standardized CLI API of your own design.
Link Why use make?
With all of the build tools and automated systems out there, why should we use make? A few reasons:
- It lets you define a simple, standardized CLI API your team can use across projects
- The verb-noun semantics of make target are easy to digest & remember
- You effectively get local command aliases for each project that are context and project aware
- The tooling under the hood is abstracted away, and can be swapped out at any time
- make is already installed on your development machines, and is designed to automate builds
So people on your team (or just you, if you’re a team of one) can just type make dev to spin up a local development environment, without having to know what happens under the hood to make that happen.
If you decide to use a different local dev environment, you can swap it out, and make dev will still “do the thing” to make it happen.
Link Real World Examples
As discussed in the An Annotated Docker Config for Frontend Web Development article, I use Docker for a local development environment, and as discussed in the An Annotated webpack 4 Config for Frontend Web Development article, I use webpack as a build system.
As such, it’s much nicer to be able to type, say, make dev than it is to type docker-compose up when I want to work on a project.
However the real key here is that no matter what machinery needs to be run under the hood, make dev spins up the development environment.
Abstracting away what does the thing from the command has many benefits
So here’s an example of some of the commands I have in my Makefile, and what they do:
- make dev — does whatever needs to be done to spin up the project’s local dev environment, so I can work on the project
- make build — does whatever needs to be done to build a project’s production ready resources for deployment
- make clean — does whatever needs to be done to rebuild the project environment from scratch
- make docs — for my plugins, does whatever needs to be done to build the documentation
There are more commands of course, but these are a few examples that show how easy it is to onboard someone onto a project.
For a real-world example, try spinning up the devMode.fm website locally!
Link How make works
When you run make via your CLI terminal, it might look something like this:
make looks for a plain text file named Makefile in the current directory. This file has any number of targets that look like this:
- Target — this is normally a file or directory that needs to be built.
- Prerequisites — other files or targets that need to be built before the target can be built.
- Recipe — preceded by a tab, a series of any number of shell commands that are executed to build the target
So in the above example when we type make build it will first do whatever is needed to build the target named up (which is a prerequisite), and then it’ll run the commands in the recipe to build the target named build.
make will rebuild a target when either the target file or directory doesn’t exist, or when any of its prerequisites have been modified and so are newer than the target.
This Makefile Cheatsheet may come in handy when learning how Makefiles work.
You also can define and use variables in your Makefiles, the ?= conditional assignment operator used above assigns a default value if the variable isn’t set already.
This can be useful in assigning default values that can be overridden via shell environment variables.
Using make, we can get local aliases to run project-specific commands
We mentioned earlier that targets are normally files or directories, but we can use the special built-in target named .PHONY to specify that the target is just a list of commands that should always be run.
We leverage this to use our Makefile to define local aliases that run commands to do various things with our project.
So let’s have a look at a few examples.
Link Craft Scaffolding Makefile
This Makefile is one I use in my Craft CMS scaffolding. The CMS or framework in use doesn’t really matter, the applied principles are what is important.
CONTAINER?=$(shell basename $(CURDIR))_php_1
BUILDCHAIN?=$(shell basename $(CURDIR))_webpack_1
.PHONY: build clean composer dev npm pulldb restoredb up
build: up
docker exec -it ${BUILDCHAIN} npm run build
clean:
docker-compose down -v
docker-compose up --build
composer: up
docker exec -it ${CONTAINER} composer \
$(filter-out $@,$(MAKECMDGOALS))
craft: up
docker exec -it ${CONTAINER} php craft \
$(filter-out $@,$(MAKECMDGOALS))
dev: up
npm: up
docker exec -it ${BUILDCHAIN} npm \
$(filter-out $@,$(MAKECMDGOALS))
pulldb: up
cd scripts/ && ./docker_pull_db.sh
restoredb: up
cd scripts/ && ./docker_restore_db.sh \
$(filter-out $@,$(MAKECMDGOALS))
update:
docker-compose down
rm -f cms/composer.lock
rm -f buildchain/package-lock.json
docker-compose up
update-clean:
docker-compose down
rm -f cms/composer.lock
rm -rf cms/vendor/
rm -f buildchain/package-lock.json
rm -rf buildchain/node_modules/
docker-compose up
up:
if [ ! "$$(docker ps -q -f name=${CONTAINER})" ]; then \
docker-compose up; \
fi
%:
@:
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
The key commands are:
- make dev — brings up the local development environment; in this case that means spinning up the Docker containers via docker-compose up
- make build — executes a frontend buildchain build to create the production resources, by running npm run build in the buildchain Docker container
- make npm xxx — runs the passed in NPM command in the buildchain Docker container
- make composer xxx — runs the passed in Composer command in the PHP Docker container
- make craft xxx — runs the passed in Craft CLI command in the PHP Docker container
If I ever changed local development environments or frontend buildchains, I wouldn’t need to re-educated my team (or re-train myself) on the new commands.
I can just swap in the machinery needed to do the thing in the Makefile.
Tip: If you try a command like make craft project-config/apply --force you’ll see an error, because the shell thinks the --force flag should be applied to the make command. To side-step this, use the -- (double-dash) to disable further option processing, like this: make -- craft project-config/apply --force
Link Craft Plugin Development Environment Makefile
This is the Makefile from my Craft CMS Plugin development environment.
It’s a skeleton project I use to build and test my Craft CMS plugins, with some specific functionality to make that easy.
CONTAINER?=$(shell basename $(CURDIR))_php_1
.PHONY: dev clean composer mysql postgres up
dev: up
clean:
docker-compose down -v
docker-compose up --build
composer: up
docker exec -it ${CONTAINER} composer \
$(filter-out $@,$(MAKECMDGOALS))
craft: up
docker exec -it ${CONTAINER} php craft \
$(filter-out $@,$(MAKECMDGOALS))
mysql: up
cp cms/config/_dbconfigs/mysql.php cms/config/db.php
postgres: up
cp cms/config/_dbconfigs/postgres.php cms/config/db.php
update:
docker-compose down
rm -f cms/composer.lock
docker-compose up
update-clean:
docker-compose down
rm -f cms/composer.lock
rm -rf cms/vendor/
docker-compose up
up:
if [ ! "$$(docker ps -q -f name=${CONTAINER})" ]; then \
docker-compose up; \
fi
%:
@:
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
The key commands are:
- make dev — brings up the local development environment; in this case that means spinning up the Docker containers via docker-compose up
- make composer xxx — runs the passed in Composer command in the PHP Docker container
- make craft xxx — runs the passed in Craft CLI command in the PHP Docker container
- make mysql — dynamically switches the project over to using the MySQL database container
- make postgres — dynamically switches the project over to using the Postgres database container
This lets me replicate my plugin development environment on any machine easily, and test against various scenarios (such as multiple database types) just as easily.
Link Craft Plugin Makefile
This is a Makefile for the Craft CMS Plugins that I develop.
I run a webpack 5 buildchain inside of a Docker container that handles the Hot Module Replacement (HMR), optimized production build, Tailwind CSS JIT, etc.
TAG?=14-alpine
CONTAINER?=$(shell basename $(CURDIR))-buildchain
DOCKERRUN=docker container run \
--name ${CONTAINER} \
--rm \
-t \
--network plugindev_default \
-p 8080:8080 \
-v "${CURDIR}":/app \
${CONTAINER}:${TAG}
DOCSDEST?=../../sites/nystudio107/web/docs/retour
.PHONY: build dev docker docs install npm
build: docker install
${DOCKERRUN} \
run build
dev: docker install
${DOCKERRUN} \
run dev
docker:
docker build \
. \
-t ${CONTAINER}:${TAG} \
--build-arg TAG=${TAG} \
--no-cache
docs: docker
${DOCKERRUN} \
run docs
rm -rf ${DOCSDEST}
mv ./docs/docs/.vuepress/dist ${DOCSDEST}
install: docker
${DOCKERRUN} \
install
update: docker
rm -f buildchain/package-lock.json
${DOCKERRUN} \
install
update-clean: docker
rm -f buildchain/package-lock.json
rm -rf buildchain/node_modules/
${DOCKERRUN} \
install
npm: docker
${DOCKERRUN} \
$(filter-out $@,$(MAKECMDGOALS))
%:
@:
# ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
The key commands are:
- make dev — brings up the local development environment; in this case that means spinning up the Docker container for the buildchain
- make build — executes a frontend buildchain build to create the production resources, by running npm run build in the buildchain Docker container
- make npm xxx — runs the passed in NPM command in the buildchain Docker container
- make docs — builds the documentation for the plugin via npm run docs in the buildchain Docker container
As you can see, some of the core commands remain the same amongst the various projects, despite the underlying machinery being quite different.
Link One Makefile to Rule Them All
Sometimes it’s convenient to be able to run a whole lot of builds with one command.
For example, when there’s a dependabot-reported security vulnerability in the buildchain that’s used in VuePress (which is what I use for my documentation), and I want to rebuild all of my Craft CMS Plugin documentation at once.
This Makefile searches through all sub-directories below it for other Makefiles to run (ignoring node_modules/ and vendor/) :
MAKEFILES:=$(shell find . -mindepth 2 -type d \( -name node_modules -o -name vendor \) -prune -false -o -type f \( -name 'GNUmakefile' -o -name 'makefile' -o -name 'Makefile' \))
SUBDIRS:=$(foreach m,$(MAKEFILES),$(realpath $(dir $(m))))
$(MAKECMDGOALS): $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
.PHONY: $(MAKECMDGOALS) $(SUBDIRS)
So at the root level of my Craft CMS Plugins development directory, I can type just make docs and it’ll run make docs for every sub-directory that has a Makefile.
This gives me the best of both worlds in terms of keeping everything in separate repositories (maybe with separate semver requirements, or even totally different documentation build systems), but also being able to rebuild everything in one fell swoop.
Link Shell aliases in Makefiles
Shell aliases won’t work in Makefiles, because by default, most shells do not evaluate aliases when they are non-interactive shells.
If you use shell aliases often, such as I talk about in the Dock Life: Using Docker for All The Things! article, this is a bummer.
But there’s a pretty easy work-around. You can just assign a Makefile variable to the alias, sourced from your rc file, and use that:
COMPOSER=$(shell grep alias\ composer= ~/.zshrc | awk -F"'" '{print $$2}')
What this does is it looks in the shell’s rc file (~/.zshrc in this case, but it could be ~/.bashrc if you’re using the Bash shell) for the text alias composer= and extracts the result into the COMPOSER Makefile variable.
Then you can use it in your Makefile like this:
foo:
$(COMPOSER) install
Don’t worry, the global aliases you’ve defined won’t override the local aliases you’ve set up in your Makefiles.
Link Old School Cool
Whenever you’re looking for a solution, try to leverage work that’s already been done by someone else.
make has been around for a long time, but it’s also proven itself to be extremely useful in automating build processes.
Sometimes old school cool is just what you need.
Happy making!