Skip to content
Dominic Barnes edited this page Jul 1, 2017 · 1 revision

Throughout a mako build, there are a total of 13 hooks that plugin authors can use depending on their use-case. In general, most plugins will use 1-3 hooks, but you may need more depending on the exact use-case.

There are 2 types of hooks:

  1. file hooks: the most common, apply to single files in the build
  2. build hooks: apply to the entire build

It is also worth noting that when registering file hooks, you must specify all the file extensions that are relevant to your handler. You can specify as many extensions that you want, but there are no wildcards. This is intentional, as it's much better to prevent unnecessary collisions between different plugins.

Handler Functions

You can write handler functions using any of the following patterns:

// synchronous
runner.postread('json', function str2js (file, build) {
  file.contents = str2js(file.contents);
  file.type = 'js';
});

// callback (just use a 3rd argument, which is a `done` callback)
runner.preread([ 'txt' ], function stat (file, build, done) {
  fs.stat(function (err, stat) {
    if (err) return done(err);
    file.stat = stat;
    done();
  });
});

// promises (return the `Promise` from your handler)
runner.read([ 'txt' ], function buffer (file, build) {
  return fs.readFile(file.path).then(function (contents) {
    file.contents = contents;
  });
});

// generators (run using [bluebird-co](https://www.npmjs.com/package/bluebird-co))
runner.write([ 'js', 'css' ], function* (file, build) {
  yield fs.writeFile(file.output, file.contents);
});

The flexibility is great, as you can use the style you are either most comfortable with, or that already happens to be supported by whatever tool you are integrating with.

Registering

You register handlers by using methods on the Runner instance. There is a distinct method for each of the hooks.

Below is an example file hook:

runner.read([ 'txt' ], function buffer (file, build, done) {
  fs.readFile(file.path, function (err, contents) {
    if (err) return done(err);
    file.contents = contents;
    done();
  });
});

The first argument is the list of extensions that the plugin will run for. This is required, and no wildcards are supported. Whatever argument is passed will be flattened into a single array of strings, so you can pass any of the following:

// single
'txt'

// list
[ 'txt', 'log' ]

// nested list
[ [ 'js', 'json' ], [ 'css' ], [ 'jpg', 'gif', 'png' ] ]

