The perfect AMD setup
I’ve been using AMD modules (RequireJS) for various projects for a while. It has some advantages over ‘CommonJS’ (ie. Browserify) solutions, but has additional complexity around package installation.
One thing I disliked about AMD was having to maintain a large paths config to resolve third-party package sub-dependencies. Here’s an example of one of these:
require.config({
baseUrl: '/js',
// 'paths' lets us alias complex
// paths to something simpler and
// resolve sub-dependency require paths
paths: {
'debug': '../bower_components/debug/index',
'attach': '../bower_components/attach/index',
'model': '../bower_components/model/index',
'view': '../bower_components/view/index',
'evt': '../bower_components/evt/index',
'drag': '../bower_components/drag/index',
'device-orientation': '../bower_components/device-orientation/index',
'gaia-header': '../bower_components/gaia-header/gaia-header',
'gaia-icons': '../bower_components/gaia-icons/gaia-icons',
'gaia-component': '../bower_components/gaia-component'
}
});This config means that anywhere in my app code or third-party code I will be able to call require('gaia-component'), and it’ll actually require js/bower_components/gaia-component, under the hood.
As gaia-component is a dependency of the gaia-header module, this path config must be in place, otherwise gaia-header will error.
Installation process
Bower/AMD package installation process is involved and error prone:
$ bower install cool-package- Search through source code of
cool-packageand identify any sub-dependencyrequire()s. - Amend the
require.configpaths to map any sub dependency paths to their correct locations. - Run app, on failure return to step 2.
By contrast the NPM installation process is dreamy:
$ npm install cool-packagerequire('cool-package');
The good news is there’s a better solution for Bower and AMD users which enables:
$ bower install cool-packagerequire('cool-package');
Seamlessly resolving any nested require('some-sub-dependency'). Read on, all will be revealed…
A better solution
I was discussing this problem with James Burke and he kindly wrote a small command-line tool called ‘adapt-pkg-main’ to help. The documentation in the repo is thorough, but I’ll try to outline briefly how it works.
Step One: Package main adapters
Adapt-pkg-main will look for packages in a given packages directory (eg. bower_components/). For each package in the directory it will look for a package description file (eg. bower.json, package.json). Both bower.json and package.json have the convention of a "main" attribute which declares the package’s entry point (eg. cool-package/cool-package.js).
Adapt-pkg-main will use this "main" value to create a new ‘adapter’ module for each package like so:
Main file: bower_components/cool-package/cool-package.js
Created adapter: bower_components/cool-package.js
The cool-package.js adapter is AMD by default, but this ‘adapterText’ can be configured:
define(['./cool-package/cool-package'], function(m) { return m; });Step Two: require.config
The second part of the trick is to configure RequireJS to assume all unprefixed paths to look in bower_components.
require.config({
baseUrl: 'bower_components',
paths: { root: '../' }
});
require(['root/app'])After this one-time configuration, paths will be resolved as follows:
require('cool-package')->bower_components/cool-package.jsrequire('some-sub-dependency')->bower_components/some-sub-dependency.jsrequire('./lib/foo')->CURRENT_MODULE_DIR/lib/foo.jsrequire('root/app')->/app.js
You’ll notice that the unprefixed paths now resolve to the adapter files that adapt-pkg-main created, so the full resolution (via the adapter module) will look like:
require('cool-package') -> bower_components/cool-package.js -> bower_components/cool-package/cool-package.js
Path convention
Once you have this require configuration, it makes sense to use relative style paths whenever you’re requiring a file that doesn’t live in bower_components/, otherwise it’ll have to prefixed with root/. Just write your require() paths exactly as you would in Node.
Automation
It makes sense to plug the adapter creation step into a Bower postinstall hook so we can forget all about it. Create a .bowerrc file at the root of you project (if you don’t already have one) with the following:
{
"scripts": {
"postinstall": "./node_modules/.bin/adapt-pkg-main bower_components configFileNames=bower.json,package.json"
}
}Profit
Drop a comment if you have any questions or improvements :)
bower install sweet-dreams
require('sweet-dreams');