Makefile recipes I
Tags: tooling, javascript
Published: 2020-09-03 21:20:01

I've been playing around Makefiles for a while, and I'm still pleased with the outcome you can achieve as task runner.

> These [Makefile](https://github.com/tacoss/plate/blob/master/Makefile) recipes are being used on my own template for frontend development: [plate](https://github.com/tacoss/plate).

## Current working directory

It's important to know where we are, final paths are relative from here.

```Makefile
PWD=$(shell pwd)
```

## Defaults

Configuration below is for deployment to GitHub Pages:

```Makefile
src=build
from=master
target=gh-pages
message=Release: $(shell date)
```

## Templates

Path substitution is used to rebase some paths:

- `page:example.md` becomes `src/pages/example.md`
- `css:home.less` becomes `src/resources/styles/home.less`
- `js:app.js` becomes `src/resources/scripts/app.js`
- `app:components:App.svelte` becomes `src/app/components/App.svelte`
- `lib:helpers:api.js` becomes `src/lib/helpers/api.js`
- `res:styles:reset.css` becomes `src/resources/styles/reset.css`

```Makefile
_src=src/$(patsubst js%,resources/scripts%,\
  $(patsubst css%,resources/styles%,\
  $(patsubst res%,resources%,\
  $(patsubst page%,pages%,$(NAME)))))
_path=$(patsubst %/,%,$(_src))
_basedir=$(dir $(_path))
```

## Directories

Those paths are used when creating needed directories and file.

```Makefile
dirname=$(patsubst %/,%,$(_basedir))
filepath=$(patsubst $(_basedir),,$(_path))
```

## Environment vars

Standard variables for the underlying processes.

> I'm getting `GIT_REVISION` to render a `<meta name="revision" content="ef825dc">` tag in generated pages.

```Makefile
GIT_REVISION=$(shell git rev-parse --short=7 HEAD)
NODE_ENV=development

export NODE_ENV GIT_REVISION
```

## Targets

> A phony target is one that is not really the name of a file; rather it is just a name for a recipe to be executed when you make an explicit request. There are two reasons to use a phony target: to avoid a conflict with a file of the same name, and to improve performance.

```Makefile
.PHONY: ? add rm dev test deps clean prune dist pages deploy
```

## Utils

The `iif(...)` helper is used to run a command conditionally, its output depends of the returned exit-code.

```Makefile
define iif
  @(($1 > /dev/null 2>&1) && printf "\r* $2\n") || printf "\r* $3\n"
endef
```

## Input

`BODY` is used for adding files, when empty it'll fallback to `STDIN` instead.

```Makefile
ifeq ($(BODY),)
BODY := $(shell bash -c 'if test ! -t 0; then cat -; fi')
endif
```

## Validation

The `check_defined(...)` helper is used to validate `INPUT` when required.

```Makefile
check_defined = $(strip $(foreach 1,$1, $(call __check_defined,$1,$(strip $(value 2)))))
__check_defined = $(if $(value $1),, $(error $2, e.g. $1=test))
```

## Display all targets in this file

If you run `make` without arguments this target will be invoked &mdash; shows a list of available tasks and usage examples.

```Makefile
?: Makefile
  @awk -F':.*?##' '/^[a-z\\%!:-]+:.*##/{gsub("%","*",$$1);gsub("\\\\",":*",$$1);printf "\033[36m%8s\033[0m %s\n",$$1,$$2}' $<
  @printf "\n  Examples:"
  @printf "\n    make add:page NAME=example.md BODY='# It works!'"
  @printf "\n    make rm:Dockerfile"
  @printf "\n    make clean dev"
  @printf "\n\n"
```

## Adding files to the project

Write new files with `make add` &mdash; where templates `add NAME=js/app.js` and `add:js:app.js` are equivalent.

```Makefile
add: ## Create files, scripts or resources
  @$(call check_defined, NAME, Missing file name)
  @$(call check_defined, BODY, Missing file content)
  @mkdir -p $(PWD)/$(dirname)
  @echo $(BODY) > $(PWD)/$(filepath)
  @printf "\r* File $(filepath) was created\n"

add\:%: ## Shortcut for adding files
  @make -s add NAME=$(subst :,/,$*)/$(NAME) BODY=$(BODY)
```

## Remove files from the project

Delete existing files with `make rm` &mdash; where templates `rm NAME=css/home.less` and `rm:css:home.less` are equivalent.

```Makefile
rm: ## Remove **any** stuff from your workspace
  @$(call check_defined, NAME, Missing file name)
  @$(call iif,rm -r $(PWD)/$(filepath),File $(filepath) was deleted,Failed to delete $(filepath))
  @$(call iif,rmdir $(PWD)/$(dirname),Parent directory clear,Parent directory is not empty...)

rm\:%: ## Shortcut for removing files
  @make -s rm NAME=$(subst :,/,$*)/$(NAME)
```

## Development tasks

Start your development workflow with the `make dev` task.

```Makefile
dev: deps ## Start development
  @npm run dev
```

## Testing tasks

Fire your testing workflow with the `make test` task.

```Makefile
test: deps ## Test for syntax issues
  @npm run check
```

## Build task

Run your build workflow with the `make dist` task.

```Makefile
dist: deps ## Compile sources for production
  @NODE_ENV=production npm run dist -- -f
```

## Check dependencies

Helpful task to validate if `node_modules` are already present and ready.

```Makefile
deps: ## Check for installed dependencies
  @(((ls node_modules | grep .) > /dev/null 2>&1) || npm i) || true
```

## Cleanup

Remove cache files from tooling, also deletes the `build` directory.

> The `.tarima` file is a cached JSON from my tooling.

```Makefile
clean: ## Remove cache and generated artifacts
  @$(call iif,rm -r $(src),Built artifacts were deleted,Artifacts already deleted)
  @$(call iif,unlink .tarima,Cache file was deleted,Cache file already deleted)
```

## Clean dependencies

Ensure `node_modules` are completely removed from your project.

```Makefile
prune: clean ## Remove all stuff from node_modules/*
  @printf "\r* Removing all dependencies... "
  @rm -rf node_modules/.{bin,cache}
  @rm -rf node_modules/*
  @echo "OK"
```

## GitHub Pages branch

In order to push to `gh-pages` the branch should exists already.

```Makefile
pages: ## Fetch or create the target branch
  @(git fetch origin $(target) 2> /dev/null || (\
    git checkout --orphan $(target);\
    git rm -rf . > /dev/null;\
    git commit --allow-empty -m "initial commit";\
    git checkout $(from)))
```

## Deployment to GitHub Pages

Build your files and push `gh-pages` changes to master.

```Makefile
deploy: pages ## Prepare and push changes on target branch
  @(mv $(src) .backup > /dev/null 2>&1) || true
  @(git worktree remove $(src) --force > /dev/null 2>&1) || true
  @(git worktree add $(src) $(target) && (cp -r .backup/* $(src) > /dev/null 2>&1)) || true
  @cd $(src) && git add . && git commit -m "$(message)" || true
  @(mv .backup $(src) > /dev/null 2>&1) || true
  @git push origin $(target) -f || true
```