Make and makefiles are lost in the past for many developers, its advantages lost in the stream of tools that are constantly reinventing the wheel of building software. It’s time we get off that crazy carousel.

If you ask many developers the first thing that comes to mind with Make and makefiles, you will likely get several answers: C/C++, native projects, huge, archaic, or perhaps even old. Some younger developers that have grown up in the JavaScript ecosystem may not have even heard of Make and don’t realize the advantages they could harvest by using an existing, well-proven, and stable tool. Do we really need to be learning a new task runner or build system every 18 months as JavaScript frameworks come and go?

Those who do not understand Unix are condemned to reinvent it, poorly. Usenet signature, November 1987

— Henry Spencer

What does this have to do with modern JavaScript development?

It’s not uncommon for larger JavaScript-based projects today to use a language that compiles down to JavaScript, either for powerful language features or for stronger type checking. We use Browserify to package all our JavaScript modules together into a bundle to allow front-end developers to have a more composition-based developer experience through modules (like back-end developers have in Node.js). We use LESS or SASS and compile out to CSS the browser can understand. We minimize our JavaScript to make the files smaller, resulting in faster downloads and page load times.

What do all these things have in common with each other? At their core, they are each about taking a set of input files and transforming them into a set of output files. And this is exactly what Make is so incredibly good at.

What do makefiles do? Makefiles are simply a declarative way to transform one file (or series of files) into another. That’s it, it’s that simple. It’s not specific to C, C++, or even to programming languages. You could just as easily use a makefile to transform markdown documentation into shipped HTML files, or to pack important files into a zip/tar archive, or do a myriad of other transformations.

Thanks to its declarative nature and implementation, Make is able to use makefiles to only run the bare transforms needed to reach the final destination format. If a source file hasn’t changed since the last transformation, the source file doesn’t have to be processed again. In larger projects, this is a huge win to speed and a boon to the developer experience.

Make first appeared over 40 years ago and a lot of software has been built with it since that time. It’s a battle-tested and stable piece of software that excels at exactly what it was meant to do: transforming files from a source format to a target format, with a very simple and easy-to-understand mechanism for declaring dependencies. It’s all text-based, doesn’t try to solve everything itself, and has a great integration experience through calling out to shell scripts. In other words, it is very Unix-like. This is hardly surprising given it’s birth within the Unix environment.

Some developers have had experiences with very complicated and convoluted makefiles in larger projects. But it doesn’t need to be that way. In fact, it can be quite simple to build an NPM package that is implemented in Typescript (from building the source code to packaging the NPM package):