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:
Raymond Feng 2016-04-27 15:55:57 -07:00 committed by David Cheung
parent 314dff9f5f
commit ac1571ccf1
34 changed files with 4342 additions and 3099 deletions

View File

@ -4,3 +4,27 @@ All breaking changes must be described here. When adding a new entry,
always describe the impact on users and instructions for upgrading always describe the impact on users and instructions for upgrading
applications from 2.x to 3.0. applications from 2.x to 3.0.
## boot.compile is now async and returns context with instructions
Users that uses `boot.compile()` in the following syntax would need to update
their implementation, it now calls callback function with `instructions` in
`context` object rather than synchronously returning instructions.
Before:
```js
var APP_PATH = path.resolve('../sandbox');
var instructions = boot.compile(APP_PATH);
```
New signature:
```js
var APP_PATH = path.resolve('../sandbox');
var instructions;
boot.compile(APP_PATH, function(err, context) {
instructions = context.instructions;
});
```
Please see [PR #181](https://github.com/strongloop/loopback-boot/pull/181) for full details of change.

View File

@ -3,7 +3,7 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var execute = require('./lib/executor'); var Bootstrapper = require('./lib/bootstrapper');
/** /**
* The browser version of `bootLoopBackApp`. * The browser version of `bootLoopBackApp`.
@ -21,17 +21,24 @@ var execute = require('./lib/executor');
* @header boot(app) * @header boot(app)
*/ */
exports = module.exports = function bootBrowserApp(app, options) { exports = module.exports = function bootBrowserApp(app, options, callback) {
// Only using options.id to identify the browserified bundle to load for // Only using options.id to identify the browserified bundle to load for
// this application. If no Id was provided, load the default bundle. // this application. If no Id was provided, load the default bundle.
var moduleName = 'loopback-boot#instructions'; var moduleName = 'loopback-boot#instructions';
if (options && typeof options === 'object' && options.appId) var appId = options && typeof options === 'object' && options.appId;
moduleName += '-' + options.appId; if (appId)
moduleName += '-' + appId;
// The name of the module containing instructions // The name of the module containing instructions
// is hard-coded in lib/bundler // is hard-coded in lib/bundler
var instructions = require(moduleName); var instructions = require(moduleName);
execute(app, instructions);
var bootstrapper = new Bootstrapper(options);
bootstrapper.phases = ['starting', 'start', 'started'];
var context = {
app: app,
instructions: instructions,
};
return bootstrapper.run(context, callback);
}; };
exports.execute = execute;

View File

@ -4,14 +4,11 @@
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
// Strong globalize // Strong globalize
var SG = require('strong-globalize'); var g = require('./lib/globalize');
SG.SetRootDir(__dirname);
var ConfigLoader = require('./lib/config-loader'); var PluginBase = require('./lib/plugin-base');
var compile = require('./lib/compiler'); var Bootstrapper = require('./lib/bootstrapper');
var execute = require('./lib/executor');
var addInstructionsToBrowserify = require('./lib/bundler'); var addInstructionsToBrowserify = require('./lib/bundler');
var utils = require('./lib/utils');
/** /**
* Initialize an application from an options object or * Initialize an application from an options object or
@ -147,11 +144,29 @@ var utils = require('./lib/utils');
*/ */
exports = module.exports = function bootLoopBackApp(app, options, callback) { exports = module.exports = function bootLoopBackApp(app, options, callback) {
if (typeof options === 'function' && callback === undefined) {
callback = options;
options = {};
}
options = options || {};
// backwards compatibility with loopback's app.boot // backwards compatibility with loopback's app.boot
options.env = options.env || app.get('env'); options.env = options.env || app.get('env');
var instructions = compile(options); var bootstrapper = new Bootstrapper(options);
execute(app, instructions, callback);
var context = {
bootstrapper: bootstrapper,
app: app,
};
return bootstrapper.run(context, callback);
};
exports.compile = function(options, done) {
var bootstrapper = new Bootstrapper(options);
bootstrapper.phases = ['load', 'compile'];
var context = {};
return bootstrapper.run(context, done);
}; };
/** /**
@ -164,14 +179,25 @@ exports = module.exports = function bootLoopBackApp(app, options, callback) {
* *
* @header boot.compileToBrowserify(options, bundler) * @header boot.compileToBrowserify(options, bundler)
*/ */
exports.compileToBrowserify = function(options, bundler) { exports.compileToBrowserify = function(options, bundler, done) {
addInstructionsToBrowserify(compile(options), bundler); return exports.compile(options, function(err, context) {
if (err) return done(err);
addInstructionsToBrowserify({ instructions: context.instructions },
bundler);
done();
});
}; };
/* -- undocumented low-level API -- */
exports.ConfigLoader = ConfigLoader;
exports.compile = compile;
exports.execute = execute;
exports.utils = utils;
exports.addInstructionsToBrowserify = addInstructionsToBrowserify; exports.addInstructionsToBrowserify = addInstructionsToBrowserify;
exports.Bootstrapper = Bootstrapper;
exports.PluginBase = PluginBase;
exports.execute = function(app, instructions, done) {
var bootstrapper = new Bootstrapper(
{ phases: ['starting', 'start', 'started'] });
var context = {
app: app,
instructions: instructions,
};
return bootstrapper.run(context, done);
};

View File

@ -1,25 +1,16 @@
{ {
"ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.",
"1e5fea50eef843cbffd1d438494912c8": "Cannot resolve path \"{0}\"", "1e5fea50eef843cbffd1d438494912c8": "Cannot resolve path \"{0}\"",
"34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"", "34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"",
"46e3ab0ef1149ce0a171b5fac2612ea3": "{{Middleware}} \"{0}\" not found: {1}", "3a7049e42006e8bc19e0f4fc8df63b6b": "The `app` is powered by an incompatible loopback version {0}. Supported versions: {1}",
"79e93b2a95e969788590c14e26bb2c1b": "The data in {{model-config.json}} is in the unsupported 1.x format.", "3f93b626dd9a1c33d67490f6e71018b5": "WARNING: Main config file \"{0}{{.json}}\" is missing",
"978a25819e71602cad691dbe7ba17592": "{0} config must be a valid JSON object", "4d052d84c8620730afd4a30832f11724": "Cannot configure unknown model {0}",
"be2dcdab7aa493ed8d77287eb45cfec8": "cannot require directory contents without directory name",
"2634623ad4b2c5902f6c6bb25e68b733": "WARNING: Main {{config}} file \"{0}.json\" is missing",
"4ed668e9187650d898acf97707df445a": "The {{phase}} \"{0}\" is not defined in the main config.", "4ed668e9187650d898acf97707df445a": "The {{phase}} \"{0}\" is not defined in the main config.",
"6de7e97f033f2cf477297b3d05a93608": "The {{middleware}} \"{0}\" in phase \"{1}\"is not defined in the main config.", "6447e6b342a2c51ab0bc53b3cbdf3742": "Ordering conflict: cannot add \"{0}\" after \"{1}\", because the opposite order was already specified",
"94a0c7d5ab6462f7892b90c63f316f42": "invalid array: {0}",
"ec34cc58612cb654742e4cd0a57aca78": "Cannot apply {0}: {1}",
"0b91d122f6459c8bbe7865be0936fc4a": "{{app.restBasePath}} is required",
"1cda77c9954be299bb7154f73cb6ab74": "{{instructions.middleware.phases}} must be an {{array}}",
"22549489736fb0d7eba5a4b08977505f": "{{app.host}} must be a {{string}}",
"4c581cc529a7aeda620d5c4b4ef5cfa8": "{{app.restApiRoot}} must start with \"/\"",
"6037512314fac9d12af6c654a3804823": "Built-in model {0} should have been defined",
"69746d336c89bf4bb371a6c2fe56304d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by app.get().",
"70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}", "70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}",
"b078ccd043437a258581e387f93dc1a5": "The `{{app}}` is powered by an incompatible {{loopback}} version {0}. Supported versions: {1}", "7f7bdcadb75abfef1bd8a126d547dd6d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by {{app.get()}}.",
"e8d29edfb313cfe64f5c96cc7d3d5b4b": "When using {{loopback-boot}} with {{loopback}} <1.9, the {{loopback}} module must be available for `{{require('loopback')}}`.", "91a742b7c3568cf6b6755741a70b3c52": "The {{middleware}} \"{0}\" in {{phase}} \"{1}\"is not defined in the main config.",
"f48405e7c61c3d665b601c9ba41da424": "{{app.port}} must be a {{string}} or {{number}}", "a3aa22086ae4976cd013065c9a3ff81c": "Cannot apply {0}: ",
"fa2a7d5137c8891693f9515d48f5b7d7": "{{app.restApiRoot}} must be a {{string}}" "be2cf2868ba54624fe38e9908dde5e9e": "The data in {{model-config.json}} is in the unsupported {{1.x}} format.",
"ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.",
"fdc23df1bd0fe55fe3faabcc89ff60f3": "Middleware \"{0}\" not found: {1}"
} }

203
lib/bootstrapper.js vendored Normal file
View File

@ -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;
};

