Building A Mimosa Module

Plug custom functionality into Mimosa

Creating a Module: Summarized

The following is a quick tutorial on how to build a simple module. You could also visit this post on the blog which covers the same topic.

The best way to figure out how to create a module is to look at an existing one. The mimosa-minify-html module is a simple module to understand: it exists to minify HTML files, making them as small as possible without removing capability or content.

Mimosa module's exports

Mimosa expects to find certain things in a module's interface.

At the bottom of the index file is the module.exports object. That object defines the module interface.

Working from the bottom up:

  • validate is optional, but if it exists it is called to validate a user's configuration. The mimosa-minify-html validate is small. One thing it does is verify that the mimosa-config's minifyHtml object is an object. If it wasn't validate would return an error, Mimosa would not start, and the error message would be displayed.
module.exports = {
  registration: registration,
  defaults: config.defaults,
  validate: config.validate
}
  • defaults, again optional, is how the module lets Mimosa know its default configuration. mimosa-minify-html's defaults returns a simple object.
  • registration is how a module informs Mimosa what code to call and when.

Registering a module

A module needs to register itself with Mimosa so that Mimosa knows what functionality to invoke and when to invoke it.

Lets break down a single line of mimosa-minify-html's registration.

  • register is a function provided by Mimosa to execute the registration.
  • The entries in the ['add', 'update', 'buildFile'] array are Mimosa workflows. mimosa-minify-html needs to be invoked to minify HTML when an HTML file is built (when Mimosa starts), added (after it starts), or updated (while it is running).
register(
  [ "add", "update", "buildFile"],
  "afterCompile",
  _execute,
  [ "html", "htm" ]
);
  • 'afterCompile' indicates what workflow step to call the module's code. In this case right before a file is written its HTML needs to be minified.
  • _execute is the function to invoke in order to perform the minification of the file being processed.
  • [ "html", "htm" ] indicates the function should only be called for HTML based extensions.

Module execution

When Mimosa execute the functionality of a module, Mimosa passes along certain objects/functions.

Lets look at the first few lines of the _execute function.

Mimosa workflows functions are invoked with three parameters.

