Andrew Welch · Insights · #docker #webpack #node.js

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Using Make & Makefiles to Automate your Frontend Workflow

Make is a build automa­tion tool that’s been used for decades to build soft­ware. Learn how you can lever­age make to auto­mate fron­tend web development

make is a Unix tool that’s been around since the mid 1970’s, and is still wide­ly used today for automat­ing soft­ware build processes.

make is the OG of build tools

The make tool is avail­able for just about every plat­form you can imag­ine, and is installed with the XCode CLI Tools on the Mac, and WSL2 on Win­dows. Prob­a­bly you have these installed already if you’re doing development.

This arti­cle describes how you can lever­age make to auto­mate your fron­tend web devel­op­ment via a stan­dard­ized CLI API of your own design.

Link Why use make?

With all of the build tools and auto­mat­ed sys­tems out there, why should we use make? A few reasons:

  • It lets you define a sim­ple, stan­dard­ized CLI API your team can use across projects
  • The verb-noun seman­tics of make target are easy to digest & remember
  • You effec­tive­ly get local com­mand alias­es for each project that are con­text and project aware
  • The tool­ing under the hood is abstract­ed away, and can be swapped out at any time
  • make is already installed on your devel­op­ment machines, and is designed to auto­mate builds

So peo­ple on your team (or just you, if you’re a team of one) can just type make dev to spin up a local devel­op­ment envi­ron­ment, with­out hav­ing to know what hap­pens under the hood to make that happen.

If you decide to use a dif­fer­ent local dev envi­ron­ment, you can swap it out, and make dev will still do the thing” to make it happen.

Link Real World Examples

As dis­cussed in the An Anno­tat­ed Dock­er Con­fig for Fron­tend Web Devel­op­ment arti­cle, I use Dock­er for a local devel­op­ment envi­ron­ment, and as dis­cussed in the An Anno­tat­ed web­pack 4 Con­fig for Fron­tend Web Devel­op­ment arti­cle, I use web­pack 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.

How­ev­er the real key here is that no mat­ter what machin­ery needs to be run under the hood, make dev spins up the devel­op­ment environment.

Abstracting away what does the thing from the command has many benefits

So here’s an exam­ple of some of the com­mands I have in my Makefile, and what they do:

  • make dev — does what­ev­er needs to be done to spin up the pro­jec­t’s local dev envi­ron­ment, so I can work on the project
  • make build — does what­ev­er needs to be done to build a pro­jec­t’s pro­duc­tion ready resources for deployment
  • make clean — does what­ev­er needs to be done to rebuild the project envi­ron­ment from scratch
  • make docs — for my plu­g­ins, does what­ev­er needs to be done to build the documentation

There are more com­mands of course, but these are a few exam­ples that show how easy it is to onboard some­one onto a project.

For a real-world exam­ple, try spin­ning up the dev​Mode​.fm web­site locally!

Link How make works

When you run make via your CLI ter­mi­nal, it might look some­thing like this:

Run­ning make

make looks for a plain text file named Makefile in the cur­rent direc­to­ry. This file has any num­ber of tar­gets that look like this:

Anato­my of a Make­file rule

  • Tar­get — this is nor­mal­ly a file or direc­to­ry that needs to be built.
  • Pre­req­ui­sites — oth­er files or tar­gets that need to be built before the tar­get can be built.
  • Recipe — pre­ced­ed by a tab, a series of any num­ber of shell com­mands that are exe­cut­ed to build the target

So in the above exam­ple when we type make build it will first do what­ev­er is need­ed to build the tar­get named up (which is a pre­req­ui­site), and then it’ll run the com­mands in the recipe to build the tar­get named build.

make will rebuild a tar­get when either the tar­get file or direc­to­ry does­n’t exist, or when any of its pre­req­ui­sites have been mod­i­fied and so are new­er than the target.

This Make­file Cheat­sheet may come in handy when learn­ing how Make­files work.

Anato­my of a Make­file preamble

You also can define and use vari­ables in your Make­files, the ?= con­di­tion­al assign­ment oper­a­tor used above assigns a default val­ue if the vari­able isn’t set already.

This can be use­ful in assign­ing default val­ues that can be over­rid­den via shell envi­ron­ment vari­ables.

Using make, we can get local aliases to run project-specific commands

We men­tioned ear­li­er that tar­gets are nor­mal­ly files or direc­to­ries, but we can use the spe­cial built-in tar­get named .PHONY to spec­i­fy that the tar­get is just a list of com­mands that should always be run.

We lever­age this to use our Makefile to define local alias­es that run com­mands to do var­i­ous things with our project.

So let’s have a look at a few examples.

Link Craft Scaffolding Makefile