View File

@ -7,46 +7,60 @@ var fs = require('fs');
var path = require('path'); var path = require('path');
var commondir = require('commondir'); var commondir = require('commondir');
var cloneDeep = require('lodash').cloneDeep; var cloneDeep = require('lodash').cloneDeep;
var g = require('strong-globalize')(); var g = require('./globalize');
/** /**
* Add boot instructions to a browserify bundler. * Add boot instructions to a browserify bundler.
* @param {Object} instructions Boot instructions. * @param {Object} instructions Boot instructions.
* @param {Object} bundler A browserify object created by `browserify()`. * @param {Object} bundler A browserify object created by `browserify()`.
*/ */
module.exports = function addInstructionsToBrowserify(context, bundler) {
module.exports = function addInstructionsToBrowserify(instructions, bundler) { addPlugins(bundler);
bundleModelScripts(instructions, bundler); // bundlePluginScripts(context, bundler);
bundleMixinScripts(instructions, bundler); bundleModelScripts(context, bundler);
bundleComponentScripts(instructions, bundler); bundleMixinScripts(context, bundler);
bundleOtherScripts(instructions, bundler); bundleComponentScripts(context, bundler);
bundleInstructions(instructions, bundler); bundleOtherScripts(context, bundler);
bundleInstructions(context, bundler);
}; };
function bundleOtherScripts(instructions, bundler) { function addPlugins(bundler) {
for (var key in instructions.files) { var dir = path.join(__dirname, './plugins');
addScriptsToBundle(key, instructions.files[key], bundler); var files = fs.readdirSync(dir);
} files.forEach(function(f) {
bundler.require(path.join(dir, f),
{ expose: './plugins/' + path.basename(f, '.js') });
});
} }
function bundleModelScripts(instructions, bundler) { function bundleOtherScripts(context, bundler) {
bundleSourceFiles(instructions, 'models', bundler); var list = context.instructions.bootScripts;
addScriptsToBundle('boot', list, bundler);
} }
function bundleMixinScripts(instructions, bundler) { function bundlePluginScripts(context, bundler) {
bundleSourceFiles(instructions, 'mixins', bundler); var list = context.instructions.pluginScripts;
addScriptsToBundle('plugins', list, bundler);
} }
function bundleComponentScripts(instructions, bundler) { function bundleModelScripts(context, bundler) {
bundleSourceFiles(instructions, 'components', bundler); bundleSourceFiles(context, 'models', bundler);
} }
function bundleSourceFiles(instructions, type, bundler) { function bundleMixinScripts(context, bundler) {
var files = instructions[type] bundleSourceFiles(context, 'mixins', bundler);
}
function bundleComponentScripts(context, bundler) {
bundleSourceFiles(context, 'components', bundler);
}
function bundleSourceFiles(context, type, bundler) {
var files = context.instructions[type]
.map(function(m) { return m.sourceFile; }) .map(function(m) { return m.sourceFile; })
.filter(function(f) { return !!f; }); .filter(function(f) { return !!f; });
var instructionToFileMapping = instructions[type] var instructionToFileMapping = context.instructions[type]
.map(function(m) { return files.indexOf(m.sourceFile); }); .map(function(m) { return files.indexOf(m.sourceFile); });
addScriptsToBundle(type, files, bundler); addScriptsToBundle(type, files, bundler);
@ -54,7 +68,7 @@ function bundleSourceFiles(instructions, type, bundler) {
// Update `sourceFile` properties with the new paths // Update `sourceFile` properties with the new paths
instructionToFileMapping.forEach(function(fileIx, sourceIx) { instructionToFileMapping.forEach(function(fileIx, sourceIx) {
if (fileIx === -1) return; if (fileIx === -1) return;
instructions[type][sourceIx].sourceFile = files[fileIx]; context.instructions[type][sourceIx].sourceFile = files[fileIx];
}); });
} }
@ -74,14 +88,14 @@ function addScriptsToBundle(name, list, bundler) {
// Add the file to the bundle. // Add the file to the bundle.
bundler.require(filepath, { expose: fileid }); bundler.require(filepath, { expose: fileid });
// Rewrite the instructions entry with the new id that will be // Rewrite the context entry with the new id that will be
// used to load the file via `require(fileid)`. // used to load the file via `require(fileid)`.
list[ix] = fileid; list[ix] = fileid;
} }
} }
function bundleInstructions(instructions, bundler) { function bundleInstructions(context, bundler) {
instructions = cloneDeep(instructions); var instructions = cloneDeep(context.instructions);
var hasMiddleware = instructions.middleware.phases.length || var hasMiddleware = instructions.middleware.phases.length ||
instructions.middleware.middleware.length; instructions.middleware.middleware.length;

View File

@ -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;
}
}

View File

@ -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';
}

View File

@ -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;
}

11
lib/globalize.js Normal file
View File

@ -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();

336
lib/plugin-base.js Normal file
View File

@ -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);
};

62
lib/plugin-loader.js Normal file
View File

@ -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;
});
};

134
lib/plugins/application.js Normal file
View File

@ -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();
});
};

View File

@ -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);
}

50
lib/plugins/component.js Normal file
View File

@ -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);
});
};

39
lib/plugins/datasource.js Normal file
View File

@ -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);
});
};

263
lib/plugins/middleware.js Normal file
View File

@ -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);
});
};

195
lib/plugins/mixin.js Normal file
View File

@ -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);
}
});
};

304
lib/plugins/model.js Normal file
View File

@ -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);
});
};

29
lib/plugins/swagger.js Normal file
View File

@ -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;
};

View File