var _execute = function ( mimosaConfig, options, next ) {
  if ( options.files && options.files.length ) { ...
  • The resolved mimosa-config, including the configuration for the module being invoked.
  • An options object which contains the information regarding the current file(s) being processed.
  • A next callback which informs Mimosa it can continue on to the next step in the workflow.

The options object contains a files array which contains references to the file(s) being processed including their file path and file text. The code above does a check to see if there are any files to process. If there isn't, there is nothing to minify, so it calls the next callback and exits.

Finally, how does one pass information along? The mimosa-minify-html module doesn't write anything, it just minifies text that is already available in the filesarray and then stores the result on the file object itself. Other modules are responsible for writing the file objects once processing for the file has finished.

Hopefully that is a simple starter to get you going building your own module.

Exhaustive details follow.

Creating a Module: The Details

Where to start?

First, check out the blog post, and if you still have questions, come back here for more details.

By far the easiest way to create a new module is to start with an existing one. There are many modules available on GitHub. Pick one that does something somewhat similar to what you want to do and make tiny modifications to it.

There is also a module skeleton. mimosa skel:new mimosa-module-javascript can be used to pull in the module skeleton.

The only hard and fast rule with Mimosa modules is that when they are created they must be prefixed with 'mimosa-'. This makes them easy to find if someone else wants to use them, but also differentiates Mimosa's modules from all of the libraries both in NPM and inside Mimosa. So you'll want to make sure that you keep the name of the module prefixed.

What assets comprise a typical module?

Here are a group of files you'll see in a typical module and that you'll find in the module skeleton.

  • package.json: Those familiar with node.js recognize this file. This defines all sorts of metadata about a node project: the project name, dependencies, repo location, a description, and more. When a project is published to NPM, NPM expects the package.json to be present. The package.json that is inside the skeleton needs some editing. Many of the fields inside of it are placeholders and should be changed before publishing.
  • index.js: This highly commented file defines a module's interface to Mimosa. Mimosa will use this file to execute a module's functionality. How a module is structured is entirely up to the developer, as long as the functions Mimosa expects are exported in the index file.
  • config.js: This file is also highly commented and contains two functions that might need to be implemented: a function that returns the default configuration and a function that validates a user's configuration for the module. Mimosa calls these functions at different times. If a module has no configuration at all, the functions and the file can be removed, or they can be left and commented out in case configuration might be introduced later.
  • A docs folder: In the skeleton is a docs folder. Using a tool called Docco the comments in the config and index files have been turned into easy to read web pages, with comments alongside code. This is a great place to start. Read through the the Docco pages and get comfy with the concepts.
  • A starter README.md with a few things filled out.

Mimosa's module interface

At different times, Mimosa will attempt to execute four different functions in a module. All of the functions are optional.

registerCommand(program, logger, retrieveConfig)

The registerCommand function is self-explanatory. This function is used to register new Mimosa commands.

The arguments passed in are:

  • program: A commander program object. Use this object to register commands, help text, flags, and the command's callback.
  • logger: A logger instance.
  • retrieveConfig: A callback function that a module can use to execute a Mimosa clean and build and get the full mimosa-config should it be needed. This function takes two parameters, an options object that can indicate if a build should be run before executing the command and a flag to turn Mimosa's debugging on, and a callback function to execute the command module's code. That callback is passed the full mimosa-config.

The mimosa-npm-web-dependencies module is a good example module for how to add a command to Mimosa. It adds one command: npmweb.

defaults()

A module's defaults function is called when Mimosa starts up and is used to assemble the configuration used by Mimosa. defaults should return a JavaScript object that is the default configuration for the module. That default configuration is merged with the user provided configuration to establish the full mimosa-config.

exports.defaults = function() {
  return {
    minifyHtml: {
      options: {}
    }
  };
};

If a module doesn't have its own configuration, then this function does not need to be implemented.

validate(mimosaConfig, validators)

A module's validate function is called when Mimosa starts up and after Mimosa has put together the full mimosa-config. That full config is passed to the validate function of all modules and each module has the opportunity to ensure that the config settings are valid. This function should return an array of strings. Each string represents an error that the module has found with the config. If a module return any errors, Mimosa will print the errors and exit to give the user a chance to update the config.

The validate function is also passed, as the second parameter, an object containing a collection of validation and utility functions. The best way to learn what those functions are is to check the source.

If there are no errors, return an empty array. If there is no config to validate, then do not implement the function.

exports.validate = function( mimosaConfig, validators ) {
  var errs = [], mHtml = mimosaConfig.minifyHtml;
  if (validators.ifExistsIsObject(errs, "minifyHtml config", mHtml)) {
    validators.ifExistsIsObject(errs, "minifyHtml.options", mHtml.options);
  }
  return errs;
};

This function is also commonly where configuration manipulation and normalization takes place. For instance, this is a good place to turn relative paths into absolute ones so your module is dealing entirely with absolute paths in the config.

registration(mimosaConfig, register)

The registration function is the most important function of a module. This function is called during Mimosa's startup and it allows a module to bind to one or many steps in a Mimosa workflow. Inside this function is where a module tells Mimosa to call its functions at certain times.

var registration = function ( mimosaConfig, register ) {
  if ( mimosaConfig.isMinify ) {
    register(
      [ "add", "update", "buildFile"],
      "afterCompile",
      _execute,
      [ "html", "htm" ]
    );
  }
};

The arguments passed in are:

  • mimosaConfig: The full mimosa-config including added flags to indicate what sort of Mimosa command is being run (like isForceClean), and an added list of extensions being used by the application. A developer may decide based on the flags in the config to not register anything, which is fine. In the case of the minification example above, if the isMinify flag isn't turned on then the module doesn't register itself.
  • register: This is a function handed to a module as a means to register a module with Mimosa. More information about how to use this register function follows in the next section.

Module Registration

As mentioned in a previous section, a module should implement a registration function that Mimosa calls when it starts up. When Mimosa calls the module's registration function, Mimosa passes the enriched mimosa-config and a register callback. The register callback is what a module uses to inform Mimosa what function in the module to call and under what circumstances to call it. The register function takes the following four parameters:

register(['add','update','buildFile'],      'afterCompile', _minifyJS, e.javascript);
register(['add','update','buildExtension'], 'beforeWrite',  _minifyJS, e.template);
  1. A list of workflows, an array of strings. Pick one-to-many workflows depending on the sort of task the module accomplishes. The possible values, like buildFile, buildExtension, postBuild, add, update, and remove, are explained below.
  2. A workflow step, a string. A workflow step for the selected workflows. For example, for the update workflow, a module's code might be executed afterCompile. The list of steps for each workflow is explained below.
  3. Module callback, a function. This is the function Mimosa will call when processing reaches the workflow(s) and step called out in the previous parameters.
  4. An optional array of extensions upon which to execute the callback. If the file or extension being processed doesn't match one of these extensions, the callback will not be executed. The extensions refer to the original extension of the file being processed, so in the case of a CoffeeScript file it would be 'coffee' and not 'js'. The passed in mimosaConfig object has an extensions object a module can use to cover all of the desired extensions. The extensions object has 4 properties: javascript, css, template, and copy. If no extensions are provided, Mimosa will send all files/extensions through the module.

Notice that the register function is executed multiple times. There is nothing stopping a module from registering multiple times for multiple workflows.

Workflows categorizing application lifecycles

A workflow is a set of steps an asset, a group of assets, or an application can go through. There are three groups of workflows that correspond to the three main processing types for an applciation: build, watch and clean.

Build Workflows

Modules registered to take place during the build workflows perform project builds. The build workflows all occur in order during Mimosa's startup. preBuild occurs first, followed by buildFile, buildExtension, and postBuild.

preBuild

preBuild is processed before any asset is handled. This is the workflow for preparing the file system and moving files around in preparation for individual file processing.

buildFile

During buildFile each file in the configured watch.sourceDir is individually processed. This step is when, for instance, CoffeeScript files are compiled to JavaScript and written to watch.compiledDir. It is also when images are copied to the watch.compiledDir.

Some assets, like micro-template files (Handlebars or Dust) and CSS preprocessor files (SASS or LESS) are not processed during this step as they, during startup, need to be dealt with as a group rather than individually.

buildExtension

After all of the assets are processed individually, buildExtension makes another pass through all the assets, but this time to handle extension-wide processing. Now is when Mimosa handles the file types that need to be managed as a group like micro-templates and CSS preprocessors.

For instance, if a project uses SASS, then it might have 20 includes/partials that result in two top-level files. Mimosa will process all 20 files at once and recognize that only two files need to be compiled.

postBuild

After the extensions are finished, individual asset handling is finished. postBuild is when post asset completion tasks, like asset optimization, take place. postBuild is also when servers are started. If a module was being built for something like installing the app to Heroku, then postBuild is a candidate workflow.

Watch Workflows

If mimosa build was executed, then after postBuild is done, Mimosa exits. If mimosa watch is executed, then Mimosa keeps running and monitors the project's codebase for changes. When changes occur, the watch workflows are executed. The watch types are very straight forward.

For watch types no delineation exists between file and extension. For each type only a single file is processed at a time. So modules like the CSS and micro-templates compilers perform extension-wide tasks based on the single file that was updated.

add

The add workflow is kicked off when a new file is added to the watch.sourceDir directory.

update

The update workflow is run when an existing file in the watch.sourceDir directory is saved.

remove

The remove workflow is executed when a file in the watch.sourceDir directory is deleted.

Clean Workflows

The clean workflows execute when using mimosa clean (but not currently when --force is used), when the --clean flag is used with mimosa watch and at the beginning of a mimosa build. These workflows are responsible for removing compiled, copied and created assets and to generally return a project to a pre-Mimosa state. The clean workflows are always executed together and in order.

Modules can create and deposit files during the other workflows. A good module will use the clean workflows to tidy up after itself.

preClean

The preClean workflow is executed first and is an opportunity to handle files before cleanFile comes through and deals with each file individually.

cleanFile

Each file in the watch.sourceDir is passed through the cleanFile workflow one at a time.

postClean

The postClean workflow is the final opportunity to tidy up files and directories. This is when, for example, a module might make a pass to remove any empty directories the module might be responsible for that remain after the files have been handled.

Workflow Steps a list of processing steps for each workflow

The previous section covered each of the workflows. Each workflow has many steps within it. An example is the best way to illustrate how steps relate to workflows.

The simplest workflow is update. If a CoffeeScript file is saved inside the watch.sourceDir while Mimosa is running, here is what might happen:

  1. The first step executed is init. During the init step, Mimosa will generate some information about the asset to be used by future steps. For instance it will set a flag into the options object (discussed in the next section) to indicate that the file is a JavaScript file, and it will set flags to indicate if the file is a vendor file or not.
  2. During a step named read the modified CoffeeScript file is read in.
  3. The compile step is next. Here the CoffeeScript gets compiled to JavaScript.
  4. Next is afterCompile. Here the compiled JavaScript is linted to get feedback regarding code quality.
  5. If minification was selected, beforeWrite is where the compiled JavaScript would be minified.
  6. During the write step the compiled JavaScript is written to the watch.compiledDir
  7. The afterWrite step is where optimization takes place if that is selected.

Modules can register to be executed for the same workflow and step. This tie is broken by the order the modules appear in the modules array in the mimosa-config.

When building a module, a developer needs to decide which step is the right place to execute a module's functionality. For instance, if building a module that will run CoffeeLint over CoffeeScript to determine CoffeeScript code quality, then afterRead is probably the best step. If, for instance, beforeRead is used, then Mimosa will have not yet read in the file, and there will be nothing on which to run CoffeeLint.

Workflow Type & Step Breakdown

preBuild

  • init
  • complete

buildFile

  • init
  • beforeRead
  • read
  • afterRead
  • betweenReadCompile
  • beforeCompile
  • compile
  • afterCompile
  • betweenCompileWrite
  • beforeWrite
  • write
  • afterWrite
  • complete

buildExtension

  • init
  • beforeRead
  • read
  • afterRead
  • betweenReadCompile
  • beforeCompile
  • compile
  • afterCompile
  • betweenCompileWrite
  • beforeWrite
  • write
  • afterWrite
  • complete

postBuild

  • init
  • beforeOptimize
  • optimize
  • afterOptimize
  • beforeServer
  • server
  • afterServer
  • beforePackage
  • package
  • afterPackage
  • beforeInstall
  • install
  • afterInstall
  • complete

add

  • init
  • beforeRead
  • read
  • afterRead
  • betweenReadCompile
  • beforeCompile
  • compile
  • afterCompile
  • betweenCompileWrite
  • beforeWrite
  • write
  • afterWrite
  • betweenWriteOptimize
  • beforeOptimize
  • optimize
  • afterOptimize
  • complete

update

  • init
  • beforeRead
  • read
  • afterRead
  • betweenReadCompile
  • beforeCompile
  • compile
  • afterCompile
  • betweenCompileWrite
  • beforeWrite
  • write
  • afterWrite
  • betweenWriteOptimize
  • beforeOptimize
  • optimize
  • afterOptimize
  • complete

remove

  • init
  • beforeRead
  • read
  • afterRead
  • beforeDelete
  • delete
  • afterDelete
  • beforeCompile
  • compile
  • afterCompile
  • betweenCompileWrite
  • beforeWrite
  • write
  • afterWrite
  • betweenWriteOptimize
  • beforeOptimize
  • optimize
  • afterOptimize
  • complete

preClean

  • init
  • complete

cleanFile

  • init
  • beforeRead
  • read
  • afterRead
  • beforeDelete
  • delete
  • afterDelete
  • complete

postClean

  • init
  • complete

Module Execution

So a module's code is registered to be called in the right spot in the workflow. What happens when Mimosa calls the function that has been registered?

The executed function is handed three arguments.

  1. mimosaConfig: The full mimosa-config enriched with all sorts of useful data beyond the default mimosa-config, and including the configuration for the module being developed.
  2. options: An object containing information about the asset(s)/extension currently being processed. At different steps of the a Mimosa workflow the options object will contain various important pieces of information. One of the first steps a module developer should take when developing a new module is to console.log the content of the options object to see what sort of information is available at their desired workflow and workflow step.
  3. next: a workflow callback. This callback must be called when a module has finished processing. It tells Mimosa to execute the next step in the workflow. If for some reason a module decides that processing for the current asset/build step needs to stop, the callback can be called passing false. Ex: next(false). In most cases the workflow should not be ended prematurely.

Adding New Commands: Command Modules

A command module has a very simple interface. A module that intends to implement a new command needs to implement and export a registerCommand function. That function takes a commander programobject for registering the command, the flags, the help text and a callback. The version of commander used is 1.3.

If a function needs access to execute a build before running, or needs the mimosa-config, it can get accomplish those things using the second parameter passed to registerCommand. The second parameter, a retrieveConfig function, takes an options object and a callback. The callback would be the command module's code and it is passed the full mimosa-config when it is executed. The options object contains 1) a buildFirst flag which indicates if a build is needed before running the command module code and a mdebug flag which turns on Mimosa's debugging.

For interface details, see the interface section above.

Module Logging

Mimosa's logger, logmimosa, is attached to the resolved mimosaConfig when Mimosa starts up. Every function in a module that Mimosa calls and includes the mimosaConfig, like the workflow functions, registration or registerCommand, can access the logger via mimosaConfig.log.

To take advantage of Mimosa's colorizing, wrap important or variable parts of log messages in [[ and ]].

Installing Modules

During Development

The mimosa mod:install command handles installing local modules to Mimosa. From inside the root module directory, the directory with the package.json file, execute mimosa mod:install. Then modify the modules array of a project's mimosa-config to use the module. Fire up Mimosa and test your module out!

Publishing to NPM

If you are familiar with NPM, this part is not new. A simple npm publish from the root of your module will install a module to NPM so that anyone can get access to it. Once it is in NPM, commands like mod:install and mod:list will be able to find it.

If you have never published to NPM before, then spend a few minutes reading the developer docs. Just a few simple steps and you are ready to publish.

If you do create and publish a module, let us know!!!