app-container
Version:
asynchronous IoC container for node.js applications
355 lines (325 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _debug = require('debug');
var _debug2 = _interopRequireDefault(_debug);
var _dependencyGraph = require('dependency-graph');
var _glob = require('glob');
var _glob2 = _interopRequireDefault(_glob);
var _lodash = require('lodash');
var _component = require('./component');
var _component2 = _interopRequireDefault(_component);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const debug = (0, _debug2.default)('app-container');
const PLUGIN = /^(all|any|container)!.*/;
class Container {
/**
* Constructor function
* @param {Object} [options={}]
* @param {String} options.namespace - namespace key
* @param {Object} options.defaults - default module options
* @return {Container}
*/
constructor(options = {}) {
this.registry = {};
this.options = options;
}
/**
* adapted from http://stackoverflow.com/questions/16697791/nodejs-get-filename-of-caller-function
* @return {String|Undefined}
* @private
*/
static _getCallerFile() {
try {
const err = new Error();
const currentfile = err.stack.shift().getFileName();
let callerfile;
while (err.stack.length) {
callerfile = err.stack.shift().getFileName();
if (currentfile !== callerfile) return callerfile;
}
} catch (err) {
return undefined;
}
return undefined;
}
/**
* Register modules that match the specified pattern
* @param {String} pattern - glob pattern, see glob.js
* @param {Object} options - glob options
*/
glob(pattern, options = {}) {
if (!Object.prototype.hasOwnProperty.call(options, 'cwd')) {
options.cwd = process.cwd(); // eslint-disable-line
}
const filenames = _glob2.default.sync(pattern, options);
const namespace = this.options.namespace || 'inject';
const caller = Container._getCallerFile();
filenames.forEach(filename => {
if (`${options.cwd}/${filename}` === caller) {
debug(`ignoring calling file: ${filename}`);
return null;
}
debug(`registering component: ${filename}`);
const result = (0, _lodash.attempt)(() => {
this._registerFile({ filename, namespace, options });
});
if (result instanceof Error) {
debug(`error registering component: ${filename}`, result);
}
return null;
});
this._validateDependencyGraph();
}
/**
* Load one or more modules
* @example Load a single component
* const myModule = await container.load('my-module')
*
* @example Load multiple components
* const [myModule, myOtherModule] = await container.load('my-module', 'my-other-module')
* // or
* const [myModule, myOtherModule] = await container.load([
* 'my-module',
* 'my-other-module'
* ])
*
* @example Load in groups
* const { services, models } = await container.load({
* services: {
* foo: 'services/foo',
* bar: 'services/bar',
* },
* models: {
* users: 'models/users',
* comments: 'models/comments',
* },
* })
* const { foo, bar } = services;
* const { users, comments } = models;
*
* @param {...String|Object} components - components to load
* @return {Bluebird}
*/
load(...components) {
return _bluebird2.default.try(() => {
if (components.length === 1 && Array.isArray(components[0])) {
return this.load(...components[0]);
}if (components.length === 1 && typeof components[0] === 'string') {
return this._load(components[0]);
}if (components.length === 1 && typeof components[0] === 'object') {
return this._loadRecursive(components[0]);
}if (Array.isArray(components)) {
return _bluebird2.default.mapSeries(components, name => this._load(name));
}
throw new Error('Invalid load signature');
});
}
/**
* Load a single component by name
* @param {String} name
* @return {Bluebird}
* @private
*/
_load(name) {
return _bluebird2.default.try(() => {
if (PLUGIN.test(name)) {
const [plugin, n] = name.split('!');
switch (plugin) {
case 'all':
return this._loadAll(n);
case 'any':
return this._loadAny(n);
case 'container':
return this;
default:
throw new Error(`Unknown plugin ${plugin}`);
}
}
const component = this.registry[name];
if (!component) {
throw new Error(`Unknown Component: ${name}`);
}
return component.load(this);
});
}
/**
* Load all components matching a given pattern into a dependency object
* @param {String} pattern
* @return {Bluebird}
* @private
*/
_loadAll(pattern) {
const re = new RegExp(pattern);
const matches = Object.keys(this.registry).reduce((memo, name) => {
/* eslint-disable no-param-reassign */
if (re.test(name)) {
memo[name] = name;
}
return memo;
}, {});
return this._loadRecursive(matches);
}
/**
* Load any components matching a given pattern into an array of dependencies
* @param {String} pattern
* @return {Bluebird}
* @private
*/
_loadAny(pattern) {
return this._loadAll(pattern).then(deps => Object.keys(deps).reduce((memo, name) => [...memo, deps[name]], []));
}
/**
* Load a component map (recursively) where string values are replaced with their
* respective components
* @param {Object} map
* @param {Object} [accum={}] - object to assign components to
* @return {Bluebird}
* @private
*/
_loadRecursive(map, accum = {}) {
return _loadRecursive(this, map, accum);
}
/**
* Manually register a module spec with the container instance
* @param {Object} mod - module spec
* @param {Object|String} name - options or module name
* @param {Object} [options] - component options
* @return {Undefined}
*/
register(mod, name, options = {}) {
if (typeof name === 'object' && Object.prototype.hasOwnProperty.call(name, 'name')) {
return this._registerNamespaced(mod, name.name, name);
}
return this._registerNamespaced(mod, name, options);
}
/**
* Register a module with the container
* @param {Object} params - parameters
* @param {String} params.filename - file name
* @param {String} params.namespace - container namespace
* @param {Object} params.options - glob options
* @return {Function}
* @private
*/
_registerFile({ filename, namespace, options }) {
const mod = require(`${options.cwd}/${filename}`); // eslint-disable-line
if (!(typeof mod[namespace] === 'object' && !Array.isArray(mod[namespace]))) {
throw new Error(`Invalid namespace declaration found for file: ${filename}`);
}
const opts = mod[namespace];
if (mod[namespace]) {
const name = opts.name || filename.replace(/\.[^/.]+$/, '');
this._registerNamespaced(mod, name, opts);
} else {
debug(`No namespace found for component, skipping: ${filename}`);
}
return null;
}
/**
* Register a given component for modules that declare themselves via the namespaced
* declaration style
* @param {Object} mod - module spec
* @param {Object} options - component options
* @private
*/
_registerNamespaced(mod, name, options) {
(0, _lodash.defaults)(options, this.options.defaults || {});
let main = mod;
if (mod.__esModule && mod.default) {
main = mod.default;
}
const component = new _component2.default({ mod: main, name, options });
this.registry[component.name] = component;
}
/**
* Build a dependency graph for all registered modules and ensure that a) there
* are no circular dependencies and b) there are no missing modules
* @private
*/
_validateDependencyGraph() {
// create a new dependency graph
const graph = new _dependencyGraph.DepGraph();
// for every component in the registry, add each to the graph along along
// with declaring its dependencies
Object.keys(this.registry).forEach(name => {
const { options } = this.registry[name];
if (!graph.hasNode(name)) {
graph.addNode(name);
}
if (options.require) {
if (Array.isArray(options.require)) {
options.require.forEach(dep => {
if (PLUGIN.test(dep)) {
return;
}
if (!graph.hasNode(dep)) {
graph.addNode(dep);
}
graph.addDependency(name, dep);
});
} else if (typeof options.require === 'object') {
_recursiveDeps(graph, name, options.require);
}
}
});
// ensure no circular dependencies and that we have a component registered
// for every node in the graph
const overallOrder = graph.overallOrder();
overallOrder.forEach(name => {
if (!PLUGIN.test(name) && !(this.registry[name] instanceof _component2.default)) {
throw new Error(`No component found for dependency (${name})`);
}
});
}
}
exports.default = Container; /**
* Recursively load a component map
* @param {Container} container
* @param {Object} map
* @param {Object} [accum={}]
* @return {Bluebird}
* @private
*/
function _loadRecursive(container, map, accum = {}) {
return _bluebird2.default.reduce(Object.keys(map), (memo, key) => {
const path = map[key];
if (typeof path === 'string') {
return container._load(path).then(mod => {
(0, _lodash.set)(memo, key, mod);
return memo;
});
}
return _loadRecursive(container, path).then(mods => {
(0, _lodash.set)(memo, key, mods);
return memo;
});
}, accum);
}
/**
* Add recursive dependencies to dependency graph
* @param {DepGraph} graph
* @param {String} name - current node name
* @param {Object} deps - dependency map
* @private
*/
function _recursiveDeps(graph, name, deps) {
return Object.keys(deps).forEach(key => {
const dep = deps[key];
if (PLUGIN.test(dep)) {
return;
}
if (typeof dep === 'string') {
if (!graph.hasNode(dep)) {
graph.addNode(dep);
}
graph.addDependency(name, dep);
} else if (typeof dep === 'object') {
_recursiveDeps(graph, name, deps[key]);
}
});
}
module.exports = exports.default;