@ -3,9 +3,333 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var debug = require('debug')('loopback:boot');
var path = require('path');
var Module = require('module');
var fs = require('fs'); var fs = require('fs');
var assert = require('assert');
var _ = require('lodash');
var g = require('./globalize');
exports.tryReadDir = tryReadDir;
exports.resolveRelativePaths = resolveRelativePaths;
exports.assertIsValidConfig = assertIsValidConfig;
exports.fileExistsSync = fileExistsSync; exports.fileExistsSync = fileExistsSync;
exports.fixFileExtension = fixFileExtension;
exports.findScripts = findScripts;
exports.resolveAppScriptPath = resolveAppScriptPath;
exports.getExcludedExtensions = getExcludedExtensions;
exports.tryResolveAppPath = tryResolveAppPath;
exports.forEachKeyedObject = forEachKeyedObject;
exports.mergePhaseNameLists = mergePhaseNameLists;
var FILE_EXTENSION_JSON = exports.FILE_EXTENSION_JSON = '.json';
/**
* Find all javascript files (except for those prefixed with _)
* and all directories.
* @param {String} dir Full path of the directory to enumerate.
* @return {Array.<String>} A list of absolute paths to pass to `require()`.
*/
function findScripts(dir, extensions) {
assert(dir, 'cannot require directory contents without directory name');
var files = tryReadDir(dir);
extensions = extensions || _.keys(require.extensions);
// sort files in lowercase alpha for linux
files.sort(function(a, b) {
a = a.toLowerCase();
b = b.toLowerCase();
if (a < b) {
return -1;
} else if (b < a) {
return 1;
} else {
return 0;
}
});
var results = [];
files.forEach(function(filename) {
// ignore index.js and files prefixed with underscore
if (filename === 'index.js' || filename[0] === '_') {
return;
}
var filepath = path.resolve(path.join(dir, filename));
var stats = fs.statSync(filepath);
// only require files supported by require.extensions (.txt .md etc.)
if (stats.isFile()) {
if (isPreferredExtension(filename))
results.push(filepath);
else
debug('Skipping file %s - unknown extension', filepath);
} else {
debug('Skipping directory %s', filepath);
}
});
return results;
}
function tryReadDir() {
try {
return fs.readdirSync.apply(fs, arguments);
} catch (e) {
return [];
}
}
function resolveRelativePaths(relativePaths, appRootDir) {
var resolveOpts = { strict: false };
relativePaths.forEach(function(relativePath, k) {
var resolvedPath = tryResolveAppPath(appRootDir, relativePath, resolveOpts);
if (resolvedPath !== undefined) {
relativePaths[k] = resolvedPath;
} else {
debug ('skipping boot script %s - unknown file', relativePath);
}
});
}
function getExcludedExtensions() {
return {
'.json': '.json',
'.node': 'node',
};
}
function isPreferredExtension(filename) {
var includeExtensions = require.extensions;
var ext = path.extname(filename);
return (ext in includeExtensions) && !(ext in getExcludedExtensions());
}
function fixFileExtension(filepath, files, onlyScriptsExportingFunction) {
var results = [];
var otherFile;
/* Prefer coffee scripts over json */
if (isPreferredExtension(filepath)) return filepath;
var basename = path.basename(filepath, FILE_EXTENSION_JSON);
var sourceDir = path.dirname(filepath);
files.forEach(function(f) {
otherFile = path.resolve(sourceDir, f);
var stats = fs.statSync(otherFile);
if (stats.isFile()) {
var otherFileExtension = path.extname(f);
if (!(otherFileExtension in getExcludedExtensions()) &&
path.basename(f, otherFileExtension) == basename) {
if (!onlyScriptsExportingFunction)
results.push(otherFile);
else if (onlyScriptsExportingFunction &&
(typeof require.extensions[otherFileExtension]) === 'function') {
results.push(otherFile);
}
}
}
});
return results.length > 0 ? results[0] : undefined;
}
function resolveAppPath(rootDir, relativePath, resolveOptions) {
var resolvedPath = tryResolveAppPath(rootDir, relativePath, resolveOptions);
if (resolvedPath === undefined && !resolveOptions.optional) {
var err = new Error(g.f('Cannot resolve path "%s"', relativePath));
err.code = 'PATH_NOT_FOUND';
throw err;
}
return resolvedPath;
}
function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
var resolvedPath = resolveAppPath(rootDir, relativePath, resolveOptions);
if (!resolvedPath) {
return false;
}
var sourceDir = path.dirname(resolvedPath);
var files = tryReadDir(sourceDir);
var fixedFile = fixFileExtension(resolvedPath, files, false);
return (fixedFile === undefined ? resolvedPath : fixedFile);
}
function tryResolveAppPath(rootDir, relativePath, resolveOptions) {
var fullPath;
var start = relativePath.substring(0, 2);
/* In order to retain backward compatibility, we need to support
* two ways how to treat values that are not relative nor absolute
* path (e.g. `relativePath = 'foobar'`)
* - `resolveOptions.strict = true` searches in `node_modules` only
* - `resolveOptions.strict = false` attempts to resolve the value
* as a relative path first before searching `node_modules`
*/
resolveOptions = resolveOptions || { strict: true };
var isModuleRelative = false;
if (relativePath[0] === '/') {
fullPath = relativePath;
} else if (start === './' || start === '..') {
fullPath = path.resolve(rootDir, relativePath);
} else if (!resolveOptions.strict) {
isModuleRelative = true;
fullPath = path.resolve(rootDir, relativePath);
}
if (fullPath) {
// This check is needed to support paths pointing to a directory
if (fileExistsSync(fullPath)) {
return fullPath;
}
try {
fullPath = require.resolve(fullPath);
return fullPath;
} catch (err) {
if (!isModuleRelative) {
debug ('Skipping %s - %s', fullPath, err);
return undefined;
}
}
}
// Handle module-relative path, e.g. `loopback/common/models`
// Module.globalPaths is a list of globally configured paths like
// [ env.NODE_PATH values, $HOME/.node_modules, etc. ]
// Module._nodeModulePaths(rootDir) returns a list of paths like
// [ rootDir/node_modules, rootDir/../node_modules, etc. ]
var modulePaths = Module.globalPaths
.concat(Module._nodeModulePaths(rootDir));
fullPath = modulePaths
.map(function(candidateDir) {
var absPath = path.join(candidateDir, relativePath);
try {
// NOTE(bajtos) We need to create a proper String object here,
// otherwise we can't attach additional properties to it
/*jshint -W053 */
var filePath = new String(require.resolve(absPath));
filePath.unresolvedPath = absPath;
return filePath;
} catch (err) {
return absPath;
}
})
.filter(function(candidate) {
return fileExistsSync(candidate.toString());
})
[0];
if (fullPath) {
if (fullPath.unresolvedPath && resolveOptions.fullResolve === false)
return fullPath.unresolvedPath;
// Convert String object back to plain string primitive
return fullPath.toString();
}
debug ('Skipping %s - module not found', fullPath);
return undefined;
}
function assertIsValidConfig(name, config) {
if (config) {
assert(typeof config === 'object',
name + ' config must be a valid JSON object');
}
}
function forEachKeyedObject(obj, fn) {
if (typeof obj !== 'object') return;
Object.keys(obj).forEach(function(key) {
fn(key, obj[key]);
});
}
/**
* Extend the list of builtin phases by merging in an array of phases
* requested by a user while preserving the relative order of phases
* as specified by both arrays.
*
* If the first new name does not match any existing phase, it is inserted
* as the first phase in the new list. The same applies for the second phase,
* and so on, until an existing phase is found.
*
* Any new names in the middle of the array are inserted immediatelly after
* the last common phase. For example, extending
* `["initial", "session", "auth"]` with `["initial", "preauth", "auth"]`
* results in `["initial", "preauth", "session", "auth"]`.
*
*
* **Example**
*
* ```js
* var result = mergePhaseNameLists(
* ['initial', 'session', 'auth', 'routes', 'files', 'final'],
* ['initial', 'postinit', 'preauth', 'auth',
* 'routes', 'subapps', 'final', 'last']
* );
*
* // result: [
* // 'initial', 'postinit', 'preauth', 'session', 'auth',
* // 'routes', 'subapps', 'files', 'final', 'last'
* // ]
* ```
*
* @param {Array} currentNames The current list of phase names.
* @param {Array} namesToMerge The items to add (zip merge) into the target
* array.
* @returns {Array} A new array containing combined items from both arrays.
*
* @header mergePhaseNameLists
*/
function mergePhaseNameLists(currentNames, namesToMerge) {
if (!namesToMerge.length) return currentNames.slice();
var targetArray = currentNames.slice();
var targetIx = targetArray.indexOf(namesToMerge[0]);
if (targetIx === -1) {
// the first new item does not match any existing one
// start adding the new items at the start of the list
targetArray.splice(0, 0, namesToMerge[0]);
targetIx = 0;
}
// merge (zip) two arrays
for (var sourceIx = 1; sourceIx < namesToMerge.length; sourceIx++) {
var valueToAdd = namesToMerge[sourceIx];
var previousValue = namesToMerge[sourceIx - 1];
var existingIx = targetArray.indexOf(valueToAdd, targetIx);
if (existingIx === -1) {
// A new phase - try to add it after the last one,
// unless it was already registered
if (targetArray.indexOf(valueToAdd) !== -1) {
var errMsg = g.f('Ordering conflict: cannot add "%s' +
'" after "%s", because the opposite order was ' +
' already specified', valueToAdd, previousValue);
throw new Error(errMsg);
}
var previousIx = targetArray.indexOf(previousValue);
targetArray.splice(previousIx + 1, 0, valueToAdd);
} else {
// An existing phase - move the pointer
targetIx = existingIx;
}
}
return targetArray;
}
/** /**
* Check synchronously if a filepath points to an existing file. * Check synchronously if a filepath points to an existing file.
@ -23,3 +347,4 @@ function fileExistsSync(filepath) {
return false; return false;
} }
} }

View File

@ -27,24 +27,26 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"async": "~0.9.0", "async": "^1.5.2",
"commondir": "0.0.1", "bluebird": "^3.4.0",
"debug": "^2.0.0", "commondir": "^1.0.1",
"lodash": "^3.6.0", "debug": "^2.2.0",
"semver": "^4.1.0", "lodash": "^4.13.1",
"strong-globalize": "^2.6.2", "semver": "^5.1.0",
"toposort": "^0.2.10" "strong-globalize": "^2.7.0",
"toposort": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"browserify": "^4.1.8", "browserify": "^4.2.3",
"chai": "^1.10.0", "chai": "^3.5.0",
"coffee-script": "^1.8.0", "coffee-script": "^1.10.0",
"coffeeify": "^0.7.0", "coffeeify": "^2.0.1",
"eslint": "^2.5.3", "dirty-chai": "^1.2.2",
"eslint": "^2.11.1",
"eslint-config-loopback": "^1.0.0", "eslint-config-loopback": "^1.0.0",
"fs-extra": "^0.12.0", "fs-extra": "^0.30.0",
"loopback": "^3.0.0", "loopback": "^3.0.0",
"mocha": "^1.19.0", "mocha": "^2.5.3",
"supertest": "^0.14.0" "supertest": "^1.2.0"
} }
} }

83
test/bootstrapper.test.js vendored Normal file
View File

@ -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;
});
});

View File

@ -4,6 +4,7 @@
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var boot = require('../'); var boot = require('../');
var async = require('async');
var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox; var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox;
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
@ -40,7 +41,7 @@ describe('browser support for multiple apps', function() {
browserifyTestApps(apps, function(err, bundlePath) { browserifyTestApps(apps, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var bundledApps = executeBundledApps(bundlePath, apps); var bundledApps = executeBundledApps(bundlePath, apps, function(err) {
var app1 = bundledApps.defaultApp; var app1 = bundledApps.defaultApp;
var app2 = bundledApps.browserApp2; var app2 = bundledApps.browserApp2;
@ -57,12 +58,15 @@ describe('browser support for multiple apps', function() {
}); });
}); });
}); });
});
function browserifyTestApps(apps, next) { function browserifyTestApps(apps, next) {
var b = browserify({ var b = browserify({
debug: true, debug: true,
basedir: path.resolve(__dirname, './fixtures'),
}); });
var bundles = [];
for (var i in apps) { for (var i in apps) {
var appDir = apps[i].appDir; var appDir = apps[i].appDir;
var appFile = apps[i].appFile; var appFile = apps[i].appFile;
@ -79,28 +83,39 @@ function browserifyTestApps(apps, next) {
appRootDir: appDir, appRootDir: appDir,
}; };
} }
boot.compileToBrowserify(opts, b); bundles.push(opts);
} }
async.eachSeries(bundles, function(opts, done) {
boot.compileToBrowserify(opts, b, done);
}, function(err) {
exportBrowserifyToFile(b, 'browser-app-bundle.js', next); exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
});
} }
function executeBundledApps(bundlePath, apps) { function executeBundledApps(bundlePath, apps, done) {
var code = fs.readFileSync(bundlePath); var code = fs.readFileSync(bundlePath);
var context = createBrowserLikeContext(); var context = createBrowserLikeContext();
vm.runInContext(code, context, bundlePath); vm.runInContext(code, context, bundlePath);
var ids = [];
var script = 'var apps = {};\n'; var script = 'var apps = {};\n';
for (var i in apps) { for (var i in apps) {
var moduleName = apps[i].moduleName; var moduleName = apps[i].moduleName;
var id = apps[i].appId || 'defaultApp'; var id = apps[i].appId || 'defaultApp';
ids.push(id);
script += 'apps.' + id + ' = require("' + moduleName + '");\n'; script += 'apps.' + id + ' = require("' + moduleName + '");\n';
} }
script += 'apps;\n'; script += 'apps;\n';
var appsInContext = vm.runInContext(script, context); var appsInContext = vm.runInContext(script, context);
async.each(ids, function(id, done) {
appsInContext[id].once('booted', function() {
done();
});
}, function(err) {
printContextLogs(context); printContextLogs(context);
done(err, appsInContext);
});
return appsInContext; return appsInContext;
} }

View File

@ -20,7 +20,6 @@ var compileStrategies = {
basedir: appDir, basedir: appDir,
debug: true, debug: true,
}); });
b.require('./app.js', { expose: 'browser-app' }); b.require('./app.js', { expose: 'browser-app' });
return b; return b;
}, },
@ -31,7 +30,6 @@ var compileStrategies = {
extensions: ['.coffee'], extensions: ['.coffee'],
debug: true, debug: true,
}); });
b.transform('coffeeify'); b.transform('coffeeify');
b.require('./app.coffee', { expose: 'browser-app' }); b.require('./app.coffee', { expose: 'browser-app' });
@ -50,8 +48,8 @@ describe('browser support', function() {
browserifyTestApp(appDir, function(err, bundlePath) { browserifyTestApp(appDir, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
if (err) return done(err);
// configured in fixtures/browser-app/boot/configure.js // configured in fixtures/browser-app/boot/configure.js
expect(app.settings).to.have.property('custom-key', 'custom-value'); expect(app.settings).to.have.property('custom-key', 'custom-value');
expect(Object.keys(app.models)).to.include('Customer'); expect(Object.keys(app.models)).to.include('Customer');
@ -61,10 +59,10 @@ describe('browser support', function() {
// configured in fixtures/browser-app/component-config.json // configured in fixtures/browser-app/component-config.json
// and fixtures/browser-app/components/dummy-component.js // and fixtures/browser-app/components/dummy-component.js
expect(app.dummyComponentOptions).to.eql({ option: 'value' }); expect(app.dummyComponentOptions).to.eql({ option: 'value' });
done(); done();
}); });
}); });
});
it('loads mixins', function(done) { it('loads mixins', function(done) {
var appDir = path.resolve(__dirname, './fixtures/browser-app'); var appDir = path.resolve(__dirname, './fixtures/browser-app');
@ -75,8 +73,7 @@ describe('browser support', function() {
browserifyTestApp(options, function(err, bundlePath) { browserifyTestApp(options, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
var modelBuilder = app.registry.modelBuilder; var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins; var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['TimeStamps']); expect(Object.keys(registry)).to.eql(['TimeStamps']);
@ -85,6 +82,7 @@ describe('browser support', function() {
done(); done();
}); });
}); });
});
it('supports coffee-script files', function(done) { it('supports coffee-script files', function(done) {
// add coffee-script to require.extensions // add coffee-script to require.extensions
@ -95,8 +93,7 @@ describe('browser support', function() {
browserifyTestApp(appDir, 'coffee', function(err, bundlePath) { browserifyTestApp(appDir, 'coffee', function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
// configured in fixtures/browser-app/boot/configure.coffee // configured in fixtures/browser-app/boot/configure.coffee
expect(app.settings).to.have.property('custom-key', 'custom-value'); expect(app.settings).to.have.property('custom-key', 'custom-value');
expect(Object.keys(app.models)).to.include('Customer'); expect(Object.keys(app.models)).to.include('Customer');
@ -106,6 +103,7 @@ describe('browser support', function() {
}); });
}); });
}); });
});
function browserifyTestApp(options, strategy, next) { function browserifyTestApp(options, strategy, next) {
// set default args // set default args
@ -119,18 +117,19 @@ function browserifyTestApp(options, strategy, next) {
var appDir = typeof(options) === 'object' ? options.appRootDir : options; var appDir = typeof(options) === 'object' ? options.appRootDir : options;
var b = compileStrategies[strategy](appDir); var b = compileStrategies[strategy](appDir);
boot.compileToBrowserify(options, b); boot.compileToBrowserify(options, b, function(err) {
exportBrowserifyToFile(b, 'browser-app-bundle.js', next); exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
});
} }
function executeBundledApp(bundlePath) { function executeBundledApp(bundlePath, done) {
var code = fs.readFileSync(bundlePath); var code = fs.readFileSync(bundlePath);
var context = createBrowserLikeContext(); var context = createBrowserLikeContext();
vm.runInContext(code, context, bundlePath); vm.runInContext(code, context, bundlePath);
var app = vm.runInContext('require("browser-app")', context); var app = vm.runInContext('require("browser-app")', context);
app.once('booted', function(err) {
printContextLogs(context); printContextLogs(context);
done(err, app);
});
return app; return app;
} }

File diff suppressed because it is too large Load Diff

View File

@ -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);
});
});

View File

@ -8,7 +8,12 @@ var boot = require('../');
var path = require('path'); var path = require('path');
var loopback = require('loopback'); var loopback = require('loopback');
var assert = require('assert'); 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 fs = require('fs-extra');
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir'); var appdir = require('./helpers/appdir');
@ -37,7 +42,7 @@ describe('executor', function() {
}); });
var dummyInstructions = someInstructions({ var dummyInstructions = someInstructions({
config: { application: {
port: 0, port: 0,
host: '127.0.0.1', host: '127.0.0.1',
restApiRoot: '/rest-api', restApiRoot: '/rest-api',
@ -62,17 +67,20 @@ describe('executor', function() {
describe('when booting', function() { describe('when booting', function() {
it('should set the `booting` flag during execution', function(done) { it('should set the `booting` flag during execution', function(done) {
expect(app.booting).to.be.undefined(); expect(app.booting).to.be.undefined();
boot.execute(app, simpleAppInstructions(), function(err) { simpleAppInstructions(function(err, context) {
expect(err).to.be.undefined(); if (err) return done(err);
boot.execute(app, context.instructions, function(err) {
expect(err).to.not.exist;
expect(process.bootingFlagSet).to.be.true(); expect(process.bootingFlagSet).to.be.true();
expect(app.booting).to.be.false(); expect(app.booting).to.be.false();
done(); done();
}); });
}); });
});
it('should emit the `booted` event in the next tick', function(done) { it('should emit the `booted` event in the next tick', function(done) {
boot.execute(app, dummyInstructions, function(err) { boot.execute(app, dummyInstructions, function(err) {
expect(err).to.be.undefined(); expect(err).to.not.exist;
}); });
app.on('booted', function() { app.on('booted', function() {
// This test fails with a timeout when the `booted` event has not been // This test fails with a timeout when the `booted` event has not been
@ -86,8 +94,9 @@ describe('executor', function() {
}); });
}); });
it('configures models', function() { it('configures models', function(done) {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions, function(err, context) {
if (err) return done(err);
assert(app.models); assert(app.models);
assert(app.models.User); assert(app.models.User);
assert.equal(app.models.User, app.registry.getModel('User'), assert.equal(app.models.User, app.registry.getModel('User'),
@ -95,9 +104,11 @@ describe('executor', function() {
assertValidDataSource(app.models.User.dataSource); assertValidDataSource(app.models.User.dataSource);
assert.isFunc(app.models.User, 'find'); assert.isFunc(app.models.User, 'find');
assert.isFunc(app.models.User, 'create'); 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 = ' + appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
function(Customer) { function(Customer) {
Customer.settings._customized = 'Customer'; Customer.settings._customized = 'Customer';
@ -116,15 +127,18 @@ describe('executor', function() {
sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), 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'); expect(app.models.Customer.settings._customized).to.be.equal('Customer');
var UserModel = app.registry.getModel('User'); var UserModel = app.registry.getModel('User');
expect(UserModel.settings._customized).to.equal('Base'); 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({ boot.execute(app, someInstructions({
models: [ models: [
{ {
@ -145,17 +159,23 @@ describe('executor', function() {
sourceFile: undefined, sourceFile: undefined,
}, },
], ],
})); }), function(err, context) {
if (err) return done(err);
expect(Object.keys(app.models)).to.eql(['Car']); expect(Object.keys(app.models)).to.eql(['Car']);
done();
});
}); });
it('attaches models to data sources', function() { it('attaches models to data sources', function(done) {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions, function(err, context) {
if (err) return done(err);
assert.equal(app.models.User.dataSource, app.dataSources.theDb); 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 = ' + appdir.writeFileSync('models/Customer.js', 'module.exports = ' +
function(Customer/*, Base*/) { function(Customer/*, Base*/) {
Customer.on('attached', function() { Customer.on('attached', function() {
@ -179,12 +199,15 @@ describe('executor', function() {
sourceFile: undefined, sourceFile: undefined,
}, },
], ],
})); }), function(err, context) {
if (err) return done(err);
expect(app.models.Customer._modelsWhenAttached).to.include('UniqueName'); 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 }); app = loopback({ localRegistry: true });
boot.execute(app, someInstructions({ boot.execute(app, someInstructions({
models: [ models: [
@ -195,35 +218,43 @@ describe('executor', function() {
sourceFile: undefined, sourceFile: undefined,
}, },
], ],
})); }), function(err, context) {
if (err) return done(err);
expect(Object.keys(loopback.registry.modelBuilder.models), 'global models') expect(Object.keys(loopback.registry.modelBuilder.models),
'global models')
.to.not.contain('LocalCustomer'); .to.not.contain('LocalCustomer');
expect(Object.keys(app.registry.modelBuilder.models), 'local models') expect(Object.keys(app.registry.modelBuilder.models), 'local models')
.to.contain('LocalCustomer'); .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', var file = appdir.writeFileSync('boot/badScript.js',
'require("doesnt-exist"); module.exports = {};'); 'require("doesnt-exist"); module.exports = {};');
function doBoot() { boot.execute(app, someInstructions({ bootScripts: [file] }),
boot.execute(app, someInstructions({ files: { boot: [file] }})); function(err) {
} expect(err && err.message)
.to.match(/Cannot find module \'doesnt-exist\'/);
expect(doBoot).to.throw(/Cannot find module \'doesnt-exist\'/); done();
});
}); });
it('instantiates data sources', function() { it('instantiates data sources', function(done) {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions, function(err, context) {
if (err) return done(err);
assert(app.dataSources); assert(app.dataSources);
assert(app.dataSources.theDb); assert(app.dataSources.theDb);
assertValidDataSource(app.dataSources.theDb); assertValidDataSource(app.dataSources.theDb);
assert(app.dataSources.TheDb); assert(app.dataSources.TheDb);
done();
});
}); });
it('does not call autoAttach', function() { it('does not call autoAttach', function(done) {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions, function(err, context) {
if (err) return done(err);
// loopback-datasource-juggler quirk: // loopback-datasource-juggler quirk:
// Model.dataSources has modelBuilder as the default value, // Model.dataSources has modelBuilder as the default value,
@ -231,12 +262,14 @@ describe('executor', function() {
var actual = loopback.Email.dataSource instanceof loopback.DataSource ? var actual = loopback.Email.dataSource instanceof loopback.DataSource ?
'attached' : 'not attached'; 'attached' : 'not attached';
expect(actual).to.equal('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 = { var builtinModel = {
name: 'User', name: 'User',
definition: fs.readJsonFileSync( definition: fs.readJsonSync(
require.resolve('loopback/common/models/user.json') require.resolve('loopback/common/models/user.json')
), ),
config: { dataSource: 'db' }, config: { dataSource: 'db' },
@ -244,32 +277,29 @@ describe('executor', function() {
}; };
builtinModel.definition.redefined = true; builtinModel.definition.redefined = true;
boot.execute(app, someInstructions({ models: [builtinModel] })); boot.execute(app, someInstructions({ models: [builtinModel] }),
function(err, context) {
expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true); if (err) return done(err);
expect(app.models.User.settings.redefined,
'redefined').to.not.equal(true);
done();
});
}); });
describe('with boot and models files', function() { describe('with boot and models files', function() {
beforeEach(function() { beforeEach(function(done) {
boot.execute(app, simpleAppInstructions()); simpleAppInstructions(function(err, context) {
if (err) return done(err);
boot.execute(app, context.instructions, done);
});
}); });
afterEach(function() { afterEach(function() {
delete process.bootFlags; 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 // 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([ expect(process.bootFlags).to.eql([
'barLoaded', 'barLoaded',
'barSyncLoaded', 'barSyncLoaded',
@ -278,14 +308,14 @@ describe('executor', function() {
'barFinished', 'barFinished',
'barSyncExecuted', 'barSyncExecuted',
]); ]);
done();
}, 10);
}); });
}); });
describe('with boot with callback', function() { describe('with boot with callback', function() {
it('should run `boot/*` files asynchronously', function(done) { 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([ expect(process.bootFlags).to.eql([
'barLoaded', 'barLoaded',
'barSyncLoaded', 'barSyncLoaded',
@ -297,6 +327,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
describe('for mixins', function() { describe('for mixins', function() {
var options; 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']; options.mixinDirs = ['./custom-mixins'];
boot(app, options); boot(app, options, function(err) {
if (err) return done(err);
var modelBuilder = app.registry.modelBuilder; var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins; var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); 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']; options.mixinSources = ['./custom-mixins'];
boot(app, options); boot(app, options, function(err) {
if (err) return done(err);
var modelBuilder = app.registry.modelBuilder; var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins; var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
done();
});
}); });
}); });
}); });
@ -356,41 +394,51 @@ describe('executor', function() {
delete process.env.npm_package_config_port; delete process.env.npm_package_config_port;
}); });
function bootWithDefaults() { function bootWithDefaults(done) {
app = loopback(); app = loopback();
boot.execute(app, someInstructions({ boot.execute(app, someInstructions({
config: { application: {
port: undefined, port: undefined,
host: undefined, host: undefined,
}, },
})); }), done);
} }
it('should apply env passed in option object', function() { it('should apply env passed in option object', function(done) {
boot.execute(app, someInstructions({ env: 'custom_env' })); boot.execute(app, someInstructions({ env: 'custom_env' }), function(err) {
if (err) return done(err);
expect(app.get('env')).to.equal('custom_env'); expect(app.get('env')).to.equal('custom_env');
done();
});
}); });
it('should honor host and port', function() { it('should honor host and port', function(done) {
function assertHonored(portKey, hostKey) { function assertHonored(portKey, hostKey, cb) {
process.env[hostKey] = randomPort(); process.env[hostKey] = randomPort();
process.env[portKey] = randomHost(); 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('port'), process.env[portKey], portKey);
assert.equal(app.get('host'), process.env[hostKey], hostKey); assert.equal(app.get('host'), process.env[hostKey], hostKey);
delete process.env[portKey]; delete process.env[portKey];
delete process.env[hostKey]; delete process.env[hostKey];
cb();
});
} }
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP'); async.eachSeries([
assertHonored('npm_config_port', 'npm_config_host'); { port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_NODEJS_IP' },
assertHonored('npm_package_config_port', 'npm_package_config_host'); { port: 'npm_config_port', host: 'npm_config_host' },
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP'); { port: 'npm_package_config_port', host: 'npm_package_config_host' },
assertHonored('VCAP_APP_PORT', 'VCAP_APP_HOST'); { port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_SLS_IP' },
assertHonored('PORT', 'HOST'); { 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 // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
/*eslint-disable camelcase*/ /*eslint-disable camelcase*/
process.env.npm_config_host = randomHost(); process.env.npm_config_host = randomHost();
@ -400,12 +448,15 @@ describe('executor', function() {
process.env.HOST = randomHost(); process.env.HOST = randomHost();
process.env.npm_package_config_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); assert.equal(app.get('host'), process.env.npm_config_host);
/*eslint-enable camelcase*/ /*eslint-enable camelcase*/
done();
});
}); });
it('should prioritize port sources', function() { it('should prioritize port sources', function(done) {
/*eslint-disable camelcase*/ /*eslint-disable camelcase*/
process.env.npm_config_port = randomPort(); process.env.npm_config_port = randomPort();
process.env.OPENSHIFT_SLS_PORT = randomPort(); process.env.OPENSHIFT_SLS_PORT = randomPort();
@ -414,9 +465,12 @@ describe('executor', function() {
process.env.PORT = randomPort(); process.env.PORT = randomPort();
process.env.npm_package_config_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); assert.equal(app.get('port'), process.env.npm_config_port);
/*eslint-enable camelcase*/ /*eslint-enable camelcase*/
done();
});
}); });
function randomHost() { function randomHost() {
@ -427,25 +481,37 @@ describe('executor', function() {
return Math.floor(Math.random() * 10000); return Math.floor(Math.random() * 10000);
} }
it('should honor 0 for free port', function() { it('should honor 0 for free port', function(done) {
boot.execute(app, someInstructions({ config: { port: 0 }})); boot.execute(app, someInstructions({ application: { port: 0 }}),
function(err) {
if (err) return done(err);
assert.equal(app.get('port'), 0); assert.equal(app.get('port'), 0);
done();
});
}); });
it('should default to port 3000', function() { it('should default to port 3000', function(done) {
boot.execute(app, someInstructions({ config: { port: undefined }})); boot.execute(app, someInstructions({ application: { port: undefined }}),
function(err) {
if (err) return done(err);
assert.equal(app.get('port'), 3000); 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'; var NAMED_PORT = '\\.\\pipe\\test';
process.env.PORT = NAMED_PORT; 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); assert.equal(app.get('port'), NAMED_PORT);
done();
});
}); });
}); });
describe('with middleware.json', function() { describe('with middleware.json', function(done) {
beforeEach(function() { beforeEach(function() {
delete process.env.restApiRoot; delete process.env.restApiRoot;
}); });
@ -453,7 +519,8 @@ describe('executor', function() {
it('should parse a simple config variable', function(done) { it('should parse a simple config variable', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' } { path: '${restApiRoot}' }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -461,12 +528,14 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse simple config variable from env var', function(done) { it('should parse simple config variable from env var', function(done) {
process.env.restApiRoot = '/url-from-env-var'; process.env.restApiRoot = '/url-from-env-var';
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' } { path: '${restApiRoot}' }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/url-from-env-var').end(function(err, res) { supertest(app).get('/url-from-env-var').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -474,6 +543,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('dynamic variable from `env var` should have' + it('dynamic variable from `env var` should have' +
' precedence over app.get()', function(done) { ' precedence over app.get()', function(done) {
@ -481,8 +551,9 @@ describe('executor', function() {
var bootInstructions; var bootInstructions;
bootInstructions = simpleMiddlewareConfig('routes', bootInstructions = simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}' }); { path: '${restApiRoot}' });
bootInstructions.config = { restApiRoot: '/url-from-config' }; bootInstructions.application = { restApiRoot: '/url-from-config' };
boot.execute(app, someInstructions(bootInstructions)); boot.execute(app, someInstructions(bootInstructions), function(err) {
if (err) return done(err);
supertest(app).get('/url-from-env-var').end(function(err, res) { supertest(app).get('/url-from-env-var').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -491,11 +562,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse multiple config variables', function(done) { it('should parse multiple config variables', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ path: '${restApiRoot}', env: '${env}' } { path: '${restApiRoot}', env: '${env}' }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -504,11 +577,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in an array', function(done) { it('should parse config variables in an array', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ paths: ['${restApiRoot}'] } { paths: ['${restApiRoot}'] }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -518,11 +593,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in an object', function(done) { it('should parse config variables in an object', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ info: { path: '${restApiRoot}' }} { info: { path: '${restApiRoot}' }}
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -532,11 +609,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in a nested object', function(done) { it('should parse config variables in a nested object', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ nested: { info: { path: '${restApiRoot}' }}} { nested: { info: { path: '${restApiRoot}' }}}
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -546,11 +625,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables with null values', function(done) { it('should parse config variables with null values', function(done) {
boot.execute(app, simpleMiddlewareConfig('routes', boot.execute(app, simpleMiddlewareConfig('routes',
{ nested: { info: { path: '${restApiRoot}', some: null }}} { nested: { info: { path: '${restApiRoot}', some: null }}}
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/').end(function(err, res) { supertest(app).get('/').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -563,14 +644,17 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should not parse invalid config variables', 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) { async.each(invalidDataTypes, function(invalidDataType, cb) {
var config = simpleMiddlewareConfig('routes', { var config = simpleMiddlewareConfig('routes', {
path: invalidDataType, path: invalidDataType,
}); });
boot.execute(app, config); boot.execute(app, config, function(err) {
if (err) return done(err);
supertest(app) supertest(app)
.get('/') .get('/')
@ -579,14 +663,17 @@ describe('executor', function() {
expect(res.body.path).to.be.undefined(); expect(res.body.path).to.be.undefined();
cb(); cb();
}); });
}, cb);
}, done); }, done);
}); });
it('should parse valid config variables', function(done) { it('should parse valid config variables', function(done) {
var config = simpleMiddlewareConfig('routes', { 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) supertest(app)
.get('/') .get('/')
@ -595,6 +682,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should preserve object prototypes', function(done) { it('should preserve object prototypes', function(done) {
var config = simpleMiddlewareConfig( var config = simpleMiddlewareConfig(
@ -602,13 +690,15 @@ describe('executor', function() {
// IMPORTANT we need more than one item to trigger the original issue // IMPORTANT we need more than one item to trigger the original issue
[/^\/foobar/, /^\/another/], [/^\/foobar/, /^\/another/],
{}); {});
boot.execute(app, config); boot.execute(app, config, function(err) {
if (err) return done(err);
supertest(app).get('/foobar') supertest(app).get('/foobar')
.expect(200) .expect(200)
.end(done); .end(done);
}); });
}); });
});
describe('with component-config.json', function() { describe('with component-config.json', function() {
beforeEach(function() { beforeEach(function() {
@ -619,7 +709,8 @@ describe('executor', function() {
it('should parse a simple config variable', function(done) { it('should parse a simple config variable', function(done) {
boot.execute(app, simpleComponentConfig( boot.execute(app, simpleComponentConfig(
{ path: '${restApiRoot}' } { path: '${restApiRoot}' }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/component').end(function(err, res) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -627,6 +718,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config from `env-var` and `config`', function(done) { it('should parse config from `env-var` and `config`', function(done) {
var bootInstructions = simpleComponentConfig( var bootInstructions = simpleComponentConfig(
@ -638,11 +730,12 @@ describe('executor', function() {
); );
// result should get value from config.json // 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 // result should get value from env var
process.env.DYNAMIC_ENVVAR = 'FOOBAR-ENVVAR'; 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) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG'); expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG');
@ -650,6 +743,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('`env-var` should have precedence over `config`', function(done) { it('`env-var` should have precedence over `config`', function(done) {
var key = 'DYNAMIC_VARIABLE'; var key = 'DYNAMIC_VARIABLE';
@ -657,21 +751,24 @@ describe('executor', function() {
path: '${restApiRoot}', path: '${restApiRoot}',
isDynamic: '${' + key + '}', isDynamic: '${' + key + '}',
}); });
bootInstructions.config[key] = 'should be overwritten'; bootInstructions.application[key] = 'should be overwritten';
process.env[key] = 'successfully 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) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
expect(res.body.isDynamic).to.equal('successfully overwritten'); expect(res.body.isDynamic).to.equal('successfully overwritten');
done(); done();
}); });
}); });
});
it('should parse multiple config variables', function(done) { it('should parse multiple config variables', function(done) {
boot.execute(app, simpleComponentConfig( boot.execute(app, simpleComponentConfig(
{ path: '${restApiRoot}', env: '${env}' } { path: '${restApiRoot}', env: '${env}' }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/component').end(function(err, res) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -680,11 +777,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in an array', function(done) { it('should parse config variables in an array', function(done) {
boot.execute(app, simpleComponentConfig( boot.execute(app, simpleComponentConfig(
{ paths: ['${restApiRoot}'] } { paths: ['${restApiRoot}'] }
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/component').end(function(err, res) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -694,11 +793,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in an object', function(done) { it('should parse config variables in an object', function(done) {
boot.execute(app, simpleComponentConfig( boot.execute(app, simpleComponentConfig(
{ info: { path: '${restApiRoot}' }} { info: { path: '${restApiRoot}' }}
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/component').end(function(err, res) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); if (err) return done(err);
@ -708,11 +809,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('should parse config variables in a nested object', function(done) { it('should parse config variables in a nested object', function(done) {
boot.execute(app, simpleComponentConfig( boot.execute(app, simpleComponentConfig(
{ nested: { info: { path: '${restApiRoot}' }}} { nested: { info: { path: '${restApiRoot}' }}}
)); ), function(err) {
if (err) return done(err);
supertest(app).get('/component').end(function(err, res) { supertest(app).get('/component').end(function(err, res) {
if (err) return done(err); 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', var file = appdir.writeFileSync('boot/init.js',
'module.exports = function(app) { app.fnCalled = true; };'); 'module.exports = function(app) { app.fnCalled = true; };');
delete app.fnCalled; 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(); expect(app.fnCalled, 'exported fn was called').to.be.true();
done();
});
}); });
it('configures middleware', function(done) { it('configures middleware', function(done) {
@ -771,7 +879,8 @@ describe('executor', function() {
}, },
], ],
}, },
})); }), function(err) {
if (err) return done(err);
supertest(app) supertest(app)
.get('/') .get('/')
@ -782,6 +891,7 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('configures middleware using shortform', function(done) { it('configures middleware using shortform', function(done) {
boot.execute(app, someInstructions({ boot.execute(app, someInstructions({
@ -797,7 +907,8 @@ describe('executor', function() {
}, },
], ],
}, },
})); }), function(err) {
if (err) return done(err);
supertest(app) supertest(app)
.get('/') .get('/')
@ -811,9 +922,13 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
it('configures middleware (end-to-end)', 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) supertest(app)
.get('/') .get('/')
@ -823,8 +938,10 @@ describe('executor', function() {
done(); done();
}); });
}); });
});
});
it('configures components', function() { it('configures components', function(done) {
appdir.writeConfigFileSync('component-config.json', { appdir.writeConfigFileSync('component-config.json', {
'./components/test-component': { './components/test-component': {
option: 'value', option: 'value',
@ -835,12 +952,15 @@ describe('executor', function() {
'module.exports = ' + 'module.exports = ' +
'function(app, options) { app.componentOptions = options; }'); '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( expect(Object.keys(require.cache)).to.include(
appdir.resolve('components/test-component/index.js')); appdir.resolve('components/test-component/index.js'));
expect(app.componentOptions).to.eql({ option: 'value' }); expect(app.componentOptions).to.eql({ option: 'value' });
done();
});
}); });
it('disables component when configuration is not set', function() { it('disables component when configuration is not set', function() {
@ -852,11 +972,13 @@ describe('executor', function() {
'module.exports = ' + 'module.exports = ' +
'function(app, options) { app.componentOptions = options; }'); '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( expect(Object.keys(require.cache)).to.not.include(
appdir.resolve('components/test-component/index.js')); appdir.resolve('components/test-component/index.js'));
}); });
});
it('disable component if overrided by production configuration', function() { it('disable component if overrided by production configuration', function() {
appdir.writeConfigFileSync('component-config.json', { appdir.writeConfigFileSync('component-config.json', {
@ -870,11 +992,13 @@ describe('executor', function() {
'module.exports = ' + 'module.exports = ' +
'function(app, options) { app.componentOptions = options; }'); '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( expect(Object.keys(require.cache)).to.not.include(
appdir.resolve('components/test-component/index.js')); appdir.resolve('components/test-component/index.js'));
}); });
});
it('configures middleware (that requires `this`)', function(done) { it('configures middleware (that requires `this`)', function(done) {
var passportPath = require.resolve('./fixtures/passport'); var passportPath = require.resolve('./fixtures/passport');
@ -892,17 +1016,21 @@ describe('executor', function() {
}, },
], ],
}, },
})); }), function(err) {
if (err) return done(err);
supertest(app) supertest(app)
.get('/') .get('/')
.expect('passport', 'initialized', done); .expect('passport', 'initialized', done);
}); });
});
describe('when booting with env', function() { describe('when booting with env', function() {
it('should set the `booting` flag during execution', function(done) { it('should set the `booting` flag during execution', function(done) {
expect(app.booting).to.be.undefined(); 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); if (err) return done(err);
expect(app.booting).to.be.false(); expect(app.booting).to.be.false();
expect(process.bootFlags).to.not.have.property('barLoadedInTest'); expect(process.bootFlags).to.not.have.property('barLoadedInTest');
@ -910,6 +1038,7 @@ describe('executor', function() {
}); });
}); });
}); });
});
describe('when booting with lazy connect', function() { describe('when booting with lazy connect', function() {
var SAMPLE_INSTRUCTION = someInstructions({ var SAMPLE_INSTRUCTION = someInstructions({
@ -997,7 +1126,7 @@ describe('executor', function() {
mydb: { host: '${DYNAMIC_HOST}' }, mydb: { host: '${DYNAMIC_HOST}' },
}; };
var bootInstructions = { var bootInstructions = {
config: { DYNAMIC_HOST: '127.0.0.4' }, application: { DYNAMIC_HOST: '127.0.0.4' },
dataSources: datasource, dataSources: datasource,
}; };
boot.execute(app, someInstructions(bootInstructions), function() { boot.execute(app, someInstructions(bootInstructions), function() {
@ -1014,7 +1143,7 @@ describe('executor', function() {
mydb: { host: '${DYNAMIC_HOST}' }, mydb: { host: '${DYNAMIC_HOST}' },
}; };
var bootInstructions = { var bootInstructions = {
config: { DYNAMIC_HOST: '127.0.0.3' }, application: { DYNAMIC_HOST: '127.0.0.3' },
dataSources: datasource, dataSources: datasource,
}; };
boot.execute(app, someInstructions(bootInstructions), function() { boot.execute(app, someInstructions(bootInstructions), function() {
@ -1095,14 +1224,12 @@ assert.isFunc = function(obj, name) {
function someInstructions(values) { function someInstructions(values) {
var result = { var result = {
config: values.config || {}, application: values.application || {},
models: values.models || [], models: values.models || [],
dataSources: values.dataSources || { db: { connector: 'memory' }}, dataSources: values.dataSources || { db: { connector: 'memory' }},
middleware: values.middleware || { phases: [], middleware: [] }, middleware: values.middleware || { phases: [], middleware: [] },
components: values.components || [], components: values.components || [],
files: { bootScripts: values.bootScripts || [],
boot: [],
},
}; };
if (values.env) if (values.env)
@ -1116,18 +1243,18 @@ function someInstructions(values) {
return result; return result;
} }
function simpleAppInstructions() { function simpleAppInstructions(done) {
// Copy it so that require will happend again // Copy it so that require will happend again
fs.copySync(SIMPLE_APP, appdir.PATH); 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); fs.copySync(ENV_APP, appdir.PATH);
return boot.compile({ boot.compile({
appRootDir: appdir.PATH, appRootDir: appdir.PATH,
env: 'test', env: 'test',
}); }, done);
} }
function normalizeEols(str) { function normalizeEols(str) {

View File

@ -7,7 +7,6 @@ var loopback = require('loopback');
var boot = require('../../../'); var boot = require('../../../');
var app = module.exports = loopback(); var app = module.exports = loopback();
boot(app, { boot(app, {
appId: 'browserApp2', appId: 'browserApp2',
appRootDir: __dirname, appRootDir: __dirname,

View File

@ -7,4 +7,4 @@ var loopback = require('loopback');
var boot = require('../../../'); var boot = require('../../../');
var app = module.exports = loopback(); var app = module.exports = loopback();
boot(app); boot(app, __dirname);

View File

@ -2,4 +2,4 @@ loopback = require 'loopback'
boot = require '../../../' boot = require '../../../'
module.exports = client = loopback() module.exports = client = loopback()
boot(client) boot(client, __dirname)

View File

@ -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);
};

View File

@ -58,6 +58,8 @@ function createContext() {
error: [], error: [],
}, },
}, },
ArrayBuffer: ArrayBuffer,
}; };
// `window` is used by loopback to detect browser runtime // `window` is used by loopback to detect browser runtime

View File

@ -3,7 +3,7 @@
// This file is licensed under the MIT License. // This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT // License text available at https://opensource.org/licenses/MIT
var boot = require('../'); var utils = require('../lib/utils');
var expect = require('chai').expect; var expect = require('chai').expect;
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir'); var appdir = require('./helpers/appdir');
@ -16,7 +16,7 @@ describe('utils', function() {
describe('fileExistsSync', function() { describe('fileExistsSync', function() {
it('returns false when a file does not exist', function() { it('returns false when a file does not exist', function() {
var doesNotExist = sandbox.resolve('does-not-exist.json'); var doesNotExist = sandbox.resolve('does-not-exist.json');
expect(boot.utils.fileExistsSync(doesNotExist)) expect(utils.fileExistsSync(doesNotExist))
.to.equal(false); .to.equal(false);
}); });
@ -24,7 +24,7 @@ describe('utils', function() {
var doesExist = appdir.writeConfigFileSync('does-exist.json', { var doesExist = appdir.writeConfigFileSync('does-exist.json', {
exists: true, exists: true,
}); });
expect(boot.utils.fileExistsSync(doesExist)) expect(utils.fileExistsSync(doesExist))
.to.equal(true); .to.equal(true);
}); });
}); });