Awesome Module Manager
This is a way to help achieve modularity in large NodeJS applications.
What is the problem ?
Generally you start a NodeJS project. And it becomes big. So you split it using NPM. However, NPM manages dependencies at the code level, but not at the application level. A complex and modular (web) application needs to provide lots of facilities:
- open connections to the different data stores
- register HTTP resources (assets, javascript, REST endpoints)
- register subscribers on an event emitter, because of, you know, lazy coupling
- ...
And maybe you will need, later, to replace a module by another, so you'll use dependency injection of some kind.
The Awsesome Module Manager aims to facilitate the organization of those kind of lerge apps. It falls into the same kind of software than The Architect.
How does it work
basics
You define states in your applications. You define the actions that the module should do in this state.
var mongo = var AwesomeModule = ;var AwesomeModuleManager = ; var manager = ;// create the datastore_connect state: all states depend of the// lib state, but we specify it anywaymanager; //create a modulevar mongoModule = 'datastore.mongo' states: // the "lib" state is always defined: it is where your module should // expose its API: it's the equivalent of var lib = require('themodule'); { var api = connected: false ; return ; } { // "this" is the api object returned in the lib state var self = this; mongoMongoClient; } ; // then register the module.manager;// and finally fire the datastore_connect state !manager;
dependencies
Now, let's say we want the mongodb connection string to come from our configuration system. We have to create a configuration module, and add a dependency of the mongo module to the config module.
Lib is a special state: the result of the lib state id what will be given to the requester module.
var AwesomeModule = ;var Dependency = AwesomeModuleAwesomeModuleDependency;var AwesomeModuleManager = ;var manager = ;manager; // create the configuration modulevar configModule = 'basic.config' states: { var api = ; // Here we return the api object. This is the object that // will be given to dependent modules return ; } ; var mongoModule = 'datastore.mongo' dependencies: // tell the system we depends on basic.config // and we want it aliased as "conf" DependencyTYPE_NAME 'basic.config' 'conf' states: { var api = connected: false ; return ; } { var self = this; // dependencies contains the dependencies we require // here config will contain the "api" object exported in the lib state // of the "basic.config" module. var config = ; mongoMongoClient; } ; // then register the module.manager;// and finally fire the datastore_connect state !manager;
dependencies niceties
optional dependencies
A module can optionally require a dependency
new Dependency(Dependency.TYPE_NAME, 'basic.config', 'conf', true)
The injected dependencies('conf') may or may not contain the conf object.
dependencies by ability
Sometimes you don't want to express a dependency by it's name, but for the API it provides. For example, you may want a logger, and don't care that it's winston or another implementation.
Modules can tell the abilities they provide:
var someModule = 'some.name' abilities: 'interface.logger' ...;
And modules can express a dependency upon an ability :
var otherModule = 'other.name' dependencies: DependencyTYPE_ABILITY 'interface.logger' 'myLogger' ...;
Dependency callback
A module can specify that it wants a callback to be fired when a dependency is fulfilled and both the module and its dependency reach a certain state.
var AMD = AwesomeModuleAwesomeModuleDependency;var exampleModule = 'example' states: { console; return ; } { console; return ; } ; var dependency = AMDTYPE_NAME 'example' 'example' true;dependency; var dependentModule = 'dependent' states: { console return ; } { console return ; } dependencies: dependency; var manager = ;manager; manager;manager;manager;manager;
will echo
example module lib
example module start
dependent module lib
dependent module start
dependent module start callback
Now, the same code, but with a different loading order, will also work:
var manager = ;manager; manager;manager;manager;manager;
will echo
dependent module lib
dependent module start
example module lib
example module start
dependent module start callback
Module loaders
basics
You can teach the module manager new ways to load modules. Right now it provides two methods:
- the code loader
Here we create the exampleModule programmatically, and then register a code loader for it. This is basically what the AwesomeModuleManager registerModule does under the hood.
var AwesomeModuleManager = ;var exampleModule = 'example.module'; mm = ;var codeLoader1 = mmloaderscodeexampleModule;mm; mm;
- the filesystem loader
This loader lookup the modules in a given path. The module name should correspond with the folder name inside of the path. A require(path/modulename) shoudl return an AwesomeModule.
var AwesomeModuleManager = ;mm = ;var fsLoader1 = mmloaders;mm; mm;// the module manager will check if there is a "test.module" folder inside /some/path.// if found, it will try : var module = require('/some/path/test.module');// and then check that module is an awesome module
The module manager loaders are middleware : they are called in order, until one of them find the module.
trusted and untrusted loaders
In such a system, maybe you'll have to load one day third party modules. The loader API allows you to flag a loader as being trusted or untrusted. This information will be propagated when a module requires another module (see Module API proxy section below).
Be default, loaders are configured as untrusted.
var AwesomeModuleManager = ;mm = ; // create an untrusted loader. All modules loaded through it (so, that are found under /some/path) will be flagged as untrusted.var fsLoader1 = mmloaders;mm; // create a trusted loader. All modules loaded through it will be flagged as trusted.var fsLoader2 = mmloaders;mm;
Creating your own loader
It's kind of easy to create your own awesome module loader. You have to provide a function that takes as arguments a module name, and a callback. This function tries to load the module, and then fires the callback, either with the module, or with nothing. Here is an example for a "require" loader:
{ var mod; try mod = ; catche // not finding a module through a loader is not to be considered as an error // the function will just call the callback with an empty "mod" argument return ;}
You then create an AwesomeModuleLoader instance, passing the loader name, the loading function, and whether thtis loader is a trusted one.
var AwesomeModuleManager = ;var requireloader = 'requireLoader' load true;mm = ;mm;
Module API proxy
Basics
You can adapt the API presented to the requesting module by adding a proxy method to the module.
var AwesomeModule = ;var Dependency = AwesomeModuleAwesomeModuleDependency;var AwesomeModuleManager = ;var manager = ;manager; // this module will export different API, depending on the requestervar configModule = 'basic.config' states: { var api = color: 'blue' return ; } { if requesterName === 'basic.sun' return color: 'yellow'; else return this; }; var skyModule = 'basic.sky' dependencies: DependencyTYPE_NAME 'basic.config' 'conf' states: { var conf = ; console; //in basic.sky module, conf color = blue return ; } ; var sunModule = 'basic.sun' dependencies: DependencyTYPE_NAME 'basic.config' 'conf' states: { var conf = ; console; //in basic.sun module, conf color = yellow return ; } ; manager;manager;// and finally fire the datastore_connect state !manager;
Use with trusted & untrusted loaders
The main interest of this feature is to be able to present a distinct API when a module is trusted, or untrusted.
var AwesomeModuleManager = ;var exampleModule = 'example.module' states: { var conf = ; conf; // will echo "I do not beliave you" because the associated loader codeLoader1 is untrusted } dependencies: DependencyTYPE_NAME 'basic.config' 'conf' ; var exampleModule2 = 'example.module2' states: { var conf = ; conf; // will echo "doing something tricky..." because the associated loader codeLoader2 is trusted } dependencies: DependencyTYPE_NAME 'basic.config' 'conf' ; var configModule = 'basic.config' states: { var api = { console; } ; return ; } { if !trusted return { console; } ; else return this; }; mm = ;var codeLoader1 = mmloaderscodeexampleModule;mm;var codeLoader2 = mmloaderscodeexampleModule2;mm;var confLoader = mmloaderscodeconfigModule;mm; mm;
events
An AwesomeModuleManager is also an event emitter.
Here is the list of emitted events:
Event | Associated data | Description |
---|---|---|
loader:loadstart | name: the module name, context: the module loading context | fired when the manager starts the loading process for a module |
loader: loaderror | name: the module name, module: the module object, context: the module loading context, error: the error | fired when the manager encountered an error while trying to load a module |
loader:loaded | name: the module name, module: the module object, context: the module loading context | fired when a module finished loading successfully |
state:fire | state: the state name, module: the module object | fired when launching a certain state on a module |
state:fulfilled | state: the state name, module: the module object | fired when a certain state has been successfully reached on a module |
state:failed | state: the state name, module: the module object, error: the error object | fired when a certain state failed to complete, either because of a direct error thrown, or because the module returns an error object |