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,20 +41,21 @@ describe('browser support for multiple apps', function() {
browserifyTestApps(apps, function(err, bundlePath) { browserifyTestApps(apps, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var bundledApps = executeBundledApps(bundlePath, apps); var bundledApps = executeBundledApps(bundlePath, apps, function(err) {
var app1 = bundledApps.defaultApp; var app1 = bundledApps.defaultApp;
var app2 = bundledApps.browserApp2; var app2 = bundledApps.browserApp2;
expect(app1.settings).to.have.property('custom-key', 'custom-value'); expect(app1.settings).to.have.property('custom-key', 'custom-value');
expect(Object.keys(app1.models)).to.include('Customer'); expect(Object.keys(app1.models)).to.include('Customer');
expect(Object.keys(app1.models)).to.not.include('Robot'); expect(Object.keys(app1.models)).to.not.include('Robot');
expect(app1.models.Customer.settings).to.have.property('_customized', expect(app1.models.Customer.settings).to.have.property('_customized',
'Customer'); 'Customer');
expect(Object.keys(app2.models)).to.include('Robot'); expect(Object.keys(app2.models)).to.include('Robot');
expect(Object.keys(app2.models)).to.not.include('Customer'); expect(Object.keys(app2.models)).to.not.include('Customer');
done(); done();
});
}); });
}); });
}); });
@ -61,8 +63,10 @@ describe('browser support for multiple apps', function() {
function browserifyTestApps(apps, next) { function browserifyTestApps(apps, next) {
var b = browserify({ var b = browserify({
debug: true, debug: true,
basedir: path.resolve(__dirname, './fixtures'),
}); });
var bundles = [];
for (var i in apps) { for (var i in apps) {
var appDir = apps[i].appDir; var appDir = apps[i].appDir;
var appFile = apps[i].appFile; var appFile = apps[i].appFile;
@ -79,28 +83,39 @@ function browserifyTestApps(apps, next) {
appRootDir: appDir, appRootDir: appDir,
}; };
} }
boot.compileToBrowserify(opts, b); bundles.push(opts);
} }
async.eachSeries(bundles, function(opts, done) {
exportBrowserifyToFile(b, 'browser-app-bundle.js', next); boot.compileToBrowserify(opts, b, done);
}, function(err) {
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
});
} }
function executeBundledApps(bundlePath, apps) { function executeBundledApps(bundlePath, apps, done) {
var code = fs.readFileSync(bundlePath); var code = fs.readFileSync(bundlePath);
var context = createBrowserLikeContext(); var context = createBrowserLikeContext();
vm.runInContext(code, context, bundlePath); vm.runInContext(code, context, bundlePath);
var ids = [];
var script = 'var apps = {};\n'; var script = 'var apps = {};\n';
for (var i in apps) { for (var i in apps) {
var moduleName = apps[i].moduleName; var moduleName = apps[i].moduleName;
var id = apps[i].appId || 'defaultApp'; var id = apps[i].appId || 'defaultApp';
ids.push(id);
script += 'apps.' + id + ' = require("' + moduleName + '");\n'; script += 'apps.' + id + ' = require("' + moduleName + '");\n';
} }
script += 'apps;\n'; script += 'apps;\n';
var appsInContext = vm.runInContext(script, context); var appsInContext = vm.runInContext(script, context);
async.each(ids, function(id, done) {
printContextLogs(context); appsInContext[id].once('booted', function() {
done();
});
}, function(err) {
printContextLogs(context);
done(err, appsInContext);
});
return appsInContext; return appsInContext;
} }

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,19 +48,19 @@ describe('browser support', function() {
browserifyTestApp(appDir, function(err, bundlePath) { browserifyTestApp(appDir, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
if (err) return done(err);
// configured in fixtures/browser-app/boot/configure.js
expect(app.settings).to.have.property('custom-key', 'custom-value');
expect(Object.keys(app.models)).to.include('Customer');
expect(app.models.Customer.settings)
.to.have.property('_customized', 'Customer');
// configured in fixtures/browser-app/boot/configure.js // configured in fixtures/browser-app/component-config.json
expect(app.settings).to.have.property('custom-key', 'custom-value'); // and fixtures/browser-app/components/dummy-component.js
expect(Object.keys(app.models)).to.include('Customer'); expect(app.dummyComponentOptions).to.eql({ option: 'value' });
expect(app.models.Customer.settings) done();
.to.have.property('_customized', 'Customer'); });
// configured in fixtures/browser-app/component-config.json
// and fixtures/browser-app/components/dummy-component.js
expect(app.dummyComponentOptions).to.eql({ option: 'value' });
done();
}); });
}); });
@ -75,14 +73,14 @@ describe('browser support', function() {
browserifyTestApp(options, function(err, bundlePath) { browserifyTestApp(options, function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['TimeStamps']);
expect(app.models.Customer.timeStampsMixin).to.eql(true);
var modelBuilder = app.registry.modelBuilder; done();
var registry = modelBuilder.mixins.mixins; });
expect(Object.keys(registry)).to.eql(['TimeStamps']);
expect(app.models.Customer.timeStampsMixin).to.eql(true);
done();
}); });
}); });
@ -95,14 +93,14 @@ describe('browser support', function() {
browserifyTestApp(appDir, 'coffee', function(err, bundlePath) { browserifyTestApp(appDir, 'coffee', function(err, bundlePath) {
if (err) return done(err); if (err) return done(err);
var app = executeBundledApp(bundlePath); var app = executeBundledApp(bundlePath, function(err) {
// configured in fixtures/browser-app/boot/configure.coffee
// configured in fixtures/browser-app/boot/configure.coffee expect(app.settings).to.have.property('custom-key', 'custom-value');
expect(app.settings).to.have.property('custom-key', 'custom-value'); expect(Object.keys(app.models)).to.include('Customer');
expect(Object.keys(app.models)).to.include('Customer'); expect(app.models.Customer.settings)
expect(app.models.Customer.settings) .to.have.property('_customized', 'Customer');
.to.have.property('_customized', 'Customer'); done();
done(); });
}); });
}); });
}); });
@ -119,18 +117,19 @@ function browserifyTestApp(options, strategy, next) {
var appDir = typeof(options) === 'object' ? options.appRootDir : options; var appDir = typeof(options) === 'object' ? options.appRootDir : options;
var b = compileStrategies[strategy](appDir); var b = compileStrategies[strategy](appDir);
boot.compileToBrowserify(options, b); boot.compileToBrowserify(options, b, function(err) {
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
exportBrowserifyToFile(b, 'browser-app-bundle.js', next); });
} }
function executeBundledApp(bundlePath) { function executeBundledApp(bundlePath, done) {
var code = fs.readFileSync(bundlePath); var code = fs.readFileSync(bundlePath);
var context = createBrowserLikeContext(); var context = createBrowserLikeContext();
vm.runInContext(code, context, bundlePath); vm.runInContext(code, context, bundlePath);
var app = vm.runInContext('require("browser-app")', context); var app = vm.runInContext('require("browser-app")', context);
app.once('booted', function(err) {
printContextLogs(context); printContextLogs(context);
done(err, app);
});
return app; return app;
} }

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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