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
|
||||
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.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var execute = require('./lib/executor');
|
||||
var Bootstrapper = require('./lib/bootstrapper');
|
||||
|
||||
/**
|
||||
* The browser version of `bootLoopBackApp`.
|
||||
|
@ -21,17 +21,24 @@ var execute = require('./lib/executor');
|
|||
* @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
|
||||
// this application. If no Id was provided, load the default bundle.
|
||||
var moduleName = 'loopback-boot#instructions';
|
||||
if (options && typeof options === 'object' && options.appId)
|
||||
moduleName += '-' + options.appId;
|
||||
var appId = options && typeof options === 'object' && options.appId;
|
||||
if (appId)
|
||||
moduleName += '-' + appId;
|
||||
|
||||
// The name of the module containing instructions
|
||||
// is hard-coded in lib/bundler
|
||||
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
|
||||
|
||||
// Strong globalize
|
||||
var SG = require('strong-globalize');
|
||||
SG.SetRootDir(__dirname);
|
||||
var g = require('./lib/globalize');
|
||||
|
||||
var ConfigLoader = require('./lib/config-loader');
|
||||
var compile = require('./lib/compiler');
|
||||
var execute = require('./lib/executor');
|
||||
var PluginBase = require('./lib/plugin-base');
|
||||
var Bootstrapper = require('./lib/bootstrapper');
|
||||
var addInstructionsToBrowserify = require('./lib/bundler');
|
||||
var utils = require('./lib/utils');
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (typeof options === 'function' && callback === undefined) {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
options = options || {};
|
||||
// backwards compatibility with loopback's app.boot
|
||||
options.env = options.env || app.get('env');
|
||||
|
||||
var instructions = compile(options);
|
||||
execute(app, instructions, callback);
|
||||
var bootstrapper = new Bootstrapper(options);
|
||||
|
||||
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)
|
||||
*/
|
||||
exports.compileToBrowserify = function(options, bundler) {
|
||||
addInstructionsToBrowserify(compile(options), bundler);
|
||||
exports.compileToBrowserify = function(options, bundler, done) {
|
||||
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.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}\"",
|
||||
"34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"",
|
||||
"46e3ab0ef1149ce0a171b5fac2612ea3": "{{Middleware}} \"{0}\" not found: {1}",
|
||||
"79e93b2a95e969788590c14e26bb2c1b": "The data in {{model-config.json}} is in the unsupported 1.x format.",
|
||||
"978a25819e71602cad691dbe7ba17592": "{0} config must be a valid JSON object",
|
||||
"be2dcdab7aa493ed8d77287eb45cfec8": "cannot require directory contents without directory name",
|
||||
"2634623ad4b2c5902f6c6bb25e68b733": "WARNING: Main {{config}} file \"{0}.json\" is missing",
|
||||
"3a7049e42006e8bc19e0f4fc8df63b6b": "The `app` is powered by an incompatible loopback version {0}. Supported versions: {1}",
|
||||
"3f93b626dd9a1c33d67490f6e71018b5": "WARNING: Main config file \"{0}{{.json}}\" is missing",
|
||||
"4d052d84c8620730afd4a30832f11724": "Cannot configure unknown model {0}",
|
||||
"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.",
|
||||
"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().",
|
||||
"6447e6b342a2c51ab0bc53b3cbdf3742": "Ordering conflict: cannot add \"{0}\" after \"{1}\", because the opposite order was already specified",
|
||||
"70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}",
|
||||
"b078ccd043437a258581e387f93dc1a5": "The `{{app}}` is powered by an incompatible {{loopback}} version {0}. Supported versions: {1}",
|
||||
"e8d29edfb313cfe64f5c96cc7d3d5b4b": "When using {{loopback-boot}} with {{loopback}} <1.9, the {{loopback}} module must be available for `{{require('loopback')}}`.",
|
||||
"f48405e7c61c3d665b601c9ba41da424": "{{app.port}} must be a {{string}} or {{number}}",
|
||||
"fa2a7d5137c8891693f9515d48f5b7d7": "{{app.restApiRoot}} must be a {{string}}"
|
||||
"7f7bdcadb75abfef1bd8a126d547dd6d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by {{app.get()}}.",
|
||||
"91a742b7c3568cf6b6755741a70b3c52": "The {{middleware}} \"{0}\" in {{phase}} \"{1}\"is not defined in the main config.",
|
||||
"a3aa22086ae4976cd013065c9a3ff81c": "Cannot apply {0}: ",
|
||||
"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 commondir = require('commondir');
|
||||
var cloneDeep = require('lodash').cloneDeep;
|
||||
var g = require('strong-globalize')();
|
||||
var g = require('./globalize');
|
||||
|
||||
/**
|
||||
* Add boot instructions to a browserify bundler.
|
||||
* @param {Object} instructions Boot instructions.
|
||||
* @param {Object} bundler A browserify object created by `browserify()`.
|
||||
*/
|
||||
|
||||
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
||||
bundleModelScripts(instructions, bundler);
|
||||
bundleMixinScripts(instructions, bundler);
|
||||
bundleComponentScripts(instructions, bundler);
|
||||
bundleOtherScripts(instructions, bundler);
|
||||
bundleInstructions(instructions, bundler);
|
||||
module.exports = function addInstructionsToBrowserify(context, bundler) {
|
||||
addPlugins(bundler);
|
||||
// bundlePluginScripts(context, bundler);
|
||||
bundleModelScripts(context, bundler);
|
||||
bundleMixinScripts(context, bundler);
|
||||
bundleComponentScripts(context, bundler);
|
||||
bundleOtherScripts(context, bundler);
|
||||
bundleInstructions(context, bundler);
|
||||
};
|
||||
|
||||
function bundleOtherScripts(instructions, bundler) {
|
||||
for (var key in instructions.files) {
|
||||
addScriptsToBundle(key, instructions.files[key], bundler);
|
||||
}
|
||||
function addPlugins(bundler) {
|
||||
var dir = path.join(__dirname, './plugins');
|
||||
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) {
|
||||
bundleSourceFiles(instructions, 'models', bundler);
|
||||
function bundleOtherScripts(context, bundler) {
|
||||
var list = context.instructions.bootScripts;
|
||||
addScriptsToBundle('boot', list, bundler);
|
||||
}
|
||||
|
||||
function bundleMixinScripts(instructions, bundler) {
|
||||
bundleSourceFiles(instructions, 'mixins', bundler);
|
||||
function bundlePluginScripts(context, bundler) {
|
||||
var list = context.instructions.pluginScripts;
|
||||
addScriptsToBundle('plugins', list, bundler);
|
||||
}
|
||||
|
||||
function bundleComponentScripts(instructions, bundler) {
|
||||
bundleSourceFiles(instructions, 'components', bundler);
|
||||
function bundleModelScripts(context, bundler) {
|
||||
bundleSourceFiles(context, 'models', bundler);
|
||||
}
|
||||
|
||||
function bundleSourceFiles(instructions, type, bundler) {
|
||||
var files = instructions[type]
|
||||
function bundleMixinScripts(context, bundler) {
|
||||
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; })
|
||||
.filter(function(f) { return !!f; });
|
||||
|
||||
var instructionToFileMapping = instructions[type]
|
||||
var instructionToFileMapping = context.instructions[type]
|
||||
.map(function(m) { return files.indexOf(m.sourceFile); });
|
||||
|
||||
addScriptsToBundle(type, files, bundler);
|
||||
|
@ -54,7 +68,7 @@ function bundleSourceFiles(instructions, type, bundler) {
|
|||
// Update `sourceFile` properties with the new paths
|
||||
instructionToFileMapping.forEach(function(fileIx, sourceIx) {
|
||||
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.
|
||||
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)`.
|
||||
list[ix] = fileid;
|
||||
}
|
||||
}
|
||||
|
||||
function bundleInstructions(instructions, bundler) {
|
||||
instructions = cloneDeep(instructions);
|
||||
function bundleInstructions(context, bundler) {
|
||||
var instructions = cloneDeep(context.instructions);
|
||||
|
||||
var hasMiddleware = instructions.middleware.phases.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.
|
||||
// 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 assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
var g = require('./globalize');
|
||||
|
||||
exports.tryReadDir = tryReadDir;
|
||||
exports.resolveRelativePaths = resolveRelativePaths;
|
||||
exports.assertIsValidConfig = assertIsValidConfig;
|
||||
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.
|
||||
|
@ -23,3 +347,4 @@ function fileExistsSync(filepath) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
32
package.json
32
package.json
|
@ -27,24 +27,26 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"commondir": "0.0.1",
|
||||
"debug": "^2.0.0",
|
||||
"lodash": "^3.6.0",
|
||||
"semver": "^4.1.0",
|
||||
"strong-globalize": "^2.6.2",
|
||||
"toposort": "^0.2.10"
|
||||
"async": "^1.5.2",
|
||||
"bluebird": "^3.4.0",
|
||||
"commondir": "^1.0.1",
|
||||
"debug": "^2.2.0",
|
||||
"lodash": "^4.13.1",
|
||||
"semver": "^5.1.0",
|
||||
"strong-globalize": "^2.7.0",
|
||||
"toposort": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^4.1.8",
|
||||
"chai": "^1.10.0",
|
||||
"coffee-script": "^1.8.0",
|
||||
"coffeeify": "^0.7.0",
|
||||
"eslint": "^2.5.3",
|
||||
"browserify": "^4.2.3",
|
||||
"chai": "^3.5.0",
|
||||
"coffee-script": "^1.10.0",
|
||||
"coffeeify": "^2.0.1",
|
||||
"dirty-chai": "^1.2.2",
|
||||
"eslint": "^2.11.1",
|
||||
"eslint-config-loopback": "^1.0.0",
|
||||
"fs-extra": "^0.12.0",
|
||||
"fs-extra": "^0.30.0",
|
||||
"loopback": "^3.0.0",
|
||||
"mocha": "^1.19.0",
|
||||
"supertest": "^0.14.0"
|
||||
"mocha": "^2.5.3",
|
||||
"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
|
||||
|
||||
var boot = require('../');
|
||||
var async = require('async');
|
||||
var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
@ -40,7 +41,7 @@ describe('browser support for multiple apps', function() {
|
|||
browserifyTestApps(apps, function(err, bundlePath) {
|
||||
if (err) return done(err);
|
||||
|
||||
var bundledApps = executeBundledApps(bundlePath, apps);
|
||||
var bundledApps = executeBundledApps(bundlePath, apps, function(err) {
|
||||
var app1 = bundledApps.defaultApp;
|
||||
var app2 = bundledApps.browserApp2;
|
||||
|
||||
|
@ -56,13 +57,16 @@ describe('browser support for multiple apps', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function browserifyTestApps(apps, next) {
|
||||
var b = browserify({
|
||||
debug: true,
|
||||
basedir: path.resolve(__dirname, './fixtures'),
|
||||
});
|
||||
|
||||
var bundles = [];
|
||||
for (var i in apps) {
|
||||
var appDir = apps[i].appDir;
|
||||
var appFile = apps[i].appFile;
|
||||
|
@ -79,28 +83,39 @@ function browserifyTestApps(apps, next) {
|
|||
appRootDir: appDir,
|
||||
};
|
||||
}
|
||||
boot.compileToBrowserify(opts, b);
|
||||
bundles.push(opts);
|
||||
}
|
||||
|
||||
async.eachSeries(bundles, function(opts, done) {
|
||||
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 context = createBrowserLikeContext();
|
||||
vm.runInContext(code, context, bundlePath);
|
||||
|
||||
var ids = [];
|
||||
var script = 'var apps = {};\n';
|
||||
for (var i in apps) {
|
||||
var moduleName = apps[i].moduleName;
|
||||
var id = apps[i].appId || 'defaultApp';
|
||||
ids.push(id);
|
||||
script += 'apps.' + id + ' = require("' + moduleName + '");\n';
|
||||
}
|
||||
script += 'apps;\n';
|
||||
|
||||
var appsInContext = vm.runInContext(script, context);
|
||||
|
||||
async.each(ids, function(id, done) {
|
||||
appsInContext[id].once('booted', function() {
|
||||
done();
|
||||
});
|
||||
}, function(err) {
|
||||
printContextLogs(context);
|
||||
done(err, appsInContext);
|
||||
});
|
||||
|
||||
return appsInContext;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ var compileStrategies = {
|
|||
basedir: appDir,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
b.require('./app.js', { expose: 'browser-app' });
|
||||
return b;
|
||||
},
|
||||
|
@ -31,7 +30,6 @@ var compileStrategies = {
|
|||
extensions: ['.coffee'],
|
||||
debug: true,
|
||||
});
|
||||
|
||||
b.transform('coffeeify');
|
||||
|
||||
b.require('./app.coffee', { expose: 'browser-app' });
|
||||
|
@ -50,8 +48,8 @@ describe('browser support', function() {
|
|||
browserifyTestApp(appDir, function(err, bundlePath) {
|
||||
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');
|
||||
|
@ -61,10 +59,10 @@ describe('browser support', function() {
|
|||
// configured in fixtures/browser-app/component-config.json
|
||||
// and fixtures/browser-app/components/dummy-component.js
|
||||
expect(app.dummyComponentOptions).to.eql({ option: 'value' });
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('loads mixins', function(done) {
|
||||
var appDir = path.resolve(__dirname, './fixtures/browser-app');
|
||||
|
@ -75,8 +73,7 @@ describe('browser support', function() {
|
|||
browserifyTestApp(options, function(err, bundlePath) {
|
||||
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']);
|
||||
|
@ -85,6 +82,7 @@ describe('browser support', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('supports coffee-script files', function(done) {
|
||||
// add coffee-script to require.extensions
|
||||
|
@ -95,8 +93,7 @@ describe('browser support', function() {
|
|||
browserifyTestApp(appDir, 'coffee', function(err, bundlePath) {
|
||||
if (err) return done(err);
|
||||
|
||||
var app = executeBundledApp(bundlePath);
|
||||
|
||||
var app = executeBundledApp(bundlePath, function(err) {
|
||||
// configured in fixtures/browser-app/boot/configure.coffee
|
||||
expect(app.settings).to.have.property('custom-key', 'custom-value');
|
||||
expect(Object.keys(app.models)).to.include('Customer');
|
||||
|
@ -105,6 +102,7 @@ describe('browser support', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function browserifyTestApp(options, strategy, next) {
|
||||
|
@ -119,18 +117,19 @@ function browserifyTestApp(options, strategy, next) {
|
|||
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
|
||||
var b = compileStrategies[strategy](appDir);
|
||||
|
||||
boot.compileToBrowserify(options, b);
|
||||
|
||||
boot.compileToBrowserify(options, b, function(err) {
|
||||
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
||||
});
|
||||
}
|
||||
|
||||
function executeBundledApp(bundlePath) {
|
||||
function executeBundledApp(bundlePath, done) {
|
||||
var code = fs.readFileSync(bundlePath);
|
||||
var context = createBrowserLikeContext();
|
||||
vm.runInContext(code, context, bundlePath);
|
||||
var app = vm.runInContext('require("browser-app")', context);
|
||||
|
||||
app.once('booted', function(err) {
|
||||
printContextLogs(context);
|
||||
|
||||
done(err, 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);
|
||||
});
|
||||
});
|
|
@ -8,7 +8,12 @@ var boot = require('../');
|
|||
var path = require('path');
|
||||
var loopback = require('loopback');
|
||||
var assert = require('assert');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
var chai = require('chai');
|
||||
var dirtyChai = require('dirty-chai');
|
||||
var expect = chai.expect;
|
||||
chai.use(dirtyChai);
|
||||
|
||||
var fs = require('fs-extra');
|
||||
var sandbox = require('./helpers/sandbox');
|
||||
var appdir = require('./helpers/appdir');
|
||||
|
@ -37,7 +42,7 @@ describe('executor', function() {
|
|||
});
|
||||
|
||||
var dummyInstructions = someInstructions({
|
||||
config: {
|
||||
application: {
|
||||
port: 0,
|
||||
host: '127.0.0.1',
|
||||
restApiRoot: '/rest-api',
|
||||
|
@ -62,17 +67,20 @@ describe('executor', function() {
|
|||
describe('when booting', function() {
|
||||
it('should set the `booting` flag during execution', function(done) {
|
||||
expect(app.booting).to.be.undefined();
|
||||
boot.execute(app, simpleAppInstructions(), function(err) {
|
||||
expect(err).to.be.undefined();
|
||||
simpleAppInstructions(function(err, context) {
|
||||
if (err) return done(err);
|
||||
boot.execute(app, context.instructions, function(err) {
|
||||
expect(err).to.not.exist;
|
||||
expect(process.bootingFlagSet).to.be.true();
|
||||
expect(app.booting).to.be.false();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit the `booted` event in the next tick', function(done) {
|
||||
boot.execute(app, dummyInstructions, function(err) {
|
||||
expect(err).to.be.undefined();
|
||||
expect(err).to.not.exist;
|
||||
});
|
||||
app.on('booted', function() {
|
||||
// This test fails with a timeout when the `booted` event has not been
|
||||
|
@ -86,8 +94,9 @@ describe('executor', function() {
|
|||
});
|
||||
});
|
||||
|
||||
it('configures models', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
it('configures models', function(done) {
|
||||
boot.execute(app, dummyInstructions, function(err, context) {
|
||||
if (err) return done(err);
|
||||
assert(app.models);
|
||||
assert(app.models.User);
|
||||
assert.equal(app.models.User, app.registry.getModel('User'),
|
||||
|
@ -95,9 +104,11 @@ describe('executor', function() {
|
|||
assertValidDataSource(app.models.User.dataSource);
|
||||
assert.isFunc(app.models.User, 'find');
|
||||
assert.isFunc(app.models.User, 'create');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('defines and customizes models', function() {
|
||||
it('defines and customizes models', function(done) {
|
||||
appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
|
||||
function(Customer) {
|
||||
Customer.settings._customized = 'Customer';
|
||||
|
@ -116,15 +127,18 @@ describe('executor', function() {
|
|||
sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}), function(err, context) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(app.models.Customer).to.exist();
|
||||
expect(app.models.Customer).to.exist;
|
||||
expect(app.models.Customer.settings._customized).to.be.equal('Customer');
|
||||
var UserModel = app.registry.getModel('User');
|
||||
expect(UserModel.settings._customized).to.equal('Base');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('defines model without attaching it', function() {
|
||||
it('defines model without attaching it', function(done) {
|
||||
boot.execute(app, someInstructions({
|
||||
models: [
|
||||
{
|
||||
|
@ -145,17 +159,23 @@ describe('executor', function() {
|
|||
sourceFile: undefined,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
}), function(err, context) {
|
||||
if (err) return done(err);
|
||||
expect(Object.keys(app.models)).to.eql(['Car']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('attaches models to data sources', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
it('attaches models to data sources', function(done) {
|
||||
boot.execute(app, dummyInstructions, function(err, context) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.models.User.dataSource, app.dataSources.theDb);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('defines all models first before running the config phase', function() {
|
||||
it('defines all models first before running the config phase',
|
||||
function(done) {
|
||||
appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
|
||||
function(Customer/*, Base*/) {
|
||||
Customer.on('attached', function() {
|
||||
|
@ -179,12 +199,15 @@ describe('executor', function() {
|
|||
sourceFile: undefined,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
expect(app.models.Customer._modelsWhenAttached).to.include('UniqueName');
|
||||
}), function(err, context) {
|
||||
if (err) return done(err);
|
||||
expect(app.models.Customer._modelsWhenAttached).
|
||||
to.include('UniqueName');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('defines models in the local app registry', function() {
|
||||
it('defines models in the local app registry', function(done) {
|
||||
app = loopback({ localRegistry: true });
|
||||
boot.execute(app, someInstructions({
|
||||
models: [
|
||||
|
@ -195,35 +218,43 @@ describe('executor', function() {
|
|||
sourceFile: undefined,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
expect(Object.keys(loopback.registry.modelBuilder.models), 'global models')
|
||||
}), function(err, context) {
|
||||
if (err) return done(err);
|
||||
expect(Object.keys(loopback.registry.modelBuilder.models),
|
||||
'global models')
|
||||
.to.not.contain('LocalCustomer');
|
||||
expect(Object.keys(app.registry.modelBuilder.models), 'local models')
|
||||
.to.contain('LocalCustomer');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on bad require() call inside boot script', function() {
|
||||
it('throws on bad require() call inside boot script', function(done) {
|
||||
var file = appdir.writeFileSync('boot/badScript.js',
|
||||
'require("doesnt-exist"); module.exports = {};');
|
||||
|
||||
function doBoot() {
|
||||
boot.execute(app, someInstructions({ files: { boot: [file] }}));
|
||||
}
|
||||
|
||||
expect(doBoot).to.throw(/Cannot find module \'doesnt-exist\'/);
|
||||
boot.execute(app, someInstructions({ bootScripts: [file] }),
|
||||
function(err) {
|
||||
expect(err && err.message)
|
||||
.to.match(/Cannot find module \'doesnt-exist\'/);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('instantiates data sources', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
it('instantiates data sources', function(done) {
|
||||
boot.execute(app, dummyInstructions, function(err, context) {
|
||||
if (err) return done(err);
|
||||
assert(app.dataSources);
|
||||
assert(app.dataSources.theDb);
|
||||
assertValidDataSource(app.dataSources.theDb);
|
||||
assert(app.dataSources.TheDb);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call autoAttach', function() {
|
||||
boot.execute(app, dummyInstructions);
|
||||
it('does not call autoAttach', function(done) {
|
||||
boot.execute(app, dummyInstructions, function(err, context) {
|
||||
if (err) return done(err);
|
||||
|
||||
// loopback-datasource-juggler quirk:
|
||||
// Model.dataSources has modelBuilder as the default value,
|
||||
|
@ -231,12 +262,14 @@ describe('executor', function() {
|
|||
var actual = loopback.Email.dataSource instanceof loopback.DataSource ?
|
||||
'attached' : 'not attached';
|
||||
expect(actual).to.equal('not attached');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('skips definition of already defined LoopBack models', function() {
|
||||
it('skips definition of already defined LoopBack models', function(done) {
|
||||
var builtinModel = {
|
||||
name: 'User',
|
||||
definition: fs.readJsonFileSync(
|
||||
definition: fs.readJsonSync(
|
||||
require.resolve('loopback/common/models/user.json')
|
||||
),
|
||||
config: { dataSource: 'db' },
|
||||
|
@ -244,32 +277,29 @@ describe('executor', function() {
|
|||
};
|
||||
builtinModel.definition.redefined = true;
|
||||
|
||||
boot.execute(app, someInstructions({ models: [builtinModel] }));
|
||||
|
||||
expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true);
|
||||
boot.execute(app, someInstructions({ models: [builtinModel] }),
|
||||
function(err, context) {
|
||||
if (err) return done(err);
|
||||
expect(app.models.User.settings.redefined,
|
||||
'redefined').to.not.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with boot and models files', function() {
|
||||
beforeEach(function() {
|
||||
boot.execute(app, simpleAppInstructions());
|
||||
beforeEach(function(done) {
|
||||
simpleAppInstructions(function(err, context) {
|
||||
if (err) return done(err);
|
||||
boot.execute(app, context.instructions, done);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete process.bootFlags;
|
||||
});
|
||||
|
||||
it('should run `boot/*` files', function(done) {
|
||||
it('should run `boot/*` files', function() {
|
||||
// scripts are loaded by the order of file names
|
||||
expect(process.bootFlags).to.eql([
|
||||
'barLoaded',
|
||||
'barSyncLoaded',
|
||||
'fooLoaded',
|
||||
'barStarted',
|
||||
]);
|
||||
|
||||
// bar finished happens in the next tick
|
||||
// barSync executed after bar finished
|
||||
setTimeout(function() {
|
||||
expect(process.bootFlags).to.eql([
|
||||
'barLoaded',
|
||||
'barSyncLoaded',
|
||||
|
@ -278,14 +308,14 @@ describe('executor', function() {
|
|||
'barFinished',
|
||||
'barSyncExecuted',
|
||||
]);
|
||||
done();
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with boot with callback', function() {
|
||||
it('should run `boot/*` files asynchronously', function(done) {
|
||||
boot.execute(app, simpleAppInstructions(), function() {
|
||||
simpleAppInstructions(function(err, context) {
|
||||
if (err) return done(err);
|
||||
boot.execute(app, context.instructions, function() {
|
||||
expect(process.bootFlags).to.eql([
|
||||
'barLoaded',
|
||||
'barSyncLoaded',
|
||||
|
@ -297,6 +327,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for mixins', function() {
|
||||
var options;
|
||||
|
@ -318,22 +349,29 @@ describe('executor', function() {
|
|||
};
|
||||
});
|
||||
|
||||
it('defines mixins from instructions - using `mixinDirs`', function() {
|
||||
it('defines mixins from instructions - using `mixinDirs`',
|
||||
function(done) {
|
||||
options.mixinDirs = ['./custom-mixins'];
|
||||
boot(app, options);
|
||||
|
||||
boot(app, options, function(err) {
|
||||
if (err) return done(err);
|
||||
var modelBuilder = app.registry.modelBuilder;
|
||||
var registry = modelBuilder.mixins.mixins;
|
||||
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('defines mixins from instructions - using `mixinSources`', function() {
|
||||
it('defines mixins from instructions - using `mixinSources`',
|
||||
function(done) {
|
||||
options.mixinSources = ['./custom-mixins'];
|
||||
boot(app, options);
|
||||
boot(app, options, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
var modelBuilder = app.registry.modelBuilder;
|
||||
var registry = modelBuilder.mixins.mixins;
|
||||
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -356,41 +394,51 @@ describe('executor', function() {
|
|||
delete process.env.npm_package_config_port;
|
||||
});
|
||||
|
||||
function bootWithDefaults() {
|
||||
function bootWithDefaults(done) {
|
||||
app = loopback();
|
||||
boot.execute(app, someInstructions({
|
||||
config: {
|
||||
application: {
|
||||
port: undefined,
|
||||
host: undefined,
|
||||
},
|
||||
}));
|
||||
}), done);
|
||||
}
|
||||
|
||||
it('should apply env passed in option object', function() {
|
||||
boot.execute(app, someInstructions({ env: 'custom_env' }));
|
||||
it('should apply env passed in option object', function(done) {
|
||||
boot.execute(app, someInstructions({ env: 'custom_env' }), function(err) {
|
||||
if (err) return done(err);
|
||||
expect(app.get('env')).to.equal('custom_env');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should honor host and port', function() {
|
||||
function assertHonored(portKey, hostKey) {
|
||||
it('should honor host and port', function(done) {
|
||||
function assertHonored(portKey, hostKey, cb) {
|
||||
process.env[hostKey] = randomPort();
|
||||
process.env[portKey] = randomHost();
|
||||
bootWithDefaults();
|
||||
bootWithDefaults(function(err) {
|
||||
if (err) return cb(err);
|
||||
assert.equal(app.get('port'), process.env[portKey], portKey);
|
||||
assert.equal(app.get('host'), process.env[hostKey], hostKey);
|
||||
delete process.env[portKey];
|
||||
delete process.env[hostKey];
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP');
|
||||
assertHonored('npm_config_port', 'npm_config_host');
|
||||
assertHonored('npm_package_config_port', 'npm_package_config_host');
|
||||
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP');
|
||||
assertHonored('VCAP_APP_PORT', 'VCAP_APP_HOST');
|
||||
assertHonored('PORT', 'HOST');
|
||||
async.eachSeries([
|
||||
{ port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_NODEJS_IP' },
|
||||
{ port: 'npm_config_port', host: 'npm_config_host' },
|
||||
{ port: 'npm_package_config_port', host: 'npm_package_config_host' },
|
||||
{ port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_SLS_IP' },
|
||||
{ port: 'VCAP_APP_PORT', host: 'VCAP_APP_HOST' },
|
||||
{ port: 'PORT', host: 'HOST' },
|
||||
], function(config, cb) {
|
||||
assertHonored(config.port, config.host, cb);
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('should prioritize host sources', function() {
|
||||
it('should prioritize host sources', function(done) {
|
||||
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
|
||||
/*eslint-disable camelcase*/
|
||||
process.env.npm_config_host = randomHost();
|
||||
|
@ -400,12 +448,15 @@ describe('executor', function() {
|
|||
process.env.HOST = randomHost();
|
||||
process.env.npm_package_config_host = randomHost();
|
||||
|
||||
bootWithDefaults();
|
||||
bootWithDefaults(function(err) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||
/*eslint-enable camelcase*/
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should prioritize port sources', function() {
|
||||
it('should prioritize port sources', function(done) {
|
||||
/*eslint-disable camelcase*/
|
||||
process.env.npm_config_port = randomPort();
|
||||
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||
|
@ -414,9 +465,12 @@ describe('executor', function() {
|
|||
process.env.PORT = randomPort();
|
||||
process.env.npm_package_config_port = randomPort();
|
||||
|
||||
bootWithDefaults();
|
||||
bootWithDefaults(function(err) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.get('port'), process.env.npm_config_port);
|
||||
/*eslint-enable camelcase*/
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function randomHost() {
|
||||
|
@ -427,25 +481,37 @@ describe('executor', function() {
|
|||
return Math.floor(Math.random() * 10000);
|
||||
}
|
||||
|
||||
it('should honor 0 for free port', function() {
|
||||
boot.execute(app, someInstructions({ config: { port: 0 }}));
|
||||
it('should honor 0 for free port', function(done) {
|
||||
boot.execute(app, someInstructions({ application: { port: 0 }}),
|
||||
function(err) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.get('port'), 0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should default to port 3000', function() {
|
||||
boot.execute(app, someInstructions({ config: { port: undefined }}));
|
||||
it('should default to port 3000', function(done) {
|
||||
boot.execute(app, someInstructions({ application: { port: undefined }}),
|
||||
function(err) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.get('port'), 3000);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect named pipes port values in ENV', function() {
|
||||
it('should respect named pipes port values in ENV', function(done) {
|
||||
var NAMED_PORT = '\\.\\pipe\\test';
|
||||
process.env.PORT = NAMED_PORT;
|
||||
boot.execute(app, someInstructions({ config: { port: 3000 }}));
|
||||
boot.execute(app, someInstructions({ application: { port: 3000 }}),
|
||||
function(err) {
|
||||
if (err) return done(err);
|
||||
assert.equal(app.get('port'), NAMED_PORT);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with middleware.json', function() {
|
||||
describe('with middleware.json', function(done) {
|
||||
beforeEach(function() {
|
||||
delete process.env.restApiRoot;
|
||||
});
|
||||
|
@ -453,7 +519,8 @@ describe('executor', function() {
|
|||
it('should parse a simple config variable', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ path: '${restApiRoot}' }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -461,12 +528,14 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse simple config variable from env var', function(done) {
|
||||
process.env.restApiRoot = '/url-from-env-var';
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ path: '${restApiRoot}' }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/url-from-env-var').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -474,6 +543,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('dynamic variable from `env var` should have' +
|
||||
' precedence over app.get()', function(done) {
|
||||
|
@ -481,8 +551,9 @@ describe('executor', function() {
|
|||
var bootInstructions;
|
||||
bootInstructions = simpleMiddlewareConfig('routes',
|
||||
{ path: '${restApiRoot}' });
|
||||
bootInstructions.config = { restApiRoot: '/url-from-config' };
|
||||
boot.execute(app, someInstructions(bootInstructions));
|
||||
bootInstructions.application = { restApiRoot: '/url-from-config' };
|
||||
boot.execute(app, someInstructions(bootInstructions), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/url-from-env-var').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -491,11 +562,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse multiple config variables', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ path: '${restApiRoot}', env: '${env}' }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -504,11 +577,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in an array', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ paths: ['${restApiRoot}'] }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -518,11 +593,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in an object', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ info: { path: '${restApiRoot}' }}
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -532,11 +609,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in a nested object', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ nested: { info: { path: '${restApiRoot}' }}}
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -546,11 +625,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables with null values', function(done) {
|
||||
boot.execute(app, simpleMiddlewareConfig('routes',
|
||||
{ nested: { info: { path: '${restApiRoot}', some: null }}}
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -563,14 +644,17 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse invalid config variables', function(done) {
|
||||
var invalidDataTypes = [undefined, function() {}];
|
||||
var invalidDataTypes = [undefined, function() {
|
||||
}];
|
||||
async.each(invalidDataTypes, function(invalidDataType, cb) {
|
||||
var config = simpleMiddlewareConfig('routes', {
|
||||
path: invalidDataType,
|
||||
});
|
||||
boot.execute(app, config);
|
||||
boot.execute(app, config, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
|
@ -579,14 +663,17 @@ describe('executor', function() {
|
|||
expect(res.body.path).to.be.undefined();
|
||||
cb();
|
||||
});
|
||||
}, cb);
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('should parse valid config variables', function(done) {
|
||||
var config = simpleMiddlewareConfig('routes', {
|
||||
props: ['a', '${vVar}', 1, true, function() {}, { x: 1, y: '${y}' }],
|
||||
props: ['a', '${vVar}', 1, true, function() {
|
||||
}, { x: 1, y: '${y}' }],
|
||||
});
|
||||
boot.execute(app, config);
|
||||
boot.execute(app, config, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
|
@ -595,6 +682,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve object prototypes', function(done) {
|
||||
var config = simpleMiddlewareConfig(
|
||||
|
@ -602,13 +690,15 @@ describe('executor', function() {
|
|||
// IMPORTANT we need more than one item to trigger the original issue
|
||||
[/^\/foobar/, /^\/another/],
|
||||
{});
|
||||
boot.execute(app, config);
|
||||
boot.execute(app, config, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/foobar')
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with component-config.json', function() {
|
||||
beforeEach(function() {
|
||||
|
@ -619,7 +709,8 @@ describe('executor', function() {
|
|||
it('should parse a simple config variable', function(done) {
|
||||
boot.execute(app, simpleComponentConfig(
|
||||
{ path: '${restApiRoot}' }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -627,6 +718,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config from `env-var` and `config`', function(done) {
|
||||
var bootInstructions = simpleComponentConfig(
|
||||
|
@ -638,11 +730,12 @@ describe('executor', function() {
|
|||
);
|
||||
|
||||
// result should get value from config.json
|
||||
bootInstructions.config['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG';
|
||||
bootInstructions.application['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG';
|
||||
// result should get value from env var
|
||||
process.env.DYNAMIC_ENVVAR = 'FOOBAR-ENVVAR';
|
||||
|
||||
boot.execute(app, bootInstructions);
|
||||
boot.execute(app, bootInstructions, function(err) {
|
||||
if (err) return done(err);
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG');
|
||||
|
@ -650,6 +743,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('`env-var` should have precedence over `config`', function(done) {
|
||||
var key = 'DYNAMIC_VARIABLE';
|
||||
|
@ -657,21 +751,24 @@ describe('executor', function() {
|
|||
path: '${restApiRoot}',
|
||||
isDynamic: '${' + key + '}',
|
||||
});
|
||||
bootInstructions.config[key] = 'should be overwritten';
|
||||
bootInstructions.application[key] = 'should be overwritten';
|
||||
process.env[key] = 'successfully overwritten';
|
||||
|
||||
boot.execute(app, bootInstructions);
|
||||
boot.execute(app, bootInstructions, function(err) {
|
||||
if (err) return done(err);
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body.isDynamic).to.equal('successfully overwritten');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse multiple config variables', function(done) {
|
||||
boot.execute(app, simpleComponentConfig(
|
||||
{ path: '${restApiRoot}', env: '${env}' }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -680,11 +777,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in an array', function(done) {
|
||||
boot.execute(app, simpleComponentConfig(
|
||||
{ paths: ['${restApiRoot}'] }
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -694,11 +793,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in an object', function(done) {
|
||||
boot.execute(app, simpleComponentConfig(
|
||||
{ info: { path: '${restApiRoot}' }}
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -708,11 +809,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse config variables in a nested object', function(done) {
|
||||
boot.execute(app, simpleComponentConfig(
|
||||
{ nested: { info: { path: '${restApiRoot}' }}}
|
||||
));
|
||||
), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app).get('/component').end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
@ -723,14 +826,19 @@ describe('executor', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('calls function exported by boot/init.js', function() {
|
||||
it('calls function exported by boot/init.js', function(done) {
|
||||
var file = appdir.writeFileSync('boot/init.js',
|
||||
'module.exports = function(app) { app.fnCalled = true; };');
|
||||
|
||||
delete app.fnCalled;
|
||||
boot.execute(app, someInstructions({ files: { boot: [file] }}));
|
||||
boot.execute(app, someInstructions({ bootScripts: [file] }),
|
||||
function(err) {
|
||||
if (err) return done(err);
|
||||
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('configures middleware', function(done) {
|
||||
|
@ -771,7 +879,8 @@ describe('executor', function() {
|
|||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
|
@ -782,6 +891,7 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('configures middleware using shortform', function(done) {
|
||||
boot.execute(app, someInstructions({
|
||||
|
@ -797,7 +907,8 @@ describe('executor', function() {
|
|||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
|
@ -811,9 +922,13 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('configures middleware (end-to-end)', function(done) {
|
||||
boot.execute(app, simpleAppInstructions());
|
||||
simpleAppInstructions(function(err, context) {
|
||||
if (err) return done(err);
|
||||
boot.execute(app, context.instructions, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
|
@ -823,8 +938,10 @@ describe('executor', function() {
|
|||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('configures components', function() {
|
||||
it('configures components', function(done) {
|
||||
appdir.writeConfigFileSync('component-config.json', {
|
||||
'./components/test-component': {
|
||||
option: 'value',
|
||||
|
@ -835,12 +952,15 @@ describe('executor', function() {
|
|||
'module.exports = ' +
|
||||
'function(app, options) { app.componentOptions = options; }');
|
||||
|
||||
boot(app, appdir.PATH);
|
||||
boot(app, appdir.PATH, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(Object.keys(require.cache)).to.include(
|
||||
appdir.resolve('components/test-component/index.js'));
|
||||
|
||||
expect(app.componentOptions).to.eql({ option: 'value' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables component when configuration is not set', function() {
|
||||
|
@ -852,11 +972,13 @@ describe('executor', function() {
|
|||
'module.exports = ' +
|
||||
'function(app, options) { app.componentOptions = options; }');
|
||||
|
||||
boot(app, appdir.PATH);
|
||||
boot(app, appdir.PATH, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(Object.keys(require.cache)).to.not.include(
|
||||
appdir.resolve('components/test-component/index.js'));
|
||||
});
|
||||
});
|
||||
|
||||
it('disable component if overrided by production configuration', function() {
|
||||
appdir.writeConfigFileSync('component-config.json', {
|
||||
|
@ -870,11 +992,13 @@ describe('executor', function() {
|
|||
'module.exports = ' +
|
||||
'function(app, options) { app.componentOptions = options; }');
|
||||
|
||||
boot(app, { appRootDir: appdir.PATH, env: 'production' });
|
||||
boot(app, { appRootDir: appdir.PATH, env: 'production' }, function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
expect(Object.keys(require.cache)).to.not.include(
|
||||
appdir.resolve('components/test-component/index.js'));
|
||||
});
|
||||
});
|
||||
|
||||
it('configures middleware (that requires `this`)', function(done) {
|
||||
var passportPath = require.resolve('./fixtures/passport');
|
||||
|
@ -892,17 +1016,21 @@ describe('executor', function() {
|
|||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
}), function(err) {
|
||||
if (err) return done(err);
|
||||
|
||||
supertest(app)
|
||||
.get('/')
|
||||
.expect('passport', 'initialized', done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when booting with env', function() {
|
||||
it('should set the `booting` flag during execution', function(done) {
|
||||
expect(app.booting).to.be.undefined();
|
||||
boot.execute(app, envAppInstructions(), function(err) {
|
||||
envAppInstructions(function(err, context) {
|
||||
if (err) return done(err);
|
||||
boot.execute(app, context.instructions, function(err) {
|
||||
if (err) return done(err);
|
||||
expect(app.booting).to.be.false();
|
||||
expect(process.bootFlags).to.not.have.property('barLoadedInTest');
|
||||
|
@ -910,6 +1038,7 @@ describe('executor', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when booting with lazy connect', function() {
|
||||
var SAMPLE_INSTRUCTION = someInstructions({
|
||||
|
@ -997,7 +1126,7 @@ describe('executor', function() {
|
|||
mydb: { host: '${DYNAMIC_HOST}' },
|
||||
};
|
||||
var bootInstructions = {
|
||||
config: { DYNAMIC_HOST: '127.0.0.4' },
|
||||
application: { DYNAMIC_HOST: '127.0.0.4' },
|
||||
dataSources: datasource,
|
||||
};
|
||||
boot.execute(app, someInstructions(bootInstructions), function() {
|
||||
|
@ -1014,7 +1143,7 @@ describe('executor', function() {
|
|||
mydb: { host: '${DYNAMIC_HOST}' },
|
||||
};
|
||||
var bootInstructions = {
|
||||
config: { DYNAMIC_HOST: '127.0.0.3' },
|
||||
application: { DYNAMIC_HOST: '127.0.0.3' },
|
||||
dataSources: datasource,
|
||||
};
|
||||
boot.execute(app, someInstructions(bootInstructions), function() {
|
||||
|
@ -1095,14 +1224,12 @@ assert.isFunc = function(obj, name) {
|
|||
|
||||
function someInstructions(values) {
|
||||
var result = {
|
||||
config: values.config || {},
|
||||
application: values.application || {},
|
||||
models: values.models || [],
|
||||
dataSources: values.dataSources || { db: { connector: 'memory' }},
|
||||
middleware: values.middleware || { phases: [], middleware: [] },
|
||||
components: values.components || [],
|
||||
files: {
|
||||
boot: [],
|
||||
},
|
||||
bootScripts: values.bootScripts || [],
|
||||
};
|
||||
|
||||
if (values.env)
|
||||
|
@ -1116,18 +1243,18 @@ function someInstructions(values) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function simpleAppInstructions() {
|
||||
function simpleAppInstructions(done) {
|
||||
// Copy it so that require will happend again
|
||||
fs.copySync(SIMPLE_APP, appdir.PATH);
|
||||
return boot.compile(appdir.PATH);
|
||||
boot.compile(appdir.PATH, done);
|
||||
}
|
||||
|
||||
function envAppInstructions() {
|
||||
function envAppInstructions(done) {
|
||||
fs.copySync(ENV_APP, appdir.PATH);
|
||||
return boot.compile({
|
||||
boot.compile({
|
||||
appRootDir: appdir.PATH,
|
||||
env: 'test',
|
||||
});
|
||||
}, done);
|
||||
}
|
||||
|
||||
function normalizeEols(str) {
|
||||
|
|
|
@ -7,7 +7,6 @@ var loopback = require('loopback');
|
|||
var boot = require('../../../');
|
||||
|
||||
var app = module.exports = loopback();
|
||||
|
||||
boot(app, {
|
||||
appId: 'browserApp2',
|
||||
appRootDir: __dirname,
|
||||
|
|
|
@ -7,4 +7,4 @@ var loopback = require('loopback');
|
|||
var boot = require('../../../');
|
||||
|
||||
var app = module.exports = loopback();
|
||||
boot(app);
|
||||
boot(app, __dirname);
|
||||
|
|
|
@ -2,4 +2,4 @@ loopback = require 'loopback'
|
|||
boot = require '../../../'
|
||||
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
|
||||
ArrayBuffer: ArrayBuffer,
|
||||
};
|
||||
|
||||
// `window` is used by loopback to detect browser runtime
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// This file is licensed under the MIT License.
|
||||
// License text available at https://opensource.org/licenses/MIT
|
||||
|
||||
var boot = require('../');
|
||||
var utils = require('../lib/utils');
|
||||
var expect = require('chai').expect;
|
||||
var sandbox = require('./helpers/sandbox');
|
||||
var appdir = require('./helpers/appdir');
|
||||
|
@ -16,7 +16,7 @@ describe('utils', function() {
|
|||
describe('fileExistsSync', function() {
|
||||
it('returns false when a file does not exist', function() {
|
||||
var doesNotExist = sandbox.resolve('does-not-exist.json');
|
||||
expect(boot.utils.fileExistsSync(doesNotExist))
|
||||
expect(utils.fileExistsSync(doesNotExist))
|
||||
.to.equal(false);
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ describe('utils', function() {
|
|||
var doesExist = appdir.writeConfigFileSync('does-exist.json', {
|
||||
exists: true,
|
||||
});
|
||||
expect(boot.utils.fileExistsSync(doesExist))
|
||||
expect(utils.fileExistsSync(doesExist))
|
||||
.to.equal(true);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue