I’ve been programming on Unix and Linux machines for many decades now, and one universal I’ve leaned on is the default availability of make, which has been labor-saving swiss-army knife for setting up my build and development flows, or for organizing any random commands that I execute repeatedly in a directory. It’s a great memory aid, reminding me when I come back later of how to work with the files in that directory.

Here are some examples of Makefiles I created a decade ago:

  1. When I was doing Palm webOS development I wrote a Makefile that has an eclectic collection of commands that comprised my hacked-together Palm build system.
  2. When I was doing Java web-server development, I wrote a blog post explaining how I used a Makefile to integrate the cool continuous-integration system of the day (Hudson) with the cool Java web framework of the day (Play Framework).

The things I like about make is that:

  1. It has very little boilerplate.
  2. It captures the commands, just as you type them in the command line.
  3. It captures the dependencies between the commands.
  4. The dependencies are simply the files generated by one command that are used by another command.
  5. The files can be file patterns, so you can express how one type of file is generated from another in general terms.
  6. It uses the timestamps of input and output files of the commands to determine if a command is out of date and needs to be run.
  7. It has variables to factor out common strings.
  8. It is ubiquitous and installed by default everywhere.

But I have been becoming increasingly dissatisfied with make. It was fine-tuned for the development environment of its day (compiling C on UNIX), and though it is remarkably versatile it still has lots of vestiges of special build-in support for that development flow. And over the decades it has accumulated a lot of complexity in how patterns and variables work.

However, I never found any replacement system that has all the positive features of make. The nearest is Bazel which is a multi-repo public version of Google’s internal monorepo build tool (which replaced an earlier make-driven build system in Google).

So I built my own tool, Bajel.

These days, most of my side projects use the npm ecosystem, so I built Bajel there. If you have npm installed, you can execute Bajel simply by doing

npx bajel

Bajel expects that the current directory contains a build file which in its most straightforward form is a JSON file. However Bajel also supports build files in other syntaxes, including YAML or TOML syntax, which make for cleaner files.

Here is an example build.yaml file, which is probably the closest of the available syntaxes to a classic Makefile:

TEST: ava test/contract_test.js

        - dist/index.cjs
    exec: python -m SimpleHTTPServer 8888

        - test_default
        - test_contract_production
        - test_contract_development
        - test_contract_no_env

        - dist/index.cjs
    exec: ava

    exec: NODE_ENV=production $(TEST)

    exec: NODE_ENV=development $(TEST)

    exec: NODE_ENV=  $(TEST)

        - src/node/perf.js", "src/common/optimizer.js"]
    exec: node $<

        - rollup.config.js
        - src/node/index.js
        - src/common/index.js
        - src/common/random.js
        - src/common/color.js
        - src/common/optimizer.js
        - src/common/contract.js
        - src/common/random.js
    exec: rollup --config $<

        - dist/index.cjs
    exec: npm publish

    exec: rm -rf dist

However YAML as a syntax has fallen out of favor in some quarters, so you might prefer the following, which is semantically identical, but in the nice clean TOML format.

TEST="ava test/contract_test.js"

deps = ["dist/index.cjs"]
exec = "python -m SimpleHTTPServer 8888"

deps = [

deps = ["dist/index.cjs"]
exec = "ava"

exec = "NODE_ENV=production $(TEST)"

exec = "NODE_ENV=development $(TEST)"

exec = "NODE_ENV=  $(TEST)"

deps = ["src/node/perf.js", "src/common/optimizer.js"]
exec = "node $<"

deps = [
exec = "rollup --config $<"

deps = ["dist/index.cjs"]
exec = "npm publish"

exec = "rm -rf dist"

Some features to note in the above:

  • TEST="ava test/contract_test.js" is setting a variable which is referred to later in the file as $(TEST). In TOML syntax all the variables have to be defined at the top of the file before anything else.
  • [serve] is the first target, and is the one whose action is executed by default when you do npx bajel. You can execute any other target by adding it as an argument to the command line, for example npx bajel test.
  • deps = ["dist/index.cjs"] says that serve has one dependency, the target dist/index.cjs. If that file does not exist or is older than any of its dependencies, then the action for dist/index.cjs is executed before serve is executed.
  • exec = "python -m SimpleHTTPServer 8888" specifies the shell command that executes for serve. This is executed just as if you had typed python -m SimpleHTTPServer 8888 on the command line.
  • exec = "rollup --config $<" contains $< which is replaced by the first dependency, in this case rollup.config.js. Also possible are $+ which is replaced by all the dependencies (blank-separated), and $@ which is replaced by the target.

Bajel implements all the features of make that I value (including % pattern patching not shown in this example), but keeps things as simple as possible.

For advanced use, there is also a JavaScript syntax, which allows the actions to be specified as JavaScript functions, making each target be like a cell in a spreadsheet. There’s also a markdown format, to allow for literate programming of build files.

For details, see the README on Github.

If you have any feedback, feel free to add a GitHub issue, or reach out to me on Twitter.

Naming note: “Bajel” is Bazel, but with a “j” for JavaScript. It is pronounced /ba-hel/ in the Spanish fashion, as it is a Spanish word for a sailing ship such the Santa Ana, which is pictured at the top of this post.