David Bashford

A web dev blog from a Washington DC based Dad, UI architect, web developer, and creator of Mimosa.

    

Making a Mimosa Module

While the site has a lot of documentation on how modules are built with a small walk-through, I wanted to dedicate some serious attention to how easy it is to build something and get it linked into your workflow.

Example Module

A great way to learn how a module is built is to look at a real simple existing module. We'll look at the handlebars-on-window module. The goal of that module is detect a handlebars.js file as it flows through Mimosa's build steps and transform the text of the file to make the Handlebars object visible on the window. If you are wondering why such a module would exist, you can check the README.

File Structure

This is the file structure for the handlebars-on-window module.

/src
  config.js
  index.js
.gitignore
.npmignore
README.md  
mimosa-config.coffee  
package.json  

Non-Mimosa Files

Lets knock out the simple stuff first. These files are more about Git, NPM and node and not about Mimosa, but they are a common part of any Mimosa module.

mimosa-config.coffee

If you are interested in building a module, you are likely using Mimosa. This file configures Mimosa. But, don't let the presence of this file confuse you, the mimosa-config file has nothing to do with the Mimosa module. A Mimosa module does not need a mimosa-config to be a module.

When I built this module I wanted to make sure I ran JSHint over the project's source code to make sure the JavaScript code is idiomatic and to catch simple syntax bugs quickly. It just so happens Mimosa can do that, so I use Mimosa to run JSHint.

README.md

README.md contains the module's documentation. How to install it, how to configure it and what its purpose is. More documentation the better! Many of the Mimosa modules have a common pattern for documentation if you care to follow it.

The handlebars-on-window has documentation that covers usage, functionality and configuration.

.gitignore

This indicates what local files you do not want to push to your git repository. Because Mimosa modules are node projects, using .gitignore to make sure you do not push the node_modules directory is common.

.npmignore

NPM is where you will deploy your module when you are done with it and it is how other people will get your module. As with .gitignore and git, .npmignore determines what files are not sent to NPM when you publish your module. The files that get published to NPM are the ones that get delivered when someone uses your module.

The mimosa-config is a good example of something to add to the .npmignore. It is only there to run JSHint during development. It isn't useful for anything else.

package.json

The package.json configures node projects. If your module needs any other node modules (like wrench for directory manipulation or request for some HTTP goodness), you would declare those as dependencies in your package.json. This file is also where you declare the name of your module, in this case mimosa-handlebars-on-window.

Nodejitsu has a great guide on the package.json.

Module Code

While some of the previous files are necessary, they don't really have anything to do with Mimosa. The files inside src are where the module's code exists.

The first thing to identify is where the module's main code is. When Mimosa uses the module, what does it use?

The package.json has a property in it named main.

"main": "./src"  

This points anything using this module to module's main entry point. In this case it is pointed at the directory ./src. By default, when a directory is the name rather than a file it means that the file to use is index.js inside that directory.

And that is where we'll start.

src/index.js

Because the index.js is the main for this NPM module, that means it is responsible for defining the interface Mimosa will use. At the bottom of the index.js file is a module.exports.

module.exports = {  
  registration: registration,
  defaults: config.defaults,
  placeholder: config.placeholder,
  validate: config.validate
};

Each property in this object is a reference to a function. These four functions are the functions that Mimosa will use in order to access the module's functionality.

Not only is module.exports the interface for this module, is also contains the interface Mimosa expects from its modules. For every module plugged into Mimosa, Mimosa will look to execute these specific functions and various times during a build or during mimosa new.

The defaults, placeholder and validate functions are all inside the config.js file that is required (imported) into index.js. We'll cover those after we touch on the most important of the interface functions.

registration

The registration function is how a Mimosa module hooks itself into a Mimosa build. It is the most important of all the functions, and it is only one line of code!

var registration = function (mc, register) {  
  register( 
    ['add','update','buildFile'],
    'afterCompile', 
    _attach, 
    mc.extensions.javascript );
};

Before digging into this code, lets refresh what this module does. It's purpose is to add the text "window.Handlebars" to the handlebars.js file. That's all. That means this module needs to process JavaScript files, capture the handlebars.js and update the text that is going to be written.

This line of code registers files with JavaScript extensions (mc.extensions.javascript) to be processed by the _attach function (which is higher up in the code) after compilation has been executed (afterCompile) whenever a file is added, updated or built (buildFile).

Lets break that down.

Callback

_attach is the callback for this registration. This is where the module's code lives. It is this code that will look for handlebars.js and update the text.

Extensions

