Summary: Using make can greatly simplify the maintainance of your project. If any command requires more than a few words to type, consider adding it to a Makefile.
It’s common to use package.json scripts to run commands in node-based projects, for example, npm run dev.
Recently, I came across several articles discussing the use of make and decided to give it a try.
As a result, I’ve started using make in all of my frontend projects, and I really like it. Here’s a simplified example of a Makefile for one of my work projects:
devpod: # start a devcontainer and ssh into it
devpod up .
devpod ssh lmst
dev: # run in a dev mode
npx vite
type:
npx tsc --noEmit
size:
npx vite build
npx size-limit
test:
npx tap
I think it’s better than scripts in a package.json file, because of:
- Multi-line commands.
- Comments: each line can be commented. You can describe why you run one command before another. In a
package.jsonfile, all your commands are placed on a single line - Works great with non-Node.js commands. In one of our projects, we use Selenium for integration tests, and I no longer struggling to remember how to install all dependencies and run tests
For development I use devcontainers + devpod setup, and connecting to a devcontainer is pretty much to type:
devpod up .
devpod ssh projname
Now I’m just typing make devpod, and after a few seconds I’m inside a devcontainer.
In a team, this approach will work only if each of team’s members uses a platform with a make available. On linux and Mac, it’s already included. On a Windows system you need to install make by yourself.
A small tutorial on how to start using “make”
It’s pretty easy to get started with make -just create a Makefile in the project’s root directory.
Makefile consists of targets. For example, in the make build command, build is a target.
Let’s add a type target, so we’ll be able to use make type to run typescript checks in our project:
type:
npx tsc --noEmit
Nice! To build our project, we’ll use build target. Typically, we want to check types and then build the project. For this, we can use dependencies:
type:
npx tsc --noEmit
build: type
npx vite build
Now, if we run make build, actually these commands will be executed:
npx tsc --noEmit
npx vite build
Variables
Some commands differ by just one flag, such as running typescript type checks mentioned above either single time or in watch mode:
npx tsc --noEmit
npx tsc --noEmit --watch
We can duplicate these commands:
type:
npx tsc --noEmit
type-watch:
npx tsc --noEmit --watch
Or we can use variables:
type:
npx tsc --noEmit $(WATCH)
type-watch: WATCH = --watch
type-watch: type
Here, the type-watch target just sets a WATCH variable, and re-uses type target.
A “docs” Pitfall
If you run a target and there’s a file or directory with the same name exists, make will think that this target is up to date and won’t do anything!
A classic example here is a docs target - most probably you would like to run make docs to generate the documentation, but if your documentation is placed in the docs directory, you’ll see this message:
make: 'docs' target is up to date.
To always run the specified target in such scenarios, we need to use a special .PHONY target:
.PHONY: docs
docs:
npx vitepress