Plug custom functionality into Mimosa
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 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.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.['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.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 ) { ...
options
object which contains the information regarding the current file(s) being processed.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 files
array 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.
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.
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.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.README.md
with a few things filled out.At different times, Mimosa will attempt to execute four different functions in a module. All of the functions are optional.
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
.
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.
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.
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.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);
buildFile
, buildExtension
, postBuild
, add
, update
, and remove
, are explained below.update
workflow, a module's code might be executed afterCompile
. The list of steps for each workflow is explained below.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.
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.
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
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.
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.
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.
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.
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.
The add
workflow is kicked off when a new file is added to the watch.sourceDir
directory.
The update
workflow is run when an existing file in the watch.sourceDir
directory is saved.
The remove
workflow is executed when a file in the watch.sourceDir
directory is deleted.
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.
The preClean
workflow is executed first and is an opportunity to handle files before cleanFile
comes through and deals with each file individually.
Each file in the watch.sourceDir
is passed through the cleanFile
workflow one at a time.
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.
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:
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.read
the modified CoffeeScript file is read in.compile
step is next. Here the CoffeeScript gets compiled to JavaScript.afterCompile
. Here the compiled JavaScript is linted to get feedback regarding code quality.beforeWrite
is where the compiled JavaScript would be minified.write
step the compiled JavaScript is written to the watch.compiledDir
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.
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.
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.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.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.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 program
object 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.
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 ]]
.
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!
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!!!