Refactor for modular and pluggable design
- refactor logic of processing artifacts into their own classes - introduce Container as the main class for bootstrapping and build a registry of handlers during boot to organize them by a hierarchy denoted by path - adopt middleware like registration and invocation - container.use(path, handler) - container.run(context, done) - allow more phases during boot - boot is now asynchronous
This commit is contained in:
parent
314dff9f5f
commit
ac1571ccf1
|
@ -4,3 +4,27 @@ All breaking changes must be described here. When adding a new entry,
|
||||||
always describe the impact on users and instructions for upgrading
|
always describe the impact on users and instructions for upgrading
|
||||||
applications from 2.x to 3.0.
|
applications from 2.x to 3.0.
|
||||||
|
|
||||||
|
## boot.compile is now async and returns context with instructions
|
||||||
|
|
||||||
|
Users that uses `boot.compile()` in the following syntax would need to update
|
||||||
|
their implementation, it now calls callback function with `instructions` in
|
||||||
|
`context` object rather than synchronously returning instructions.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var APP_PATH = path.resolve('../sandbox');
|
||||||
|
var instructions = boot.compile(APP_PATH);
|
||||||
|
```
|
||||||
|
|
||||||
|
New signature:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var APP_PATH = path.resolve('../sandbox');
|
||||||
|
var instructions;
|
||||||
|
boot.compile(APP_PATH, function(err, context) {
|
||||||
|
instructions = context.instructions;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Please see [PR #181](https://github.com/strongloop/loopback-boot/pull/181) for full details of change.
|
||||||
|
|
19
browser.js
19
browser.js
|
@ -3,7 +3,7 @@
|
||||||
// This file is licensed under the MIT License.
|
// This file is licensed under the MIT License.
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
var execute = require('./lib/executor');
|
var Bootstrapper = require('./lib/bootstrapper');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The browser version of `bootLoopBackApp`.
|
* The browser version of `bootLoopBackApp`.
|
||||||
|
@ -21,17 +21,24 @@ var execute = require('./lib/executor');
|
||||||
* @header boot(app)
|
* @header boot(app)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports = module.exports = function bootBrowserApp(app, options) {
|
exports = module.exports = function bootBrowserApp(app, options, callback) {
|
||||||
// Only using options.id to identify the browserified bundle to load for
|
// Only using options.id to identify the browserified bundle to load for
|
||||||
// this application. If no Id was provided, load the default bundle.
|
// this application. If no Id was provided, load the default bundle.
|
||||||
var moduleName = 'loopback-boot#instructions';
|
var moduleName = 'loopback-boot#instructions';
|
||||||
if (options && typeof options === 'object' && options.appId)
|
var appId = options && typeof options === 'object' && options.appId;
|
||||||
moduleName += '-' + options.appId;
|
if (appId)
|
||||||
|
moduleName += '-' + appId;
|
||||||
|
|
||||||
// The name of the module containing instructions
|
// The name of the module containing instructions
|
||||||
// is hard-coded in lib/bundler
|
// is hard-coded in lib/bundler
|
||||||
var instructions = require(moduleName);
|
var instructions = require(moduleName);
|
||||||
execute(app, instructions);
|
|
||||||
|
var bootstrapper = new Bootstrapper(options);
|
||||||
|
bootstrapper.phases = ['starting', 'start', 'started'];
|
||||||
|
var context = {
|
||||||
|
app: app,
|
||||||
|
instructions: instructions,
|
||||||
|
};
|
||||||
|
return bootstrapper.run(context, callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.execute = execute;
|
|
||||||
|
|
58
index.js
58
index.js
|
@ -4,14 +4,11 @@
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
// Strong globalize
|
// Strong globalize
|
||||||
var SG = require('strong-globalize');
|
var g = require('./lib/globalize');
|
||||||
SG.SetRootDir(__dirname);
|
|
||||||
|
|
||||||
var ConfigLoader = require('./lib/config-loader');
|
var PluginBase = require('./lib/plugin-base');
|
||||||
var compile = require('./lib/compiler');
|
var Bootstrapper = require('./lib/bootstrapper');
|
||||||
var execute = require('./lib/executor');
|
|
||||||
var addInstructionsToBrowserify = require('./lib/bundler');
|
var addInstructionsToBrowserify = require('./lib/bundler');
|
||||||
var utils = require('./lib/utils');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize an application from an options object or
|
* Initialize an application from an options object or
|
||||||
|
@ -147,11 +144,29 @@ var utils = require('./lib/utils');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports = module.exports = function bootLoopBackApp(app, options, callback) {
|
exports = module.exports = function bootLoopBackApp(app, options, callback) {
|
||||||
|
if (typeof options === 'function' && callback === undefined) {
|
||||||
|
callback = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
// backwards compatibility with loopback's app.boot
|
// backwards compatibility with loopback's app.boot
|
||||||
options.env = options.env || app.get('env');
|
options.env = options.env || app.get('env');
|
||||||
|
|
||||||
var instructions = compile(options);
|
var bootstrapper = new Bootstrapper(options);
|
||||||
execute(app, instructions, callback);
|
|
||||||
|
var context = {
|
||||||
|
bootstrapper: bootstrapper,
|
||||||
|
app: app,
|
||||||
|
};
|
||||||
|
|
||||||
|
return bootstrapper.run(context, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.compile = function(options, done) {
|
||||||
|
var bootstrapper = new Bootstrapper(options);
|
||||||
|
bootstrapper.phases = ['load', 'compile'];
|
||||||
|
var context = {};
|
||||||
|
return bootstrapper.run(context, done);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,14 +179,25 @@ exports = module.exports = function bootLoopBackApp(app, options, callback) {
|
||||||
*
|
*
|
||||||
* @header boot.compileToBrowserify(options, bundler)
|
* @header boot.compileToBrowserify(options, bundler)
|
||||||
*/
|
*/
|
||||||
exports.compileToBrowserify = function(options, bundler) {
|
exports.compileToBrowserify = function(options, bundler, done) {
|
||||||
addInstructionsToBrowserify(compile(options), bundler);
|
return exports.compile(options, function(err, context) {
|
||||||
|
if (err) return done(err);
|
||||||
|
addInstructionsToBrowserify({ instructions: context.instructions },
|
||||||
|
bundler);
|
||||||
|
done();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/* -- undocumented low-level API -- */
|
|
||||||
|
|
||||||
exports.ConfigLoader = ConfigLoader;
|
|
||||||
exports.compile = compile;
|
|
||||||
exports.execute = execute;
|
|
||||||
exports.utils = utils;
|
|
||||||
exports.addInstructionsToBrowserify = addInstructionsToBrowserify;
|
exports.addInstructionsToBrowserify = addInstructionsToBrowserify;
|
||||||
|
exports.Bootstrapper = Bootstrapper;
|
||||||
|
exports.PluginBase = PluginBase;
|
||||||
|
|
||||||
|
exports.execute = function(app, instructions, done) {
|
||||||
|
var bootstrapper = new Bootstrapper(
|
||||||
|
{ phases: ['starting', 'start', 'started'] });
|
||||||
|
var context = {
|
||||||
|
app: app,
|
||||||
|
instructions: instructions,
|
||||||
|
};
|
||||||
|
return bootstrapper.run(context, done);
|
||||||
|
};
|
||||||
|
|
|
@ -1,25 +1,16 @@
|
||||||
{
|
{
|
||||||
"ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.",
|
|
||||||
"1e5fea50eef843cbffd1d438494912c8": "Cannot resolve path \"{0}\"",
|
"1e5fea50eef843cbffd1d438494912c8": "Cannot resolve path \"{0}\"",
|
||||||
"34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"",
|
"34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"",
|
||||||
"46e3ab0ef1149ce0a171b5fac2612ea3": "{{Middleware}} \"{0}\" not found: {1}",
|
"3a7049e42006e8bc19e0f4fc8df63b6b": "The `app` is powered by an incompatible loopback version {0}. Supported versions: {1}",
|
||||||
"79e93b2a95e969788590c14e26bb2c1b": "The data in {{model-config.json}} is in the unsupported 1.x format.",
|
"3f93b626dd9a1c33d67490f6e71018b5": "WARNING: Main config file \"{0}{{.json}}\" is missing",
|
||||||
"978a25819e71602cad691dbe7ba17592": "{0} config must be a valid JSON object",
|
"4d052d84c8620730afd4a30832f11724": "Cannot configure unknown model {0}",
|
||||||
"be2dcdab7aa493ed8d77287eb45cfec8": "cannot require directory contents without directory name",
|
|
||||||
"2634623ad4b2c5902f6c6bb25e68b733": "WARNING: Main {{config}} file \"{0}.json\" is missing",
|
|
||||||
"4ed668e9187650d898acf97707df445a": "The {{phase}} \"{0}\" is not defined in the main config.",
|
"4ed668e9187650d898acf97707df445a": "The {{phase}} \"{0}\" is not defined in the main config.",
|
||||||
"6de7e97f033f2cf477297b3d05a93608": "The {{middleware}} \"{0}\" in phase \"{1}\"is not defined in the main config.",
|
"6447e6b342a2c51ab0bc53b3cbdf3742": "Ordering conflict: cannot add \"{0}\" after \"{1}\", because the opposite order was already specified",
|
||||||
"94a0c7d5ab6462f7892b90c63f316f42": "invalid array: {0}",
|
|
||||||
"ec34cc58612cb654742e4cd0a57aca78": "Cannot apply {0}: {1}",
|
|
||||||
"0b91d122f6459c8bbe7865be0936fc4a": "{{app.restBasePath}} is required",
|
|
||||||
"1cda77c9954be299bb7154f73cb6ab74": "{{instructions.middleware.phases}} must be an {{array}}",
|
|
||||||
"22549489736fb0d7eba5a4b08977505f": "{{app.host}} must be a {{string}}",
|
|
||||||
"4c581cc529a7aeda620d5c4b4ef5cfa8": "{{app.restApiRoot}} must start with \"/\"",
|
|
||||||
"6037512314fac9d12af6c654a3804823": "Built-in model {0} should have been defined",
|
|
||||||
"69746d336c89bf4bb371a6c2fe56304d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by app.get().",
|
|
||||||
"70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}",
|
"70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}",
|
||||||
"b078ccd043437a258581e387f93dc1a5": "The `{{app}}` is powered by an incompatible {{loopback}} version {0}. Supported versions: {1}",
|
"7f7bdcadb75abfef1bd8a126d547dd6d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by {{app.get()}}.",
|
||||||
"e8d29edfb313cfe64f5c96cc7d3d5b4b": "When using {{loopback-boot}} with {{loopback}} <1.9, the {{loopback}} module must be available for `{{require('loopback')}}`.",
|
"91a742b7c3568cf6b6755741a70b3c52": "The {{middleware}} \"{0}\" in {{phase}} \"{1}\"is not defined in the main config.",
|
||||||
"f48405e7c61c3d665b601c9ba41da424": "{{app.port}} must be a {{string}} or {{number}}",
|
"a3aa22086ae4976cd013065c9a3ff81c": "Cannot apply {0}: ",
|
||||||
"fa2a7d5137c8891693f9515d48f5b7d7": "{{app.restApiRoot}} must be a {{string}}"
|
"be2cf2868ba54624fe38e9908dde5e9e": "The data in {{model-config.json}} is in the unsupported {{1.x}} format.",
|
||||||
|
"ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.",
|
||||||
|
"fdc23df1bd0fe55fe3faabcc89ff60f3": "Middleware \"{0}\" not found: {1}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,203 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -7,46 +7,60 @@ var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var commondir = require('commondir');
|
var commondir = require('commondir');
|
||||||
var cloneDeep = require('lodash').cloneDeep;
|
var cloneDeep = require('lodash').cloneDeep;
|
||||||
var g = require('strong-globalize')();
|
var g = require('./globalize');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add boot instructions to a browserify bundler.
|
* Add boot instructions to a browserify bundler.
|
||||||
* @param {Object} instructions Boot instructions.
|
* @param {Object} instructions Boot instructions.
|
||||||
* @param {Object} bundler A browserify object created by `browserify()`.
|
* @param {Object} bundler A browserify object created by `browserify()`.
|
||||||
*/
|
*/
|
||||||
|
module.exports = function addInstructionsToBrowserify(context, bundler) {
|
||||||
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
addPlugins(bundler);
|
||||||
bundleModelScripts(instructions, bundler);
|
// bundlePluginScripts(context, bundler);
|
||||||
bundleMixinScripts(instructions, bundler);
|
bundleModelScripts(context, bundler);
|
||||||
bundleComponentScripts(instructions, bundler);
|
bundleMixinScripts(context, bundler);
|
||||||
bundleOtherScripts(instructions, bundler);
|
bundleComponentScripts(context, bundler);
|
||||||
bundleInstructions(instructions, bundler);
|
bundleOtherScripts(context, bundler);
|
||||||
|
bundleInstructions(context, bundler);
|
||||||
};
|
};
|
||||||
|
|
||||||
function bundleOtherScripts(instructions, bundler) {
|
function addPlugins(bundler) {
|
||||||
for (var key in instructions.files) {
|
var dir = path.join(__dirname, './plugins');
|
||||||
addScriptsToBundle(key, instructions.files[key], bundler);
|
var files = fs.readdirSync(dir);
|
||||||
}
|
files.forEach(function(f) {
|
||||||
|
bundler.require(path.join(dir, f),
|
||||||
|
{ expose: './plugins/' + path.basename(f, '.js') });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleModelScripts(instructions, bundler) {
|
function bundleOtherScripts(context, bundler) {
|
||||||
bundleSourceFiles(instructions, 'models', bundler);
|
var list = context.instructions.bootScripts;
|
||||||
|
addScriptsToBundle('boot', list, bundler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleMixinScripts(instructions, bundler) {
|
function bundlePluginScripts(context, bundler) {
|
||||||
bundleSourceFiles(instructions, 'mixins', bundler);
|
var list = context.instructions.pluginScripts;
|
||||||
|
addScriptsToBundle('plugins', list, bundler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleComponentScripts(instructions, bundler) {
|
function bundleModelScripts(context, bundler) {
|
||||||
bundleSourceFiles(instructions, 'components', bundler);
|
bundleSourceFiles(context, 'models', bundler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleSourceFiles(instructions, type, bundler) {
|
function bundleMixinScripts(context, bundler) {
|
||||||
var files = instructions[type]
|
bundleSourceFiles(context, 'mixins', bundler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundleComponentScripts(context, bundler) {
|
||||||
|
bundleSourceFiles(context, 'components', bundler);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundleSourceFiles(context, type, bundler) {
|
||||||
|
var files = context.instructions[type]
|
||||||
.map(function(m) { return m.sourceFile; })
|
.map(function(m) { return m.sourceFile; })
|
||||||
.filter(function(f) { return !!f; });
|
.filter(function(f) { return !!f; });
|
||||||
|
|
||||||
var instructionToFileMapping = instructions[type]
|
var instructionToFileMapping = context.instructions[type]
|
||||||
.map(function(m) { return files.indexOf(m.sourceFile); });
|
.map(function(m) { return files.indexOf(m.sourceFile); });
|
||||||
|
|
||||||
addScriptsToBundle(type, files, bundler);
|
addScriptsToBundle(type, files, bundler);
|
||||||
|
@ -54,7 +68,7 @@ function bundleSourceFiles(instructions, type, bundler) {
|
||||||
// Update `sourceFile` properties with the new paths
|
// Update `sourceFile` properties with the new paths
|
||||||
instructionToFileMapping.forEach(function(fileIx, sourceIx) {
|
instructionToFileMapping.forEach(function(fileIx, sourceIx) {
|
||||||
if (fileIx === -1) return;
|
if (fileIx === -1) return;
|
||||||
instructions[type][sourceIx].sourceFile = files[fileIx];
|
context.instructions[type][sourceIx].sourceFile = files[fileIx];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,14 +88,14 @@ function addScriptsToBundle(name, list, bundler) {
|
||||||
// Add the file to the bundle.
|
// Add the file to the bundle.
|
||||||
bundler.require(filepath, { expose: fileid });
|
bundler.require(filepath, { expose: fileid });
|
||||||
|
|
||||||
// Rewrite the instructions entry with the new id that will be
|
// Rewrite the context entry with the new id that will be
|
||||||
// used to load the file via `require(fileid)`.
|
// used to load the file via `require(fileid)`.
|
||||||
list[ix] = fileid;
|
list[ix] = fileid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleInstructions(instructions, bundler) {
|
function bundleInstructions(context, bundler) {
|
||||||
instructions = cloneDeep(instructions);
|
var instructions = cloneDeep(context.instructions);
|
||||||
|
|
||||||
var hasMiddleware = instructions.middleware.phases.length ||
|
var hasMiddleware = instructions.middleware.phases.length ||
|
||||||
instructions.middleware.middleware.length;
|
instructions.middleware.middleware.length;
|
||||||
|
|
823
lib/compiler.js
823
lib/compiler.js
|
@ -1,823 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
||||||
var assert = require('assert');
|
|
||||||
var cloneDeep = require('lodash').cloneDeep;
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var toposort = require('toposort');
|
|
||||||
var ConfigLoader = require('./config-loader');
|
|
||||||
var utils = require('./utils');
|
|
||||||
var debug = require('debug')('loopback:boot:compiler');
|
|
||||||
var Module = require('module');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var g = require('strong-globalize')();
|
|
||||||
|
|
||||||
var FILE_EXTENSION_JSON = '.json';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gather all bootstrap-related configuration data and compile it into
|
|
||||||
* a single object containing instruction for `boot.execute`.
|
|
||||||
*
|
|
||||||
* @options {String|Object} options Boot options; If String, this is
|
|
||||||
* the application root directory; if object, has the properties
|
|
||||||
* described in `bootLoopBackApp` options above.
|
|
||||||
* @return {Object}
|
|
||||||
*
|
|
||||||
* @header boot.compile(options)
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = function compile(options) {
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
if (typeof options === 'string') {
|
|
||||||
options = { appRootDir: options };
|
|
||||||
}
|
|
||||||
|
|
||||||
var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
|
|
||||||
var env = options.env || process.env.NODE_ENV || 'development';
|
|
||||||
|
|
||||||
var appConfigRootDir = options.appConfigRootDir || appRootDir;
|
|
||||||
var appConfig = options.config ||
|
|
||||||
ConfigLoader.loadAppConfig(appConfigRootDir, env);
|
|
||||||
assertIsValidConfig('app', appConfig);
|
|
||||||
|
|
||||||
var modelsRootDir = options.modelsRootDir || appRootDir;
|
|
||||||
var modelsConfig = options.models ||
|
|
||||||
ConfigLoader.loadModels(modelsRootDir, env);
|
|
||||||
assertIsValidModelConfig(modelsConfig);
|
|
||||||
|
|
||||||
var dsRootDir = options.dsRootDir || appRootDir;
|
|
||||||
var dataSourcesConfig = options.dataSources ||
|
|
||||||
ConfigLoader.loadDataSources(dsRootDir, env);
|
|
||||||
assertIsValidConfig('data source', dataSourcesConfig);
|
|
||||||
|
|
||||||
var middlewareRootDir = options.middlewareRootDir || appRootDir;
|
|
||||||
|
|
||||||
var middlewareConfig = options.middleware ||
|
|
||||||
ConfigLoader.loadMiddleware(middlewareRootDir, env);
|
|
||||||
var middlewareInstructions =
|
|
||||||
buildMiddlewareInstructions(middlewareRootDir, middlewareConfig);
|
|
||||||
|
|
||||||
var componentRootDir = options.componentRootDir || appRootDir;
|
|
||||||
var componentConfig = options.components ||
|
|
||||||
ConfigLoader.loadComponents(componentRootDir, env);
|
|
||||||
var componentInstructions =
|
|
||||||
buildComponentInstructions(componentRootDir, componentConfig);
|
|
||||||
|
|
||||||
// require directories
|
|
||||||
var bootDirs = options.bootDirs || []; // precedence
|
|
||||||
bootDirs = bootDirs.concat(path.join(appRootDir, 'boot'));
|
|
||||||
resolveRelativePaths(bootDirs, appRootDir);
|
|
||||||
|
|
||||||
var bootScripts = options.bootScripts || [];
|
|
||||||
resolveRelativePaths(bootScripts, appRootDir);
|
|
||||||
|
|
||||||
bootDirs.forEach(function(dir) {
|
|
||||||
bootScripts = bootScripts.concat(findScripts(dir));
|
|
||||||
var envdir = dir + '/' + env;
|
|
||||||
bootScripts = bootScripts.concat(findScripts(envdir));
|
|
||||||
});
|
|
||||||
|
|
||||||
// de-dedup boot scripts -ERS
|
|
||||||
// https://github.com/strongloop/loopback-boot/issues/64
|
|
||||||
bootScripts = _.uniq(bootScripts);
|
|
||||||
|
|
||||||
var modelsMeta = modelsConfig._meta || {};
|
|
||||||
delete modelsConfig._meta;
|
|
||||||
|
|
||||||
var modelSources = options.modelSources || modelsMeta.sources || ['./models'];
|
|
||||||
var modelInstructions = buildAllModelInstructions(
|
|
||||||
modelsRootDir, modelsConfig, modelSources, options.modelDefinitions);
|
|
||||||
|
|
||||||
var mixinDirs = options.mixinDirs || [];
|
|
||||||
var mixinSources = options.mixinSources || modelsMeta.mixins || ['./mixins'];
|
|
||||||
var mixinInstructions = buildAllMixinInstructions(
|
|
||||||
appRootDir, mixinDirs, mixinSources, options, modelInstructions);
|
|
||||||
|
|
||||||
// When executor passes the instruction to loopback methods,
|
|
||||||
// loopback modifies the data. Since we are loading the data using `require`,
|
|
||||||
// such change affects also code that calls `require` for the same file.
|
|
||||||
var instructions = {
|
|
||||||
env: env,
|
|
||||||
config: appConfig,
|
|
||||||
dataSources: dataSourcesConfig,
|
|
||||||
models: modelInstructions,
|
|
||||||
middleware: middlewareInstructions,
|
|
||||||
components: componentInstructions,
|
|
||||||
mixins: mixinInstructions,
|
|
||||||
files: {
|
|
||||||
boot: bootScripts,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.appId)
|
|
||||||
instructions.appId = options.appId;
|
|
||||||
|
|
||||||
return cloneDeep(instructions);
|
|
||||||
};
|
|
||||||
|
|
||||||
function assertIsValidConfig(name, config) {
|
|
||||||
if (config) {
|
|
||||||
assert(typeof config === 'object',
|
|
||||||
g.f('%s config must be a valid JSON object', name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertIsValidModelConfig(config) {
|
|
||||||
assertIsValidConfig('model', config);
|
|
||||||
for (var name in config) {
|
|
||||||
var entry = config[name];
|
|
||||||
var options = entry.options || {};
|
|
||||||
var unsupported = entry.properties ||
|
|
||||||
entry.base || options.base ||
|
|
||||||
entry.plural || options.plural;
|
|
||||||
|
|
||||||
if (unsupported) {
|
|
||||||
throw new Error(
|
|
||||||
g.f('The data in {{model-config.json}}' +
|
|
||||||
' is in the unsupported 1.x format.'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all javascript files (except for those prefixed with _)
|
|
||||||
* and all directories.
|
|
||||||
* @param {String} dir Full path of the directory to enumerate.
|
|
||||||
* @return {Array.<String>} A list of absolute paths to pass to `require()`.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
|
|
||||||
function findScripts(dir, extensions) {
|
|
||||||
assert(dir, g.f('cannot require directory contents without directory name'));
|
|
||||||
|
|
||||||
var files = tryReadDir(dir);
|
|
||||||
extensions = extensions || _.keys(require.extensions);
|
|
||||||
|
|
||||||
// sort files in lowercase alpha for linux
|
|
||||||
files.sort(function(a, b) {
|
|
||||||
a = a.toLowerCase();
|
|
||||||
b = b.toLowerCase();
|
|
||||||
|
|
||||||
if (a < b) {
|
|
||||||
return -1;
|
|
||||||
} else if (b < a) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var results = [];
|
|
||||||
files.forEach(function(filename) {
|
|
||||||
// ignore index.js and files prefixed with underscore
|
|
||||||
if (filename === 'index.js' || filename[0] === '_') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filepath = path.resolve(path.join(dir, filename));
|
|
||||||
var stats = fs.statSync(filepath);
|
|
||||||
|
|
||||||
// only require files supported by require.extensions (.txt .md etc.)
|
|
||||||
if (stats.isFile()) {
|
|
||||||
if (isPreferredExtension(filename))
|
|
||||||
results.push(filepath);
|
|
||||||
else
|
|
||||||
debug('Skipping file %s - unknown extension', filepath);
|
|
||||||
} else {
|
|
||||||
debug('Skipping directory %s', filepath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryReadDir() {
|
|
||||||
try {
|
|
||||||
return fs.readdirSync.apply(fs, arguments);
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAllModelInstructions(rootDir, modelsConfig, sources,
|
|
||||||
modelDefinitions) {
|
|
||||||
var registry = verifyModelDefinitions(rootDir, modelDefinitions) ||
|
|
||||||
findModelDefinitions(rootDir, sources);
|
|
||||||
|
|
||||||
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
|
|
||||||
|
|
||||||
var instructions = modelNamesToBuild
|
|
||||||
.map(function createModelInstructions(name) {
|
|
||||||
var config = modelsConfig[name];
|
|
||||||
var definition = registry[name] || {};
|
|
||||||
|
|
||||||
debug('Using model "%s"\nConfiguration: %j\nDefinition %j',
|
|
||||||
name, config, definition.definition);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
config: config,
|
|
||||||
definition: definition.definition,
|
|
||||||
sourceFile: definition.sourceFile,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortByInheritance(instructions);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addAllBaseModels(registry, modelNames) {
|
|
||||||
var result = [];
|
|
||||||
var visited = {};
|
|
||||||
|
|
||||||
while (modelNames.length) {
|
|
||||||
var name = modelNames.shift();
|
|
||||||
|
|
||||||
if (visited[name]) continue;
|
|
||||||
visited[name] = true;
|
|
||||||
result.push(name);
|
|
||||||
|
|
||||||
var definition = registry[name] && registry[name].definition;
|
|
||||||
if (!definition) continue;
|
|
||||||
|
|
||||||
var base = getBaseModelName(definition);
|
|
||||||
|
|
||||||
// ignore built-in models like User
|
|
||||||
if (!registry[base]) continue;
|
|
||||||
|
|
||||||
modelNames.push(base);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBaseModelName(modelDefinition) {
|
|
||||||
if (!modelDefinition)
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
return modelDefinition.base ||
|
|
||||||
modelDefinition.options && modelDefinition.options.base;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortByInheritance(instructions) {
|
|
||||||
// create edges Base name -> Model name
|
|
||||||
var edges = instructions
|
|
||||||
.map(function(inst) {
|
|
||||||
return [getBaseModelName(inst.definition), inst.name];
|
|
||||||
});
|
|
||||||
|
|
||||||
var sortedNames = toposort(edges);
|
|
||||||
|
|
||||||
var instructionsByModelName = {};
|
|
||||||
instructions.forEach(function(inst) {
|
|
||||||
instructionsByModelName[inst.name] = inst;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sortedNames
|
|
||||||
// convert to instructions
|
|
||||||
.map(function(name) {
|
|
||||||
return instructionsByModelName[name];
|
|
||||||
})
|
|
||||||
// remove built-in models
|
|
||||||
.filter(function(inst) {
|
|
||||||
return !!inst;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyModelDefinitions(rootDir, modelDefinitions) {
|
|
||||||
if (!modelDefinitions || modelDefinitions.length < 1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
var registry = {};
|
|
||||||
modelDefinitions.forEach(function(definition, idx) {
|
|
||||||
if (definition.sourceFile) {
|
|
||||||
var fullPath = path.resolve(rootDir, definition.sourceFile);
|
|
||||||
definition.sourceFile = fixFileExtension(
|
|
||||||
fullPath,
|
|
||||||
tryReadDir(path.dirname(fullPath)),
|
|
||||||
true);
|
|
||||||
if (!definition.sourceFile) {
|
|
||||||
debug('Model source code not found: %s - %s', definition.sourceFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Found model "%s" - %s %s',
|
|
||||||
definition.definition.name,
|
|
||||||
'from options',
|
|
||||||
definition.sourceFile ?
|
|
||||||
path.relative(rootDir, definition.sourceFile) :
|
|
||||||
'(no source file)');
|
|
||||||
|
|
||||||
var modelName = definition.definition.name;
|
|
||||||
if (!modelName) {
|
|
||||||
debug('Skipping model definition without Model name ' +
|
|
||||||
'(from options.modelDefinitions @ index %s)',
|
|
||||||
idx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
registry[modelName] = definition;
|
|
||||||
});
|
|
||||||
|
|
||||||
return registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findModelDefinitions(rootDir, sources) {
|
|
||||||
var registry = {};
|
|
||||||
|
|
||||||
sources.forEach(function(src) {
|
|
||||||
var srcDir = tryResolveAppPath(rootDir, src, { strict: false });
|
|
||||||
if (!srcDir) {
|
|
||||||
debug('Skipping unknown module source dir %j', src);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = tryReadDir(srcDir);
|
|
||||||
|
|
||||||
files
|
|
||||||
.filter(function(f) {
|
|
||||||
return f[0] !== '_' && path.extname(f) === '.json';
|
|
||||||
})
|
|
||||||
.forEach(function(f) {
|
|
||||||
var fullPath = path.resolve(srcDir, f);
|
|
||||||
var entry = loadModelDefinition(rootDir, fullPath, files);
|
|
||||||
var modelName = entry.definition.name;
|
|
||||||
if (!modelName) {
|
|
||||||
debug('Skipping model definition without Model name: %s',
|
|
||||||
path.relative(srcDir, fullPath));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
registry[modelName] = entry;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return registry;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAppPath(rootDir, relativePath, resolveOptions) {
|
|
||||||
var resolvedPath = tryResolveAppPath(rootDir, relativePath, resolveOptions);
|
|
||||||
if (resolvedPath === undefined && !resolveOptions.optional) {
|
|
||||||
var err = new Error(g.f('Cannot resolve path "%s"', relativePath));
|
|
||||||
err.code = 'PATH_NOT_FOUND';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return resolvedPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryResolveAppPath(rootDir, relativePath, resolveOptions) {
|
|
||||||
var fullPath;
|
|
||||||
var start = relativePath.substring(0, 2);
|
|
||||||
|
|
||||||
/* In order to retain backward compatibility, we need to support
|
|
||||||
* two ways how to treat values that are not relative nor absolute
|
|
||||||
* path (e.g. `relativePath = 'foobar'`)
|
|
||||||
* - `resolveOptions.strict = true` searches in `node_modules` only
|
|
||||||
* - `resolveOptions.strict = false` attempts to resolve the value
|
|
||||||
* as a relative path first before searching `node_modules`
|
|
||||||
*/
|
|
||||||
resolveOptions = resolveOptions || { strict: true };
|
|
||||||
|
|
||||||
var isModuleRelative = false;
|
|
||||||
if (relativePath[0] === '/') {
|
|
||||||
fullPath = relativePath;
|
|
||||||
} else if (start === './' || start === '..') {
|
|
||||||
fullPath = path.resolve(rootDir, relativePath);
|
|
||||||
} else if (!resolveOptions.strict) {
|
|
||||||
isModuleRelative = true;
|
|
||||||
fullPath = path.resolve(rootDir, relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullPath) {
|
|
||||||
// This check is needed to support paths pointing to a directory
|
|
||||||
if (utils.fileExistsSync(fullPath)) {
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fullPath = require.resolve(fullPath);
|
|
||||||
return fullPath;
|
|
||||||
} catch (err) {
|
|
||||||
if (!isModuleRelative) {
|
|
||||||
debug ('Skipping %s - %s', fullPath, err);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle module-relative path, e.g. `loopback/common/models`
|
|
||||||
|
|
||||||
// Module.globalPaths is a list of globally configured paths like
|
|
||||||
// [ env.NODE_PATH values, $HOME/.node_modules, etc. ]
|
|
||||||
// Module._nodeModulePaths(rootDir) returns a list of paths like
|
|
||||||
// [ rootDir/node_modules, rootDir/../node_modules, etc. ]
|
|
||||||
var modulePaths = Module.globalPaths
|
|
||||||
.concat(Module._nodeModulePaths(rootDir));
|
|
||||||
|
|
||||||
fullPath = modulePaths
|
|
||||||
.map(function(candidateDir) {
|
|
||||||
var absPath = path.join(candidateDir, relativePath);
|
|
||||||
try {
|
|
||||||
// NOTE(bajtos) We need to create a proper String object here,
|
|
||||||
// otherwise we can't attach additional properties to it
|
|
||||||
var filePath = new String(require.resolve(absPath));
|
|
||||||
filePath.unresolvedPath = absPath;
|
|
||||||
return filePath;
|
|
||||||
} catch (err) {
|
|
||||||
return absPath;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(function(candidate) {
|
|
||||||
return utils.fileExistsSync(candidate.toString());
|
|
||||||
})
|
|
||||||
[0];
|
|
||||||
|
|
||||||
if (fullPath) {
|
|
||||||
if (fullPath.unresolvedPath && resolveOptions.fullResolve === false)
|
|
||||||
return fullPath.unresolvedPath;
|
|
||||||
// Convert String object back to plain string primitive
|
|
||||||
return fullPath.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
debug ('Skipping %s - module not found', fullPath);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadModelDefinition(rootDir, jsonFile, allFiles) {
|
|
||||||
var definition = require(jsonFile);
|
|
||||||
var basename = path.basename(jsonFile, path.extname(jsonFile));
|
|
||||||
definition.name = definition.name || _.capitalize(_.camelCase(basename));
|
|
||||||
|
|
||||||
// find a matching file with a supported extension like `.js` or `.coffee`
|
|
||||||
var sourceFile = fixFileExtension(jsonFile, allFiles, true);
|
|
||||||
|
|
||||||
if (sourceFile === undefined) {
|
|
||||||
debug('Model source code not found: %s', sourceFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Found model "%s" - %s %s', definition.name,
|
|
||||||
path.relative(rootDir, jsonFile),
|
|
||||||
sourceFile ? path.relative(rootDir, sourceFile) : '(no source file)');
|
|
||||||
|
|
||||||
return {
|
|
||||||
definition: definition,
|
|
||||||
sourceFile: sourceFile,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMiddlewareInstructions(rootDir, config) {
|
|
||||||
var phasesNames = Object.keys(config);
|
|
||||||
var middlewareList = [];
|
|
||||||
phasesNames.forEach(function(phase) {
|
|
||||||
var phaseConfig = config[phase];
|
|
||||||
Object.keys(phaseConfig).forEach(function(middleware) {
|
|
||||||
var allConfigs = phaseConfig[middleware];
|
|
||||||
if (!Array.isArray(allConfigs))
|
|
||||||
allConfigs = [allConfigs];
|
|
||||||
|
|
||||||
allConfigs.forEach(function(config) {
|
|
||||||
var resolved = resolveMiddlewarePath(rootDir, middleware, config);
|
|
||||||
|
|
||||||
// resolved.sourceFile will be false-y if an optional middleware
|
|
||||||
// is not resolvable.
|
|
||||||
// if a non-optional middleware is not resolvable, it will throw
|
|
||||||
// at resolveAppPath() and not reach here
|
|
||||||
if (!resolved.sourceFile) {
|
|
||||||
return g.log('{{Middleware}} "%s" not found: %s',
|
|
||||||
middleware,
|
|
||||||
resolved.optional
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var middlewareConfig = cloneDeep(config);
|
|
||||||
middlewareConfig.phase = phase;
|
|
||||||
|
|
||||||
if (middlewareConfig.params) {
|
|
||||||
middlewareConfig.params = resolveMiddlewareParams(
|
|
||||||
rootDir, middlewareConfig.params);
|
|
||||||
}
|
|
||||||
|
|
||||||
var item = {
|
|
||||||
sourceFile: resolved.sourceFile,
|
|
||||||
config: middlewareConfig,
|
|
||||||
};
|
|
||||||
if (resolved.fragment) {
|
|
||||||
item.fragment = resolved.fragment;
|
|
||||||
}
|
|
||||||
middlewareList.push(item);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var flattenedPhaseNames = phasesNames
|
|
||||||
.map(function getBaseName(name) {
|
|
||||||
return name.replace(/:[^:]+$/, '');
|
|
||||||
})
|
|
||||||
.filter(function differsFromPreviousItem(value, ix, source) {
|
|
||||||
// Skip duplicate entries. That happens when
|
|
||||||
// `name:before` and `name:after` are both translated to `name`
|
|
||||||
return ix === 0 || value !== source[ix - 1];
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
phases: flattenedPhaseNames,
|
|
||||||
middleware: middlewareList,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMiddlewarePath(rootDir, middleware, config) {
|
|
||||||
var resolved = {
|
|
||||||
optional: !!config.optional,
|
|
||||||
};
|
|
||||||
|
|
||||||
var segments = middleware.split('#');
|
|
||||||
var pathName = segments[0];
|
|
||||||
var fragment = segments[1];
|
|
||||||
var middlewarePath = pathName;
|
|
||||||
var opts = {
|
|
||||||
strict: true,
|
|
||||||
optional: !!config.optional,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fragment) {
|
|
||||||
resolved.fragment = fragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathName.indexOf('./') === 0 || pathName.indexOf('../') === 0) {
|
|
||||||
// Relative path
|
|
||||||
pathName = path.resolve(rootDir, pathName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolveOpts = _.extend(opts, {
|
|
||||||
// Workaround for strong-agent to allow probes to detect that
|
|
||||||
// strong-express-middleware was loaded: exclude the path to the
|
|
||||||
// module main file from the source file path.
|
|
||||||
// For example, return
|
|
||||||
// node_modules/strong-express-metrics
|
|
||||||
// instead of
|
|
||||||
// node_modules/strong-express-metrics/index.js
|
|
||||||
fullResolve: false,
|
|
||||||
});
|
|
||||||
var sourceFile = resolveAppScriptPath(rootDir, middlewarePath, resolveOpts);
|
|
||||||
|
|
||||||
if (!fragment) {
|
|
||||||
resolved.sourceFile = sourceFile;
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to require the module and check if <module>.<fragment> is a valid
|
|
||||||
// function
|
|
||||||
var m = require(pathName);
|
|
||||||
if (typeof m[fragment] === 'function') {
|
|
||||||
resolved.sourceFile = sourceFile;
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* module/server/middleware/fragment
|
|
||||||
* module/middleware/fragment
|
|
||||||
*/
|
|
||||||
var candidates = [
|
|
||||||
pathName + '/server/middleware/' + fragment,
|
|
||||||
pathName + '/middleware/' + fragment,
|
|
||||||
// TODO: [rfeng] Should we support the following flavors?
|
|
||||||
// pathName + '/lib/' + fragment,
|
|
||||||
// pathName + '/' + fragment
|
|
||||||
];
|
|
||||||
|
|
||||||
var err = undefined; // see https://github.com/eslint/eslint/issues/5744
|
|
||||||
for (var ix in candidates) {
|
|
||||||
try {
|
|
||||||
resolved.sourceFile = resolveAppScriptPath(rootDir, candidates[ix], opts);
|
|
||||||
delete resolved.fragment;
|
|
||||||
return resolved;
|
|
||||||
} catch (e) {
|
|
||||||
// Report the error for the first candidate when no candidate matches
|
|
||||||
if (!err) err = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match values starting with `$!./` or `$!../`
|
|
||||||
var MIDDLEWARE_PATH_PARAM_REGEX = /^\$!(\.\/|\.\.\/)/;
|
|
||||||
|
|
||||||
function resolveMiddlewareParams(rootDir, params) {
|
|
||||||
return cloneDeep(params, function resolvePathParam(value) {
|
|
||||||
if (typeof value === 'string' && MIDDLEWARE_PATH_PARAM_REGEX.test(value)) {
|
|
||||||
return path.resolve(rootDir, value.slice(2));
|
|
||||||
} else {
|
|
||||||
return undefined; // no change
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildComponentInstructions(rootDir, componentConfig) {
|
|
||||||
return Object.keys(componentConfig)
|
|
||||||
.filter(function(name) { return !!componentConfig[name]; })
|
|
||||||
.map(function(name) {
|
|
||||||
return {
|
|
||||||
sourceFile: resolveAppScriptPath(rootDir, name, { strict: true }),
|
|
||||||
config: componentConfig[name],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveRelativePaths(relativePaths, appRootDir) {
|
|
||||||
var resolveOpts = { strict: false };
|
|
||||||
relativePaths.forEach(function(relativePath, k) {
|
|
||||||
var resolvedPath = tryResolveAppPath(appRootDir, relativePath, resolveOpts);
|
|
||||||
if (resolvedPath !== undefined) {
|
|
||||||
relativePaths[k] = resolvedPath;
|
|
||||||
} else {
|
|
||||||
debug ('skipping boot script %s - unknown file', relativePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getExcludedExtensions() {
|
|
||||||
return {
|
|
||||||
'.json': '.json',
|
|
||||||
'.node': 'node',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPreferredExtension(filename) {
|
|
||||||
var includeExtensions = require.extensions;
|
|
||||||
|
|
||||||
var ext = path.extname(filename);
|
|
||||||
return (ext in includeExtensions) && !(ext in getExcludedExtensions());
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixFileExtension(filepath, files, onlyScriptsExportingFunction) {
|
|
||||||
var results = [];
|
|
||||||
var otherFile;
|
|
||||||
|
|
||||||
/* Prefer coffee scripts over json */
|
|
||||||
if (isPreferredExtension(filepath)) return filepath;
|
|
||||||
|
|
||||||
var basename = path.basename(filepath, FILE_EXTENSION_JSON);
|
|
||||||
var sourceDir = path.dirname(filepath);
|
|
||||||
|
|
||||||
files.forEach(function(f) {
|
|
||||||
otherFile = path.resolve(sourceDir, f);
|
|
||||||
|
|
||||||
var stats = fs.statSync(otherFile);
|
|
||||||
if (stats.isFile()) {
|
|
||||||
var otherFileExtension = path.extname(f);
|
|
||||||
|
|
||||||
if (!(otherFileExtension in getExcludedExtensions()) &&
|
|
||||||
path.basename(f, otherFileExtension) == basename) {
|
|
||||||
if (!onlyScriptsExportingFunction)
|
|
||||||
results.push(otherFile);
|
|
||||||
else if (onlyScriptsExportingFunction &&
|
|
||||||
(typeof require.extensions[otherFileExtension]) === 'function') {
|
|
||||||
results.push(otherFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return (results.length > 0 ? results[0] : undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
|
|
||||||
var resolvedPath = resolveAppPath(rootDir, relativePath, resolveOptions);
|
|
||||||
if (!resolvedPath) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var sourceDir = path.dirname(resolvedPath);
|
|
||||||
var files = tryReadDir(sourceDir);
|
|
||||||
var fixedFile = fixFileExtension(resolvedPath, files, false);
|
|
||||||
return (fixedFile === undefined ? resolvedPath : fixedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAllMixinInstructions(appRootDir, mixinDirs, mixinSources, options,
|
|
||||||
modelInstructions) {
|
|
||||||
var extensions = _.without(_.keys(require.extensions),
|
|
||||||
_.keys(getExcludedExtensions()));
|
|
||||||
|
|
||||||
// load mixins from `options.mixins`
|
|
||||||
var sourceFiles = options.mixins || [];
|
|
||||||
var instructionsFromMixins = loadMixins(sourceFiles, options);
|
|
||||||
|
|
||||||
// load mixins from `options.mixinDirs`
|
|
||||||
sourceFiles = findMixinDefinitions(appRootDir, mixinDirs, extensions);
|
|
||||||
if (sourceFiles === undefined) return;
|
|
||||||
var instructionsFromMixinDirs = loadMixins(sourceFiles, options);
|
|
||||||
|
|
||||||
/* If `mixinDirs` and `mixinSources` have any directories in common,
|
|
||||||
* then remove the common directories from `mixinSources` */
|
|
||||||
mixinSources = _.difference(mixinSources, mixinDirs);
|
|
||||||
|
|
||||||
// load mixins from `options.mixinSources`
|
|
||||||
sourceFiles = findMixinDefinitions(appRootDir, mixinSources, extensions);
|
|
||||||
if (sourceFiles === undefined) return;
|
|
||||||
var instructionsFromMixinSources = loadMixins(sourceFiles, options);
|
|
||||||
|
|
||||||
// Fetch unique list of mixin names, used in models
|
|
||||||
var modelMixins = fetchMixinNamesUsedInModelInstructions(modelInstructions);
|
|
||||||
modelMixins = _.uniq(modelMixins);
|
|
||||||
|
|
||||||
// Filter-in only mixins, that are used in models
|
|
||||||
instructionsFromMixinSources = filterMixinInstructionsUsingWhitelist(
|
|
||||||
instructionsFromMixinSources, modelMixins);
|
|
||||||
|
|
||||||
var mixins = _.assign(
|
|
||||||
instructionsFromMixins,
|
|
||||||
instructionsFromMixinDirs,
|
|
||||||
instructionsFromMixinSources);
|
|
||||||
|
|
||||||
return _.values(mixins);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findMixinDefinitions(appRootDir, sourceDirs, extensions) {
|
|
||||||
var files = [];
|
|
||||||
sourceDirs.forEach(function(dir) {
|
|
||||||
var path = tryResolveAppPath(appRootDir, dir);
|
|
||||||
if (!path) {
|
|
||||||
debug('Skipping unknown module source dir %j', dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
files = files.concat(findScripts(path, extensions));
|
|
||||||
});
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadMixins(sourceFiles, options) {
|
|
||||||
var mixinInstructions = {};
|
|
||||||
sourceFiles.forEach(function(filepath) {
|
|
||||||
var dir = path.dirname(filepath);
|
|
||||||
var ext = path.extname(filepath);
|
|
||||||
var name = path.basename(filepath, ext);
|
|
||||||
var metafile = path.join(dir, name + FILE_EXTENSION_JSON);
|
|
||||||
|
|
||||||
name = normalizeMixinName(name, options);
|
|
||||||
var meta = {};
|
|
||||||
meta.name = name;
|
|
||||||
if (utils.fileExistsSync(metafile)) {
|
|
||||||
// May overwrite name, not sourceFile
|
|
||||||
_.extend(meta, require(metafile));
|
|
||||||
}
|
|
||||||
meta.sourceFile = filepath;
|
|
||||||
mixinInstructions[meta.name] = meta;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mixinInstructions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchMixinNamesUsedInModelInstructions(modelInstructions) {
|
|
||||||
return _.flatten(modelInstructions
|
|
||||||
.map(function(model) {
|
|
||||||
return model.definition && model.definition.mixins ?
|
|
||||||
Object.keys(model.definition.mixins) : [];
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterMixinInstructionsUsingWhitelist(instructions, includeMixins) {
|
|
||||||
var instructionKeys = Object.keys(instructions);
|
|
||||||
includeMixins = _.intersection(instructionKeys, includeMixins);
|
|
||||||
|
|
||||||
var filteredInstructions = {};
|
|
||||||
instructionKeys.forEach(function(mixinName) {
|
|
||||||
if (includeMixins.indexOf(mixinName) !== -1) {
|
|
||||||
filteredInstructions[mixinName] = instructions[mixinName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return filteredInstructions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMixinName(str, options) {
|
|
||||||
var normalization = options.normalization;
|
|
||||||
switch (normalization) {
|
|
||||||
case false:
|
|
||||||
case 'none': return str;
|
|
||||||
|
|
||||||
case undefined:
|
|
||||||
case 'classify':
|
|
||||||
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
|
|
||||||
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
|
|
||||||
str = str.replace(/(?:^|\s|-)\S/g, function(c) {
|
|
||||||
return c.toUpperCase();
|
|
||||||
});
|
|
||||||
str = str.replace(/\s+/g, '');
|
|
||||||
return str;
|
|
||||||
|
|
||||||
case 'dasherize':
|
|
||||||
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
|
|
||||||
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
|
|
||||||
str = str.replace(/\s+/g, '-');
|
|
||||||
return str;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (typeof normalization === 'function') {
|
|
||||||
return normalization(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
var err = new Error(g.f('Invalid normalization format - "%s"',
|
|
||||||
normalization));
|
|
||||||
err.code = 'INVALID_NORMALIZATION_FORMAT';
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,313 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
||||||
var cloneDeep = require('lodash').cloneDeep;
|
|
||||||
var path = require('path');
|
|
||||||
var utils = require('./utils.js');
|
|
||||||
var debug = require('debug')('loopback:boot:config-loader');
|
|
||||||
var assert = require('assert');
|
|
||||||
var g = require('strong-globalize')();
|
|
||||||
|
|
||||||
var ConfigLoader = exports;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load application config from `config.json` and friends.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
ConfigLoader.loadAppConfig = function(rootDir, env) {
|
|
||||||
return loadNamed(rootDir, env, 'config', mergeAppConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load data-sources config from `datasources.json` and friends.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
ConfigLoader.loadDataSources = function(rootDir, env) {
|
|
||||||
return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load model config from `model-config.json` and friends.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
ConfigLoader.loadModels = function(rootDir, env) {
|
|
||||||
return loadNamed(rootDir, env, 'model-config', mergeModelConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load middleware config from `middleware.json` and friends.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
ConfigLoader.loadMiddleware = function(rootDir, env) {
|
|
||||||
return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load component config from `component-config.json` and friends.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
ConfigLoader.loadComponents = function(rootDir, env) {
|
|
||||||
return loadNamed(rootDir, env, 'component-config', mergeComponentConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*-- Implementation --*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load named configuration.
|
|
||||||
* @param {String} rootDir Directory where to look for files.
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @param {String} name
|
|
||||||
* @param {function(target:Object, config:Object, filename:String)} mergeFn
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function loadNamed(rootDir, env, name, mergeFn) {
|
|
||||||
var files = findConfigFiles(rootDir, env, name);
|
|
||||||
if (files.length) {
|
|
||||||
debug('found %s %s files', env, name);
|
|
||||||
files.forEach(function(f) { debug(' %s', f); });
|
|
||||||
}
|
|
||||||
var configs = loadConfigFiles(files);
|
|
||||||
var merged = mergeConfigurations(configs, mergeFn);
|
|
||||||
|
|
||||||
debug('merged %s %s configuration %j', env, name, merged);
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search `appRootDir` for all files containing configuration for `name`.
|
|
||||||
* @param {String} appRootDir
|
|
||||||
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
|
||||||
* @param {String} name
|
|
||||||
* @returns {Array.<String>} Array of absolute file paths.
|
|
||||||
*/
|
|
||||||
function findConfigFiles(appRootDir, env, name) {
|
|
||||||
var master = ifExists(name + '.json');
|
|
||||||
if (!master && (ifExistsWithAnyExt(name + '.local') ||
|
|
||||||
ifExistsWithAnyExt(name + '.' + env))) {
|
|
||||||
g.warn('WARNING: Main {{config}} file "%s.json" is missing', name);
|
|
||||||
}
|
|
||||||
if (!master) return [];
|
|
||||||
|
|
||||||
var candidates = [
|
|
||||||
master,
|
|
||||||
ifExistsWithAnyExt(name + '.local'),
|
|
||||||
ifExistsWithAnyExt(name + '.' + env),
|
|
||||||
];
|
|
||||||
|
|
||||||
return candidates.filter(function(c) { return c !== undefined; });
|
|
||||||
|
|
||||||
function ifExists(fileName) {
|
|
||||||
var filepath = path.resolve(appRootDir, fileName);
|
|
||||||
return utils.fileExistsSync(filepath) ? filepath : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ifExistsWithAnyExt(fileName) {
|
|
||||||
return ifExists(fileName + '.js') || ifExists(fileName + '.json');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load configuration files into an array of objects.
|
|
||||||
* Attach non-enumerable `_filename` property to each object.
|
|
||||||
* @param {Array.<String>} files
|
|
||||||
* @returns {Array.<Object>}
|
|
||||||
*/
|
|
||||||
function loadConfigFiles(files) {
|
|
||||||
return files.map(function(f) {
|
|
||||||
var config = cloneDeep(require(f));
|
|
||||||
Object.defineProperty(config, '_filename', {
|
|
||||||
enumerable: false,
|
|
||||||
value: f,
|
|
||||||
});
|
|
||||||
debug('loaded config file %s: %j', f, config);
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge multiple configuration objects into a single one.
|
|
||||||
* @param {Array.<Object>} configObjects
|
|
||||||
* @param {function(target:Object, config:Object, filename:String)} mergeFn
|
|
||||||
*/
|
|
||||||
function mergeConfigurations(configObjects, mergeFn) {
|
|
||||||
var result = configObjects.shift() || {};
|
|
||||||
while (configObjects.length) {
|
|
||||||
var next = configObjects.shift();
|
|
||||||
mergeFn(result, next, next._filename);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeDataSourceConfig(target, config, fileName) {
|
|
||||||
var err = mergeObjects(target, config);
|
|
||||||
if (err) {
|
|
||||||
throw new Error(g.f('Cannot apply %s: %s', fileName, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeModelConfig(target, config, fileName) {
|
|
||||||
var err = mergeObjects(target, config);
|
|
||||||
if (err) {
|
|
||||||
throw new Error(g.f('Cannot apply %s: %s', fileName, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeAppConfig(target, config, fileName) {
|
|
||||||
var err = mergeObjects(target, config);
|
|
||||||
if (err) {
|
|
||||||
throw new Error(g.f('Cannot apply %s: %s', fileName, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeMiddlewareConfig(target, config, fileName) {
|
|
||||||
var err = undefined; // see https://github.com/eslint/eslint/issues/5744
|
|
||||||
for (var phase in config) {
|
|
||||||
if (phase in target) {
|
|
||||||
err = mergePhaseConfig(target[phase], config[phase], phase);
|
|
||||||
} else {
|
|
||||||
err = g.f('The {{phase}} "%s" is not defined in the main config.', phase);
|
|
||||||
}
|
|
||||||
if (err)
|
|
||||||
throw new Error(g.f('Cannot apply %s: %s', fileName, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeNamedItems(arr1, arr2, key) {
|
|
||||||
assert(Array.isArray(arr1), g.f('invalid array: %s', arr1));
|
|
||||||
assert(Array.isArray(arr2), g.f('invalid array: %s', arr2));
|
|
||||||
key = key || 'name';
|
|
||||||
var result = [].concat(arr1);
|
|
||||||
for (var i = 0, n = arr2.length; i < n; i++) {
|
|
||||||
var item = arr2[i];
|
|
||||||
var found = false;
|
|
||||||
if (item[key]) {
|
|
||||||
for (var j = 0, k = result.length; j < k; j++) {
|
|
||||||
if (result[j][key] === item[key]) {
|
|
||||||
mergeObjects(result[j], item);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergePhaseConfig(target, config, phase) {
|
|
||||||
var err = undefined; // see https://github.com/eslint/eslint/issues/5744
|
|
||||||
for (var mw in config) {
|
|
||||||
if (mw in target) {
|
|
||||||
var targetMiddleware = target[mw];
|
|
||||||
var configMiddleware = config[mw];
|
|
||||||
if (Array.isArray(targetMiddleware) && Array.isArray(configMiddleware)) {
|
|
||||||
// Both are arrays, combine them
|
|
||||||
target[mw] = mergeNamedItems(targetMiddleware, configMiddleware);
|
|
||||||
} else if (Array.isArray(targetMiddleware)) {
|
|
||||||
if (typeof configMiddleware === 'object' &&
|
|
||||||
Object.keys(configMiddleware).length) {
|
|
||||||
// Config side is an non-empty object
|
|
||||||
target[mw] = mergeNamedItems(targetMiddleware, [configMiddleware]);
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(configMiddleware)) {
|
|
||||||
if (typeof targetMiddleware === 'object' &&
|
|
||||||
Object.keys(targetMiddleware).length) {
|
|
||||||
// Target side is an non-empty object
|
|
||||||
target[mw] = mergeNamedItems([targetMiddleware], configMiddleware);
|
|
||||||
} else {
|
|
||||||
// Target side is empty
|
|
||||||
target[mw] = configMiddleware;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = mergeObjects(targetMiddleware, configMiddleware);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = g.f('The {{middleware}} "%s" in phase "%s"' +
|
|
||||||
'is not defined in the main config.', mw, phase);
|
|
||||||
}
|
|
||||||
if (err) return err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeComponentConfig(target, config, fileName) {
|
|
||||||
var err = mergeObjects(target, config);
|
|
||||||
if (err) {
|
|
||||||
throw new Error(g.f('Cannot apply %s: %s', fileName, err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeObjects(target, config, keyPrefix) {
|
|
||||||
for (var key in config) {
|
|
||||||
var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
|
|
||||||
var err = mergeSingleItemOrProperty(target, config, key, fullKey);
|
|
||||||
if (err) return err;
|
|
||||||
}
|
|
||||||
return null; // no error
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeSingleItemOrProperty(target, config, key, fullKey) {
|
|
||||||
var origValue = target[key];
|
|
||||||
var newValue = config[key];
|
|
||||||
|
|
||||||
if (!hasCompatibleType(origValue, newValue)) {
|
|
||||||
return 'Cannot merge values of incompatible types for the option `' +
|
|
||||||
fullKey + '`.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(origValue)) {
|
|
||||||
return mergeArrays(origValue, newValue, fullKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValue !== null && typeof origValue === 'object') {
|
|
||||||
return mergeObjects(origValue, newValue, fullKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
target[key] = newValue;
|
|
||||||
return null; // no error
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeArrays(target, config, keyPrefix) {
|
|
||||||
if (target.length !== config.length) {
|
|
||||||
return 'Cannot merge array values of different length' +
|
|
||||||
' for the option `' + keyPrefix + '`.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use for(;;) to iterate over undefined items, for(in) would skip them.
|
|
||||||
for (var ix = 0; ix < target.length; ix++) {
|
|
||||||
var fullKey = keyPrefix + '[' + ix + ']';
|
|
||||||
var err = mergeSingleItemOrProperty(target, config, ix, fullKey);
|
|
||||||
if (err) return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // no error
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCompatibleType(origValue, newValue) {
|
|
||||||
if (origValue === null || origValue === undefined)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (Array.isArray(origValue))
|
|
||||||
return Array.isArray(newValue);
|
|
||||||
|
|
||||||
if (typeof origValue === 'object')
|
|
||||||
return typeof newValue === 'object';
|
|
||||||
|
|
||||||
// Note: typeof Array() is 'object' too,
|
|
||||||
// we don't need to explicitly check array types
|
|
||||||
return typeof newValue !== 'object';
|
|
||||||
}
|
|
454
lib/executor.js
454
lib/executor.js
|
@ -1,454 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
||||||
var assert = require('assert');
|
|
||||||
var semver = require('semver');
|
|
||||||
var debug = require('debug')('loopback:boot:executor');
|
|
||||||
var async = require('async');
|
|
||||||
var path = require('path');
|
|
||||||
var format = require('util').format;
|
|
||||||
var g = require('strong-globalize')();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute bootstrap instructions gathered by `boot.compile`.
|
|
||||||
*
|
|
||||||
* @param {Object} app The loopback app to boot.
|
|
||||||
* @options {Object} instructions Boot instructions.
|
|
||||||
* @param {Function} [callback] Callback function.
|
|
||||||
*
|
|
||||||
* @header boot.execute(instructions)
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = function execute(app, instructions, callback) {
|
|
||||||
callback = callback || function() {};
|
|
||||||
|
|
||||||
app.booting = true;
|
|
||||||
|
|
||||||
patchAppLoopback(app);
|
|
||||||
assertLoopBackVersion(app);
|
|
||||||
|
|
||||||
setEnv(app, instructions);
|
|
||||||
setHost(app, instructions);
|
|
||||||
setPort(app, instructions);
|
|
||||||
setApiRoot(app, instructions);
|
|
||||||
applyAppConfig(app, instructions);
|
|
||||||
|
|
||||||
setupDataSources(app, instructions);
|
|
||||||
setupModels(app, instructions);
|
|
||||||
setupMiddleware(app, instructions);
|
|
||||||
setupComponents(app, instructions);
|
|
||||||
|
|
||||||
// Run the boot scripts in series synchronously or asynchronously
|
|
||||||
// Please note async supports both styles
|
|
||||||
async.series([
|
|
||||||
function(done) {
|
|
||||||
runBootScripts(app, instructions, done);
|
|
||||||
},
|
|
||||||
function(done) {
|
|
||||||
enableAnonymousSwagger(app, instructions);
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
// Ensure both the "booted" event and the callback are always called
|
|
||||||
// in the next tick of the even loop.
|
|
||||||
// See http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony
|
|
||||||
process.nextTick,
|
|
||||||
], function(err) {
|
|
||||||
app.booting = false;
|
|
||||||
|
|
||||||
if (err) return callback(err);
|
|
||||||
|
|
||||||
app.emit('booted');
|
|
||||||
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function patchAppLoopback(app) {
|
|
||||||
if (app.loopback) return;
|
|
||||||
// app.loopback was introduced in 1.9.0
|
|
||||||
// patch the app object to make loopback-boot work with older versions too
|
|
||||||
try {
|
|
||||||
app.loopback = require('loopback');
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === 'MODULE_NOT_FOUND') {
|
|
||||||
g.error(
|
|
||||||
'When using {{loopback-boot}} with {{loopback}} <1.9, ' +
|
|
||||||
'the {{loopback}} module must be available ' +
|
|
||||||
'for `{{require(\'loopback\')}}`.');
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertLoopBackVersion(app) {
|
|
||||||
var RANGE = '1.x || 2.x || ^3.0.0-alpha';
|
|
||||||
|
|
||||||
var loopback = app.loopback;
|
|
||||||
// remove any pre-release tag from the version string,
|
|
||||||
// because semver has special treatment of pre-release versions,
|
|
||||||
// while loopback-boot treats pre-releases the same way as regular versions
|
|
||||||
var version = (loopback.version || '1.0.0').replace(/-.*$/, '');
|
|
||||||
if (!semver.satisfies(version, RANGE)) {
|
|
||||||
var msg = g.f(
|
|
||||||
'The `{{app}}` is powered by an incompatible {{loopback}} version %s. ' +
|
|
||||||
'Supported versions: %s',
|
|
||||||
loopback.version || '(unknown)',
|
|
||||||
RANGE);
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setEnv(app, instructions) {
|
|
||||||
var env = instructions.env;
|
|
||||||
if (env !== undefined)
|
|
||||||
app.set('env', env);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHost(app, instructions) {
|
|
||||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
|
||||||
var host =
|
|
||||||
process.env.npm_config_host ||
|
|
||||||
process.env.OPENSHIFT_SLS_IP ||
|
|
||||||
process.env.OPENSHIFT_NODEJS_IP ||
|
|
||||||
process.env.HOST ||
|
|
||||||
process.env.VCAP_APP_HOST ||
|
|
||||||
instructions.config.host ||
|
|
||||||
process.env.npm_package_config_host ||
|
|
||||||
app.get('host');
|
|
||||||
|
|
||||||
if (host !== undefined) {
|
|
||||||
assert(typeof host === 'string', g.f('{{app.host}} must be a {{string}}'));
|
|
||||||
app.set('host', host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPort(app, instructions) {
|
|
||||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
|
||||||
var port = find([
|
|
||||||
process.env.npm_config_port,
|
|
||||||
process.env.OPENSHIFT_SLS_PORT,
|
|
||||||
process.env.OPENSHIFT_NODEJS_PORT,
|
|
||||||
process.env.PORT,
|
|
||||||
process.env.VCAP_APP_PORT,
|
|
||||||
instructions.config.port,
|
|
||||||
process.env.npm_package_config_port,
|
|
||||||
app.get('port'),
|
|
||||||
3000,
|
|
||||||
], function(p) {
|
|
||||||
return p != null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (port !== undefined) {
|
|
||||||
var portType = typeof port;
|
|
||||||
assert(portType === 'string' || portType === 'number',
|
|
||||||
g.f('{{app.port}} must be a {{string}} or {{number}}'));
|
|
||||||
app.set('port', port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function find(array, predicate) {
|
|
||||||
return array.filter(predicate)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setApiRoot(app, instructions) {
|
|
||||||
var restApiRoot =
|
|
||||||
instructions.config.restApiRoot ||
|
|
||||||
app.get('restApiRoot') ||
|
|
||||||
'/api';
|
|
||||||
|
|
||||||
assert(restApiRoot !== undefined, g.f('{{app.restBasePath}} is required'));
|
|
||||||
assert(typeof restApiRoot === 'string',
|
|
||||||
g.f('{{app.restApiRoot}} must be a {{string}}'));
|
|
||||||
assert(/^\//.test(restApiRoot),
|
|
||||||
g.f('{{app.restApiRoot}} must start with "/"'));
|
|
||||||
app.set('restApiRoot', restApiRoot);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyAppConfig(app, instructions) {
|
|
||||||
var appConfig = instructions.config;
|
|
||||||
for (var configKey in appConfig) {
|
|
||||||
var cur = app.get(configKey);
|
|
||||||
if (cur === undefined || cur === null) {
|
|
||||||
app.set(configKey, appConfig[configKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupDataSources(app, instructions) {
|
|
||||||
forEachKeyedObject(instructions.dataSources, function(key, obj) {
|
|
||||||
var opts = {
|
|
||||||
useEnvVars: true,
|
|
||||||
};
|
|
||||||
obj = getUpdatedConfigObject(app, obj, opts);
|
|
||||||
var lazyConnect = process.env.LB_LAZYCONNECT_DATASOURCES;
|
|
||||||
if (lazyConnect) {
|
|
||||||
obj.lazyConnect =
|
|
||||||
lazyConnect === 'false' || lazyConnect === '0' ? false : true;
|
|
||||||
}
|
|
||||||
app.dataSource(key, obj);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupModels(app, instructions) {
|
|
||||||
defineMixins(app, instructions);
|
|
||||||
defineModels(app, instructions);
|
|
||||||
|
|
||||||
instructions.models.forEach(function(data) {
|
|
||||||
// Skip base models that are not exported to the app
|
|
||||||
if (!data.config) return;
|
|
||||||
|
|
||||||
app.model(data._model, data.config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function defineMixins(app, instructions) {
|
|
||||||
var modelBuilder = (app.registry || app.loopback).modelBuilder;
|
|
||||||
var BaseClass = app.loopback.Model;
|
|
||||||
var mixins = instructions.mixins || [];
|
|
||||||
|
|
||||||
if (!modelBuilder.mixins || !mixins.length) return;
|
|
||||||
|
|
||||||
mixins.forEach(function(obj) {
|
|
||||||
var mixin = require(obj.sourceFile);
|
|
||||||
|
|
||||||
if (typeof mixin === 'function' || mixin.prototype instanceof BaseClass) {
|
|
||||||
debug('Defining mixin %s', obj.name);
|
|
||||||
modelBuilder.mixins.define(obj.name, mixin); // TODO (name, mixin, meta)
|
|
||||||
} else {
|
|
||||||
debug('Skipping mixin file %s - `module.exports` is not a function' +
|
|
||||||
' or Loopback model', obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function defineModels(app, instructions) {
|
|
||||||
var registry = app.registry || app.loopback;
|
|
||||||
instructions.models.forEach(function(data) {
|
|
||||||
var name = data.name;
|
|
||||||
var model;
|
|
||||||
|
|
||||||
if (!data.definition) {
|
|
||||||
model = registry.getModel(name);
|
|
||||||
if (!model) {
|
|
||||||
throw new Error(g.f('Cannot configure unknown model %s', name));
|
|
||||||
}
|
|
||||||
debug('Configuring existing model %s', name);
|
|
||||||
} else if (isBuiltinLoopBackModel(app, data)) {
|
|
||||||
model = registry.getModel(name);
|
|
||||||
assert(model, g.f('Built-in model %s should have been defined', name));
|
|
||||||
debug('Configuring built-in LoopBack model %s', name);
|
|
||||||
} else {
|
|
||||||
debug('Creating new model %s %j', name, data.definition);
|
|
||||||
model = registry.createModel(data.definition);
|
|
||||||
if (data.sourceFile) {
|
|
||||||
debug('Loading customization script %s', data.sourceFile);
|
|
||||||
var code = require(data.sourceFile);
|
|
||||||
if (typeof code === 'function') {
|
|
||||||
debug('Customizing model %s', name);
|
|
||||||
code(model);
|
|
||||||
} else {
|
|
||||||
debug('Skipping model file %s - `module.exports` is not a function',
|
|
||||||
data.sourceFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data._model = model;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular expression to match built-in loopback models
|
|
||||||
var LOOPBACK_MODEL_REGEXP = new RegExp(
|
|
||||||
['', 'node_modules', 'loopback', '[^\\/\\\\]+', 'models', '[^\\/\\\\]+\\.js$']
|
|
||||||
.join('\\' + path.sep));
|
|
||||||
|
|
||||||
function isBuiltinLoopBackModel(app, data) {
|
|
||||||
// 1. Built-in models are exposed on the loopback object
|
|
||||||
if (!app.loopback[data.name]) return false;
|
|
||||||
|
|
||||||
// 2. Built-in models have a script file `loopback/{facet}/models/{name}.js`
|
|
||||||
var srcFile = data.sourceFile;
|
|
||||||
return srcFile &&
|
|
||||||
LOOPBACK_MODEL_REGEXP.test(srcFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
function forEachKeyedObject(obj, fn) {
|
|
||||||
if (typeof obj !== 'object') return;
|
|
||||||
|
|
||||||
Object.keys(obj).forEach(function(key) {
|
|
||||||
fn(key, obj[key]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function runScripts(app, list, callback) {
|
|
||||||
list = list || [];
|
|
||||||
var functions = [];
|
|
||||||
list.forEach(function(filepath) {
|
|
||||||
debug('Requiring script %s', filepath);
|
|
||||||
try {
|
|
||||||
var exports = require(filepath);
|
|
||||||
if (typeof exports === 'function') {
|
|
||||||
debug('Exported function detected %s', filepath);
|
|
||||||
functions.push({
|
|
||||||
path: filepath,
|
|
||||||
func: exports,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
g.error('Failed loading boot script: %s\n%s', filepath, err.stack);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async.eachSeries(functions, function(f, done) {
|
|
||||||
debug('Running script %s', f.path);
|
|
||||||
if (f.func.length >= 2) {
|
|
||||||
debug('Starting async function %s', f.path);
|
|
||||||
f.func(app, function(err) {
|
|
||||||
debug('Async function finished %s', f.path);
|
|
||||||
done(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
debug('Starting sync function %s', f.path);
|
|
||||||
f.func(app);
|
|
||||||
debug('Sync function finished %s', f.path);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupMiddleware(app, instructions) {
|
|
||||||
if (!instructions.middleware) {
|
|
||||||
// the browserified client does not support middleware
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phases can be empty
|
|
||||||
var phases = instructions.middleware.phases || [];
|
|
||||||
assert(Array.isArray(phases),
|
|
||||||
g.f('{{instructions.middleware.phases}} must be an {{array}}'));
|
|
||||||
|
|
||||||
var middleware = instructions.middleware.middleware;
|
|
||||||
assert(Array.isArray(middleware),
|
|
||||||
'instructions.middleware.middleware must be an object');
|
|
||||||
|
|
||||||
debug('Defining middleware phases %j', phases);
|
|
||||||
app.defineMiddlewarePhases(phases);
|
|
||||||
|
|
||||||
middleware.forEach(function(data) {
|
|
||||||
debug('Configuring middleware %j%s', data.sourceFile,
|
|
||||||
data.fragment ? ('#' + data.fragment) : '');
|
|
||||||
var factory = require(data.sourceFile);
|
|
||||||
if (data.fragment) {
|
|
||||||
factory = factory[data.fragment].bind(factory);
|
|
||||||
}
|
|
||||||
assert(typeof factory === 'function',
|
|
||||||
'Middleware factory must be a function');
|
|
||||||
var opts = {
|
|
||||||
useEnvVars: true,
|
|
||||||
};
|
|
||||||
data.config = getUpdatedConfigObject(app, data.config, opts);
|
|
||||||
app.middlewareFromConfig(factory, data.config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUpdatedConfigObject(app, config, opts) {
|
|
||||||
var DYNAMIC_CONFIG_PARAM = /\$\{(\w+)\}$/;
|
|
||||||
var useEnvVars = opts && opts.useEnvVars;
|
|
||||||
|
|
||||||
function getConfigVariable(param) {
|
|
||||||
var configVariable = param;
|
|
||||||
var match = configVariable.match(DYNAMIC_CONFIG_PARAM);
|
|
||||||
if (match) {
|
|
||||||
var varName = match[1];
|
|
||||||
if (useEnvVars && process.env[varName] !== undefined) {
|
|
||||||
debug('Dynamic Configuration: Resolved via process.env: %s as %s',
|
|
||||||
process.env[varName], param);
|
|
||||||
configVariable = process.env[varName];
|
|
||||||
} else if (app.get(varName) !== undefined) {
|
|
||||||
debug('Dynamic Configuration: Resolved via app.get(): %s as %s',
|
|
||||||
app.get(varName), param);
|
|
||||||
var appValue = app.get(varName);
|
|
||||||
configVariable = appValue;
|
|
||||||
} else {
|
|
||||||
// previously it returns the original string such as "${restApiRoot}"
|
|
||||||
// it will now return `undefined`, for the use case of
|
|
||||||
// dynamic datasources url:`undefined` to fallback to other parameters
|
|
||||||
configVariable = undefined;
|
|
||||||
g.warn('%s does not resolve to a valid value, returned as %s. ' +
|
|
||||||
'"%s" must be resolvable in Environment variable or by app.get().',
|
|
||||||
param, configVariable, varName);
|
|
||||||
debug('Dynamic Configuration: Cannot resolve variable for `%s`, ' +
|
|
||||||
'returned as %s', varName, configVariable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return configVariable;
|
|
||||||
}
|
|
||||||
|
|
||||||
function interpolateVariables(config) {
|
|
||||||
// config is a string and contains a config variable ('${var}')
|
|
||||||
if (typeof config === 'string')
|
|
||||||
return getConfigVariable(config);
|
|
||||||
|
|
||||||
// anything but an array or object
|
|
||||||
if (typeof config !== 'object' || config == null)
|
|
||||||
return config;
|
|
||||||
|
|
||||||
// recurse into array elements
|
|
||||||
if (Array.isArray(config))
|
|
||||||
return config.map(interpolateVariables);
|
|
||||||
|
|
||||||
// Not a plain object. Examples: RegExp, Date,
|
|
||||||
if (!config.constructor || config.constructor !== Object)
|
|
||||||
return config;
|
|
||||||
|
|
||||||
// recurse into object props
|
|
||||||
var interpolated = {};
|
|
||||||
Object.keys(config).forEach(function(configKey) {
|
|
||||||
var value = config[configKey];
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
interpolated[configKey] = value.map(interpolateVariables);
|
|
||||||
} else if (typeof value === 'string') {
|
|
||||||
interpolated[configKey] = getConfigVariable(value);
|
|
||||||
} else if (value === null) {
|
|
||||||
interpolated[configKey] = value;
|
|
||||||
} else if (typeof value === 'object' && Object.keys(value).length) {
|
|
||||||
interpolated[configKey] = interpolateVariables(value);
|
|
||||||
} else {
|
|
||||||
interpolated[configKey] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return interpolated;
|
|
||||||
}
|
|
||||||
|
|
||||||
return interpolateVariables(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupComponents(app, instructions) {
|
|
||||||
instructions.components.forEach(function(data) {
|
|
||||||
debug('Configuring component %j', data.sourceFile);
|
|
||||||
var configFn = require(data.sourceFile);
|
|
||||||
var opts = {
|
|
||||||
useEnvVars: true,
|
|
||||||
};
|
|
||||||
data.config = getUpdatedConfigObject(app, data.config, opts);
|
|
||||||
configFn(app, data.config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function runBootScripts(app, instructions, callback) {
|
|
||||||
runScripts(app, instructions.files.boot, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableAnonymousSwagger(app, instructions) {
|
|
||||||
// disable token requirement for swagger, if available
|
|
||||||
var swagger = app.remotes().exports.swagger;
|
|
||||||
if (!swagger) return;
|
|
||||||
|
|
||||||
var appConfig = instructions.config;
|
|
||||||
var requireTokenForSwagger = appConfig.swagger &&
|
|
||||||
appConfig.swagger.requireToken;
|
|
||||||
swagger.requireToken = requireTokenForSwagger || false;
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// 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 path = require('path');
|
||||||
|
var SG = require('strong-globalize');
|
||||||
|
SG.SetRootDir(path.join(__dirname, '..'), { autonomousMsgLoading: 'all' });
|
||||||
|
module.exports = SG();
|
|
@ -0,0 +1,336 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var debug = require('debug')('loopback:boot:plugin');
|
||||||
|
var assert = require('assert');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var util = require('./utils');
|
||||||
|
var g = require('./globalize');
|
||||||
|
|
||||||
|
module.exports = PluginBase;
|
||||||
|
|
||||||
|
function PluginBase(options, name, artifact) {
|
||||||
|
this.options = options || {};
|
||||||
|
this.name = name || options.name;
|
||||||
|
this.artifact = artifact || options.artifact;
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginBase.prototype.getRootDir = function() {
|
||||||
|
return this.options.rootDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype.load = function(context) {
|
||||||
|
var rootDir = this.getRootDir() || this.options.rootDir;
|
||||||
|
var env = this.options.env;
|
||||||
|
assert(this.name, 'Plugin name must to be set');
|
||||||
|
debug('Root dir: %s, env: %s, artifact: %s', rootDir, env, this.artifact);
|
||||||
|
var config = {};
|
||||||
|
if (this.options[this.name]) {
|
||||||
|
// First check if options have the corresponding config object
|
||||||
|
debug('Artifact: %s is using provided config obj instead' +
|
||||||
|
' of config file');
|
||||||
|
config = this.options[this.name];
|
||||||
|
} else {
|
||||||
|
if (this.artifact) {
|
||||||
|
config = this.loadNamed(rootDir, env, this.artifact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Register as context.configurations.<plugin-name>
|
||||||
|
return this.configure(context, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype.configure = function(context, config) {
|
||||||
|
config = config || {};
|
||||||
|
// Register as context.configurations.<plugin-name>
|
||||||
|
if (!context.configurations) {
|
||||||
|
context.configurations = {};
|
||||||
|
}
|
||||||
|
context.configurations[this.name] = config;
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype.merge = function(target, config, keyPrefix) {
|
||||||
|
return this._mergeObjects(target, config, keyPrefix);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load named configuration.
|
||||||
|
* @param {String} rootDir Directory where to look for files.
|
||||||
|
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
||||||
|
* @param {String} name
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
PluginBase.prototype.loadNamed = function(rootDir, env, name) {
|
||||||
|
var files = this.findConfigFiles(rootDir, env, name);
|
||||||
|
debug('Looking in dir %s for %s configs', rootDir, this.name);
|
||||||
|
if (files.length) {
|
||||||
|
debug('found %s %s files: %j', env, name, files);
|
||||||
|
files.forEach(function(f) {
|
||||||
|
debug(' %s', f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var configs = this._loadConfigFiles(files);
|
||||||
|
var merged = this._mergeConfigurations(configs);
|
||||||
|
|
||||||
|
debug('merged %s %s configuration %j', env, name, merged);
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search `rootDir` for all files containing configuration for `name`.
|
||||||
|
* @param {String} rootDir Root directory
|
||||||
|
* @param {String} env Environment, usually `process.env.NODE_ENV`
|
||||||
|
* @param {String} name Name
|
||||||
|
* @param {Array.<String>} exts An array of extension names
|
||||||
|
* @returns {Array.<String>} Array of absolute file paths.
|
||||||
|
*/
|
||||||
|
PluginBase.prototype.findConfigFiles = function(rootDir, env, name, exts) {
|
||||||
|
var master = ifExists(name + '.json');
|
||||||
|
if (!master && (ifExistsWithAnyExt(name + '.local') ||
|
||||||
|
ifExistsWithAnyExt(name + '.' + env))) {
|
||||||
|
g.warn('WARNING: Main config file "%s{{.json}}" is missing', name);
|
||||||
|
}
|
||||||
|
if (!master) return [];
|
||||||
|
|
||||||
|
var candidates = [
|
||||||
|
master,
|
||||||
|
ifExistsWithAnyExt(name + '.local'),
|
||||||
|
ifExistsWithAnyExt(name + '.' + env),
|
||||||
|
];
|
||||||
|
|
||||||
|
return candidates.filter(function(c) {
|
||||||
|
return c !== undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
function ifExists(fileName) {
|
||||||
|
var filePath = path.resolve(rootDir, fileName);
|
||||||
|
return util.fileExistsSync(filePath) ? filePath : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ifExistsWithAnyExt(fileName) {
|
||||||
|
var extensions = exts || ['js', 'json'];
|
||||||
|
var file;
|
||||||
|
for (var i = 0, n = extensions.length; i < n; i++) {
|
||||||
|
file = ifExists(fileName + '.' + extensions[i]);
|
||||||
|
if (file) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load configuration files into an array of objects.
|
||||||
|
* Attach non-enumerable `_filename` property to each object.
|
||||||
|
* @param {Array.<String>} files
|
||||||
|
* @returns {Array.<Object>}
|
||||||
|
*/
|
||||||
|
PluginBase.prototype._loadConfigFiles = function(files) {
|
||||||
|
return files.map(function(f) {
|
||||||
|
var config = require(f);
|
||||||
|
config = _.cloneDeep(config);
|
||||||
|
Object.defineProperty(config, '_filename', {
|
||||||
|
enumerable: false,
|
||||||
|
value: f,
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge multiple configuration objects into a single one.
|
||||||
|
* @param {Array.<Object>} configObjects
|
||||||
|
*/
|
||||||
|
PluginBase.prototype._mergeConfigurations = function(configObjects) {
|
||||||
|
var result = configObjects.shift() || {};
|
||||||
|
while (configObjects.length) {
|
||||||
|
var next = configObjects.shift();
|
||||||
|
this.merge(result, next, next._filename);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype._mergeObjects = function(target, config, keyPrefix) {
|
||||||
|
for (var key in config) {
|
||||||
|
var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
|
||||||
|
var err = this._mergeSingleItemOrProperty(target, config, key, fullKey);
|
||||||
|
if (err) throw err;
|
||||||
|
}
|
||||||
|
return null; // no error
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype._mergeNamedItems = function(arr1, arr2, key) {
|
||||||
|
assert(Array.isArray(arr1), 'invalid array: ' + arr1);
|
||||||
|
assert(Array.isArray(arr2), 'invalid array: ' + arr2);
|
||||||
|
key = key || 'name';
|
||||||
|
var result = [].concat(arr1);
|
||||||
|
for (var i = 0, n = arr2.length; i < n; i++) {
|
||||||
|
var item = arr2[i];
|
||||||
|
var found = false;
|
||||||
|
if (item[key]) {
|
||||||
|
for (var j = 0, k = result.length; j < k; j++) {
|
||||||
|
if (result[j][key] === item[key]) {
|
||||||
|
this._mergeObjects(result[j], item);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype._mergeSingleItemOrProperty =
|
||||||
|
function(target, config, key, fullKey) {
|
||||||
|
var origValue = target[key];
|
||||||
|
var newValue = config[key];
|
||||||
|
|
||||||
|
if (!hasCompatibleType(origValue, newValue)) {
|
||||||
|
return 'Cannot merge values of incompatible types for the option `' +
|
||||||
|
fullKey + '`.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(origValue)) {
|
||||||
|
return this._mergeArrays(origValue, newValue, fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue !== null && typeof origValue === 'object') {
|
||||||
|
return this._mergeObjects(origValue, newValue, fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
target[key] = newValue;
|
||||||
|
return null; // no error
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginBase.prototype._mergeArrays = function(target, config, keyPrefix) {
|
||||||
|
if (target.length !== config.length) {
|
||||||
|
return 'Cannot merge array values of different length' +
|
||||||
|
' for the option `' + keyPrefix + '`.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use for(;;) to iterate over undefined items, for(in) would skip them.
|
||||||
|
for (var ix = 0; ix < target.length; ix++) {
|
||||||
|
var fullKey = keyPrefix + '[' + ix + ']';
|
||||||
|
var err = this._mergeSingleItemOrProperty(target, config, ix, fullKey);
|
||||||
|
if (err) return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // no error
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasCompatibleType(origValue, newValue) {
|
||||||
|
if (origValue === null || origValue === undefined)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (Array.isArray(origValue))
|
||||||
|
return Array.isArray(newValue);
|
||||||
|
|
||||||
|
if (typeof origValue === 'object')
|
||||||
|
return typeof newValue === 'object';
|
||||||
|
|
||||||
|
// Note: typeof Array() is 'object' too,
|
||||||
|
// we don't need to explicitly check array types
|
||||||
|
return typeof newValue !== 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginBase.prototype.compile = function(context) {
|
||||||
|
var instructions;
|
||||||
|
if (typeof this.buildInstructions === 'function') {
|
||||||
|
var rootDir = this.options.rootDir;
|
||||||
|
var config = context.configurations[this.name] || {};
|
||||||
|
instructions = this.buildInstructions(context, rootDir, config);
|
||||||
|
} else {
|
||||||
|
instructions = context.configurations[this.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register as context.instructions.<plugin-name>
|
||||||
|
if (!context.instructions) {
|
||||||
|
context.instructions = {};
|
||||||
|
if (this.options.appId) {
|
||||||
|
context.instructions.appId = this.options.appId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.instructions[this.name] = instructions;
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
var DYNAMIC_CONFIG_PARAM = /\$\{(\w+)\}$/;
|
||||||
|
function getConfigVariable(app, param, useEnvVars) {
|
||||||
|
var configVariable = param;
|
||||||
|
var match = configVariable.match(DYNAMIC_CONFIG_PARAM);
|
||||||
|
if (match) {
|
||||||
|
var varName = match[1];
|
||||||
|
if (useEnvVars && process.env[varName] !== undefined) {
|
||||||
|
debug('Dynamic Configuration: Resolved via process.env: %s as %s',
|
||||||
|
process.env[varName], param);
|
||||||
|
configVariable = process.env[varName];
|
||||||
|
} else if (app.get(varName) !== undefined) {
|
||||||
|
debug('Dynamic Configuration: Resolved via app.get(): %s as %s',
|
||||||
|
app.get(varName), param);
|
||||||
|
var appValue = app.get(varName);
|
||||||
|
configVariable = appValue;
|
||||||
|
} else {
|
||||||
|
// previously it returns the original string such as "${restApiRoot}"
|
||||||
|
// it will now return `undefined`, for the use case of
|
||||||
|
// dynamic datasources url:`undefined` to fallback to other parameters
|
||||||
|
configVariable = undefined;
|
||||||
|
g.warn('%s does not resolve to a valid value, returned as %s. ' +
|
||||||
|
'"%s" must be resolvable in Environment variable or by {{app.get()}}.',
|
||||||
|
param, configVariable, varName);
|
||||||
|
debug('Dynamic Configuration: Cannot resolve variable for `%s`, ' +
|
||||||
|
'returned as %s', varName, configVariable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configVariable;
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginBase.prototype.getUpdatedConfigObject = function(context, config, opts) {
|
||||||
|
var app = context.app;
|
||||||
|
var useEnvVars = opts && opts.useEnvVars;
|
||||||
|
|
||||||
|
function interpolateVariables(config) {
|
||||||
|
// config is a string and contains a config variable ('${var}')
|
||||||
|
if (typeof config === 'string')
|
||||||
|
return getConfigVariable(app, config, useEnvVars);
|
||||||
|
|
||||||
|
// anything but an array or object
|
||||||
|
if (typeof config !== 'object' || config == null)
|
||||||
|
return config;
|
||||||
|
|
||||||
|
// recurse into array elements
|
||||||
|
if (Array.isArray(config))
|
||||||
|
return config.map(interpolateVariables);
|
||||||
|
|
||||||
|
// Not a plain object. Examples: RegExp, Date,
|
||||||
|
if (!config.constructor || config.constructor !== Object)
|
||||||
|
return config;
|
||||||
|
|
||||||
|
// recurse into object props
|
||||||
|
var interpolated = {};
|
||||||
|
Object.keys(config).forEach(function(configKey) {
|
||||||
|
var value = config[configKey];
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
interpolated[configKey] = value.map(interpolateVariables);
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
interpolated[configKey] = getConfigVariable(app, value, useEnvVars);
|
||||||
|
} else if (value === null) {
|
||||||
|
interpolated[configKey] = value;
|
||||||
|
} else if (typeof value === 'object' && Object.keys(value).length) {
|
||||||
|
interpolated[configKey] = interpolateVariables(value);
|
||||||
|
} else {
|
||||||
|
interpolated[configKey] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return interpolated;
|
||||||
|
}
|
||||||
|
return interpolateVariables(config);
|
||||||
|
};
|
|
@ -0,0 +1,62 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var utils = require('./utils');
|
||||||
|
var path = require('path');
|
||||||
|
var async = require('async');
|
||||||
|
var debug = require('debug')('loopback:boot:plugin-loader');
|
||||||
|
var PluginBase = require('./plugin-base');
|
||||||
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new PluginScript(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function PluginScript(options) {
|
||||||
|
PluginBase.call(this, options, 'pluginScripts', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(PluginScript, PluginBase);
|
||||||
|
|
||||||
|
PluginScript.prototype.load = function(context) {
|
||||||
|
var options = this.options;
|
||||||
|
var appRootDir = options.rootDir;
|
||||||
|
// require directories
|
||||||
|
var pluginDirs = options.pluginDirs || []; // precedence
|
||||||
|
pluginDirs = pluginDirs.concat(path.join(appRootDir, 'plugins'));
|
||||||
|
utils.resolveRelativePaths(pluginDirs, appRootDir);
|
||||||
|
|
||||||
|
var pluginScripts = options.pluginScripts || [];
|
||||||
|
utils.resolveRelativePaths(pluginScripts, appRootDir);
|
||||||
|
|
||||||
|
pluginDirs.forEach(function(dir) {
|
||||||
|
pluginScripts = pluginScripts.concat(utils.findScripts(dir));
|
||||||
|
var envdir = dir + '/' + options.env;
|
||||||
|
pluginScripts = pluginScripts.concat(utils.findScripts(envdir));
|
||||||
|
});
|
||||||
|
|
||||||
|
pluginScripts = _.uniq(pluginScripts);
|
||||||
|
debug('Plugin scripts: %j', pluginScripts);
|
||||||
|
this.configure(context, pluginScripts);
|
||||||
|
return pluginScripts;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginScript.prototype.compile = function(context) {
|
||||||
|
var pluginScripts = context.configurations.pluginScripts;
|
||||||
|
context.instructions = context.instructions || {};
|
||||||
|
var plugins = context.instructions.pluginScripts = {};
|
||||||
|
var self = this;
|
||||||
|
pluginScripts.forEach(function(ps) {
|
||||||
|
debug('Loading %s', ps);
|
||||||
|
var factory = require(ps);
|
||||||
|
var handler = factory(self.options);
|
||||||
|
var name = handler.name || path.basename(ps, '.js');
|
||||||
|
debug('Loaded plugin name: %s', name);
|
||||||
|
plugins[name] = handler;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var assert = require('assert');
|
||||||
|
var semver = require('semver');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var g = require('../globalize');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Application(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Application(options) {
|
||||||
|
PluginBase.call(this, options, 'application', 'config');
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Application, PluginBase);
|
||||||
|
|
||||||
|
function assertLoopBackVersion(app) {
|
||||||
|
var RANGE = '2.x || 3.x';
|
||||||
|
|
||||||
|
var loopback = app.loopback;
|
||||||
|
// remove any pre-release tag from the version string,
|
||||||
|
// because semver has special treatment of pre-release versions,
|
||||||
|
// while loopback-boot treats pre-releases the same way as regular versions
|
||||||
|
var version = (loopback.version || '1.0.0').replace(/-.*$/, '');
|
||||||
|
if (!semver.satisfies(version, RANGE)) {
|
||||||
|
var msg = g.f(
|
||||||
|
'The `app` is powered by an incompatible loopback version %s. ' +
|
||||||
|
'Supported versions: %s',
|
||||||
|
loopback.version || '(unknown)',
|
||||||
|
RANGE);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnv(app, env) {
|
||||||
|
if (env !== undefined)
|
||||||
|
app.set('env', env);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHost(app, appConfig) {
|
||||||
|
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||||
|
var host =
|
||||||
|
process.env.npm_config_host ||
|
||||||
|
process.env.OPENSHIFT_SLS_IP ||
|
||||||
|
process.env.OPENSHIFT_NODEJS_IP ||
|
||||||
|
process.env.HOST ||
|
||||||
|
process.env.VCAP_APP_HOST ||
|
||||||
|
appConfig.host ||
|
||||||
|
process.env.npm_package_config_host ||
|
||||||
|
app.get('host');
|
||||||
|
|
||||||
|
if (host !== undefined) {
|
||||||
|
assert(typeof host === 'string', 'app.host must be a string');
|
||||||
|
app.set('host', host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPort(app, appConfig) {
|
||||||
|
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||||
|
var port = find([
|
||||||
|
process.env.npm_config_port,
|
||||||
|
process.env.OPENSHIFT_SLS_PORT,
|
||||||
|
process.env.OPENSHIFT_NODEJS_PORT,
|
||||||
|
process.env.PORT,
|
||||||
|
process.env.VCAP_APP_PORT,
|
||||||
|
appConfig.port,
|
||||||
|
process.env.npm_package_config_port,
|
||||||
|
app.get('port'),
|
||||||
|
3000,
|
||||||
|
], function(p) {
|
||||||
|
return p != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (port !== undefined) {
|
||||||
|
var portType = typeof port;
|
||||||
|
assert(portType === 'string' || portType === 'number',
|
||||||
|
'app.port must be a string or number');
|
||||||
|
app.set('port', port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(array, predicate) {
|
||||||
|
return array.filter(predicate)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApiRoot(app, appConfig) {
|
||||||
|
var restApiRoot =
|
||||||
|
appConfig.restApiRoot ||
|
||||||
|
app.get('restApiRoot') ||
|
||||||
|
'/api';
|
||||||
|
|
||||||
|
assert(restApiRoot !== undefined, 'app.restBasePath is required');
|
||||||
|
assert(typeof restApiRoot === 'string',
|
||||||
|
'app.restApiRoot must be a string');
|
||||||
|
assert(/^\//.test(restApiRoot),
|
||||||
|
'app.restApiRoot must start with "/"');
|
||||||
|
app.set('restApiRoot', restApiRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAppConfig(app, appConfig) {
|
||||||
|
for (var configKey in appConfig) {
|
||||||
|
var cur = app.get(configKey);
|
||||||
|
if (cur === undefined || cur === null) {
|
||||||
|
app.set(configKey, appConfig[configKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Application.prototype.starting = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
app.booting = true;
|
||||||
|
assertLoopBackVersion(app);
|
||||||
|
|
||||||
|
var appConfig = context.instructions.application;
|
||||||
|
setEnv(app, context.instructions.env || this.options.env);
|
||||||
|
setHost(app, appConfig);
|
||||||
|
setPort(app, appConfig);
|
||||||
|
setApiRoot(app, appConfig);
|
||||||
|
applyAppConfig(app, appConfig);
|
||||||
|
};
|
||||||
|
|
||||||
|
Application.prototype.started = function(context, done) {
|
||||||
|
var app = context.app;
|
||||||
|
app.booting = false;
|
||||||
|
process.nextTick(function() {
|
||||||
|
app.emit('booted');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var utils = require('../utils');
|
||||||
|
var path = require('path');
|
||||||
|
var async = require('async');
|
||||||
|
var debug = require('debug')('loopback:boot:script');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var g = require('../globalize');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Script(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Script(options) {
|
||||||
|
PluginBase.call(this, options, 'bootScripts', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Script, PluginBase);
|
||||||
|
|
||||||
|
Script.prototype.load = function(context) {
|
||||||
|
var options = this.options;
|
||||||
|
var appRootDir = options.rootDir;
|
||||||
|
// require directories
|
||||||
|
var bootDirs = options.bootDirs || []; // precedence
|
||||||
|
bootDirs = bootDirs.concat(path.join(appRootDir, 'boot'));
|
||||||
|
utils.resolveRelativePaths(bootDirs, appRootDir);
|
||||||
|
|
||||||
|
var bootScripts = options.bootScripts || [];
|
||||||
|
utils.resolveRelativePaths(bootScripts, appRootDir);
|
||||||
|
|
||||||
|
bootDirs.forEach(function(dir) {
|
||||||
|
bootScripts = bootScripts.concat(utils.findScripts(dir));
|
||||||
|
var envdir = dir + '/' + options.env;
|
||||||
|
bootScripts = bootScripts.concat(utils.findScripts(envdir));
|
||||||
|
});
|
||||||
|
|
||||||
|
// de-dedup boot scripts -ERS
|
||||||
|
// https://github.com/strongloop/loopback-boot/issues/64
|
||||||
|
bootScripts = _.uniq(bootScripts);
|
||||||
|
debug('Boot scripts: %j', bootScripts);
|
||||||
|
this.configure(context, bootScripts);
|
||||||
|
return bootScripts;
|
||||||
|
};
|
||||||
|
|
||||||
|
Script.prototype.start = function(context, done) {
|
||||||
|
var app = context.app;
|
||||||
|
var instructions = context.instructions[this.name];
|
||||||
|
runScripts(app, instructions, done);
|
||||||
|
};
|
||||||
|
|
||||||
|
function runScripts(app, list, callback) {
|
||||||
|
list = list || [];
|
||||||
|
var functions = [];
|
||||||
|
list.forEach(function(filepath) {
|
||||||
|
debug('Requiring script %s', filepath);
|
||||||
|
try {
|
||||||
|
var exports = require(filepath);
|
||||||
|
if (typeof exports === 'function') {
|
||||||
|
debug('Exported function detected %s', filepath);
|
||||||
|
functions.push({
|
||||||
|
path: filepath,
|
||||||
|
func: exports,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
g.error('Failed loading boot script: %s\n%s', filepath, err.stack);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.eachSeries(functions, function(f, done) {
|
||||||
|
debug('Running script %s', f.path);
|
||||||
|
if (f.func.length >= 2) {
|
||||||
|
debug('Starting async function %s', f.path);
|
||||||
|
f.func(app, function(err) {
|
||||||
|
debug('Async function finished %s', f.path);
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
debug('Starting sync function %s', f.path);
|
||||||
|
var error;
|
||||||
|
try {
|
||||||
|
f.func(app);
|
||||||
|
debug('Sync function finished %s', f.path);
|
||||||
|
} catch (err) {
|
||||||
|
debug('Sync function failed %s', f.path, err);
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
done(error);
|
||||||
|
}
|
||||||
|
}, callback);
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var debug = require('debug')('loopback:boot:component');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
|
||||||
|
var utils = require('../utils');
|
||||||
|
|
||||||
|
var resolveAppScriptPath = utils.resolveAppScriptPath;
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Component(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Component(options) {
|
||||||
|
PluginBase.call(this, options, 'components', 'component-config');
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Component, PluginBase);
|
||||||
|
|
||||||
|
Component.prototype.getRootDir = function() {
|
||||||
|
return this.options.componentRootDir || this.options.rootDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.prototype.buildInstructions = function(context, rootDir, config) {
|
||||||
|
return Object.keys(config)
|
||||||
|
.filter(function(name) {
|
||||||
|
return !!config[name];
|
||||||
|
}).map(function(name) {
|
||||||
|
return {
|
||||||
|
sourceFile: resolveAppScriptPath(rootDir, name, { strict: true }),
|
||||||
|
config: config[name],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.prototype.start = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
var self = this;
|
||||||
|
context.instructions[this.name].forEach(function(data) {
|
||||||
|
debug('Configuring component %j', data.sourceFile);
|
||||||
|
var configFn = require(data.sourceFile);
|
||||||
|
data.config = self.getUpdatedConfigObject(context, data.config,
|
||||||
|
{ useEnvVars: true });
|
||||||
|
configFn(app, data.config);
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var utils = require('../utils');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var debug = require('debug')('loopback:boot:datasource');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new DataSource(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function DataSource(options) {
|
||||||
|
PluginBase.call(this, options, 'dataSources', 'datasources');
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(DataSource, PluginBase);
|
||||||
|
|
||||||
|
DataSource.prototype.getRootDir = function() {
|
||||||
|
return this.options.dsRootDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
DataSource.prototype.start = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
var self = this;
|
||||||
|
var lazyConnect = process.env.LB_LAZYCONNECT_DATASOURCES;
|
||||||
|
utils.forEachKeyedObject(context.instructions[this.name], function(key, obj) {
|
||||||
|
obj = self.getUpdatedConfigObject(context, obj, { useEnvVars: true });
|
||||||
|
debug('Registering data source %s %j', key, obj);
|
||||||
|
if (lazyConnect) {
|
||||||
|
obj.lazyConnect =
|
||||||
|
lazyConnect === 'false' || lazyConnect === '0' ? false : true;
|
||||||
|
}
|
||||||
|
app.dataSource(key, obj);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,263 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var assert = require('assert');
|
||||||
|
var path = require('path');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var cloneDeepWith = _.cloneDeepWith;
|
||||||
|
var cloneDeep = _.cloneDeep;
|
||||||
|
var debug = require('debug')('loopback:boot:middleware');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var utils = require('../utils');
|
||||||
|
var g = require('../globalize');
|
||||||
|
|
||||||
|
var resolveAppScriptPath = utils.resolveAppScriptPath;
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Middleware(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Middleware(options) {
|
||||||
|
PluginBase.call(this, options, 'middleware', 'middleware');
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Middleware, PluginBase);
|
||||||
|
|
||||||
|
Middleware.prototype.getRootDir = function() {
|
||||||
|
return this.options.middlewareRootDir || this.options.rootDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
Middleware.prototype.merge = function(target, config, fileName) {
|
||||||
|
var err, phase;
|
||||||
|
for (phase in config) {
|
||||||
|
if (phase in target) {
|
||||||
|
err = this.mergePhaseConfig(target[phase], config[phase], phase);
|
||||||
|
} else {
|
||||||
|
err = g.f('The {{phase}} "%s" is not defined in the main config.', phase);
|
||||||
|
}
|
||||||
|
if (err)
|
||||||
|
throw new Error(g.f('Cannot apply %s: ', fileName) + err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Middleware.prototype.mergePhaseConfig = function(target, config, phase) {
|
||||||
|
var err, mw;
|
||||||
|
for (mw in config) {
|
||||||
|
if (mw in target) {
|
||||||
|
var targetMiddleware = target[mw];
|
||||||
|
var configMiddleware = config[mw];
|
||||||
|
if (Array.isArray(targetMiddleware) && Array.isArray(configMiddleware)) {
|
||||||
|
// Both are arrays, combine them
|
||||||
|
target[mw] = this._mergeNamedItems(targetMiddleware, configMiddleware);
|
||||||
|
} else if (Array.isArray(targetMiddleware)) {
|
||||||
|
if (typeof configMiddleware === 'object' &&
|
||||||
|
Object.keys(configMiddleware).length) {
|
||||||
|
// Config side is an non-empty object
|
||||||
|
target[mw] = this._mergeNamedItems(targetMiddleware,
|
||||||
|
[configMiddleware]);
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(configMiddleware)) {
|
||||||
|
if (typeof targetMiddleware === 'object' &&
|
||||||
|
Object.keys(targetMiddleware).length) {
|
||||||
|
// Target side is an non-empty object
|
||||||
|
target[mw] = this._mergeNamedItems([targetMiddleware],
|
||||||
|
configMiddleware);
|
||||||
|
} else {
|
||||||
|
// Target side is empty
|
||||||
|
target[mw] = configMiddleware;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = this._mergeObjects(targetMiddleware, configMiddleware);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = g.f('The {{middleware}} "%s" in {{phase}} "%s"' +
|
||||||
|
'is not defined in the main config.', mw, phase);
|
||||||
|
}
|
||||||
|
if (err) return err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Middleware.prototype.buildInstructions = function(context, rootDir, config) {
|
||||||
|
var phasesNames = Object.keys(config);
|
||||||
|
var middlewareList = [];
|
||||||
|
phasesNames.forEach(function(phase) {
|
||||||
|
var phaseConfig = config[phase];
|
||||||
|
Object.keys(phaseConfig).forEach(function(middleware) {
|
||||||
|
var allConfigs = phaseConfig[middleware];
|
||||||
|
if (!Array.isArray(allConfigs))
|
||||||
|
allConfigs = [allConfigs];
|
||||||
|
|
||||||
|
allConfigs.forEach(function(config) {
|
||||||
|
var resolved = resolveMiddlewarePath(rootDir, middleware, config);
|
||||||
|
// resolved.sourceFile will be false-y if an optional middleware
|
||||||
|
// is not resolvable.
|
||||||
|
// if a non-optional middleware is not resolvable, it will throw
|
||||||
|
// at resolveAppPath() and not reach here
|
||||||
|
if (!resolved.sourceFile) {
|
||||||
|
return g.log('Middleware "%s" not found: %s',
|
||||||
|
middleware,
|
||||||
|
resolved.optional
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var middlewareConfig = cloneDeep(config);
|
||||||
|
middlewareConfig.phase = phase;
|
||||||
|
|
||||||
|
if (middlewareConfig.params) {
|
||||||
|
middlewareConfig.params = resolveMiddlewareParams(
|
||||||
|
rootDir, middlewareConfig.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = {
|
||||||
|
sourceFile: resolved.sourceFile,
|
||||||
|
config: middlewareConfig,
|
||||||
|
};
|
||||||
|
if (resolved.fragment) {
|
||||||
|
item.fragment = resolved.fragment;
|
||||||
|
}
|
||||||
|
middlewareList.push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var flattenedPhaseNames = phasesNames
|
||||||
|
.map(function getBaseName(name) {
|
||||||
|
return name.replace(/:[^:]+$/, '');
|
||||||
|
})
|
||||||
|
.filter(function differsFromPreviousItem(value, ix, source) {
|
||||||
|
// Skip duplicate entries. That happens when
|
||||||
|
// `name:before` and `name:after` are both translated to `name`
|
||||||
|
return ix === 0 || value !== source[ix - 1];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
phases: flattenedPhaseNames,
|
||||||
|
middleware: middlewareList,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveMiddlewarePath(rootDir, middleware, config) {
|
||||||
|
var resolved = {
|
||||||
|
optional: !!config.optional,
|
||||||
|
};
|
||||||
|
|
||||||
|
var segments = middleware.split('#');
|
||||||
|
var pathName = segments[0];
|
||||||
|
var fragment = segments[1];
|
||||||
|
var middlewarePath = pathName;
|
||||||
|
var opts = {
|
||||||
|
strict: true,
|
||||||
|
optional: !!config.optional,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fragment) {
|
||||||
|
resolved.fragment = fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathName.indexOf('./') === 0 || pathName.indexOf('../') === 0) {
|
||||||
|
// Relative path
|
||||||
|
pathName = path.resolve(rootDir, pathName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveOpts = _.extend(opts, {
|
||||||
|
// Workaround for strong-agent to allow probes to detect that
|
||||||
|
// strong-express-middleware was loaded: exclude the path to the
|
||||||
|
// module main file from the source file path.
|
||||||
|
// For example, return
|
||||||
|
// node_modules/strong-express-metrics
|
||||||
|
// instead of
|
||||||
|
// node_modules/strong-express-metrics/index.js
|
||||||
|
fullResolve: false,
|
||||||
|
});
|
||||||
|
var sourceFile = resolveAppScriptPath(rootDir, middlewarePath, resolveOpts);
|
||||||
|
|
||||||
|
if (!fragment) {
|
||||||
|
resolved.sourceFile = sourceFile;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to require the module and check if <module>.<fragment> is a valid
|
||||||
|
// function
|
||||||
|
var m = require(sourceFile);
|
||||||
|
if (typeof m[fragment] === 'function') {
|
||||||
|
resolved.sourceFile = sourceFile;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* module/server/middleware/fragment
|
||||||
|
* module/middleware/fragment
|
||||||
|
*/
|
||||||
|
var candidates = [
|
||||||
|
pathName + '/server/middleware/' + fragment,
|
||||||
|
pathName + '/middleware/' + fragment,
|
||||||
|
// TODO: [rfeng] Should we support the following flavors?
|
||||||
|
// pathName + '/lib/' + fragment,
|
||||||
|
// pathName + '/' + fragment
|
||||||
|
];
|
||||||
|
|
||||||
|
var err, ix;
|
||||||
|
for (ix in candidates) {
|
||||||
|
try {
|
||||||
|
resolved.sourceFile = resolveAppScriptPath(rootDir, candidates[ix], opts);
|
||||||
|
delete resolved.fragment;
|
||||||
|
return resolved;
|
||||||
|
} catch (e) {
|
||||||
|
// Report the error for the first candidate when no candidate matches
|
||||||
|
if (!err) err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match values starting with `$!./` or `$!../`
|
||||||
|
var MIDDLEWARE_PATH_PARAM_REGEX = /^\$!(\.\/|\.\.\/)/;
|
||||||
|
|
||||||
|
function resolveMiddlewareParams(rootDir, params) {
|
||||||
|
return cloneDeepWith(params, function resolvePathParam(value) {
|
||||||
|
if (typeof value === 'string' && MIDDLEWARE_PATH_PARAM_REGEX.test(value)) {
|
||||||
|
return path.resolve(rootDir, value.slice(2));
|
||||||
|
} else {
|
||||||
|
return undefined; // no change
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Middleware.prototype.start = function(context) {
|
||||||
|
var self = this;
|
||||||
|
var app = context.app;
|
||||||
|
var instructions = context.instructions.middleware;
|
||||||
|
if (!instructions) {
|
||||||
|
// the browserified client does not support middleware
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phases can be empty
|
||||||
|
var phases = instructions.phases || [];
|
||||||
|
assert(Array.isArray(phases),
|
||||||
|
'Middleware phases must be an array');
|
||||||
|
|
||||||
|
var middleware = instructions.middleware;
|
||||||
|
assert(Array.isArray(middleware),
|
||||||
|
'Middleware must be an array');
|
||||||
|
|
||||||
|
debug('Defining middleware phases %j', phases);
|
||||||
|
app.defineMiddlewarePhases(phases);
|
||||||
|
|
||||||
|
middleware.forEach(function(data) {
|
||||||
|
debug('Configuring middleware %j%s', data.sourceFile,
|
||||||
|
data.fragment ? ('#' + data.fragment) : '');
|
||||||
|
var factory = require(data.sourceFile);
|
||||||
|
if (data.fragment) {
|
||||||
|
factory = factory[data.fragment].bind(factory);
|
||||||
|
}
|
||||||
|
assert(typeof factory === 'function',
|
||||||
|
'Middleware factory must be a function');
|
||||||
|
data.config = self.getUpdatedConfigObject(context, data.config,
|
||||||
|
{ useEnvVars: true });
|
||||||
|
app.middlewareFromConfig(factory, data.config);
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,195 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var debug = require('debug')('loopback:boot:mixin');
|
||||||
|
var utils = require('../utils');
|
||||||
|
var g = require('../globalize');
|
||||||
|
|
||||||
|
var tryResolveAppPath = utils.tryResolveAppPath;
|
||||||
|
var getExcludedExtensions = utils.getExcludedExtensions;
|
||||||
|
var findScripts = utils.findScripts;
|
||||||
|
var FILE_EXTENSION_JSON = utils.FILE_EXTENSION_JSON;
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Mixin(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Mixin(options) {
|
||||||
|
PluginBase.call(this, options, 'mixins', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Mixin, PluginBase);
|
||||||
|
|
||||||
|
Mixin.prototype.buildInstructions = function(context, rootDir, config) {
|
||||||
|
var modelsMeta = context.configurations.mixins._meta || {};
|
||||||
|
var modelInstructions = context.instructions.models;
|
||||||
|
var mixinDirs = this.options.mixinDirs || [];
|
||||||
|
var mixinSources = this.options.mixinSources || modelsMeta.mixins ||
|
||||||
|
['./mixins'];
|
||||||
|
|
||||||
|
var mixinInstructions = buildAllMixinInstructions(
|
||||||
|
rootDir, mixinDirs, mixinSources, this.options, modelInstructions);
|
||||||
|
|
||||||
|
return mixinInstructions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildAllMixinInstructions(appRootDir, mixinDirs, mixinSources, options,
|
||||||
|
modelInstructions) {
|
||||||
|
var extensions = _.without(_.keys(require.extensions),
|
||||||
|
_.keys(getExcludedExtensions()));
|
||||||
|
|
||||||
|
// load mixins from `options.mixins`
|
||||||
|
var sourceFiles = options.mixins || [];
|
||||||
|
var instructionsFromMixins = loadMixins(sourceFiles, options);
|
||||||
|
|
||||||
|
// load mixins from `options.mixinDirs`
|
||||||
|
sourceFiles = findMixinDefinitions(appRootDir, mixinDirs, extensions);
|
||||||
|
if (sourceFiles === undefined) return;
|
||||||
|
var instructionsFromMixinDirs = loadMixins(sourceFiles, options);
|
||||||
|
|
||||||
|
/* If `mixinDirs` and `mixinSources` have any directories in common,
|
||||||
|
* then remove the common directories from `mixinSources` */
|
||||||
|
mixinSources = _.difference(mixinSources, mixinDirs);
|
||||||
|
|
||||||
|
// load mixins from `options.mixinSources`
|
||||||
|
sourceFiles = findMixinDefinitions(appRootDir, mixinSources, extensions);
|
||||||
|
if (sourceFiles === undefined) return;
|
||||||
|
var instructionsFromMixinSources = loadMixins(sourceFiles, options);
|
||||||
|
|
||||||
|
// Fetch unique list of mixin names, used in models
|
||||||
|
var modelMixins = fetchMixinNamesUsedInModelInstructions(modelInstructions);
|
||||||
|
modelMixins = _.uniq(modelMixins);
|
||||||
|
|
||||||
|
// Filter-in only mixins, that are used in models
|
||||||
|
instructionsFromMixinSources = filterMixinInstructionsUsingWhitelist(
|
||||||
|
instructionsFromMixinSources, modelMixins);
|
||||||
|
|
||||||
|
var mixins = _.assign(
|
||||||
|
instructionsFromMixins,
|
||||||
|
instructionsFromMixinDirs,
|
||||||
|
instructionsFromMixinSources);
|
||||||
|
|
||||||
|
return _.values(mixins);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMixinDefinitions(appRootDir, sourceDirs, extensions) {
|
||||||
|
var files = [];
|
||||||
|
sourceDirs.forEach(function(dir) {
|
||||||
|
var path = tryResolveAppPath(appRootDir, dir);
|
||||||
|
if (!path) {
|
||||||
|
debug('Skipping unknown module source dir %j', dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
files = files.concat(findScripts(path, extensions));
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMixins(sourceFiles, options) {
|
||||||
|
var mixinInstructions = {};
|
||||||
|
sourceFiles.forEach(function(filepath) {
|
||||||
|
var dir = path.dirname(filepath);
|
||||||
|
var ext = path.extname(filepath);
|
||||||
|
var name = path.basename(filepath, ext);
|
||||||
|
var metafile = path.join(dir, name + FILE_EXTENSION_JSON);
|
||||||
|
|
||||||
|
name = normalizeMixinName(name, options);
|
||||||
|
var meta = {};
|
||||||
|
meta.name = name;
|
||||||
|
if (utils.fileExistsSync(metafile)) {
|
||||||
|
// May overwrite name, not sourceFile
|
||||||
|
_.extend(meta, require(metafile));
|
||||||
|
}
|
||||||
|
meta.sourceFile = filepath;
|
||||||
|
mixinInstructions[meta.name] = meta;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mixinInstructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMixinNamesUsedInModelInstructions(modelInstructions) {
|
||||||
|
return _.flatten(modelInstructions
|
||||||
|
.map(function(model) {
|
||||||
|
return model.definition && model.definition.mixins ?
|
||||||
|
Object.keys(model.definition.mixins) : [];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterMixinInstructionsUsingWhitelist(instructions, includeMixins) {
|
||||||
|
var instructionKeys = Object.keys(instructions);
|
||||||
|
includeMixins = _.intersection(instructionKeys, includeMixins);
|
||||||
|
|
||||||
|
var filteredInstructions = {};
|
||||||
|
instructionKeys.forEach(function(mixinName) {
|
||||||
|
if (includeMixins.indexOf(mixinName) !== -1) {
|
||||||
|
filteredInstructions[mixinName] = instructions[mixinName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filteredInstructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMixinName(str, options) {
|
||||||
|
var normalization = options.normalization;
|
||||||
|
switch (normalization) {
|
||||||
|
case false:
|
||||||
|
case 'none':
|
||||||
|
return str;
|
||||||
|
|
||||||
|
case undefined:
|
||||||
|
case 'classify':
|
||||||
|
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
|
||||||
|
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
|
||||||
|
str = str.replace(/(?:^|\s|-)\S/g, function(c) {
|
||||||
|
return c.toUpperCase();
|
||||||
|
});
|
||||||
|
str = str.replace(/\s+/g, '');
|
||||||
|
return str;
|
||||||
|
|
||||||
|
case 'dasherize':
|
||||||
|
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
|
||||||
|
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
|
||||||
|
str = str.replace(/\s+/g, '-');
|
||||||
|
return str;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (typeof normalization === 'function') {
|
||||||
|
return normalization(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
var err = new Error(g.f('Invalid normalization format - "%s"',
|
||||||
|
normalization));
|
||||||
|
err.code = 'INVALID_NORMALIZATION_FORMAT';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mixin.prototype.starting = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
var instructions = context.instructions.mixins;
|
||||||
|
|
||||||
|
var modelBuilder = (app.registry || app.loopback).modelBuilder;
|
||||||
|
var BaseClass = app.loopback.Model;
|
||||||
|
var mixins = instructions || [];
|
||||||
|
|
||||||
|
if (!modelBuilder.mixins || !mixins.length) return;
|
||||||
|
|
||||||
|
mixins.forEach(function(obj) {
|
||||||
|
debug('Requiring mixin %s', obj.sourceFile);
|
||||||
|
var mixin = require(obj.sourceFile);
|
||||||
|
|
||||||
|
if (typeof mixin === 'function' || mixin.prototype instanceof BaseClass) {
|
||||||
|
debug('Defining mixin %s', obj.name);
|
||||||
|
modelBuilder.mixins.define(obj.name, mixin); // TODO (name, mixin, meta)
|
||||||
|
} else {
|
||||||
|
debug('Skipping mixin file %s - `module.exports` is not a function' +
|
||||||
|
' or Loopback model', obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,304 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var assert = require('assert');
|
||||||
|
var util = require('util');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
var path = require('path');
|
||||||
|
var debug = require('debug')('loopback:boot:model');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var toposort = require('toposort');
|
||||||
|
var utils = require('../utils');
|
||||||
|
|
||||||
|
var tryReadDir = utils.tryReadDir;
|
||||||
|
var assertIsValidConfig = utils.assertIsValidConfig;
|
||||||
|
var tryResolveAppPath = utils.tryResolveAppPath;
|
||||||
|
var fixFileExtension = utils.fixFileExtension;
|
||||||
|
var g = require('../globalize');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Model(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Model(options) {
|
||||||
|
PluginBase.call(this, options, 'models', 'model-config');
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Model, PluginBase);
|
||||||
|
|
||||||
|
Model.prototype.getRootDir = function() {
|
||||||
|
return this.options.modelsRootDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
Model.prototype.load = function(context) {
|
||||||
|
var config = PluginBase.prototype.load.apply(this, arguments);
|
||||||
|
assertIsValidModelConfig(config);
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
Model.prototype.buildInstructions = function(context, rootDir, modelsConfig) {
|
||||||
|
var modelsMeta = modelsConfig._meta || {};
|
||||||
|
delete modelsConfig._meta;
|
||||||
|
context.configurations.mixins._meta = modelsMeta;
|
||||||
|
|
||||||
|
var modelSources = this.options.modelSources || modelsMeta.sources ||
|
||||||
|
['./models'];
|
||||||
|
var modelInstructions = buildAllModelInstructions(
|
||||||
|
rootDir, modelsConfig, modelSources, this.options.modelDefinitions);
|
||||||
|
return modelInstructions;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildAllModelInstructions(rootDir, modelsConfig, sources,
|
||||||
|
modelDefinitions) {
|
||||||
|
var registry = verifyModelDefinitions(rootDir, modelDefinitions) ||
|
||||||
|
findModelDefinitions(rootDir, sources);
|
||||||
|
|
||||||
|
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
|
||||||
|
|
||||||
|
var instructions = modelNamesToBuild
|
||||||
|
.map(function createModelInstructions(name) {
|
||||||
|
var config = modelsConfig[name];
|
||||||
|
var definition = registry[name] || {};
|
||||||
|
|
||||||
|
debug('Using model "%s"\nConfiguration: %j\nDefinition %j',
|
||||||
|
name, config, definition.definition);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name,
|
||||||
|
config: config,
|
||||||
|
definition: definition.definition,
|
||||||
|
sourceFile: definition.sourceFile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByInheritance(instructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAllBaseModels(registry, modelNames) {
|
||||||
|
var result = [];
|
||||||
|
var visited = {};
|
||||||
|
|
||||||
|
while (modelNames.length) {
|
||||||
|
var name = modelNames.shift();
|
||||||
|
|
||||||
|
if (visited[name]) continue;
|
||||||
|
visited[name] = true;
|
||||||
|
result.push(name);
|
||||||
|
|
||||||
|
var definition = registry[name] && registry[name].definition;
|
||||||
|
if (!definition) continue;
|
||||||
|
|
||||||
|
var base = getBaseModelName(definition);
|
||||||
|
|
||||||
|
// ignore built-in models like User
|
||||||
|
if (!registry[base]) continue;
|
||||||
|
|
||||||
|
modelNames.push(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseModelName(modelDefinition) {
|
||||||
|
if (!modelDefinition)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
return modelDefinition.base ||
|
||||||
|
modelDefinition.options && modelDefinition.options.base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByInheritance(instructions) {
|
||||||
|
// create edges Base name -> Model name
|
||||||
|
var edges = instructions
|
||||||
|
.map(function(inst) {
|
||||||
|
return [getBaseModelName(inst.definition), inst.name];
|
||||||
|
});
|
||||||
|
|
||||||
|
var sortedNames = toposort(edges);
|
||||||
|
|
||||||
|
var instructionsByModelName = {};
|
||||||
|
instructions.forEach(function(inst) {
|
||||||
|
instructionsByModelName[inst.name] = inst;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedNames
|
||||||
|
// convert to instructions
|
||||||
|
.map(function(name) {
|
||||||
|
return instructionsByModelName[name];
|
||||||
|
})
|
||||||
|
// remove built-in models
|
||||||
|
.filter(function(inst) {
|
||||||
|
return !!inst;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyModelDefinitions(rootDir, modelDefinitions) {
|
||||||
|
if (!modelDefinitions || modelDefinitions.length < 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry = {};
|
||||||
|
modelDefinitions.forEach(function(definition, idx) {
|
||||||
|
if (definition.sourceFile) {
|
||||||
|
var fullPath = path.resolve(rootDir, definition.sourceFile);
|
||||||
|
definition.sourceFile = fixFileExtension(
|
||||||
|
fullPath,
|
||||||
|
tryReadDir(path.dirname(fullPath)),
|
||||||
|
true);
|
||||||
|
if (!definition.sourceFile) {
|
||||||
|
debug('Model source code not found: %s - %s', definition.sourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Found model "%s" - %s %s',
|
||||||
|
definition.definition.name,
|
||||||
|
'from options',
|
||||||
|
definition.sourceFile ?
|
||||||
|
path.relative(rootDir, definition.sourceFile) :
|
||||||
|
'(no source file)');
|
||||||
|
|
||||||
|
var modelName = definition.definition.name;
|
||||||
|
if (!modelName) {
|
||||||
|
debug('Skipping model definition without Model name ' +
|
||||||
|
'(from options.modelDefinitions @ index %s)',
|
||||||
|
idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registry[modelName] = definition;
|
||||||
|
});
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findModelDefinitions(rootDir, sources) {
|
||||||
|
var registry = {};
|
||||||
|
|
||||||
|
sources.forEach(function(src) {
|
||||||
|
var srcDir = tryResolveAppPath(rootDir, src, { strict: false });
|
||||||
|
if (!srcDir) {
|
||||||
|
debug('Skipping unknown module source dir %j', src);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = tryReadDir(srcDir);
|
||||||
|
|
||||||
|
files
|
||||||
|
.filter(function(f) {
|
||||||
|
return f[0] !== '_' && path.extname(f) === '.json';
|
||||||
|
})
|
||||||
|
.forEach(function(f) {
|
||||||
|
var fullPath = path.resolve(srcDir, f);
|
||||||
|
var entry = loadModelDefinition(rootDir, fullPath, files);
|
||||||
|
var modelName = entry.definition.name;
|
||||||
|
if (!modelName) {
|
||||||
|
debug('Skipping model definition without Model name: %s',
|
||||||
|
path.relative(srcDir, fullPath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registry[modelName] = entry;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadModelDefinition(rootDir, jsonFile, allFiles) {
|
||||||
|
var definition = require(jsonFile);
|
||||||
|
var basename = path.basename(jsonFile, path.extname(jsonFile));
|
||||||
|
definition.name = definition.name || _.upperFirst(_.camelCase(basename));
|
||||||
|
|
||||||
|
// find a matching file with a supported extension like `.js` or `.coffee`
|
||||||
|
var sourceFile = fixFileExtension(jsonFile, allFiles, true);
|
||||||
|
if (sourceFile === undefined) {
|
||||||
|
debug('Model source code not found: %s', sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Found model "%s" - %s %s', definition.name,
|
||||||
|
path.relative(rootDir, jsonFile),
|
||||||
|
sourceFile ? path.relative(rootDir, sourceFile) : '(no source file)');
|
||||||
|
|
||||||
|
return {
|
||||||
|
definition: definition,
|
||||||
|
sourceFile: sourceFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIsValidModelConfig(config) {
|
||||||
|
assertIsValidConfig('model', config);
|
||||||
|
for (var name in config) {
|
||||||
|
var entry = config[name];
|
||||||
|
var options = entry.options || {};
|
||||||
|
var unsupported = entry.properties ||
|
||||||
|
entry.base || options.base ||
|
||||||
|
entry.plural || options.plural;
|
||||||
|
|
||||||
|
if (unsupported) {
|
||||||
|
throw new Error(g.f(
|
||||||
|
'The data in {{model-config.json}} ' +
|
||||||
|
'is in the unsupported {{1.x}} format.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular expression to match built-in loopback models
|
||||||
|
var LOOPBACK_MODEL_REGEXP = new RegExp(
|
||||||
|
['', 'node_modules', 'loopback', '[^\\/\\\\]+', 'models', '[^\\/\\\\]+\\.js$']
|
||||||
|
.join('\\' + path.sep));
|
||||||
|
|
||||||
|
function isBuiltinLoopBackModel(app, data) {
|
||||||
|
// 1. Built-in models are exposed on the loopback object
|
||||||
|
if (!app.loopback[data.name]) return false;
|
||||||
|
|
||||||
|
// 2. Built-in models have a script file `loopback/{facet}/models/{name}.js`
|
||||||
|
var srcFile = data.sourceFile;
|
||||||
|
return srcFile &&
|
||||||
|
LOOPBACK_MODEL_REGEXP.test(srcFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Model.prototype.start = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
var instructions = context.instructions[this.name];
|
||||||
|
|
||||||
|
var registry = app.registry || app.loopback;
|
||||||
|
instructions.forEach(function(data) {
|
||||||
|
var name = data.name;
|
||||||
|
var model;
|
||||||
|
|
||||||
|
if (!data.definition) {
|
||||||
|
model = registry.getModel(name);
|
||||||
|
if (!model) {
|
||||||
|
throw new Error(g.f('Cannot configure unknown model %s', name));
|
||||||
|
}
|
||||||
|
debug('Configuring existing model %s', name);
|
||||||
|
} else if (isBuiltinLoopBackModel(app, data)) {
|
||||||
|
model = registry.getModel(name);
|
||||||
|
assert(model, 'Built-in model ' + name + ' should have been defined');
|
||||||
|
debug('Configuring built-in LoopBack model %s', name);
|
||||||
|
} else {
|
||||||
|
debug('Creating new model %s %j', name, data.definition);
|
||||||
|
model = registry.createModel(data.definition);
|
||||||
|
if (data.sourceFile) {
|
||||||
|
debug('Loading customization script %s', data.sourceFile);
|
||||||
|
var code = require(data.sourceFile);
|
||||||
|
if (typeof code === 'function') {
|
||||||
|
debug('Customizing model %s', name);
|
||||||
|
code(model);
|
||||||
|
} else {
|
||||||
|
debug('Skipping model file %s - `module.exports` is not a function',
|
||||||
|
data.sourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data._model = model;
|
||||||
|
});
|
||||||
|
|
||||||
|
instructions.forEach(function(data) {
|
||||||
|
// Skip base models that are not exported to the app
|
||||||
|
if (!data.config) return;
|
||||||
|
|
||||||
|
app.model(data._model, data.config);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
var util = require('util');
|
||||||
|
var PluginBase = require('../plugin-base');
|
||||||
|
|
||||||
|
module.exports = function(options) {
|
||||||
|
return new Swagger(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Swagger(options) {
|
||||||
|
PluginBase.call(this, options, 'apis', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
util.inherits(Swagger, PluginBase);
|
||||||
|
|
||||||
|
Swagger.prototype.start = function(context) {
|
||||||
|
var app = context.app;
|
||||||
|
var appConfig = context.instructions.application;
|
||||||
|
// disable token requirement for swagger, if available
|
||||||
|
var swagger = app.remotes().exports.swagger;
|
||||||
|
if (!swagger) return;
|
||||||
|
|
||||||
|
var requireTokenForSwagger = appConfig.swagger &&
|
||||||
|
appConfig.swagger.requireToken;
|
||||||
|
swagger.requireToken = requireTokenForSwagger || false;
|
||||||
|
};
|
325
lib/utils.js
325
lib/utils.js
|
@ -3,9 +3,333 @@
|
||||||
// This file is licensed under the MIT License.
|
// This file is licensed under the MIT License.
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
var debug = require('debug')('loopback:boot');
|
||||||
|
var path = require('path');
|
||||||
|
var Module = require('module');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
var assert = require('assert');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var g = require('./globalize');
|
||||||
|
|
||||||
|
exports.tryReadDir = tryReadDir;
|
||||||
|
exports.resolveRelativePaths = resolveRelativePaths;
|
||||||
|
exports.assertIsValidConfig = assertIsValidConfig;
|
||||||
exports.fileExistsSync = fileExistsSync;
|
exports.fileExistsSync = fileExistsSync;
|
||||||
|
exports.fixFileExtension = fixFileExtension;
|
||||||
|
exports.findScripts = findScripts;
|
||||||
|
exports.resolveAppScriptPath = resolveAppScriptPath;
|
||||||
|
exports.getExcludedExtensions = getExcludedExtensions;
|
||||||
|
exports.tryResolveAppPath = tryResolveAppPath;
|
||||||
|
exports.forEachKeyedObject = forEachKeyedObject;
|
||||||
|
exports.mergePhaseNameLists = mergePhaseNameLists;
|
||||||
|
|
||||||
|
var FILE_EXTENSION_JSON = exports.FILE_EXTENSION_JSON = '.json';
|
||||||
|
/**
|
||||||
|
* Find all javascript files (except for those prefixed with _)
|
||||||
|
* and all directories.
|
||||||
|
* @param {String} dir Full path of the directory to enumerate.
|
||||||
|
* @return {Array.<String>} A list of absolute paths to pass to `require()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function findScripts(dir, extensions) {
|
||||||
|
assert(dir, 'cannot require directory contents without directory name');
|
||||||
|
|
||||||
|
var files = tryReadDir(dir);
|
||||||
|
extensions = extensions || _.keys(require.extensions);
|
||||||
|
|
||||||
|
// sort files in lowercase alpha for linux
|
||||||
|
files.sort(function(a, b) {
|
||||||
|
a = a.toLowerCase();
|
||||||
|
b = b.toLowerCase();
|
||||||
|
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
} else if (b < a) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = [];
|
||||||
|
files.forEach(function(filename) {
|
||||||
|
// ignore index.js and files prefixed with underscore
|
||||||
|
if (filename === 'index.js' || filename[0] === '_') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filepath = path.resolve(path.join(dir, filename));
|
||||||
|
var stats = fs.statSync(filepath);
|
||||||
|
|
||||||
|
// only require files supported by require.extensions (.txt .md etc.)
|
||||||
|
if (stats.isFile()) {
|
||||||
|
if (isPreferredExtension(filename))
|
||||||
|
results.push(filepath);
|
||||||
|
else
|
||||||
|
debug('Skipping file %s - unknown extension', filepath);
|
||||||
|
} else {
|
||||||
|
debug('Skipping directory %s', filepath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryReadDir() {
|
||||||
|
try {
|
||||||
|
return fs.readdirSync.apply(fs, arguments);
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRelativePaths(relativePaths, appRootDir) {
|
||||||
|
var resolveOpts = { strict: false };
|
||||||
|
relativePaths.forEach(function(relativePath, k) {
|
||||||
|
var resolvedPath = tryResolveAppPath(appRootDir, relativePath, resolveOpts);
|
||||||
|
if (resolvedPath !== undefined) {
|
||||||
|
relativePaths[k] = resolvedPath;
|
||||||
|
} else {
|
||||||
|
debug ('skipping boot script %s - unknown file', relativePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExcludedExtensions() {
|
||||||
|
return {
|
||||||
|
'.json': '.json',
|
||||||
|
'.node': 'node',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPreferredExtension(filename) {
|
||||||
|
var includeExtensions = require.extensions;
|
||||||
|
|
||||||
|
var ext = path.extname(filename);
|
||||||
|
return (ext in includeExtensions) && !(ext in getExcludedExtensions());
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixFileExtension(filepath, files, onlyScriptsExportingFunction) {
|
||||||
|
var results = [];
|
||||||
|
var otherFile;
|
||||||
|
|
||||||
|
/* Prefer coffee scripts over json */
|
||||||
|
if (isPreferredExtension(filepath)) return filepath;
|
||||||
|
|
||||||
|
var basename = path.basename(filepath, FILE_EXTENSION_JSON);
|
||||||
|
var sourceDir = path.dirname(filepath);
|
||||||
|
|
||||||
|
files.forEach(function(f) {
|
||||||
|
otherFile = path.resolve(sourceDir, f);
|
||||||
|
|
||||||
|
var stats = fs.statSync(otherFile);
|
||||||
|
if (stats.isFile()) {
|
||||||
|
var otherFileExtension = path.extname(f);
|
||||||
|
|
||||||
|
if (!(otherFileExtension in getExcludedExtensions()) &&
|
||||||
|
path.basename(f, otherFileExtension) == basename) {
|
||||||
|
if (!onlyScriptsExportingFunction)
|
||||||
|
results.push(otherFile);
|
||||||
|
else if (onlyScriptsExportingFunction &&
|
||||||
|
(typeof require.extensions[otherFileExtension]) === 'function') {
|
||||||
|
results.push(otherFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results.length > 0 ? results[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAppPath(rootDir, relativePath, resolveOptions) {
|
||||||
|
var resolvedPath = tryResolveAppPath(rootDir, relativePath, resolveOptions);
|
||||||
|
if (resolvedPath === undefined && !resolveOptions.optional) {
|
||||||
|
var err = new Error(g.f('Cannot resolve path "%s"', relativePath));
|
||||||
|
err.code = 'PATH_NOT_FOUND';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
|
||||||
|
var resolvedPath = resolveAppPath(rootDir, relativePath, resolveOptions);
|
||||||
|
if (!resolvedPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var sourceDir = path.dirname(resolvedPath);
|
||||||
|
var files = tryReadDir(sourceDir);
|
||||||
|
var fixedFile = fixFileExtension(resolvedPath, files, false);
|
||||||
|
return (fixedFile === undefined ? resolvedPath : fixedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryResolveAppPath(rootDir, relativePath, resolveOptions) {
|
||||||
|
var fullPath;
|
||||||
|
var start = relativePath.substring(0, 2);
|
||||||
|
|
||||||
|
/* In order to retain backward compatibility, we need to support
|
||||||
|
* two ways how to treat values that are not relative nor absolute
|
||||||
|
* path (e.g. `relativePath = 'foobar'`)
|
||||||
|
* - `resolveOptions.strict = true` searches in `node_modules` only
|
||||||
|
* - `resolveOptions.strict = false` attempts to resolve the value
|
||||||
|
* as a relative path first before searching `node_modules`
|
||||||
|
*/
|
||||||
|
resolveOptions = resolveOptions || { strict: true };
|
||||||
|
|
||||||
|
var isModuleRelative = false;
|
||||||
|
if (relativePath[0] === '/') {
|
||||||
|
fullPath = relativePath;
|
||||||
|
} else if (start === './' || start === '..') {
|
||||||
|
fullPath = path.resolve(rootDir, relativePath);
|
||||||
|
} else if (!resolveOptions.strict) {
|
||||||
|
isModuleRelative = true;
|
||||||
|
fullPath = path.resolve(rootDir, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath) {
|
||||||
|
// This check is needed to support paths pointing to a directory
|
||||||
|
if (fileExistsSync(fullPath)) {
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fullPath = require.resolve(fullPath);
|
||||||
|
return fullPath;
|
||||||
|
} catch (err) {
|
||||||
|
if (!isModuleRelative) {
|
||||||
|
debug ('Skipping %s - %s', fullPath, err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle module-relative path, e.g. `loopback/common/models`
|
||||||
|
|
||||||
|
// Module.globalPaths is a list of globally configured paths like
|
||||||
|
// [ env.NODE_PATH values, $HOME/.node_modules, etc. ]
|
||||||
|
// Module._nodeModulePaths(rootDir) returns a list of paths like
|
||||||
|
// [ rootDir/node_modules, rootDir/../node_modules, etc. ]
|
||||||
|
var modulePaths = Module.globalPaths
|
||||||
|
.concat(Module._nodeModulePaths(rootDir));
|
||||||
|
|
||||||
|
fullPath = modulePaths
|
||||||
|
.map(function(candidateDir) {
|
||||||
|
var absPath = path.join(candidateDir, relativePath);
|
||||||
|
try {
|
||||||
|
// NOTE(bajtos) We need to create a proper String object here,
|
||||||
|
// otherwise we can't attach additional properties to it
|
||||||
|
/*jshint -W053 */
|
||||||
|
var filePath = new String(require.resolve(absPath));
|
||||||
|
filePath.unresolvedPath = absPath;
|
||||||
|
return filePath;
|
||||||
|
} catch (err) {
|
||||||
|
return absPath;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(function(candidate) {
|
||||||
|
return fileExistsSync(candidate.toString());
|
||||||
|
})
|
||||||
|
[0];
|
||||||
|
|
||||||
|
if (fullPath) {
|
||||||
|
if (fullPath.unresolvedPath && resolveOptions.fullResolve === false)
|
||||||
|
return fullPath.unresolvedPath;
|
||||||
|
// Convert String object back to plain string primitive
|
||||||
|
return fullPath.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug ('Skipping %s - module not found', fullPath);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIsValidConfig(name, config) {
|
||||||
|
if (config) {
|
||||||
|
assert(typeof config === 'object',
|
||||||
|
name + ' config must be a valid JSON object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEachKeyedObject(obj, fn) {
|
||||||
|
if (typeof obj !== 'object') return;
|
||||||
|
|
||||||
|
Object.keys(obj).forEach(function(key) {
|
||||||
|
fn(key, obj[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the list of builtin phases by merging in an array of phases
|
||||||
|
* requested by a user while preserving the relative order of phases
|
||||||
|
* as specified by both arrays.
|
||||||
|
*
|
||||||
|
* If the first new name does not match any existing phase, it is inserted
|
||||||
|
* as the first phase in the new list. The same applies for the second phase,
|
||||||
|
* and so on, until an existing phase is found.
|
||||||
|
*
|
||||||
|
* Any new names in the middle of the array are inserted immediatelly after
|
||||||
|
* the last common phase. For example, extending
|
||||||
|
* `["initial", "session", "auth"]` with `["initial", "preauth", "auth"]`
|
||||||
|
* results in `["initial", "preauth", "session", "auth"]`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* **Example**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* var result = mergePhaseNameLists(
|
||||||
|
* ['initial', 'session', 'auth', 'routes', 'files', 'final'],
|
||||||
|
* ['initial', 'postinit', 'preauth', 'auth',
|
||||||
|
* 'routes', 'subapps', 'final', 'last']
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // result: [
|
||||||
|
* // 'initial', 'postinit', 'preauth', 'session', 'auth',
|
||||||
|
* // 'routes', 'subapps', 'files', 'final', 'last'
|
||||||
|
* // ]
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Array} currentNames The current list of phase names.
|
||||||
|
* @param {Array} namesToMerge The items to add (zip merge) into the target
|
||||||
|
* array.
|
||||||
|
* @returns {Array} A new array containing combined items from both arrays.
|
||||||
|
*
|
||||||
|
* @header mergePhaseNameLists
|
||||||
|
*/
|
||||||
|
function mergePhaseNameLists(currentNames, namesToMerge) {
|
||||||
|
if (!namesToMerge.length) return currentNames.slice();
|
||||||
|
|
||||||
|
var targetArray = currentNames.slice();
|
||||||
|
var targetIx = targetArray.indexOf(namesToMerge[0]);
|
||||||
|
|
||||||
|
if (targetIx === -1) {
|
||||||
|
// the first new item does not match any existing one
|
||||||
|
// start adding the new items at the start of the list
|
||||||
|
targetArray.splice(0, 0, namesToMerge[0]);
|
||||||
|
targetIx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge (zip) two arrays
|
||||||
|
for (var sourceIx = 1; sourceIx < namesToMerge.length; sourceIx++) {
|
||||||
|
var valueToAdd = namesToMerge[sourceIx];
|
||||||
|
var previousValue = namesToMerge[sourceIx - 1];
|
||||||
|
var existingIx = targetArray.indexOf(valueToAdd, targetIx);
|
||||||
|
|
||||||
|
if (existingIx === -1) {
|
||||||
|
// A new phase - try to add it after the last one,
|
||||||
|
// unless it was already registered
|
||||||
|
if (targetArray.indexOf(valueToAdd) !== -1) {
|
||||||
|
var errMsg = g.f('Ordering conflict: cannot add "%s' +
|
||||||
|
'" after "%s", because the opposite order was ' +
|
||||||
|
' already specified', valueToAdd, previousValue);
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
var previousIx = targetArray.indexOf(previousValue);
|
||||||
|
targetArray.splice(previousIx + 1, 0, valueToAdd);
|
||||||
|
} else {
|
||||||
|
// An existing phase - move the pointer
|
||||||
|
targetIx = existingIx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetArray;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check synchronously if a filepath points to an existing file.
|
* Check synchronously if a filepath points to an existing file.
|
||||||
|
@ -23,3 +347,4 @@ function fileExistsSync(filepath) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
package.json
32
package.json
|
@ -27,24 +27,26 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "~0.9.0",
|
"async": "^1.5.2",
|
||||||
"commondir": "0.0.1",
|
"bluebird": "^3.4.0",
|
||||||
"debug": "^2.0.0",
|
"commondir": "^1.0.1",
|
||||||
"lodash": "^3.6.0",
|
"debug": "^2.2.0",
|
||||||
"semver": "^4.1.0",
|
"lodash": "^4.13.1",
|
||||||
"strong-globalize": "^2.6.2",
|
"semver": "^5.1.0",
|
||||||
"toposort": "^0.2.10"
|
"strong-globalize": "^2.7.0",
|
||||||
|
"toposort": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^4.1.8",
|
"browserify": "^4.2.3",
|
||||||
"chai": "^1.10.0",
|
"chai": "^3.5.0",
|
||||||
"coffee-script": "^1.8.0",
|
"coffee-script": "^1.10.0",
|
||||||
"coffeeify": "^0.7.0",
|
"coffeeify": "^2.0.1",
|
||||||
"eslint": "^2.5.3",
|
"dirty-chai": "^1.2.2",
|
||||||
|
"eslint": "^2.11.1",
|
||||||
"eslint-config-loopback": "^1.0.0",
|
"eslint-config-loopback": "^1.0.0",
|
||||||
"fs-extra": "^0.12.0",
|
"fs-extra": "^0.30.0",
|
||||||
"loopback": "^3.0.0",
|
"loopback": "^3.0.0",
|
||||||
"mocha": "^1.19.0",
|
"mocha": "^2.5.3",
|
||||||
"supertest": "^0.14.0"
|
"supertest": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
var path = require('path');
|
||||||
|
var loopback = require('loopback');
|
||||||
|
|
||||||
|
var chai = require('chai');
|
||||||
|
var dirtyChai = require('dirty-chai');
|
||||||
|
var expect = chai.expect;
|
||||||
|
chai.use(dirtyChai);
|
||||||
|
|
||||||
|
var Bootstrapper = require('../lib/bootstrapper');
|
||||||
|
|
||||||
|
describe('Bootstrapper', function() {
|
||||||
|
var app;
|
||||||
|
beforeEach(function() {
|
||||||
|
app = loopback();
|
||||||
|
process.bootFlags = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should honor options.phases', function(done) {
|
||||||
|
var options = {
|
||||||
|
app: app,
|
||||||
|
appRootDir: path.join(__dirname, './fixtures/simple-app'),
|
||||||
|
phases: ['load'],
|
||||||
|
};
|
||||||
|
|
||||||
|
var bootstrapper = new Bootstrapper(options);
|
||||||
|
|
||||||
|
var context = {
|
||||||
|
app: app,
|
||||||
|
};
|
||||||
|
|
||||||
|
bootstrapper.run(context, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
var configs = context.configurations;
|
||||||
|
expect(configs.application, 'application').to.be.an('object');
|
||||||
|
expect(configs.bootScripts, 'bootScripts').to.be.an('array');
|
||||||
|
expect(configs.middleware, 'middleware').to.be.an('object');
|
||||||
|
expect(configs.models, 'models').to.be.an('object');
|
||||||
|
expect(configs.tracker, 'tracker').to.eql('load');
|
||||||
|
expect(context.instructions, 'instructions').to.be.undefined();
|
||||||
|
expect(process.bootFlags.length).to.eql(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should honor options.plugins', function(done) {
|
||||||
|
var options = {
|
||||||
|
app: app,
|
||||||
|
appRootDir: path.join(__dirname, './fixtures/simple-app'),
|
||||||
|
plugins: ['application', 'boot-script'],
|
||||||
|
};
|
||||||
|
|
||||||
|
var bootstrapper = new Bootstrapper(options);
|
||||||
|
|
||||||
|
var context = {
|
||||||
|
app: app,
|
||||||
|
};
|
||||||
|
|
||||||
|
bootstrapper.run(context, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
var configs = context.configurations;
|
||||||
|
var instructions = context.instructions;
|
||||||
|
expect(configs.application, 'application').to.be.an('object');
|
||||||
|
expect(configs.middleware, 'middleware').to.be.undefined();
|
||||||
|
expect(configs.models, 'models').to.be.undefined();
|
||||||
|
expect(configs.bootScripts, 'bootScripts').to.be.an('array');
|
||||||
|
expect(instructions.application, 'application').to.be.an('object');
|
||||||
|
expect(instructions.tracker, 'instruction: tracker').to.eql('compile');
|
||||||
|
expect(context.executions.tracker, 'execution: tracker').to.eql('start');
|
||||||
|
expect(process.bootFlags, 'process: bootFlags').to.eql(['barLoaded',
|
||||||
|
'barSyncLoaded',
|
||||||
|
'fooLoaded',
|
||||||
|
'barStarted',
|
||||||
|
'barFinished',
|
||||||
|
'barSyncExecuted',
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
delete process.bootFlags;
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,6 +4,7 @@
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
var boot = require('../');
|
var boot = require('../');
|
||||||
|
var async = require('async');
|
||||||
var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox;
|
var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
|
@ -40,20 +41,21 @@ describe('browser support for multiple apps', function() {
|
||||||
browserifyTestApps(apps, function(err, bundlePath) {
|
browserifyTestApps(apps, function(err, bundlePath) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
var bundledApps = executeBundledApps(bundlePath, apps);
|
var bundledApps = executeBundledApps(bundlePath, apps, function(err) {
|
||||||
var app1 = bundledApps.defaultApp;
|
var app1 = bundledApps.defaultApp;
|
||||||
var app2 = bundledApps.browserApp2;
|
var app2 = bundledApps.browserApp2;
|
||||||
|
|
||||||
expect(app1.settings).to.have.property('custom-key', 'custom-value');
|
expect(app1.settings).to.have.property('custom-key', 'custom-value');
|
||||||
expect(Object.keys(app1.models)).to.include('Customer');
|
expect(Object.keys(app1.models)).to.include('Customer');
|
||||||
expect(Object.keys(app1.models)).to.not.include('Robot');
|
expect(Object.keys(app1.models)).to.not.include('Robot');
|
||||||
expect(app1.models.Customer.settings).to.have.property('_customized',
|
expect(app1.models.Customer.settings).to.have.property('_customized',
|
||||||
'Customer');
|
'Customer');
|
||||||
|
|
||||||
expect(Object.keys(app2.models)).to.include('Robot');
|
expect(Object.keys(app2.models)).to.include('Robot');
|
||||||
expect(Object.keys(app2.models)).to.not.include('Customer');
|
expect(Object.keys(app2.models)).to.not.include('Customer');
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -61,8 +63,10 @@ describe('browser support for multiple apps', function() {
|
||||||
function browserifyTestApps(apps, next) {
|
function browserifyTestApps(apps, next) {
|
||||||
var b = browserify({
|
var b = browserify({
|
||||||
debug: true,
|
debug: true,
|
||||||
|
basedir: path.resolve(__dirname, './fixtures'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var bundles = [];
|
||||||
for (var i in apps) {
|
for (var i in apps) {
|
||||||
var appDir = apps[i].appDir;
|
var appDir = apps[i].appDir;
|
||||||
var appFile = apps[i].appFile;
|
var appFile = apps[i].appFile;
|
||||||
|
@ -79,28 +83,39 @@ function browserifyTestApps(apps, next) {
|
||||||
appRootDir: appDir,
|
appRootDir: appDir,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
boot.compileToBrowserify(opts, b);
|
bundles.push(opts);
|
||||||
}
|
}
|
||||||
|
async.eachSeries(bundles, function(opts, done) {
|
||||||
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
boot.compileToBrowserify(opts, b, done);
|
||||||
|
}, function(err) {
|
||||||
|
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeBundledApps(bundlePath, apps) {
|
function executeBundledApps(bundlePath, apps, done) {
|
||||||
var code = fs.readFileSync(bundlePath);
|
var code = fs.readFileSync(bundlePath);
|
||||||
var context = createBrowserLikeContext();
|
var context = createBrowserLikeContext();
|
||||||
vm.runInContext(code, context, bundlePath);
|
vm.runInContext(code, context, bundlePath);
|
||||||
|
|
||||||
|
var ids = [];
|
||||||
var script = 'var apps = {};\n';
|
var script = 'var apps = {};\n';
|
||||||
for (var i in apps) {
|
for (var i in apps) {
|
||||||
var moduleName = apps[i].moduleName;
|
var moduleName = apps[i].moduleName;
|
||||||
var id = apps[i].appId || 'defaultApp';
|
var id = apps[i].appId || 'defaultApp';
|
||||||
|
ids.push(id);
|
||||||
script += 'apps.' + id + ' = require("' + moduleName + '");\n';
|
script += 'apps.' + id + ' = require("' + moduleName + '");\n';
|
||||||
}
|
}
|
||||||
script += 'apps;\n';
|
script += 'apps;\n';
|
||||||
|
|
||||||
var appsInContext = vm.runInContext(script, context);
|
var appsInContext = vm.runInContext(script, context);
|
||||||
|
async.each(ids, function(id, done) {
|
||||||
printContextLogs(context);
|
appsInContext[id].once('booted', function() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
printContextLogs(context);
|
||||||
|
done(err, appsInContext);
|
||||||
|
});
|
||||||
|
|
||||||
return appsInContext;
|
return appsInContext;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ var compileStrategies = {
|
||||||
basedir: appDir,
|
basedir: appDir,
|
||||||
debug: true,
|
debug: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
b.require('./app.js', { expose: 'browser-app' });
|
b.require('./app.js', { expose: 'browser-app' });
|
||||||
return b;
|
return b;
|
||||||
},
|
},
|
||||||
|
@ -31,7 +30,6 @@ var compileStrategies = {
|
||||||
extensions: ['.coffee'],
|
extensions: ['.coffee'],
|
||||||
debug: true,
|
debug: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
b.transform('coffeeify');
|
b.transform('coffeeify');
|
||||||
|
|
||||||
b.require('./app.coffee', { expose: 'browser-app' });
|
b.require('./app.coffee', { expose: 'browser-app' });
|
||||||
|
@ -50,19 +48,19 @@ describe('browser support', function() {
|
||||||
browserifyTestApp(appDir, function(err, bundlePath) {
|
browserifyTestApp(appDir, function(err, bundlePath) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
var app = executeBundledApp(bundlePath);
|
var app = executeBundledApp(bundlePath, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
// configured in fixtures/browser-app/boot/configure.js
|
||||||
|
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
||||||
|
expect(Object.keys(app.models)).to.include('Customer');
|
||||||
|
expect(app.models.Customer.settings)
|
||||||
|
.to.have.property('_customized', 'Customer');
|
||||||
|
|
||||||
// configured in fixtures/browser-app/boot/configure.js
|
// configured in fixtures/browser-app/component-config.json
|
||||||
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
// and fixtures/browser-app/components/dummy-component.js
|
||||||
expect(Object.keys(app.models)).to.include('Customer');
|
expect(app.dummyComponentOptions).to.eql({ option: 'value' });
|
||||||
expect(app.models.Customer.settings)
|
done();
|
||||||
.to.have.property('_customized', 'Customer');
|
});
|
||||||
|
|
||||||
// configured in fixtures/browser-app/component-config.json
|
|
||||||
// and fixtures/browser-app/components/dummy-component.js
|
|
||||||
expect(app.dummyComponentOptions).to.eql({ option: 'value' });
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,14 +73,14 @@ describe('browser support', function() {
|
||||||
browserifyTestApp(options, function(err, bundlePath) {
|
browserifyTestApp(options, function(err, bundlePath) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
var app = executeBundledApp(bundlePath);
|
var app = executeBundledApp(bundlePath, function(err) {
|
||||||
|
var modelBuilder = app.registry.modelBuilder;
|
||||||
|
var registry = modelBuilder.mixins.mixins;
|
||||||
|
expect(Object.keys(registry)).to.eql(['TimeStamps']);
|
||||||
|
expect(app.models.Customer.timeStampsMixin).to.eql(true);
|
||||||
|
|
||||||
var modelBuilder = app.registry.modelBuilder;
|
done();
|
||||||
var registry = modelBuilder.mixins.mixins;
|
});
|
||||||
expect(Object.keys(registry)).to.eql(['TimeStamps']);
|
|
||||||
expect(app.models.Customer.timeStampsMixin).to.eql(true);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -95,14 +93,14 @@ describe('browser support', function() {
|
||||||
browserifyTestApp(appDir, 'coffee', function(err, bundlePath) {
|
browserifyTestApp(appDir, 'coffee', function(err, bundlePath) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
var app = executeBundledApp(bundlePath);
|
var app = executeBundledApp(bundlePath, function(err) {
|
||||||
|
// configured in fixtures/browser-app/boot/configure.coffee
|
||||||
// configured in fixtures/browser-app/boot/configure.coffee
|
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
||||||
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
expect(Object.keys(app.models)).to.include('Customer');
|
||||||
expect(Object.keys(app.models)).to.include('Customer');
|
expect(app.models.Customer.settings)
|
||||||
expect(app.models.Customer.settings)
|
.to.have.property('_customized', 'Customer');
|
||||||
.to.have.property('_customized', 'Customer');
|
done();
|
||||||
done();
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -119,18 +117,19 @@ function browserifyTestApp(options, strategy, next) {
|
||||||
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
|
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
|
||||||
var b = compileStrategies[strategy](appDir);
|
var b = compileStrategies[strategy](appDir);
|
||||||
|
|
||||||
boot.compileToBrowserify(options, b);
|
boot.compileToBrowserify(options, b, function(err) {
|
||||||
|
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
||||||
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeBundledApp(bundlePath) {
|
function executeBundledApp(bundlePath, done) {
|
||||||
var code = fs.readFileSync(bundlePath);
|
var code = fs.readFileSync(bundlePath);
|
||||||
var context = createBrowserLikeContext();
|
var context = createBrowserLikeContext();
|
||||||
vm.runInContext(code, context, bundlePath);
|
vm.runInContext(code, context, bundlePath);
|
||||||
var app = vm.runInContext('require("browser-app")', context);
|
var app = vm.runInContext('require("browser-app")', context);
|
||||||
|
app.once('booted', function(err) {
|
||||||
printContextLogs(context);
|
printContextLogs(context);
|
||||||
|
done(err, app);
|
||||||
|
});
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,37 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
||||||
var configLoader = require('../lib/config-loader');
|
|
||||||
var fs = require('fs-extra');
|
|
||||||
var path = require('path');
|
|
||||||
var expect = require('chai').expect;
|
|
||||||
var sandbox = require('./helpers/sandbox');
|
|
||||||
var appdir = require('./helpers/appdir');
|
|
||||||
|
|
||||||
describe('config-loader', function() {
|
|
||||||
beforeEach(sandbox.reset);
|
|
||||||
beforeEach(appdir.init);
|
|
||||||
|
|
||||||
it('does not cache loaded values', function() {
|
|
||||||
appdir.createConfigFilesSync();
|
|
||||||
appdir.writeConfigFileSync('middleware.json', {
|
|
||||||
'strong-error-handler': { params: { debug: false }},
|
|
||||||
});
|
|
||||||
appdir.writeConfigFileSync('middleware.development.json', {
|
|
||||||
'strong-error-handler': { params: { debug: true }},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Here we load main config and merge it with DEV overrides
|
|
||||||
var config = configLoader.loadMiddleware(appdir.PATH, 'development');
|
|
||||||
expect(config['strong-error-handler'].params.debug, 'debug in development')
|
|
||||||
.to.equal(true);
|
|
||||||
|
|
||||||
// When we load the config file again in different environment,
|
|
||||||
// only the main file is loaded and no overrides are applied.
|
|
||||||
config = configLoader.loadMiddleware(appdir.PATH, 'production');
|
|
||||||
expect(config['strong-error-handler'].params.debug, 'debug in production')
|
|
||||||
.to.equal(false);
|
|
||||||
});
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,6 @@ var loopback = require('loopback');
|
||||||
var boot = require('../../../');
|
var boot = require('../../../');
|
||||||
|
|
||||||
var app = module.exports = loopback();
|
var app = module.exports = loopback();
|
||||||
|
|
||||||
boot(app, {
|
boot(app, {
|
||||||
appId: 'browserApp2',
|
appId: 'browserApp2',
|
||||||
appRootDir: __dirname,
|
appRootDir: __dirname,
|
||||||
|
|
|
@ -7,4 +7,4 @@ var loopback = require('loopback');
|
||||||
var boot = require('../../../');
|
var boot = require('../../../');
|
||||||
|
|
||||||
var app = module.exports = loopback();
|
var app = module.exports = loopback();
|
||||||
boot(app);
|
boot(app, __dirname);
|
||||||
|
|
|
@ -2,4 +2,4 @@ loopback = require 'loopback'
|
||||||
boot = require '../../../'
|
boot = require '../../../'
|
||||||
|
|
||||||
module.exports = client = loopback()
|
module.exports = client = loopback()
|
||||||
boot(client)
|
boot(client, __dirname)
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
module.exports = function(opitions) {
|
||||||
|
return new Tracker(opitions);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Tracker(options) {
|
||||||
|
this.name = 'tracker';
|
||||||
|
this.options = options || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Tracker.prototype.load = function(context) {
|
||||||
|
context.configurations.tracker = 'load';
|
||||||
|
};
|
||||||
|
|
||||||
|
Tracker.prototype.compile = function(context, done) {
|
||||||
|
context.instructions.tracker = 'compile';
|
||||||
|
process.nextTick(done);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tracker.prototype.start = function(context, done) {
|
||||||
|
context.executions = context.executions || {};
|
||||||
|
context.executions.tracker = 'start';
|
||||||
|
process.nextTick(done);
|
||||||
|
};
|
|
@ -58,6 +58,8 @@ function createContext() {
|
||||||
error: [],
|
error: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ArrayBuffer: ArrayBuffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// `window` is used by loopback to detect browser runtime
|
// `window` is used by loopback to detect browser runtime
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// This file is licensed under the MIT License.
|
// This file is licensed under the MIT License.
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
var boot = require('../');
|
var utils = require('../lib/utils');
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
var sandbox = require('./helpers/sandbox');
|
var sandbox = require('./helpers/sandbox');
|
||||||
var appdir = require('./helpers/appdir');
|
var appdir = require('./helpers/appdir');
|
||||||
|
@ -16,7 +16,7 @@ describe('utils', function() {
|
||||||
describe('fileExistsSync', function() {
|
describe('fileExistsSync', function() {
|
||||||
it('returns false when a file does not exist', function() {
|
it('returns false when a file does not exist', function() {
|
||||||
var doesNotExist = sandbox.resolve('does-not-exist.json');
|
var doesNotExist = sandbox.resolve('does-not-exist.json');
|
||||||
expect(boot.utils.fileExistsSync(doesNotExist))
|
expect(utils.fileExistsSync(doesNotExist))
|
||||||
.to.equal(false);
|
.to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ describe('utils', function() {
|
||||||
var doesExist = appdir.writeConfigFileSync('does-exist.json', {
|
var doesExist = appdir.writeConfigFileSync('does-exist.json', {
|
||||||
exists: true,
|
exists: true,
|
||||||
});
|
});
|
||||||
expect(boot.utils.fileExistsSync(doesExist))
|
expect(utils.fileExistsSync(doesExist))
|
||||||
.to.equal(true);
|
.to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue