A clean Makefile without the tab traps
A good Makefile is the single entry point to a project: make test, make build, make deploy. But Make has sharp edges — recipes must start with real tabs, and command targets need .PHONY declarations. This builder generates a correct, self-documenting Makefile so newcomers can run make help and see everything available.
How it works
The output declares variables at the top so paths and binaries are set once. Every command target you enable is added to a .PHONY line and given a recipe that starts with a literal tab, as Make requires. Each target carries a ## description comment, and the help target uses a short grep/awk one-liner to print those descriptions, keeping documentation in sync with the targets. The first declared target is help, so running make with no arguments lists the commands instead of accidentally building.
The anatomy of a generated Makefile
A Makefile from this builder looks roughly like:
BIN := ./node_modules/.bin
.DEFAULT_GOAL := help
.PHONY: help build test lint clean
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
build: ## Compile the project
$(BIN)/tsc --project tsconfig.build.json
test: ## Run the test suite
$(BIN)/jest
lint: ## Lint and type-check
$(BIN)/eslint src && $(BIN)/tsc --noEmit
clean: ## Remove build artifacts
rm -rf dist
Every line inside a recipe begins with a real tab character, not spaces — this is a hard requirement of Make’s syntax. The builder emits correct tabs, so as long as you copy the output verbatim (and your editor does not convert tabs to spaces on paste), the Makefile will work.
Common pitfalls and how this builder avoids them
Tab vs space confusion
This is the most notorious Make trap. Make was written in 1976 and requires a literal ASCII tab (0x09) at the start of every recipe line. If your editor converts tabs to spaces or you retype a recipe, make throws Makefile:N: *** missing separator. Stop.. The builder emits correct tabs; copy the output as-is.
Missing .PHONY declarations
A target without .PHONY is treated as a filename. If a file named test or clean exists in the project root, make test silently does nothing because the “target” is already “up to date”. Declaring every command target as .PHONY prevents this, and the builder does it for you.
Default goal surprises
The first target in a Makefile is the default. If that target is build and you run a bare make expecting to see usage, you get a build instead. Setting help as the first target or setting .DEFAULT_GOAL := help gives every user a safe, informative default.
Tips and practical advice
- Choose the Node preset to get
npm run build,npm test, andnpm run lintwired to the matching targets automatically. - Use variables at the top for paths:
SRC := src,DIST := dist. Reference them in recipes with$(SRC)so refactoring means editing one line. - Add a
watchtarget that invokes your build in watch mode for local development — pair it with## Start dev watcherso it appears inmake help. - For monorepos, a root Makefile can delegate to sub-packages:
make -C packages/api test.
Run make help first to verify the output is correct, then run make build.