mc.extensions.javascript is a reference to an array of JavaScript extensions being processed by the application. By registering this array of extensions, it ensures the _attach callback gets called only when a JavaScript file is being processed. This would include CoffeeScript files as well as JavaScript files. But it would ensure that, for instance, CSS files or images being processed by Mimosa would not trigger the _attach callback.

mc is the fully evaluated and amplified mimosa-confg and it is passed to the registration function. It includes all the configuration for all the modules, but it also includes information like the extensions object.

But when does the processing of JavaScript files in the _attach function occur?

Workflows

A Mimosa workflow is a set of steps that get executed during a Mimosa command.

The buildFile workflow runs during mimosa build and when mimosa watch first starts. When it first starts up, mimosa watch builds all the assets, but it keeps watching. add and update are executed during mimosa watch after initial startup when a file is added or updated. In all of these scenarios -- when building, adding or updating files -- we want the module to be checking the JavaScript files being processed to see if any of the files are handlebars.js.

Workflow Steps

When a file is processed through a workflow it goes through several workflow "steps". Steps include read when the file is read, compile when the file's input text is transformed into the desired output text, and write when it is written. This module wants to process files during the afterCompile step. This ensures that the module is working with JavaScript. If this module ran beforeCompile, then the file's output text may not have been determined and it is the output text we want to tweak.

When the callback gets called

When _attach gets called by Mimosa at the configured time, it gets passed some information.

var _attach = function ( mimosaConfig, options, next ){ ... }  
mimosaConfig

The first parameter is again the full mimosa-config with all of the application's configuration and plenty of other goodies. What we care about is that the mimosaConfig contains the configuration for this module: mimosaConfig.handlebarsOnWindows.

options

The options object has information about the file being processed. It has a files array, options.files, that contains any files being processed. Each entry in the files array has an inputFileText and an outputFileText. As mentioned above, this module is concerned with transforming the outputFileText.

next

next is Mimosa's lifecycle callback. When the module has finished doing whatever it needs to do, it must call this callback or else Mimosa will stop processing this file.

Read the code!

Armed with the information above, you should be able to read the 19 lines of code that comprise the _attach callback and understand what is happening. The key part is this one line...

file.outputFileText = file.outputFileText.replace(  
  mimosaConfig.handlebarsOnWindow.replace,
  "window.Handlebars = " +     mimosaConfig.handlebarsOnWindow.replace );

This line rewrites the outputFileText with window.Handlebars inside of it.

Regarding mimosaConfig and options

These two objects have lots of useful stuff in them. Just for curiosity's sake, you may want to console.log these objects just to see what is inside them.

src/config.js

The functions in this file are really straight forward. They are also all optional. If your module has no configuration, it wouldn't need this file at all.

Remember the index.js looks into the config.js file to expose some of its functions.

module.exports = {  
  registration: registration,
  defaults: config.defaults,
  placeholder: config.placeholder,
  validate: config.validate
};

defaults

This function returns the module's default configuration. Pretty simple!

exports.defaults = function() {  
  return {
    handlebarsOnWindow: {
      libName: 'handlebars.js',
      replace: "__exports__ = Handlebars"
    }
  };
};

placeholder

This function returns a string snippet that explains the default config that is placed in the mimosa-config-documented.coffee file during mimosa new and mimosa config. The string it returns is commented CoffeeScript.

exports.placeholder = function() {  
  return "\t\n\n"+
         " # handlebarsOnWindow:\n" +
         "   # libName: 'handlebars.js' # file name of handlebars library\n" +
         "   # replace: '__exports__ = Handlebars' # code to prepend 'window.Handlebars = ' to\n";
};

validate

This function is called by Mimosa to give the module a chance to validate its own config.

exports.validate = function(config, validators) {  
  var errors = [];

  if ( validators.ifExistsIsObject( errors, "handlebarsOnWindow config", config.handlebarsOnWindow ) ) {
    validators.ifExistsIsString( errors, "handlebarsOnWindow.libName", config.handlebarsOnWindow.libName );
    validators.ifExistsIsString( errors, "handlebarsOnWindow.replace", config.handlebarsOnWindow.replace );
  }

  return errors;
};

If this function returns an array of strings (error messages), Mimosa will error out after printing the errors. So, if, for instance, the libName property wasn't a string, the validate function would return something like:

["handlebarsOnWindow.libName must be a string"]

The validators parameter passed in contains a set of helpful validation methods like ifExistsIsString. The full set can be found in the Mimosa source.

So you wrote a module, now what?

Testing Locally

Run mimosa mod:install from the root of your module's directory and your Mimosa will install your module inside itself. Now you can use your module on local projects.

Put it in NPM

As long as your package.json is in working order, npm publish your module! And then tell the world about it (and let me know via @mimosajs).

comments powered by Disqus