Tuesday, February 23, 2016

JavaScript Tools with GNU Make

In the beginning

I speak what appears to me the general opinion; and where an opinion is general, it is usually correct.
— Mansfield Park

… there was no transcompiling in JavaScript world whatsoever & everyone who was programming back then for the front-side portion of the web was greatly admiring the fact of an instant gratification.

One day Sass appeared from the direction of Rails campfire site, where folks were having a good time singing Kumbaya. Many looked closely at Sass & thought that adding a new level of abstraction was never a bad thing & quickly joined the movement. Humble designers tried to held a convention but their feeble voices were swamped by the music.

Shortly after, CoffeeScript came along. Although compiling it in the browser on-the-fly was possible, rarely somebody did that for it was considered uncivil & rude. The sites were lean & jQuery was still a King.

Then a guy who was competing with TJ for a number of pushed packages to the npm registry, wrote Browserify. It became possible to write isomorphic code before everyone realized that such a word exists.

Starting a project started to mean a process of thinking of a build system.

$ mkdir ~/projects/money-printer
$ cd !$
$ touch █

Some transferred their Rake skills to their brave new SPA world, some tried to employ tiny shell scripts, the majority, though, was unsure what to do & how to behave properly.

Something had to happen because however it used to be, it used to be somehow, it never happened yet that is was no-how, thus several nice tools materialized. Although most of them work fine, occasionally I catch myself thinking that, perhaps, those tools are a little unnecessary for my needs.

Theories of galaxies

It turns out, people in different industries had similar problems for years. For instance, sometime during the end of the Middle Ages, Bell Labs engineers were bitterly complaining to each other how they kept making the classic mistake of debugging an incorrect program because they would forget to compile the change.

One day Steve Johnson (the author of yacc) came storming into the office of his colleague. The colleague name was Stuart Feldman. After they pitied each other on the miscompilation misfortune, they sketched up on the board a general idea of how to prevent this kind of errors in the future. The result of the sketching & the vigorous one-night coding is know by a program called Make.

Many years later Feldman will say “One of the reasons Make was so simple was that it was only supposed to solve my problem, and help Steve too.”

Why use Make today? Or more importantly, why use Make as a build tool for writing SPAs?

I remember the first time I was forced to read a makefile. It was circa 2003, when the FreeBSD port I was installing failed to compile properly. While trying to resolve the problem, I’ve discovered, to my surprise, that the whole FreeBSD ports system was written in a dialect of Make language. I didn’t like its syntax & the whole construction seemed overly complex, unintuitive, weird.

A modern JavaScript developer meets Make only if he tries to install some peace of software that is absent in a collection of packages for his favourite OS. Such software is usually written in old languages like C & uses autoconf system to generate a bunch of makefiles. The process looks foreign, too exotic, ancient, outdated, uninteresting, not relevant.

Despite its alien nature, Make has managed to become a happy witness to the first mass-produced personal computers, to the eradication of Smallpox, to the invention of WWW, to the collapse of USSR, to the end of apartheid in South Africa, to the introduction of €, to DHH’s “blog in 15 minutes” video, to the end of Great Recession & to SpaceX drone ship landing.

Make today is typically twice old then a typical web developer. If you learned Make, say, in 1986 (when the first edition of O’Reilly’s Managing Projects with GNU Make came out), you can still employ that knowledge to this very day, usually being the only 1 person in the whole building who can fix some random broken makefile.

“This rubbish doesn’t compile, man.”
“What does it say?”
“Something about a missing rule for a target.”
(covers his face with hands)
“Call Jane.”

The only communities that rejected Make completely were Java & Go. For the former you could use Make in theory but in practice no one except you then could parse & maintain your makefile. For the latter, Rob Pike’s idea of build conditions so rigorously constrained that no external tool is necessary, proved to be a winner. Unfortunately, this is not the case for JavaScript world.