The second argument is a function that matches 1 of the 4 supported patterns mentioned above. (see #registering)

Next is an example of a build hook:

runner.precompile(function walk (build) {
  build.tree.getFiles().forEach(function (file) {
    // walking the tree...
  });
});

Arguments

The handler for each hook accepts a consistent set of arguments. File hooks receive both file and build, whereas build hooks only receive build. (both are described below)

file

This is the current file being processed. Check out the mako-tree documentation for more information on the File API.

While there are a handful of core properties you will always find, other plugins are free to add other properties of their own. (those plugins should also document those effects)

build

This object contains information relevant to this particular build. It includes the list of entry files being processed, as well as the current tree.

Parse Hooks

These plugins are run during the parse phase, which is responsible for assembling the entire dependency tree by traversing each entry file and it's dependencies recursively.

Parse Phase Flow Chart

preparse (build)

This hook runs prior to the parse phase for the build. This is largely here for consistency, as I don't know of many use-cases off-hand. (this will be amended in the future)

preread (file, build)

This hook is run prior to attempting to read the file from any source. The simplest example is the local filesystem, where you may want to stat the file to check for existence before a later plugin attempts to read. (eg: mako-stat) This allows a missing file to fail as quickly as possible, and usually with a pretty distinct message making that indication.

There is another distinct feature of the preread hook. When running parse multiple times, this hook will always be invoked. (unlike later hooks which will only be run once) This gives it the opportunity to mark a file as "dirty" (ie: needs to be parsed again) because it has been changed.

This can be accomplished by running build.dirty(file). You can see an example of this in mako-stat, which marks a file as dirty whenever the mtime (ie: modification time) has changed.

By the end of this hook, you should know that this file exists. If this is a repeated build, you should also set the file as "dirty" if it needs to be parsed again.

read (file, build)

This hook is responsible for reading the file into memory. The simplest example is reading a text file from the local filesystem. (eg: mako-buffer)

In general, this hook ends up populating file.contents. Not every file type will need to be read, such as images linked to in CSS which may only need to be copied from the source to the destination.

By the end of this hook, any files that are being worked on in memory should have file.contents populated.

postread (file, build)

This hook is responsible for doing any sort of transformations on the file after it has been read. Most transpilers (eg: babel) will use this hook.

If a transpiler results in changing the extension of the file, such as coffee becoming js, this must be reflected by changing file.type as well. This will allow later plugins to recognize the file for processing.

By the end of this hook, the file should be ready to be parsed for dependencies.

predependencies (file, build)

This hook can be used separately from postread to transform a file prior to being parsed for dependencies. For example, you may have a plugin that only converts ES6 module syntax to CommonJS syntax. (rather than being a full-blown transpiler invoked during the postread hook)

By the end of this hook, the handlers invoked during the dependencies hook should be able to properly parse this file's dependencies. (if not already after the postread hook)

dependencies (file, build)

This hook is responsible for identifying the direct dependencies of the current file and adding them to the tree. Check out mako-js and mako-css for a practical example of this.

In general, you will end up invoking file.addDependency(path) where path is the absolute path to whatever dependency you've resolved. Notice that mako does not care about mapping relative to absolute paths, it only wants to know that 2 different absolute paths are linked.

By the end of this plugin, all a file's direct dependencies should be added to the dependency tree, as mako will inspect that list to determine where it will traverse next.

postparse (build)

This hook runs after the parse phase for the build. You can use this hook to traverse the tree and mark files with any unique metadata. (such as marking shared dependencies that can be extracted into their own bundle)

Compile Hooks

These hooks are responsible for the compile phase, which takes the full dependency tree generated by parse and writes to some destination. (the simplest case being writing to the local filesystem)

Parse Phase Flow Chart

precompile (build)

This hook runs prior to the compile phase for the build. You can use this hook to traverse the tree and mark files with any unique metadata. (such as marking shared dependencies that can be extracted into their own bundle)

This hook is also the opportunity plugins have to resolve any cycles (aka: circular dependencies) in the tree. Before starting on the postdependencies hook, the tree will need to be acyclic so topological ordering is possible. Thus, mako will naively break cycles so this becomes possible. If your plugin can introduce cycles, it is best practice to resolve these depending on your use-case.

postdependencies (file, build)

This hook is responsible for trimming down the dependency tree if needed. For example, mako-js takes the entire JS dependency tree for each entry file and combines it into a single file. Along the way, all the dependencies are pruned out.

Make will ensure this hook is run in topological order, meaning that all dependency links are respected. It also runs this particular hook in series, not in parallel. (unlike all other hooks) This means it is safe for you to make modifications to the tree, and that you won't lose any links because of race conditions. With that being said, make sure you be careful not to overload this hook, since it doesn't run in parallel.

By the end of this hook, the only files that should remain in the tree are those that you intend to have written to your destination.

prewrite (file, build)

This hook is responsible for making any preparations before writing the file to it's destination. Most minifiers (eg: uglify, cssnano) and some transpilers (eg: cssnext) will use this hook. In addition, you may generate a checksum that will be used by the postwrite hook to ensure that the write was successful.

This hook is treated a little differently than other compile hooks in a key way. This hook is able to add files that will also be processed by this same hook. For example, you may want to add a .map file when generating source-maps for your JS/CSS. You can safely add files to the build tree and they will still go through the prewrite hook.

By the end of this hook, the file should be ready to be written to the destination.

write (file, build)

This hook is responsible for actually writing the file to it's destination. The simplest example is writing to the local filesystem. Generally, the destination is indicated by file.output. Earlier plugins will set that value as needed, and this hook should only do the actual write.

For files being worked on in memory, mako-write writes those text files back to disk. Other related files, such as images linked to in CSS, can be handled by something like mako-copy or mako-symlink depending on the use-case.

By the end of this hook, each file should be written to the destination.

postwrite (file, build)

This hook can be used to check and ensure that a file was properly written to the destination. In the simplest case, a file written to the local filesystem can be checked via a simple checksum. (or you can simply stat to check for it's existence)

By the end of this hook, we should be certain that the file was successfully written.

postcompile (build)

This hook runs prior after the compile phase for the build. (in other words, after the entire build is complete)