There is an even simpler way, just use a bash file with each function being a task, saves you from the .PHONEY hack and the "bash but not really bash" quirks of makefile.
Only quirk I can think of is that each line in a recipe is a separate shell and it makes inline comments wonky
The beauty of Make is the automatic parallelization and incremental building
Make was made for system languages with slow compilation time where avoiding unneccesarily rebuilding and paralellization become crucial features. If you don't utilize or have a need for that then make does not bring anything to the table. If you DO have such need because your project is big now, you prolly also need monorepo workspace management etc, at which point you just use a modern tool like bazel.
It's make interpolation if it's using Make syntax and shell if it's not. It's not the same syntax
#!/usr/bin/env bash
[ "${DEBUG:-0}" = "1" ] && set -x
if [ "${FORCE:-0}" = "1" ]; then set +eu ; else set -eu ; fi
export PATH="$(cd -P "$(dirname "${BASH_SOURCE[0]}")")/node_modules/.bin:$PATH"
This will do the following: 1. Use whatever Bash executable is in your path, which is necessary for portability (fixes many bugs)
2. If env var DEBUG is "1", turn on bash tracing
3. If env var FORCE is not "1", die on non-zero return status or unset variables
4. Prepend to the PATH the "node_modules/.bin" path, but find that directory from where this script lives, not the current working directory of wherever you executed this script from