Over the years, there were many attempts to “fix” Make via either enhancing it syntax, introducing incompatible features, or rethinking the whole idea. The majority of this this attempts if not failed, withal didn’t acquire much advocacy. Even such cosy tools as Rake have never gained popularity beyond the language domain they are written in & belong to.

JavaScript community is not unique. It follows the same waves of “it’s own way”, where the introduction of new revolutionary tools every 6 months inevitably leads to the psychological state called “tools fatigue”.

A dull speaker always talks long

Instead of a yet another reintroduction to a particulars of some Make implementation, I’ll try to show how to use GNU Make in a small but a real web application. There will be some shotrcuts & simplifications along the way; I did them not because of Make limitations but to keep this text short.

A small notice: the text implies that a reader is very comfortable with the command line. If it’s not you to a t (for example, you come from a designer’s background), please stop reading now, go read The Unix Programming Environment book, practice for a month, then return. It’s the only book you will ever need to read to become a jolly good Unix user.

Our example is a web-based RSS feeds filter. Suppose you want to subscribe to Back to Work podcast but only listen to episodes where hosts do not talk about Apple (or vice versa, Apple is your only interest). Or to find all the great shows where the guest was John Roderick? The b2w feed contains > 250 episodes; each episode contains extensive metadata. It’s not hard to programatically search through the XML & generate a smaller feed for the RSS reader of your choice.

The app consists of 2 parts: (a) a web component, where a user specifies a URI for the feed alongside a couple of filtering patterns & (b) a small server proxy, required mainly because of a same-origin policy.

The app source tree looks like this:

/home/alex/lib/software/alex/grepfeed/grepfeed
├── cli/
│   └── grepfeed*
├── client/
│   ├── index.html
│   ├── main.jsx
│   ├── moomintroll.svg
│   └── style.sass
├── lib/
│   ├── dom.js
│   ├── feed.js
│   └── u.js
├── mk/
│   ├── build.sh*
│   └── watchman.json
├── server/
│   └── index.js*
├── test/
│   ├── data/
│   │   ├── back2work.xml
│   │   ├── irishhistorypodcast.xml
│   │   └── simple.xml
│   └── test_feed.js
├── Makefile
├── package.json
└── README.md

lib directory contains a shared code, used both by a server component & a “client” side. The HTTP server doesn’t require any additional build step to function, but the web app is all about transcompiling:

  • it is written in a subset of ES2015 & we use Babel to transform the code to ES5;

  • instead of CSS we use a mix of hand-written Sass & plain CSS from NProgress npm package. The result will be in 1 style.css file.

  • the browser-facing part of the code is written in JSX thus requires both an additional compilation step to ES5 before ES2015 (i.e., ES5 → ES2015 → JSX) and React libraries at runtime.

  • to employ some Node.js libraries in the browser & to automatically manage the dependencies we use Browserify. The resulting app will be squeezed in 1 file main.js.

  • to be able to focus on the coding, instead of typing over & over again the same commands in the terminal, we use Watchman to automatically run Make for us whenever any of our files that needs recompiling change.

In real life you always have at least 2 different builds: one is for a development phase only, another for a production deployment. In the ‘devel’ version we use source maps, for the production version–the code minification.

It is not enough to have an ability to produce 2 builds, the goal is to have those 2 build at the same time.

For example, depending on the value of NODE_ENV environment variable we can decide how to compile & where to put the output files. There is a whole separate history of having a single source tree but multiple builds per “platform”, in which I won’t get into here. We are going to separate the source tree with the compiled output to a point that you may mark the source three as a read-only & don’t worry of ever accumulating random junk there over time. As an additional bonus this eliminates the need of any ‘clean’ operations that never work properly anyway.

This is how the result looks like:

/home/alex/lib/software/alex/grepfeed/_out
├── development/
│   ├── client/
│   │   ├── index.html
│   │   ├── main.browserify.js
│   │   ├── moomintroll.svg
│   │   ├── style.css
│   │   └── style.css.map
│   └── lib/
│       ├── dom.js
│       ├── feed.js
│       └── u.js
├── node_modules/ [438 entries exceeds filelimit, not opening dir]
├── production/
│   ├── client/
│   │   ├── index.html
│   │   ├── main.browserify.js
│   │   ├── moomintroll.svg
│   │   └── style.css
│   └── lib/
│       ├── dom.js
│       ├── feed.js
│       └── u.js
└── package.json

