loopback-boot/lib/bootstrapper.js

214 lines
5.8 KiB
JavaScript

// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback-boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var _ = require('lodash');
var assert = require('assert');
var async = require('async');
var utils = require('./utils');
var path = require('path');
var pluginLoader = require('./plugin-loader');
var debug = require('debug')('loopback:boot:bootstrapper');
var Promise = require('bluebird');
var arrayToObject = require('./utils').arrayToObject;
module.exports = Bootstrapper;
function createPromiseCallback() {
var cb;
var promise = new Promise(function(resolve, reject) {
cb = function(err, data) {
if (err) return reject(err);
return resolve(data);
};
});
cb.promise = promise;
return cb;
}
var builtinPlugins = [
'application', 'datasource', 'model', 'mixin',
'middleware', 'component', 'boot-script', 'swagger',
];
var builtinPhases = [
'load', 'compile', 'starting', 'start', 'started',
];
function loadAndRegisterPlugins(bootstrapper, options) {
var loader = pluginLoader(options);
var loaderContext = {};
loader.load(loaderContext);
loader.compile(loaderContext);
for (var i in loaderContext.instructions.pluginScripts) {
bootstrapper.use('/boot/' + i, loaderContext.instructions.pluginScripts[i]);
}
}
/**
* Create a new Bootstrapper with options
* @param options
* @constructor
*/
function Bootstrapper(options) {
this.plugins = [];
options = options || {};
if (typeof options === 'string') {
options = {appRootDir: options};
}
// For setting properties without modifying the original object
options = Object.create(options);
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
var env = options.env || process.env.NODE_ENV || 'development';
var scriptExtensions = options.scriptExtensions ?
arrayToObject(options.scriptExtensions) :
require.extensions;
var appConfigRootDir = options.appConfigRootDir || appRootDir;
options.rootDir = appConfigRootDir;
options.env = env;
options.scriptExtensions = scriptExtensions;
this.options = options;
this.phases = options.phases || builtinPhases;
this.builtinPlugins = options.plugins || builtinPlugins;
assert(Array.isArray(this.phases), 'Invalid phases: ' + this.phases);
assert(Array.isArray(this.plugins), 'Invalid plugins: ' +
this.builtinPlugins);
var self = this;
self.builtinPlugins.forEach(function(p) {
var factory = require('./plugins/' + p);
self.use('/boot/' + p, factory(options));
});
try {
loadAndRegisterPlugins(self, options);
} catch (err) {
debug('Cannot load & register plugins: %s', err.stack || err);
}
}
/**
* Register a handler to a given path
* @param {String} path
* @param {Function} handler
*/
Bootstrapper.prototype.use = function(path, handler) {
var plugin = {
path: path,
handler: handler,
};
this.plugins.push(plugin);
};
/**
* Get a list of plugins for the given path
* @param {String} path
* @returns {*}
*/
Bootstrapper.prototype.getPlugins = function(path) {
if (path[path.length - 1] !== '/') {
path = path + '/';
}
return this.plugins.filter(function(p) {
return p.path.indexOf(path) === 0;
});
};
/**
* Get a list of extensions for the given path
* @param {String} path
* @returns {*}
*/
Bootstrapper.prototype.getExtensions = function(path) {
if (path[path.length - 1] !== '/') {
path = path + '/';
}
return this.plugins.filter(function(p) {
if (p.path.indexOf(path) === -1) return false;
var name = p.path.substring(path.length);
return name && name.indexOf('/') === -1;
});
};
/**
* Add more phases. The order of phases is decided by the sequence of phase
* names
* @param {String[]} phases An array of phase names
* @returns {String[]} New list of phases
*/
Bootstrapper.prototype.addPhases = function(phases) {
this.phases = utils.mergePhaseNameLists(this.phases, phases || []);
return this.phases;
};
function pluginIteratorFactory(context, phase) {
return function executePluginPhase(plugin, done) {
var result;
if (typeof plugin.handler[phase] !== 'function') {
debug('Skipping %s.%s', plugin.handler.name, phase);
return done();
}
debug('Invoking %s.%s', plugin.handler.name, phase);
try {
if (plugin.handler[phase].length === 2) {
plugin.handler[phase](context, done);
} else {
result = plugin.handler[phase](context);
Promise.resolve(result)
.then(function onPluginPhaseResolved(value) {
done(null, value);
}, function onPluginPhaseRejected(err) {
debug('Unable to invoke %s.%s()', plugin.name, phase, err);
done(err);
});
}
} catch (err) {
debug('Unable to invoke %s.%s()', plugin.name, phase, err);
done(err);
}
};
}
/**
* Invoke the plugins phase by phase with the given context
* @param {Object} context Context object
* @param {Function} done Callback function. If not provided, a promise will be
* returned
* @returns {*}
*/
Bootstrapper.prototype.run = function(context, done) {
if (!done) {
done = createPromiseCallback();
}
var options = this.options;
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
var env = options.env || process.env.NODE_ENV || 'development';
var appConfigRootDir = options.appConfigRootDir || appRootDir;
options.rootDir = appConfigRootDir;
options.env = env;
context = context || {};
var phases = context.phases || this.phases;
var bootPlugins = this.getExtensions('/boot');
async.eachSeries(phases, function(phase, done) {
debug('Phase %s', phase);
async.eachSeries(bootPlugins, pluginIteratorFactory(context, phase), done);
}, function(err) {
return done(err, context);
});
return done.promise;
};