This Make­file is one I use in my Craft CMS scaf­fold­ing. The CMS or frame­work in use does­n’t real­ly mat­ter, the applied prin­ci­ples 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 com­mands are:

  • make dev — brings up the local devel­op­ment envi­ron­ment; in this case that means spin­ning up the Dock­er con­tain­ers via docker-compose up
  • make build — exe­cutes a fron­tend build­chain build to cre­ate the pro­duc­tion resources, by run­ning npm run build in the build­chain Dock­er container
  • make npm xxx — runs the passed in NPM com­mand in the build­chain Dock­er container
  • make composer xxx — runs the passed in Com­pos­er com­mand in the PHP Dock­er container
  • make craft xxx — runs the passed in Craft CLI com­mand in the PHP Dock­er container

If I ever changed local devel­op­ment envi­ron­ments or fron­tend build­chains, I would­n’t need to re-edu­cat­ed my team (or re-train myself) on the new commands.

I can just swap in the machin­ery need­ed to do the thing in the Makefile.

Tip: If you try a com­mand 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 com­mand. To side-step this, use the -- (dou­ble-dash) to dis­able fur­ther option pro­cess­ing, like this: make -- craft project-config/apply --force

Link Craft Plugin Development Environment Makefile

This is the Make­file from my Craft CMS Plu­g­in devel­op­ment envi­ron­ment.

It’s a skele­ton project I use to build and test my Craft CMS plu­g­ins, with some spe­cif­ic func­tion­al­i­ty 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 com­mands are:

  • make dev — brings up the local devel­op­ment envi­ron­ment; in this case that means spin­ning up the Dock­er con­tain­ers via docker-compose up
  • make composer xxx — runs the passed in Com­pos­er com­mand in the PHP Dock­er container
  • make craft xxx — runs the passed in Craft CLI com­mand in the PHP Dock­er container
  • make mysql — dynam­i­cal­ly switch­es the project over to using the MySQL data­base container
  • make postgres — dynam­i­cal­ly switch­es the project over to using the Post­gres data­base container

This lets me repli­cate my plu­g­in devel­op­ment envi­ron­ment on any machine eas­i­ly, and test against var­i­ous sce­nar­ios (such as mul­ti­ple data­base types) just as easily.

Link Craft Plugin Makefile

This is a Make­file for the Craft CMS Plu­g­ins that I develop.

I run a web­pack 5 build­chain inside of a Dock­er con­tain­er that han­dles the Hot Mod­ule Replace­ment (HMR), opti­mized pro­duc­tion build, Tail­wind 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 com­mands are:

  • make dev — brings up the local devel­op­ment envi­ron­ment; in this case that means spin­ning up the Dock­er con­tain­er for the buildchain
  • make build — exe­cutes a fron­tend build­chain build to cre­ate the pro­duc­tion resources, by run­ning npm run build in the build­chain Dock­er container
  • make npm xxx — runs the passed in NPM com­mand in the build­chain Dock­er container
  • make docs — builds the doc­u­men­ta­tion for the plu­g­in via npm run docs in the build­chain Dock­er container

As you can see, some of the core com­mands remain the same amongst the var­i­ous projects, despite the under­ly­ing machin­ery being quite different.

Link One Makefile to Rule Them All

Some­times it’s con­ve­nient to be able to run a whole lot of builds with one command.

For exam­ple, when there’s a depend­abot-report­ed secu­ri­ty vul­ner­a­bil­i­ty in the build­chain that’s used in Vue­Press (which is what I use for my doc­u­men­ta­tion), and I want to rebuild all of my Craft CMS Plu­g­in doc­u­men­ta­tion at once.

This Makefile search­es through all sub-direc­to­ries below it for oth­er Makefiles to run (ignor­ing 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 lev­el of my Craft CMS Plu­g­ins devel­op­ment direc­to­ry, I can type just make docs and it’ll run make docs for every sub-direc­to­ry that has a Makefile.

This gives me the best of both worlds in terms of keep­ing every­thing in sep­a­rate repos­i­to­ries (maybe with sep­a­rate semver require­ments, or even total­ly dif­fer­ent doc­u­men­ta­tion build sys­tems), but also being able to rebuild every­thing in one fell swoop.

Link Shell aliases in Makefiles

Shell alias­es won’t work in Make­files, because by default, most shells do not eval­u­ate alias­es when they are non-inter­ac­tive shells.

If you use shell alias­es often, such as I talk about in the Dock Life: Using Dock­er for All The Things! arti­cle, this is a bummer.

But there’s a pret­ty easy work-around. You can just assign a Make­file vari­able 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 shel­l’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 Make­file variable.

Then you can use it in your Make­file like this:

foo:
	$(COMPOSER) install

Don’t wor­ry, the glob­al alias­es you’ve defined won’t over­ride the local alias­es you’ve set up in your Makefiles.

Link Old School Cool

When­ev­er you’re look­ing for a solu­tion, try to lever­age work that’s already been done by some­one else.

make has been around for a long time, but it’s also proven itself to be extreme­ly use­ful in automat­ing build processes.

Some­times old school cool is just what you need.

Hap­py making!