The contents of development/client directory is what our server uses to serve to the end-user. Files in development/lib can be safely ignored; they are temporal & placed there for Browserify, that uses them for producing development/client/main.browserify.js bundle. You may notice that the contents of development/client & production/client directories is different. As a matter of fact, the size is quite different too:

$ du -h --max-depth=1
3.3M ./development
444K ./production
62M ./node_modules
65M .

This is what you get after leaving out embedded source maps from main.browserify.js & an aggressive JavaScript minification.

You can read the app source code at https://github.com/gromnitsky/grepfeed. It won’t hurt if, before carrying on, you clone it & try to reproduce one of the builds by yourself.

Static assets

We have files in our app that don’t require any transformation. It’s an .svg image of a rather happy Moomintroll & index.html. If we were living in the past & were compiling non-static assets in the same directory where their sources were, our .svg & .html files would have required no attention from a build system. But we chose the different route–to move everything outside of the source tree directory.

Our first steps are:

  1. Grab a list of files in the source tree.
  2. Chose a destination for selected files.
  3. Write a rule that specifies how to copy data.
  4. Invoke the rule.

In the root of our source tree we create a file named Makefile. When you run Make, it searches the current directory for a file with that name & starts parsing it.

NODE_ENV ?= development
out := $(NODE_ENV)
src.mkf := $(lastword $(MAKEFILE_LIST))
src := $(dir $(src.mkf))

We defined 4 variables. If you have NODE_ENV variable already defined in your environment, Make grabs it value, otherwise we set it to development string. out variable gets the value of NODE_ENV. We will use $(out) everywhere in our Makefile later on where we will need to prefix the file destination.

src.mkf gets the (relative) path to the Makefile itself. Ignore for now how it manages to do that. During the definition of src variable, we invoke Make internal $(dir) function to cut of the file name portion of src.mkf value. $(dir) is very similar to dirname(1) or Node path.dirname().

