Extend Mimosa's default functionality, and publish your own extensions
So maybe Mimosa doesn't come with exactly what you need out of the box. Maybe you want to use a different linter, or you want to kick off your testing suite on every file save. A module can probably help.
A Mimosa module is a node.js project that, when installed inside Mimosa, can be used as part of a Mimosa workflow or introduce a new Mimosa command. Mimosa is able to use a module because modules implement a simple interface Mimosa can access.
There's a strong chance what you need can be created as a new module. In most cases all that is required to install a module is to include the module's name in the Mimosa configuration. Mimosa self-installs modules it comes across but does not already know about.
The table below lists the modules that come baked into Mimosa. These do not need to be installed separately.
| Name | Description |
|---|---|
| lint | CSS and JS linting/hinting |
| require | AMD/RequireJS support including optimization |
| server | Mimosa-based hosting and managing user hosting |
| live-reload | Application reloads when the code does |
| minify | Minification of JavaScript assets |
The table below lists the modules that do not come with Mimosa, but can be installed.
| Name | Description |
|---|---|
| web-package | Packaging of node-based user server for deployment |
| require-commonjs | CommonJS support via AMD/RequireJS |
| combine | Combine all non-binary files in a folder into a single file |
| karma | Karma (formerly Testacular) test runner integration |
| testem-simple | Simple Testem test runner integration. |
| server-reload | Restart a node server when server assets change |
| import-source | Copy other files into a project to be included as part of the project. |
| server-template-compile | Copy your server templates into static HTML as part of a build. |
| requirebuild-textplugin-include | Both an example module for modifying the r.js configuration dynamically, and a functional module that will include files attached to the text plugin to r.js runs. |
| volo | Introduces a import command that wraps Volo functionality. Comprises the import command that came bundled with Mimosa pre 0.7.2. |
| skeleton | Adds three new commands to interact with Mimosa skeletons. This module will eventually be a default included Mimosa module once more skeletons have been developed. For more information, visit the github page. |
| client-jade-static | Compiles Jade files with a `.html.jade` extension statically and writes them out as plain HTML. |
There are four ways to install a module.
One way to install a module is to just add it to the modules array in the mimosa-config for a project. This is a shortcut for the project level npm install (covered next). The next time Mimosa is started for that project it will install the module into the Mimosa project from NPM. For example, to add the mimosa-web-package module, include web-package in the uncommented modules config. Run, for instance, mimosa watch, and the first thing Mimosa will do is go fetch mimosa-web-package from NPM.
To use a module it needs to be added to the modules array anyway, so this shortcut saves the installation step.
Place and install Mimosa modules in the project itself and Mimosa will prioritize those modules above all others. Run npm install mimosa-nameOfModule and the module will be installed. Modules installed inside a project take precedence over any modules installed into other scopes.
One way to install a module is the mod:install command discussed further below. mimosa mod:install mimosa-web-package will install mimosa-web-package from NPM. The module will still need to be added to any projects that need to use it. This command adds the module right into the Mimosa install as if it was installed with Mimosa itself. This allows Mimosa to draw from it when building a new mimosa-config. If a module is installed inside Mimosa, Mimosa will include that module's configuration snippet inside any generated mimosa-config.
Use npm install -g to install any modules into the global node scope. Modules installed with mod:install take priority, so if any modules are installed with both npm install -g and mod:install, the npm install -g module will be ignored.
To install a specific version of a module, to, for instance, ensure everyone on a project stays on the same version, add a @ followed by the version after the module name. Ex: web-package@0.1.0. This is also accomplished using by managing versions in a project's package.json.
Execute mod:list to see a list of all the installed Mimosa modules. The displayed list includes the name, version, and URL of the website of the module.
$ mimosa mod:list
Add a --verbose flag and Mimosa will including a short description and the list of dependencies the module has on other node.js modules.
$ mimosa mod:list --verbose $ mimosa mod:list -v
Davids-MacBook-Pro:webapp dbashford$ mimosa mod:list
The following is a list of the Mimosa modules you have installed.
Name Version Website
mimosa-server 0.2.1 http://www.mimosajs.com
mimosa-require 0.2.0 http://www.mimosajs.com
mimosa-minify 0.2.0 http://www.mimosajs.com
mimosa-lint 0.2.0 http://www.mimosajs.com
To view more module details, execute 'mimosa mod:list -v' for 'verbose' logging.
Davids-MacBook-Pro:webapp dbashford$
mod:search will search NPM to find all the available Mimosa modules. This command can take some time as it talks over HTTP to NPM, finding all available modules and then narrowing down the list to just those prefixed with 'mimosa-'.
$ mimosa mod:search
Included in the list of modules is the name, current version, last time it was updated, whether or not it is already installed, and a website for the module.
Add a --verbose flag and Mimosa will include additional information including a short description and the list of dependencies the module has on other node.js modules.
$ mimosa mod:search --verbose $ mimosa mod:search -v
Davids-MacBook-Pro:webapp dbashford$ mimosa mod:search
Searching NPM for Mimosa modules, this might take a few seconds...
The following is a list of the Mimosa modules in NPM.
Name Version Updated Installed? Website
mimosa-logger 0.1.0
2012-10-21 no
http://www.mimosajs.com
mimosa-server 0.2.1
2012-10-25 yes
http://www.mimosajs.com
mimosa-lint 0.2.0
2012-10-24 yes
http://www.mimosajs.com
mimosa-require 0.2.0
2012-10-24 yes
http://www.mimosajs.com
mimosa-minify 0.2.0
2012-10-24 yes
http://www.mimosajs.com
To view more module details, execute 'mimosa mod:search -v' for 'verbose' logging.
Install modules by executing 'mimosa mod:install [name of module]'
Davids-MacBook-Pro:webapp dbashford$
To install a module execute mimosa mod:install and provide the name of the module.
$ mimosa mod:install mimosa-server
If the module has already been installed, this will update it to the latest version. To install a specific version of a module, add the version after a @.
$ mimosa mod:install mimosa-server@0.2.0
To use and test a module under local development that isn't in NPM execute mod:install without providing a name for the module. For this to work properly, the command must be executed from the same directory as the module's package.json, which should be the root of the module. Once the module is installed, Mimosa treats it the same as a module it installed from NPM.
Davids-MacBook-Pro:webapp dbashford$ mimosa mod:install mimosa-server@0.2.0
09:57:43 - Install of 'mimosa-server@0.2.0' successful
Davids-MacBook-Pro:webapp dbashford$
Execute mod:uninstall to remove a module from Mimosa.
$ mimosa mod:uninstall mimosa-server
Davids-MacBook-Pro:webapp dbashford$ mimosa mod:uninstall mimosa-server
09:57:43 - Uninstall of 'mimosa-server' successful
Davids-MacBook-Pro:webapp dbashford$
Execute mod:init and provide the command with a 'mimosa-' prefixed module name and Mimosa will create a directory using the name (or use an existing directory), and place a module skeleton inside. The skeleton contains heavily documented JavaScript code to get started, docco-generated documentation for that code, and a starter package.json for deploying the module to NPM.
All of the package.json placeholders need to be changed before publishing to NPM. The following sections go into detail about what steps to take with the new module skeleton.
$ mimosa mod:init mimosa-foo
Add a --coffeescript flag and Mimosa will provide a CoffeeScript skeleton instead of JavaScript.
$ mimosa mod:init -c $ mimosa mod:init --coffee
Davids-MacBook-Pro:mimosaplay dbashford$ mimosa mod:init mimosa-foo
10:06:59 - Module skeleton successfully placed in mimosa-foo directory. The first thing you'll want to do is go into mimosa-foo/package.json and replace the placeholders.
Davids-MacBook-Pro:mimosaplay dbashford$
Execute mod:config to have Mimosa write to the console the configuration placeholder for a module. This is useful when needing to add the configuration for a newly installed module to the mimosa-config.
$ mimosa mod:config server
Davids-MacBook-Pro:foo dbashford$ mimosa mod:config live-reload
# liveReload: # Configuration for live-reload
# enabled:true # Whether or not live-reload is enabled
# additionalDirs:["views"] # Additional directories outside the watch.compiledDir
# that you would like to have trigger a page refresh,
# like, by default, static views
Davids-MacBook-Pro:foo dbashford$
Mimosa can help module development get off the ground with a module skeleton. Discussed above, the mod:init command will create a 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.
One great place to start, if you want to skip reading and get right to code, is to check out some existing modules, like mimosa-lint and mimosa-minify.
mod:init provides a group of files to get module development started.
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 Mimosa provides needs some editing. Many of the fields inside of it are placeholders and should be changed before publishing.index.js/index.coffee: 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/config.coffee: This file is also highly commented and contains three functions that might need to be implemented: a function that returns the default configuration, a function that returns a string placeholder for the mimosa-config, 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: 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 the real 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 five 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.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, a flag to indicate if a build should be run before executing the command, and a callback function to execute the command module's code. That callback is passed the full mimosa-config.
The mimosa-volo module is a good example module for how to add a command to Mimosa. It adds an import command and makes use of the mimosa-config during execution.
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 {
minify: {
exclude: [/\.min\./]
}
};
};
If a module doesn't have its own configuration, then this function does not need to be implemented.
A module's placeholder function is called when Mimosa is building the mimosa-config.coffee file that is delivered to a user during both the mimosa new and mimosa config commands. Use this function to return a string that shows what the module's configuration is and then explains each setting.
exports.placeholder = function() {
return "\t\n\n"+
" # minify: # Configuration for non-require minification/compression via uglify\n" +
" # using the --minify flag.\n" +
" # exclude:[/\\.min\\./] # List of regexes to exclude files when running minification.\n" +
" # Any path with \".min.\" in its name, like jquery.min.js, is assumed to\n" +
" # already be minified and is ignored by default. Override this property\n" +
" # if you have other files that you'd like to exempt from minification.";
};
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 method of all modules and each module has the opportunity to ensure that the config settings are valid. This method 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 method.
This method is also the last opportunity to edit the module's configuration. After this method is called, the configuration is completely locked down and cannot be edited. For example, a module may take a list of regexs and turn them into a single RegExp object.
exports.validate = function(config, validators) {
var errors = [];
if validators.ifExistsIsObject(errors, "minify config", config.minify) {
if validators.ifExistsIsArray(errors, "minify.exclude", config.minify.exclude) {
var exls = config.minify.exclude;
for (var _i = 0, _len = exls.length; _i < _len; _i++) {
var ex = exls[_i];
if (typeof ex !== "string") {
errors.push("minify.exclude must be an array of strings");
break;
}
}
}
}
if (errors.length === 0 && config.minify.exclude && config.minify.exclude.length > 0) {
config.minify.exclude = new RegExp(config.minify.exclude.join("|"), "i");
}
return errors;
};
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.
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, or isVirgin), 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 below, 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.registration = (mimosaConfig, register) ->
if mimosaConfig.isMinify
e = mimosaConfig.extensions
register ['add','update','buildFile'], 'afterCompile', _minifyJS, [e.javascript...]
register ['add','update','buildExtension'], 'beforeWrite', _minifyJS, [e.template...]
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:
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.register ['add','update','buildFile'], 'afterCompile', _minifyJS, [e.javascript...] register ['add','update','buildExtension'], 'beforeWrite', _minifyJS, [e.template...]
Notice that the register method 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: build, watch and clean.
Modules registered to take place during the build workflows perform project builds. The build workflows are all prefixed with build and they 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 virgin or 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), and when the --clean flag is used with mimosa watch. 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 happens:
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.compiledDirafterWrite step is where optimization takes place if that is selected.And there are many steps in this example that have no functionality registered against them. The beforeRead, afterRead and beforeCompile steps, for example, have no modules that registered to execute against them, but they are there in the event a module comes along that logically has tasks to perform at that point in the workflow.
Also, many modules can register to be executed for the same workflow and step, they'll just be executed in order that they get registered which is determined by their order 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.
Below is the exhaustive list of all of the steps by workflow. Use the radio buttons to toggle between the different default Mimosa modules.
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.
config: 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. The config object is immutable. It cannot be changed. This prevents modules from breaking one another inadvertently.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.
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 a flag indicating if a build is needed, and a callback. That callback should contain the command module's code, and it is passed the full mimosa-config.
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:search 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.
With upcoming Mimosa releases, Mimosa will no longer recognize CoffeeScript modules. The CoffeeScript module skeleton delivered with mod:init -c accounts for this by using Mimosa to build the CoffeeScript module when npm install and publish are used.
If you do create and publish a module, let us know!!!