Steps 1-2: grab source & destination
static.src := $(wildcard $(src)/client/*.html $(src)/client/*.svg)
static.dest := $(subst $(src), $(out), $(static.src))

Here we define another 2 variables: the source of our static assets & its destination.

$(wildcard glob1 glob2 ...) is a Make function that internally uses fnmatch(2) to get a list of files. If it doesn’t match anything it returns an empty string. Think of $(wildcard) as primitive analogue of ls(1); it’s not recursive and uses glob patterns instead of regexps.

$(subst FROM, TO, TEXT) returns a new string with all matches of FROM replaced by TO. E.g. the next line in Make language

$(subst lamb,lambda,Mary had a little lamb)

is equivalent to JavaScript

"Mary had a little lamb".replace(/lamb/g, "lambda")

The only difference that $(subst) doesn’t support any regexp.

What we did in static.dest is got /foo/bar/grepfeed/client/index.html replaced with development/client/index.html.

Step 3: a rule

Now we can write a custom pattern rule that copies source files to their destination:

$(out)/%: $(src)/%
» mkdir -p $(dir $@)
» cp -a $< $@

Make language is all about rules. You may think of rules as functions (or rather, procedures) that take 2 parameters: target & source. A body of a “function” is any number of shell commands prefixed by a TAB character (marked by » above and everywhere below in this text).

target: source1 source2 ...
» body

Terms “source” & “body” are non-standard. I use them in this section only for clarity. The official GNU Make terms are “prerequisites” (also “dependency”) & “recipe”, e.g.

target: prerequisite1 prerequisite2 ...
» recipe

% character in the target & in the source is a wildcard. Cryptic $@ & $< inside of the body (recipe) of the rule are automatic variables. When (& only when) Make invokes the rule, it substitutes $@ with a target name & $< with a source name. There could be several sources (prerequisites, dependencies); $< means the first one, thus Make have another autovar $^ that means “the whole list”.

The exact meaning of $@, $<, $^ often escapes from newcomers. Here is the picture to help you to remember which autovar corresponds to what

The problem with our rule that it’s too broad. % in $(src)/% can match anything, not only client/index.html but client/main.jsx too. It’s possible to limit severely the applicability of a pattern rule by prefixing it with an explicit list of targets:

$(static.dest): $(out)/%: $(src)/%
» mkdir -p $(dir $@)
» cp -a $< $@
Step 4: invoking the rule

There is no way to explicitly invoke a pattern rule. But if we ask Make to create a target that match some rule, Make checks for the match & internally transforms the pattern rule to several simple file-based rules.

If we run in some temporal directory

$ make -f ../grepfeed/Makefile development/client/index.html

(-f CLO tells Make what file to read instead of Makefile in the current directory.)

Make automatically creates this rule on-the-fly:

development/client/index.html: /foo/bar/grepfeed/client/index.html
»  mkdir -p development/client
»  cp -a /foo/bar/grepfeed/client/index.html development/client/index.html

Or in an action:

$ make -f ../grepfeed/Makefile development/client/index.html development/client/moomintroll.svg
mkdir -p development/client/
cp -a ../grepfeed//client/index.html development/client/index.html
mkdir -p development/client/
cp -a ../grepfeed//client/moomintroll.svg development/client/moomintroll.svg

but it we provide a file name that do not match any pattern, Make aborts:

$ make -f ../grepfeed/Makefile foo/index.html bar/moomintroll.svg
make: *** No rule to make target 'foo/index.html'.  Stop.

It’s a little inconvenient to pass a list of file names to Make directly, for the list can be huge. We can write another rule that has a target with an arbitrary name but in sources has an actual list of the desired targets.

compile: development/client/index.html development/client/moomintroll.svg

or even better:

compile: $(static.dest)

This rule doesn’t have to have any recipe (body). Then we can execute Make as

$ make -f ../grepfeed/Makefile compile
Aftermath

One of the most lucrative Make features is that if you write your rules with caution, prudence & tact, it won’t rebuild targets that are up-to-date.

$ rm -rf development
$ make -f ../grepfeed/Makefile compile
mkdir -p development/client/
cp -a ../grepfeed//client/index.html development/client/index.html
mkdir -p development/client/
cp -a ../grepfeed//client/moomintroll.svg development/client/moomintroll.svg

$ make -f ../grepfeed/Makefile compile
make: Nothing to be done for 'compile'.

$ touch ../grepfeed/client/moomintroll.svg
$ make -f ../grepfeed/Makefile compile
mkdir -p development/client/
cp -a ../grepfeed//client/moomintroll.svg development/client/moomintroll.svg

How Make decides which target is up-to-date? By the simplest possible way: via checking last modification time for a target & its source. Over the years there were multiple attempts to enhance this algo by looking, for example, into a message digest of a file, but nobody had bothered to actually implement them efficiently enough to be included in GNU Make.

The next Make appeal comes from realization that by writing makefiles you’re constructing an acyclic graph of targets & their dependencies. So far we wrote 1 node with 2 leaves, each of which is generated via a pattern rule.

The arrow means “depends on.”

Debug

Debug facilities is where the original GNU Make distribution falls short. Partially it comes from the dynamic Make nature, for it is impossible to fully answer what would happen without doing it.

There is no REPL of any kind. Some primitive hacks exists, for example, https://github.com/gromnitsky/ims, that could help mainly to experience internal Make functions like $(filter) or $(patsubst) without manually creating a makefile & running it.

ims> .pwd
[...]/grepfeed/_out
ims> src = ../grepfeed/
ims> out = development
ims> static.src = $(wildcard $(src)/client/*.html $(src)/client/*.svg)
ims> . $(static.src)
../grepfeed//client/index.html ../grepfeed//client/moomintroll.svg
ims> . $(subst $(src), $(out), $(static.src))
  development/client/index.html  development/client/moomintroll.svg
ims>

There is also a forked version of GNU Make called remake, that can show an additional information about targets, plus it contains a real debugger.

Bare-bone

If you don’t want to install any additional tools, prepare to grieve.

The most annoying GNU Make misconduct is inability to print the variable value without modifying makefiles. A clever trick, popularized by John Graham-Cumming, comprises of adding a special patter rule to a makefile. A modified version of the rule splits a variable value into separate lines for I find the trick most useful for displaying a list of files:

pp-%:
» @echo "$(strip $($*))" | tr ' ' \\n

Then, we can print the value of static.dest

$ make -f ../grepfeed/Makefile pp-static.dest
development/client/index.html
development/client/moomintroll.svg

When you want to print what targets will be remade, try -n & -t options together:

$ make -f ../grepfeed/Makefile -tn compile
touch development/client/index.html
touch development/client/moomintroll.svg

npm

Before transforming any of sass/js/jsx files we need to make sure we have all the installed tools. In package.json, among other things, we have:

  • node-sass
  • babel-cli
  • babel-preset-es2015
  • babel-preset-react
  • browserify
  • uglify-js

We can switch the responsibility to the user by asking in a readme to “install those in global mode” or we can be more polite & write a simple rule that checks if our packages are installed after each change in package.json.

export NODE_PATH = $(realpath node_modules)

node_modules: package.json
»   npm install --loglevel=error --depth=0 $(NPM_OPT)
»   touch $@

package.json: $(src)/package.json
»   cp -a $< $@

We have here 2 new simple file-based rules.

We need to copy package.json from the source code directory because if our output directory isn’t a descendant of the $(src), npm fails to find package.json at all, or picks up a wrong one.

Explicitly setting NODE_PATH is required for Babel because when the source code is in a different subtree, Babel searches for packages in node_modules directory downwards starting from a particular .js file.

Notice that we are telling Make to export NODE_PATH to child processes, such as for a CLI wrapper of Babel & that it’s not a regular variable, but a macro.

Regular Make variables, distinguished in their definition by := (as in foo := bar) have nothing interesting about them; they work exactly as you expect, by setting the value of a left-hand side immediately. Macros on the other hand, create only a stub, that is not evaluated until macro is accessed.

When we run Make for the first time, the directory node_modules may not exist yet, thus had we defined NODE_PATH as a regular variable, its value would have been an empty string & Babel would have failed to find any Node.js modules. But when it’s a macro, Make evaluates it when somebody (a child process) tries to read it. At that point node_modules definitely exists, & Make expands it to a full path with the help of its internal $(realpath) function.

To test all this, run Make in the $(out) directory:

$ make -f ../grepfeed/Makefile node_modules
cp -a ../grepfeed//package.json package.json
npm install --loglevel=error --depth=0 --cache-min 99999999

> node-sass@3.4.2 install /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out/node_modules/node-sass
> node scripts/install.js

Binary downloaded and installed at /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out/node_modules/node-sass/vendor/linux-ia32-47/binding.node

> spawn-sync@1.0.15 postinstall /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out/node_modules/spawn-sync
> node postinstall


> node-sass@3.4.2 postinstall /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out/node_modules/node-sass
> node scripts/build.js

` /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out/node_modules/node-sass/vendor/linux-ia32-47/binding.node ` exists. 
 testing binary.
Binary is fine; exiting.
grepfeed@0.0.1 /home/alex/lib/writing/articles/javascript-tools-with-gnu-make/_out.blogger/s06/grepfeed/_out
├── babel-cli@6.5.1 
├── babel-polyfill@6.5.0 
├── babel-preset-es2015@6.5.0 
├── babel-preset-react@6.5.0 
├── browserify@13.0.0 
├── node-sass@3.4.2 
└── uglify-js@2.6.2 

touch node_modules

and again, to check if we indeed wrote 2 rules properly:

$ make -f ../grepfeed/Makefile node_modules
make: 'node_modules' is up to date.

Npm gets slower & slower with every release so this could take a while. There is NPM_OPT in the node_modules recipe; use it to pass any additional options to npm, for example:

$ make -f ../grepfeed/Makefile node_modules NPM_OPT="--cache-min 99999999"

Sass

To compile .sass files to .css we employ the same algo we have used for static assets.

node-sass := node_modules/.bin/node-sass
SASS_OPT := -q --output-style compressed
ifeq ($(NODE_ENV), development)
SASS_OPT := -q --source-map true
endif
sass.src := $(wildcard $(src)/client/*.sass)
sass.dest := $(patsubst $(src)/%.sass, $(out)/%.css, $(sass.src))

$(out)/client/%.css: $(src)/client/%.sass
»   @mkdir -p $(dir $@)
»   $(node-sass) $(SASS_OPT) --include-path node_modules -o $(dir $@) $<

$(sass.dest): node_modules

compile: $(sass.dest)

Here we use NODE_ENV for the first time to modify the compiler behaviour: to include source maps when we are in a developer mode & to turn on the minification for a production mode.

Output .css files depend on node_module target, which depends on package.json which means if we modify the latter, Make considers our .css files outdated & remakes them.

You may not like a useless rebuilding every time you fix a typo in package.json, but it ensures that after updating a version string of a css package (like Nprogress), you won’t end up with an old code in $(out).

$ grep import ../grepfeed/client/style.sass
@import "nprogress/nprogress"

The usage of NODE_ENV for turning on/off minification through modifying node-sass command line options seems easy & convenient, but it’s inherently un-Unix: the minification step should be done by a separate command.

A more civilized way to do conversions would be to use chains of implicit rules. In a production mode

.css → .uncompressed_css → .sass

& in a development mode

.css → .sass

I’ll live this to you as a homework. For tips of how to achieve that, read the section about .js files transformation below.

ES2015 & Browserify

As our app is written in a subset of ES2015 we need another pattern rule to convert .js files to a ES5. lib directory is the location of the ES2015 code.

babel := node_modules/.bin/babel
ifeq ($(NODE_ENV), development)
BABEL_OPT := -s inline
endif
js.src := $(wildcard $(src)/lib/*.js)
js.dest := $(patsubst $(src)/%.js, $(out)/%.js, $(js.src))

$(js.dest): node_modules

$(out)/%.js: $(src)/%.js
»   @mkdir -p $(dir $@)
»   $(babel) --presets es2015 $(BABEL_OPT) $< -o $@

There is no sign of minification because the destination files in $(out)/lib are for temporal purposes.

The same goes for .jsx files (of which we have only 1) in client directory.

jsx.src := $(wildcard $(src)/client/*.jsx)
jsx.dest := $(patsubst $(src)/%.jsx, $(out)/%.js, $(jsx.src))

$(jsx.dest): node_modules
# we use .jsx files only as input for browserify
.INTERMEDIATE: $(jsx.dest)

$(out)/client/%.js: $(src)/client/%.jsx
»   @mkdir -p $(dir $@)
»   $(babel) --presets es2015,react $(BABEL_OPT) $< -o $@

One thing is different here: the temporal output is placed in $(out)/client, that is the directory that our server uses for its root for static files. After all compilation steps are finished there should be no temporal files. Make doesn’t know that the result of .jsx transcompiling is a temporal link in the chain thus we mark such targets as intermediates, by adding them as dependencies to a special .INTERMEDIATE target. You’ll see shortly what happens to such targets.

This rule contains another shortcut: the transformation from JSX to ES5 is done in 1 step. Ideologically this is Not Right because after JSX conversion we should get plain ES6 which we could then convert to ES5 code.

To get a final bundle with the name $(out)/client/main.browserify.js we write a simple file-based rule:

browserify := node_modules/.bin/browserify
browserify.dest.sfx := .es5
ifeq ($(NODE_ENV), development)
browserify.dest.sfx := .js
BROWSERIFY_OPT := -d
endif

bundle1 := $(out)/client/main.browserify$(browserify.dest.sfx)
$(bundle1): $(out)/client/main.js $(js.dest)
»   @mkdir -p $(dir $@)
»   $(browserify) $(BROWSERIFY_OPT) $< -o $@

js:

Notice how we manually add to bundle dependencies a list of files in $(out)/lib & that our modest js target is empty for now.

This rules contains a catch: if it’s a development mode, the chain is simple

main.browserify.js → .js deps

where everything is compiled with source maps. For a production mode, there is an additional link:

main.browserify.js → main.browserify.es5 → .js deps

where main.browserify.js, despite its name, is created not by browserify but by a separate uglifyjs program.

# will be empty in development mode
es5.dest := $(patsubst %.es5, %.js, $(bundle1))

UGLIFYJS_OPT := --screw-ie8 -m -c
%.js: %.es5
»   node_modules/.bin/uglifyjs $(UGLIFYJS_OPT) -o $@ -- $<

ifneq ($(browserify.dest.sfx), .js)
js: $(es5.dest)
# we don't need .es5 files around
.INTERMEDIATE: $(bundle1)
else
js: $(bundle1)
endif

compile: js

js target gets its prerequisites depending on NODE_ENV value.

The whole dependency graph of JavaScript files look like this (ellipse-shaped nodes are intermediates; dashed are production-mode only):

Now we can test the production mode:

$ NODE_ENV=production make -f ../grepfeed/Makefile js
node_modules/.bin/babel --presets es2015  ../grepfeed//lib/feed.js -o production/lib/feed.js
node_modules/.bin/babel --presets es2015  ../grepfeed//lib/dom.js -o production/lib/dom.js
node_modules/.bin/babel --presets es2015  ../grepfeed//lib/u.js -o production/lib/u.js
node_modules/.bin/babel --presets es2015,react  ../grepfeed//client/main.jsx -o production/client/main.js
node_modules/.bin/browserify  production/client/main.js -o production/client/main.browserify.es5
node_modules/.bin/uglifyjs --screw-ie8 -m -c -o production/client/main.browserify.js -- production/client/main.browserify.es5
rm production/client/main.browserify.es5 production/client/main.js

$ NODE_ENV=production make -f ../grepfeed/Makefile js
make: Nothing to be done for 'js'.
$ touch ../grepfeed/lib/u.js
$ NODE_ENV=production make -f ../grepfeed/Makefile js
node_modules/.bin/babel --presets es2015  ../grepfeed//lib/u.js -o production/lib/u.js
node_modules/.bin/babel --presets es2015,react  ../grepfeed//client/main.jsx -o production/client/main.js
node_modules/.bin/browserify  production/client/main.js -o production/client/main.browserify.es5
node_modules/.bin/uglifyjs --screw-ie8 -m -c -o production/client/main.browserify.js -- production/client/main.browserify.es5
rm production/client/main.browserify.es5 production/client/main.js

Albeit we didn’t explicitly put in any of the recipes a rm command, Make automatically removes targets that were prerequisites in .INTERMEDIATE target.

A watched pot never boils

2 of 4 stand-alone utils we use in our makefile have a built-in “watch” feature & browserify has a separate (& quite popular) watchify wrapper. It’s hard to imagine a project that needs to recompile only those files that watchify watches or the project that only uses node-sass. It’s also hard to imagine a reason why would you include the “watch” feature into your CLI program in the first place, knowing very well that your tool will never be the only one third-party tool in a project.

The only valid reason I can think of is a conscientious endeavour to set a world benchmark for software bloat, but we won’t get into that.

Instead of running 3 separate processes in parallel we will use watchman. It will look out for files we specify & run Make on every change automatically. If we have wrote Makefile properly, only files that are outdated (in $(out) directory) will be recompiled.

To make this more developer-friendly we can play a confirmation sound when Make finishes without errors; run a separate terminal window for watchman output & raise the window in case of a compilation error.

watchman configuration is not exactly the easiest one for it has 2 modes for reading it: from stdin in a json format or via command line options. The latter is more limited & the former is too verbose. We’ll use the json only to escape from the shell quoting issues.

We add another target to the makefile:

watch:
»   watchman trigger-del $(src) assets
»   @mkdir -p $(out)
»   m4 -D_SRC="$(src)" -D_TTY=`tty` \
»   »   -D_OUT_PARENT=`pwd` \
»   »   -D_MAKE="$(MAKE)" -D_MK="$(src.mkf)" \
»   »   $(src)/mk/watchman.json | watchman -n -j

m4 macro processor is absolutely not required, you can replace it with any other you like; we use it only to transform $(src)/mk/watchman.json file:

[
    "trigger",
    "_SRC",
    {
        "expression": [
            "anyof",
            ["pcre", "^(client|lib)/[^#]+$", "wholename"],
            ["pcre", "^package.json$", "wholename"]
        ],
        "name": "assets",
        "command": [
            "_SRC/mk/build.sh",
            "_MAKE", "-C", "_OUT_PARENT", "-f", "_MK", "compile"
        ],
        "append_files": false,
        "stderr": ">_TTY",
        "stdout": ">_TTY"
    }
]

I won’t describe what all this means, please refer to watchman manual for the particulars. What we need to note is that on each file change, watchman will run $(src)/mk/build.sh script that in turn will run Make as

make -C $(src) -f $(src)/Makefile compile

where -C instruct Make to chdir before parsing a makefile provided in the -f CLO.

$(src)/mk/build.sh is closely tied to my Fedora installation, so you will need to modify it for your machine:

#!/bin/sh

# See `watch` target in Makefile.

# clear xterm history
printf "\033c"
# what is to be done
printf "\033[0;33m%s\033[0;m\n" "$*"

# run make
"$@"

ec=$?
media=/usr/share/sounds/freedesktop/stereo

if [ $ec -eq 0 ]; then
    play $media/message.oga 2> /dev/null
else
    play $media/bell.oga 2> /dev/null
    # raise xterm window
    printf "\033[05t"
fi

exit $ec

If everything works fine, you open a new xterm window, run make -f ../grepfeed/Makefile watch there & forget about it. On any compilation error, that xterm window pops up, alerting us that build has failed.

Conclusion

As we see, with a little help of shell scripting & a little knowledge of Make language it is possible to construct a build system for a SPA that uses all the latest JavaScript tools under the hood. There is 0 magic in it & no dependencies on any “plugins”. For why would you need a “plugin” to use a program that is already capable of transforming input?

Much more could be said about the Make language itself. We wrote our build system in 1 big makefile only to stay simple. You don’t have to be such a simpleton in your projects. There were no talks about what is a list in terms of Make, nothing about scoping rules, user-defined functions, canned recipes, etc.

I didn’t cover giant topics like auto-discovering dependencies for .js files (we have cheated by explicitly stating the dependencies for the output browserify bundle) or parallel jobs.

If you’re interested in GNU Make & want to know more, start with its official manual that covers most of the Make language details. After that, read Robert Mecklenburg’s Managing Projects with GNU Make book that will feed you with many ideas that you might otherwise be missing out. If that will be not enough, read The GNU Make Book by John Graham-Cumming. There is nothing to read about Make beyond that book for it contains maximum hardcore staff you will ever extract about the topic.

I want only to remind you that it doesn’t matter what toolchain you chose for a project (based on Make or not). If you fail to deliver a working app, no build system in the world will save you. Nobody cares about your polished infrastructure, for it’s the app that is important to the end user.

Enjoy!

PS. Here is an alternate version of this post that can be more readable on your phone.

1 comment:

  1. Been using Make to build my frontend stuff for a while now. Thanks for compiling all this.
    You may find entr(1) a simpler alternative to Watchman: http://entrproject.org

    ReplyDelete