From ac1571ccf1ae574763fe46ae019d440533b0758a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 27 Apr 2016 15:55:57 -0700 Subject: [PATCH] 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 --- 3.0-RELEASE-NOTES.md | 24 + browser.js | 19 +- index.js | 58 +- intl/en/messages.json | 29 +- lib/bootstrapper.js | 203 ++ lib/bundler.js | 64 +- lib/compiler.js | 823 ------ lib/config-loader.js | 313 --- lib/executor.js | 454 ---- lib/globalize.js | 11 + lib/plugin-base.js | 336 +++ lib/plugin-loader.js | 62 + lib/plugins/application.js | 134 + lib/plugins/boot-script.js | 97 + lib/plugins/component.js | 50 + lib/plugins/datasource.js | 39 + lib/plugins/middleware.js | 263 ++ lib/plugins/mixin.js | 195 ++ lib/plugins/model.js | 304 +++ lib/plugins/swagger.js | 29 + lib/utils.js | 325 +++ package.json | 32 +- test/bootstrapper.test.js | 83 + test/browser.multiapp.test.js | 49 +- test/browser.test.js | 71 +- test/compiler.test.js | 2492 +++++++++++-------- test/config-loader.test.js | 37 - test/executor.test.js | 809 +++--- test/fixtures/browser-app-2/app.js | 1 - test/fixtures/browser-app/app.js | 2 +- test/fixtures/coffee-app/app.coffee | 2 +- test/fixtures/simple-app/plugins/tracker.js | 23 + test/helpers/browser.js | 2 + test/utils.test.js | 6 +- 34 files changed, 4342 insertions(+), 3099 deletions(-) create mode 100644 lib/bootstrapper.js delete mode 100644 lib/compiler.js delete mode 100644 lib/config-loader.js delete mode 100644 lib/executor.js create mode 100644 lib/globalize.js create mode 100644 lib/plugin-base.js create mode 100644 lib/plugin-loader.js create mode 100644 lib/plugins/application.js create mode 100644 lib/plugins/boot-script.js create mode 100644 lib/plugins/component.js create mode 100644 lib/plugins/datasource.js create mode 100644 lib/plugins/middleware.js create mode 100644 lib/plugins/mixin.js create mode 100644 lib/plugins/model.js create mode 100644 lib/plugins/swagger.js create mode 100644 test/bootstrapper.test.js delete mode 100644 test/config-loader.test.js create mode 100644 test/fixtures/simple-app/plugins/tracker.js diff --git a/3.0-RELEASE-NOTES.md b/3.0-RELEASE-NOTES.md index 776380e..2f09c83 100644 --- a/3.0-RELEASE-NOTES.md +++ b/3.0-RELEASE-NOTES.md @@ -4,3 +4,27 @@ All breaking changes must be described here. When adding a new entry, always describe the impact on users and instructions for upgrading applications from 2.x to 3.0. +## boot.compile is now async and returns context with instructions + +Users that uses `boot.compile()` in the following syntax would need to update +their implementation, it now calls callback function with `instructions` in +`context` object rather than synchronously returning instructions. + +Before: + +```js +var APP_PATH = path.resolve('../sandbox'); +var instructions = boot.compile(APP_PATH); +``` + +New signature: + +```js +var APP_PATH = path.resolve('../sandbox'); +var instructions; +boot.compile(APP_PATH, function(err, context) { + instructions = context.instructions; +}); +``` + +Please see [PR #181](https://github.com/strongloop/loopback-boot/pull/181) for full details of change. diff --git a/browser.js b/browser.js index 98c66c7..7053a42 100644 --- a/browser.js +++ b/browser.js @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var execute = require('./lib/executor'); +var Bootstrapper = require('./lib/bootstrapper'); /** * The browser version of `bootLoopBackApp`. @@ -21,17 +21,24 @@ var execute = require('./lib/executor'); * @header boot(app) */ -exports = module.exports = function bootBrowserApp(app, options) { +exports = module.exports = function bootBrowserApp(app, options, callback) { // Only using options.id to identify the browserified bundle to load for // this application. If no Id was provided, load the default bundle. var moduleName = 'loopback-boot#instructions'; - if (options && typeof options === 'object' && options.appId) - moduleName += '-' + options.appId; + var appId = options && typeof options === 'object' && options.appId; + if (appId) + moduleName += '-' + appId; // The name of the module containing instructions // is hard-coded in lib/bundler var instructions = require(moduleName); - execute(app, instructions); + + var bootstrapper = new Bootstrapper(options); + bootstrapper.phases = ['starting', 'start', 'started']; + var context = { + app: app, + instructions: instructions, + }; + return bootstrapper.run(context, callback); }; -exports.execute = execute; diff --git a/index.js b/index.js index d3e723c..6c63d7e 100644 --- a/index.js +++ b/index.js @@ -4,14 +4,11 @@ // License text available at https://opensource.org/licenses/MIT // Strong globalize -var SG = require('strong-globalize'); -SG.SetRootDir(__dirname); +var g = require('./lib/globalize'); -var ConfigLoader = require('./lib/config-loader'); -var compile = require('./lib/compiler'); -var execute = require('./lib/executor'); +var PluginBase = require('./lib/plugin-base'); +var Bootstrapper = require('./lib/bootstrapper'); var addInstructionsToBrowserify = require('./lib/bundler'); -var utils = require('./lib/utils'); /** * Initialize an application from an options object or @@ -147,11 +144,29 @@ var utils = require('./lib/utils'); */ exports = module.exports = function bootLoopBackApp(app, options, callback) { + if (typeof options === 'function' && callback === undefined) { + callback = options; + options = {}; + } + options = options || {}; // backwards compatibility with loopback's app.boot options.env = options.env || app.get('env'); - var instructions = compile(options); - execute(app, instructions, callback); + var bootstrapper = new Bootstrapper(options); + + var context = { + bootstrapper: bootstrapper, + app: app, + }; + + return bootstrapper.run(context, callback); +}; + +exports.compile = function(options, done) { + var bootstrapper = new Bootstrapper(options); + bootstrapper.phases = ['load', 'compile']; + var context = {}; + return bootstrapper.run(context, done); }; /** @@ -164,14 +179,25 @@ exports = module.exports = function bootLoopBackApp(app, options, callback) { * * @header boot.compileToBrowserify(options, bundler) */ -exports.compileToBrowserify = function(options, bundler) { - addInstructionsToBrowserify(compile(options), bundler); +exports.compileToBrowserify = function(options, bundler, done) { + return exports.compile(options, function(err, context) { + if (err) return done(err); + addInstructionsToBrowserify({ instructions: context.instructions }, + bundler); + done(); + }); }; -/* -- undocumented low-level API -- */ - -exports.ConfigLoader = ConfigLoader; -exports.compile = compile; -exports.execute = execute; -exports.utils = utils; exports.addInstructionsToBrowserify = addInstructionsToBrowserify; +exports.Bootstrapper = Bootstrapper; +exports.PluginBase = PluginBase; + +exports.execute = function(app, instructions, done) { + var bootstrapper = new Bootstrapper( + { phases: ['starting', 'start', 'started'] }); + var context = { + app: app, + instructions: instructions, + }; + return bootstrapper.run(context, done); +}; diff --git a/intl/en/messages.json b/intl/en/messages.json index c0e92c4..950be94 100644 --- a/intl/en/messages.json +++ b/intl/en/messages.json @@ -1,25 +1,16 @@ { - "ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.", "1e5fea50eef843cbffd1d438494912c8": "Cannot resolve path \"{0}\"", "34319676975b1abf107da7a056abb434": "Invalid normalization format - \"{0}\"", - "46e3ab0ef1149ce0a171b5fac2612ea3": "{{Middleware}} \"{0}\" not found: {1}", - "79e93b2a95e969788590c14e26bb2c1b": "The data in {{model-config.json}} is in the unsupported 1.x format.", - "978a25819e71602cad691dbe7ba17592": "{0} config must be a valid JSON object", - "be2dcdab7aa493ed8d77287eb45cfec8": "cannot require directory contents without directory name", - "2634623ad4b2c5902f6c6bb25e68b733": "WARNING: Main {{config}} file \"{0}.json\" is missing", + "3a7049e42006e8bc19e0f4fc8df63b6b": "The `app` is powered by an incompatible loopback version {0}. Supported versions: {1}", + "3f93b626dd9a1c33d67490f6e71018b5": "WARNING: Main config file \"{0}{{.json}}\" is missing", + "4d052d84c8620730afd4a30832f11724": "Cannot configure unknown model {0}", "4ed668e9187650d898acf97707df445a": "The {{phase}} \"{0}\" is not defined in the main config.", - "6de7e97f033f2cf477297b3d05a93608": "The {{middleware}} \"{0}\" in phase \"{1}\"is not defined in the main config.", - "94a0c7d5ab6462f7892b90c63f316f42": "invalid array: {0}", - "ec34cc58612cb654742e4cd0a57aca78": "Cannot apply {0}: {1}", - "0b91d122f6459c8bbe7865be0936fc4a": "{{app.restBasePath}} is required", - "1cda77c9954be299bb7154f73cb6ab74": "{{instructions.middleware.phases}} must be an {{array}}", - "22549489736fb0d7eba5a4b08977505f": "{{app.host}} must be a {{string}}", - "4c581cc529a7aeda620d5c4b4ef5cfa8": "{{app.restApiRoot}} must start with \"/\"", - "6037512314fac9d12af6c654a3804823": "Built-in model {0} should have been defined", - "69746d336c89bf4bb371a6c2fe56304d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by app.get().", + "6447e6b342a2c51ab0bc53b3cbdf3742": "Ordering conflict: cannot add \"{0}\" after \"{1}\", because the opposite order was already specified", "70654dc6eb565613a33344efed3de998": "Failed loading boot script: {0}\n{1}", - "b078ccd043437a258581e387f93dc1a5": "The `{{app}}` is powered by an incompatible {{loopback}} version {0}. Supported versions: {1}", - "e8d29edfb313cfe64f5c96cc7d3d5b4b": "When using {{loopback-boot}} with {{loopback}} <1.9, the {{loopback}} module must be available for `{{require('loopback')}}`.", - "f48405e7c61c3d665b601c9ba41da424": "{{app.port}} must be a {{string}} or {{number}}", - "fa2a7d5137c8891693f9515d48f5b7d7": "{{app.restApiRoot}} must be a {{string}}" + "7f7bdcadb75abfef1bd8a126d547dd6d": "{0} does not resolve to a valid value, returned as {1}. \"{2}\" must be resolvable in Environment variable or by {{app.get()}}.", + "91a742b7c3568cf6b6755741a70b3c52": "The {{middleware}} \"{0}\" in {{phase}} \"{1}\"is not defined in the main config.", + "a3aa22086ae4976cd013065c9a3ff81c": "Cannot apply {0}: ", + "be2cf2868ba54624fe38e9908dde5e9e": "The data in {{model-config.json}} is in the unsupported {{1.x}} format.", + "ec551b6f2fafd8d40af801ebe5bb09f6": "Discarding {{middleware}} instructions, {{loopback}} client does not support {{middleware}}.", + "fdc23df1bd0fe55fe3faabcc89ff60f3": "Middleware \"{0}\" not found: {1}" } diff --git a/lib/bootstrapper.js b/lib/bootstrapper.js new file mode 100644 index 0000000..600fafc --- /dev/null +++ b/lib/bootstrapper.js @@ -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; +}; + diff --git a/lib/bundler.js b/lib/bundler.js index 41ba1a1..fa15860 100644 --- a/lib/bundler.js +++ b/lib/bundler.js @@ -7,46 +7,60 @@ var fs = require('fs'); var path = require('path'); var commondir = require('commondir'); var cloneDeep = require('lodash').cloneDeep; -var g = require('strong-globalize')(); +var g = require('./globalize'); /** * Add boot instructions to a browserify bundler. * @param {Object} instructions Boot instructions. * @param {Object} bundler A browserify object created by `browserify()`. */ - -module.exports = function addInstructionsToBrowserify(instructions, bundler) { - bundleModelScripts(instructions, bundler); - bundleMixinScripts(instructions, bundler); - bundleComponentScripts(instructions, bundler); - bundleOtherScripts(instructions, bundler); - bundleInstructions(instructions, bundler); +module.exports = function addInstructionsToBrowserify(context, bundler) { + addPlugins(bundler); + // bundlePluginScripts(context, bundler); + bundleModelScripts(context, bundler); + bundleMixinScripts(context, bundler); + bundleComponentScripts(context, bundler); + bundleOtherScripts(context, bundler); + bundleInstructions(context, bundler); }; -function bundleOtherScripts(instructions, bundler) { - for (var key in instructions.files) { - addScriptsToBundle(key, instructions.files[key], bundler); - } +function addPlugins(bundler) { + var dir = path.join(__dirname, './plugins'); + var files = fs.readdirSync(dir); + files.forEach(function(f) { + bundler.require(path.join(dir, f), + { expose: './plugins/' + path.basename(f, '.js') }); + }); } -function bundleModelScripts(instructions, bundler) { - bundleSourceFiles(instructions, 'models', bundler); +function bundleOtherScripts(context, bundler) { + var list = context.instructions.bootScripts; + addScriptsToBundle('boot', list, bundler); } -function bundleMixinScripts(instructions, bundler) { - bundleSourceFiles(instructions, 'mixins', bundler); +function bundlePluginScripts(context, bundler) { + var list = context.instructions.pluginScripts; + addScriptsToBundle('plugins', list, bundler); } -function bundleComponentScripts(instructions, bundler) { - bundleSourceFiles(instructions, 'components', bundler); +function bundleModelScripts(context, bundler) { + bundleSourceFiles(context, 'models', bundler); } -function bundleSourceFiles(instructions, type, bundler) { - var files = instructions[type] +function bundleMixinScripts(context, bundler) { + bundleSourceFiles(context, 'mixins', bundler); +} + +function bundleComponentScripts(context, bundler) { + bundleSourceFiles(context, 'components', bundler); +} + +function bundleSourceFiles(context, type, bundler) { + var files = context.instructions[type] .map(function(m) { return m.sourceFile; }) .filter(function(f) { return !!f; }); - var instructionToFileMapping = instructions[type] + var instructionToFileMapping = context.instructions[type] .map(function(m) { return files.indexOf(m.sourceFile); }); addScriptsToBundle(type, files, bundler); @@ -54,7 +68,7 @@ function bundleSourceFiles(instructions, type, bundler) { // Update `sourceFile` properties with the new paths instructionToFileMapping.forEach(function(fileIx, sourceIx) { if (fileIx === -1) return; - instructions[type][sourceIx].sourceFile = files[fileIx]; + context.instructions[type][sourceIx].sourceFile = files[fileIx]; }); } @@ -74,14 +88,14 @@ function addScriptsToBundle(name, list, bundler) { // Add the file to the bundle. bundler.require(filepath, { expose: fileid }); - // Rewrite the instructions entry with the new id that will be + // Rewrite the context entry with the new id that will be // used to load the file via `require(fileid)`. list[ix] = fileid; } } -function bundleInstructions(instructions, bundler) { - instructions = cloneDeep(instructions); +function bundleInstructions(context, bundler) { + var instructions = cloneDeep(context.instructions); var hasMiddleware = instructions.middleware.phases.length || instructions.middleware.middleware.length; diff --git a/lib/compiler.js b/lib/compiler.js deleted file mode 100644 index 7258e6a..0000000 --- a/lib/compiler.js +++ /dev/null @@ -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.} 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 . 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; - } -} diff --git a/lib/config-loader.js b/lib/config-loader.js deleted file mode 100644 index 1c34fab..0000000 --- a/lib/config-loader.js +++ /dev/null @@ -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.} 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.} files - * @returns {Array.} - */ -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.} 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'; -} diff --git a/lib/executor.js b/lib/executor.js deleted file mode 100644 index 17fc02d..0000000 --- a/lib/executor.js +++ /dev/null @@ -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; -} diff --git a/lib/globalize.js b/lib/globalize.js new file mode 100644 index 0000000..030d089 --- /dev/null +++ b/lib/globalize.js @@ -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(); diff --git a/lib/plugin-base.js b/lib/plugin-base.js new file mode 100644 index 0000000..2505d83 --- /dev/null +++ b/lib/plugin-base.js @@ -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. + return this.configure(context, config); +}; + +PluginBase.prototype.configure = function(context, config) { + config = config || {}; + // Register as context.configurations. + 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.} exts An array of extension names + * @returns {Array.} 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.} files + * @returns {Array.} + */ +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.} 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. + 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); +}; diff --git a/lib/plugin-loader.js b/lib/plugin-loader.js new file mode 100644 index 0000000..3f11159 --- /dev/null +++ b/lib/plugin-loader.js @@ -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; + }); +}; + + diff --git a/lib/plugins/application.js b/lib/plugins/application.js new file mode 100644 index 0000000..a8c487f --- /dev/null +++ b/lib/plugins/application.js @@ -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(); + }); +}; diff --git a/lib/plugins/boot-script.js b/lib/plugins/boot-script.js new file mode 100644 index 0000000..17be856 --- /dev/null +++ b/lib/plugins/boot-script.js @@ -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); +} diff --git a/lib/plugins/component.js b/lib/plugins/component.js new file mode 100644 index 0000000..47e3ed7 --- /dev/null +++ b/lib/plugins/component.js @@ -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); + }); +}; diff --git a/lib/plugins/datasource.js b/lib/plugins/datasource.js new file mode 100644 index 0000000..f99a6dc --- /dev/null +++ b/lib/plugins/datasource.js @@ -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); + }); +}; + diff --git a/lib/plugins/middleware.js b/lib/plugins/middleware.js new file mode 100644 index 0000000..61eddfb --- /dev/null +++ b/lib/plugins/middleware.js @@ -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 . 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); + }); +}; diff --git a/lib/plugins/mixin.js b/lib/plugins/mixin.js new file mode 100644 index 0000000..24081f4 --- /dev/null +++ b/lib/plugins/mixin.js @@ -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); + } + }); +}; diff --git a/lib/plugins/model.js b/lib/plugins/model.js new file mode 100644 index 0000000..027818b --- /dev/null +++ b/lib/plugins/model.js @@ -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); + }); +}; + diff --git a/lib/plugins/swagger.js b/lib/plugins/swagger.js new file mode 100644 index 0000000..24770d0 --- /dev/null +++ b/lib/plugins/swagger.js @@ -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; +}; diff --git a/lib/utils.js b/lib/utils.js index 49f5579..5d1ce74 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -3,9 +3,333 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +var debug = require('debug')('loopback:boot'); +var path = require('path'); +var Module = require('module'); var fs = require('fs'); +var assert = require('assert'); +var _ = require('lodash'); +var g = require('./globalize'); +exports.tryReadDir = tryReadDir; +exports.resolveRelativePaths = resolveRelativePaths; +exports.assertIsValidConfig = assertIsValidConfig; exports.fileExistsSync = fileExistsSync; +exports.fixFileExtension = fixFileExtension; +exports.findScripts = findScripts; +exports.resolveAppScriptPath = resolveAppScriptPath; +exports.getExcludedExtensions = getExcludedExtensions; +exports.tryResolveAppPath = tryResolveAppPath; +exports.forEachKeyedObject = forEachKeyedObject; +exports.mergePhaseNameLists = mergePhaseNameLists; + +var FILE_EXTENSION_JSON = exports.FILE_EXTENSION_JSON = '.json'; +/** + * Find all javascript files (except for those prefixed with _) + * and all directories. + * @param {String} dir Full path of the directory to enumerate. + * @return {Array.} A list of absolute paths to pass to `require()`. + */ + +function findScripts(dir, extensions) { + assert(dir, 'cannot require directory contents without directory name'); + + var files = tryReadDir(dir); + extensions = extensions || _.keys(require.extensions); + + // sort files in lowercase alpha for linux + files.sort(function(a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + + if (a < b) { + return -1; + } else if (b < a) { + return 1; + } else { + return 0; + } + }); + + var results = []; + files.forEach(function(filename) { + // ignore index.js and files prefixed with underscore + if (filename === 'index.js' || filename[0] === '_') { + return; + } + + var filepath = path.resolve(path.join(dir, filename)); + var stats = fs.statSync(filepath); + + // only require files supported by require.extensions (.txt .md etc.) + if (stats.isFile()) { + if (isPreferredExtension(filename)) + results.push(filepath); + else + debug('Skipping file %s - unknown extension', filepath); + } else { + debug('Skipping directory %s', filepath); + } + }); + + return results; +} + +function tryReadDir() { + try { + return fs.readdirSync.apply(fs, arguments); + } catch (e) { + return []; + } +} + +function resolveRelativePaths(relativePaths, appRootDir) { + var resolveOpts = { strict: false }; + relativePaths.forEach(function(relativePath, k) { + var resolvedPath = tryResolveAppPath(appRootDir, relativePath, resolveOpts); + if (resolvedPath !== undefined) { + relativePaths[k] = resolvedPath; + } else { + debug ('skipping boot script %s - unknown file', relativePath); + } + }); +} + +function getExcludedExtensions() { + return { + '.json': '.json', + '.node': 'node', + }; +} + +function isPreferredExtension(filename) { + var includeExtensions = require.extensions; + + var ext = path.extname(filename); + return (ext in includeExtensions) && !(ext in getExcludedExtensions()); +} + +function fixFileExtension(filepath, files, onlyScriptsExportingFunction) { + var results = []; + var otherFile; + + /* Prefer coffee scripts over json */ + if (isPreferredExtension(filepath)) return filepath; + + var basename = path.basename(filepath, FILE_EXTENSION_JSON); + var sourceDir = path.dirname(filepath); + + files.forEach(function(f) { + otherFile = path.resolve(sourceDir, f); + + var stats = fs.statSync(otherFile); + if (stats.isFile()) { + var otherFileExtension = path.extname(f); + + if (!(otherFileExtension in getExcludedExtensions()) && + path.basename(f, otherFileExtension) == basename) { + if (!onlyScriptsExportingFunction) + results.push(otherFile); + else if (onlyScriptsExportingFunction && + (typeof require.extensions[otherFileExtension]) === 'function') { + results.push(otherFile); + } + } + } + }); + return results.length > 0 ? results[0] : undefined; +} + +function resolveAppPath(rootDir, relativePath, resolveOptions) { + var resolvedPath = tryResolveAppPath(rootDir, relativePath, resolveOptions); + if (resolvedPath === undefined && !resolveOptions.optional) { + var err = new Error(g.f('Cannot resolve path "%s"', relativePath)); + err.code = 'PATH_NOT_FOUND'; + throw err; + } + return resolvedPath; +} + +function resolveAppScriptPath(rootDir, relativePath, resolveOptions) { + var resolvedPath = resolveAppPath(rootDir, relativePath, resolveOptions); + if (!resolvedPath) { + return false; + } + var sourceDir = path.dirname(resolvedPath); + var files = tryReadDir(sourceDir); + var fixedFile = fixFileExtension(resolvedPath, files, false); + return (fixedFile === undefined ? resolvedPath : fixedFile); +} + +function tryResolveAppPath(rootDir, relativePath, resolveOptions) { + var fullPath; + var start = relativePath.substring(0, 2); + + /* In order to retain backward compatibility, we need to support + * two ways how to treat values that are not relative nor absolute + * path (e.g. `relativePath = 'foobar'`) + * - `resolveOptions.strict = true` searches in `node_modules` only + * - `resolveOptions.strict = false` attempts to resolve the value + * as a relative path first before searching `node_modules` + */ + resolveOptions = resolveOptions || { strict: true }; + + var isModuleRelative = false; + if (relativePath[0] === '/') { + fullPath = relativePath; + } else if (start === './' || start === '..') { + fullPath = path.resolve(rootDir, relativePath); + } else if (!resolveOptions.strict) { + isModuleRelative = true; + fullPath = path.resolve(rootDir, relativePath); + } + + if (fullPath) { + // This check is needed to support paths pointing to a directory + if (fileExistsSync(fullPath)) { + return fullPath; + } + + try { + fullPath = require.resolve(fullPath); + return fullPath; + } catch (err) { + if (!isModuleRelative) { + debug ('Skipping %s - %s', fullPath, err); + return undefined; + } + } + } + + // Handle module-relative path, e.g. `loopback/common/models` + + // Module.globalPaths is a list of globally configured paths like + // [ env.NODE_PATH values, $HOME/.node_modules, etc. ] + // Module._nodeModulePaths(rootDir) returns a list of paths like + // [ rootDir/node_modules, rootDir/../node_modules, etc. ] + var modulePaths = Module.globalPaths + .concat(Module._nodeModulePaths(rootDir)); + + fullPath = modulePaths + .map(function(candidateDir) { + var absPath = path.join(candidateDir, relativePath); + try { + // NOTE(bajtos) We need to create a proper String object here, + // otherwise we can't attach additional properties to it + /*jshint -W053 */ + var filePath = new String(require.resolve(absPath)); + filePath.unresolvedPath = absPath; + return filePath; + } catch (err) { + return absPath; + } + }) + .filter(function(candidate) { + return fileExistsSync(candidate.toString()); + }) + [0]; + + if (fullPath) { + if (fullPath.unresolvedPath && resolveOptions.fullResolve === false) + return fullPath.unresolvedPath; + // Convert String object back to plain string primitive + return fullPath.toString(); + } + + debug ('Skipping %s - module not found', fullPath); + return undefined; +} + +function assertIsValidConfig(name, config) { + if (config) { + assert(typeof config === 'object', + name + ' config must be a valid JSON object'); + } +} + +function forEachKeyedObject(obj, fn) { + if (typeof obj !== 'object') return; + + Object.keys(obj).forEach(function(key) { + fn(key, obj[key]); + }); +} + +/** + * Extend the list of builtin phases by merging in an array of phases + * requested by a user while preserving the relative order of phases + * as specified by both arrays. + * + * If the first new name does not match any existing phase, it is inserted + * as the first phase in the new list. The same applies for the second phase, + * and so on, until an existing phase is found. + * + * Any new names in the middle of the array are inserted immediatelly after + * the last common phase. For example, extending + * `["initial", "session", "auth"]` with `["initial", "preauth", "auth"]` + * results in `["initial", "preauth", "session", "auth"]`. + * + * + * **Example** + * + * ```js + * var result = mergePhaseNameLists( + * ['initial', 'session', 'auth', 'routes', 'files', 'final'], + * ['initial', 'postinit', 'preauth', 'auth', + * 'routes', 'subapps', 'final', 'last'] + * ); + * + * // result: [ + * // 'initial', 'postinit', 'preauth', 'session', 'auth', + * // 'routes', 'subapps', 'files', 'final', 'last' + * // ] + * ``` + * + * @param {Array} currentNames The current list of phase names. + * @param {Array} namesToMerge The items to add (zip merge) into the target + * array. + * @returns {Array} A new array containing combined items from both arrays. + * + * @header mergePhaseNameLists + */ +function mergePhaseNameLists(currentNames, namesToMerge) { + if (!namesToMerge.length) return currentNames.slice(); + + var targetArray = currentNames.slice(); + var targetIx = targetArray.indexOf(namesToMerge[0]); + + if (targetIx === -1) { + // the first new item does not match any existing one + // start adding the new items at the start of the list + targetArray.splice(0, 0, namesToMerge[0]); + targetIx = 0; + } + + // merge (zip) two arrays + for (var sourceIx = 1; sourceIx < namesToMerge.length; sourceIx++) { + var valueToAdd = namesToMerge[sourceIx]; + var previousValue = namesToMerge[sourceIx - 1]; + var existingIx = targetArray.indexOf(valueToAdd, targetIx); + + if (existingIx === -1) { + // A new phase - try to add it after the last one, + // unless it was already registered + if (targetArray.indexOf(valueToAdd) !== -1) { + var errMsg = g.f('Ordering conflict: cannot add "%s' + + '" after "%s", because the opposite order was ' + + ' already specified', valueToAdd, previousValue); + throw new Error(errMsg); + } + var previousIx = targetArray.indexOf(previousValue); + targetArray.splice(previousIx + 1, 0, valueToAdd); + } else { + // An existing phase - move the pointer + targetIx = existingIx; + } + } + + return targetArray; +} /** * Check synchronously if a filepath points to an existing file. @@ -23,3 +347,4 @@ function fileExistsSync(filepath) { return false; } } + diff --git a/package.json b/package.json index 7238002..ad78a88 100644 --- a/package.json +++ b/package.json @@ -27,24 +27,26 @@ }, "license": "MIT", "dependencies": { - "async": "~0.9.0", - "commondir": "0.0.1", - "debug": "^2.0.0", - "lodash": "^3.6.0", - "semver": "^4.1.0", - "strong-globalize": "^2.6.2", - "toposort": "^0.2.10" + "async": "^1.5.2", + "bluebird": "^3.4.0", + "commondir": "^1.0.1", + "debug": "^2.2.0", + "lodash": "^4.13.1", + "semver": "^5.1.0", + "strong-globalize": "^2.7.0", + "toposort": "^1.0.0" }, "devDependencies": { - "browserify": "^4.1.8", - "chai": "^1.10.0", - "coffee-script": "^1.8.0", - "coffeeify": "^0.7.0", - "eslint": "^2.5.3", + "browserify": "^4.2.3", + "chai": "^3.5.0", + "coffee-script": "^1.10.0", + "coffeeify": "^2.0.1", + "dirty-chai": "^1.2.2", + "eslint": "^2.11.1", "eslint-config-loopback": "^1.0.0", - "fs-extra": "^0.12.0", + "fs-extra": "^0.30.0", "loopback": "^3.0.0", - "mocha": "^1.19.0", - "supertest": "^0.14.0" + "mocha": "^2.5.3", + "supertest": "^1.2.0" } } diff --git a/test/bootstrapper.test.js b/test/bootstrapper.test.js new file mode 100644 index 0000000..68fac8e --- /dev/null +++ b/test/bootstrapper.test.js @@ -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; + }); +}); diff --git a/test/browser.multiapp.test.js b/test/browser.multiapp.test.js index bfcafd4..e2e6e33 100644 --- a/test/browser.multiapp.test.js +++ b/test/browser.multiapp.test.js @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT var boot = require('../'); +var async = require('async'); var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox; var fs = require('fs'); var path = require('path'); @@ -40,20 +41,21 @@ describe('browser support for multiple apps', function() { browserifyTestApps(apps, function(err, bundlePath) { if (err) return done(err); - var bundledApps = executeBundledApps(bundlePath, apps); - var app1 = bundledApps.defaultApp; - var app2 = bundledApps.browserApp2; + var bundledApps = executeBundledApps(bundlePath, apps, function(err) { + var app1 = bundledApps.defaultApp; + var app2 = bundledApps.browserApp2; - expect(app1.settings).to.have.property('custom-key', 'custom-value'); - expect(Object.keys(app1.models)).to.include('Customer'); - expect(Object.keys(app1.models)).to.not.include('Robot'); - expect(app1.models.Customer.settings).to.have.property('_customized', - 'Customer'); + expect(app1.settings).to.have.property('custom-key', 'custom-value'); + expect(Object.keys(app1.models)).to.include('Customer'); + expect(Object.keys(app1.models)).to.not.include('Robot'); + expect(app1.models.Customer.settings).to.have.property('_customized', + 'Customer'); - expect(Object.keys(app2.models)).to.include('Robot'); - expect(Object.keys(app2.models)).to.not.include('Customer'); + expect(Object.keys(app2.models)).to.include('Robot'); + 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) { var b = browserify({ debug: true, + basedir: path.resolve(__dirname, './fixtures'), }); + var bundles = []; for (var i in apps) { var appDir = apps[i].appDir; var appFile = apps[i].appFile; @@ -79,28 +83,39 @@ function browserifyTestApps(apps, next) { appRootDir: appDir, }; } - boot.compileToBrowserify(opts, b); + bundles.push(opts); } - - exportBrowserifyToFile(b, 'browser-app-bundle.js', next); + async.eachSeries(bundles, function(opts, done) { + boot.compileToBrowserify(opts, b, done); + }, function(err) { + exportBrowserifyToFile(b, 'browser-app-bundle.js', next); + }); } -function executeBundledApps(bundlePath, apps) { +function executeBundledApps(bundlePath, apps, done) { var code = fs.readFileSync(bundlePath); var context = createBrowserLikeContext(); vm.runInContext(code, context, bundlePath); + var ids = []; var script = 'var apps = {};\n'; for (var i in apps) { var moduleName = apps[i].moduleName; var id = apps[i].appId || 'defaultApp'; + ids.push(id); script += 'apps.' + id + ' = require("' + moduleName + '");\n'; } script += 'apps;\n'; var appsInContext = vm.runInContext(script, context); - - printContextLogs(context); + async.each(ids, function(id, done) { + appsInContext[id].once('booted', function() { + done(); + }); + }, function(err) { + printContextLogs(context); + done(err, appsInContext); + }); return appsInContext; } diff --git a/test/browser.test.js b/test/browser.test.js index d179aa5..98db56e 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -20,7 +20,6 @@ var compileStrategies = { basedir: appDir, debug: true, }); - b.require('./app.js', { expose: 'browser-app' }); return b; }, @@ -31,7 +30,6 @@ var compileStrategies = { extensions: ['.coffee'], debug: true, }); - b.transform('coffeeify'); b.require('./app.coffee', { expose: 'browser-app' }); @@ -50,19 +48,19 @@ describe('browser support', function() { browserifyTestApp(appDir, function(err, bundlePath) { if (err) return done(err); - var app = executeBundledApp(bundlePath); + var app = executeBundledApp(bundlePath, function(err) { + if (err) return done(err); + // configured in fixtures/browser-app/boot/configure.js + expect(app.settings).to.have.property('custom-key', 'custom-value'); + expect(Object.keys(app.models)).to.include('Customer'); + expect(app.models.Customer.settings) + .to.have.property('_customized', 'Customer'); - // 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/component-config.json - // and fixtures/browser-app/components/dummy-component.js - expect(app.dummyComponentOptions).to.eql({ option: 'value' }); - - done(); + // 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) { 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; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['TimeStamps']); - expect(app.models.Customer.timeStampsMixin).to.eql(true); - - done(); + done(); + }); }); }); @@ -95,14 +93,14 @@ describe('browser support', function() { browserifyTestApp(appDir, 'coffee', function(err, bundlePath) { if (err) return done(err); - var app = executeBundledApp(bundlePath); - - // configured in fixtures/browser-app/boot/configure.coffee - expect(app.settings).to.have.property('custom-key', 'custom-value'); - expect(Object.keys(app.models)).to.include('Customer'); - expect(app.models.Customer.settings) - .to.have.property('_customized', 'Customer'); - done(); + var app = executeBundledApp(bundlePath, function(err) { + // configured in fixtures/browser-app/boot/configure.coffee + expect(app.settings).to.have.property('custom-key', 'custom-value'); + expect(Object.keys(app.models)).to.include('Customer'); + expect(app.models.Customer.settings) + .to.have.property('_customized', 'Customer'); + done(); + }); }); }); }); @@ -119,18 +117,19 @@ function browserifyTestApp(options, strategy, next) { var appDir = typeof(options) === 'object' ? options.appRootDir : options; var b = compileStrategies[strategy](appDir); - boot.compileToBrowserify(options, b); - - exportBrowserifyToFile(b, 'browser-app-bundle.js', next); + boot.compileToBrowserify(options, b, function(err) { + exportBrowserifyToFile(b, 'browser-app-bundle.js', next); + }); } -function executeBundledApp(bundlePath) { +function executeBundledApp(bundlePath, done) { var code = fs.readFileSync(bundlePath); var context = createBrowserLikeContext(); vm.runInContext(code, context, bundlePath); var app = vm.runInContext('require("browser-app")', context); - - printContextLogs(context); - + app.once('booted', function(err) { + printContextLogs(context); + done(err, app); + }); return app; } diff --git a/test/compiler.test.js b/test/compiler.test.js index 41b28c9..9178e65 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -19,12 +19,38 @@ describe('compiler', function() { beforeEach(sandbox.reset); beforeEach(appdir.init); + function expectCompileToThrow(err, options, done) { + if (typeof options === 'function') { + done = options; + options = undefined; + } + boot.compile(options || appdir.PATH, function(err) { + expect(function() { + if (err) throw err; + }).to.throw(err); + done(); + }); + } + + function expectCompileToNotThrow(options, done) { + if (typeof options === 'function') { + done = options; + options = undefined; + } + boot.compile(options || appdir.PATH, function(err) { + expect(function() { + if (err) throw err; + }).to.not.throw(); + done(); + }); + } + describe('from options', function() { var options, instructions, appConfig; - beforeEach(function() { + beforeEach(function(done) { options = { - config: { + application: { port: 3000, host: '127.0.0.1', restApiRoot: '/rest-api', @@ -43,8 +69,12 @@ describe('compiler', function() { }, }, }; - instructions = boot.compile(options); - appConfig = instructions.config; + boot.compile(options, function(err, context) { + if (err) return done(err); + appConfig = context.instructions.application; + instructions = context.instructions; + done(); + }); }); it('has port setting', function() { @@ -82,12 +112,12 @@ describe('compiler', function() { expect(instructions.dataSources).to.eql(options.dataSources); }); - describe('with custom model definitions', function() { + describe('with custom model definitions', function(done) { var dataSources = { 'the-db': { connector: 'memory' }, }; - it('loads model without definition', function() { + it('loads model without definition', function(done) { var instruction = boot.compile({ appRootDir: appdir.PATH, models: { @@ -97,17 +127,21 @@ describe('compiler', function() { }, modelDefinitions: [], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name) + .to.equal('model-without-definition'); + expect(instructions.models[0].definition).to.equal(undefined); + expect(instructions.models[0].sourceFile).to.equal(undefined); + done(); }); - expect(instruction.models[0].name) - .to.equal('model-without-definition'); - expect(instruction.models[0].definition).to.equal(undefined); - expect(instruction.models[0].sourceFile).to.equal(undefined); }); - it('loads coffeescript models', function() { + it('loads coffeescript models', function(done) { var modelScript = appdir.writeFileSync( 'custom-models/coffee-model-with-definition.coffee', ''); - var instruction = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, models: { 'coffee-model-with-definition': { @@ -123,20 +157,24 @@ describe('compiler', function() { }, ], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name) + .to.equal('coffee-model-with-definition'); + expect(instructions.models[0].definition).to.eql({ + name: 'coffee-model-with-definition', + }); + expect(instructions.models[0].sourceFile).to.equal(modelScript); + done(); }); - expect(instruction.models[0].name) - .to.equal('coffee-model-with-definition'); - expect(instruction.models[0].definition).to.eql({ - name: 'coffee-model-with-definition', - }); - expect(instruction.models[0].sourceFile).to.equal(modelScript); }); - it('handles sourceFile path without extension (.js)', function() { + it('handles sourceFile path without extension (.js)', function(done) { var modelScript = appdir.writeFileSync( 'custom-models/model-without-ext.coffee', ''); - var instruction = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, models: { 'model-without-ext': { @@ -150,16 +188,20 @@ describe('compiler', function() { sourceFile: pathWithoutExtension(modelScript), }], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name).to.equal('model-without-ext'); + expect(instructions.models[0].sourceFile).to.equal(modelScript); + done(); }); - expect(instruction.models[0].name).to.equal('model-without-ext'); - expect(instruction.models[0].sourceFile).to.equal(modelScript); }); - it('handles sourceFile path without extension (.coffee)', function() { + it('handles sourceFile path without extension (.coffee)', function(done) { var modelScript = appdir.writeFileSync( 'custom-models/model-without-ext.coffee', ''); - var instruction = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, models: { 'model-without-ext': { @@ -173,16 +215,20 @@ describe('compiler', function() { sourceFile: pathWithoutExtension(modelScript), }], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name).to.equal('model-without-ext'); + expect(instructions.models[0].sourceFile).to.equal(modelScript); + done(); }); - expect(instruction.models[0].name).to.equal('model-without-ext'); - expect(instruction.models[0].sourceFile).to.equal(modelScript); }); - it('sets source file path if the file exist', function() { + it('sets source file path if the file exist', function(done) { var modelScript = appdir.writeFileSync( 'custom-models/model-with-definition.js', ''); - var instruction = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, models: { 'model-with-definition': { @@ -198,65 +244,77 @@ describe('compiler', function() { }, ], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name).to.equal('model-with-definition'); + expect(instructions.models[0].definition).not.to.equal(undefined); + expect(instructions.models[0].sourceFile).to.equal(modelScript); + done(); }); - expect(instruction.models[0].name).to.equal('model-with-definition'); - expect(instruction.models[0].definition).not.to.equal(undefined); - expect(instruction.models[0].sourceFile).to.equal(modelScript); }); it('does not set source file path if the file does not exist.', - function() { - var instruction = boot.compile({ - appRootDir: appdir.PATH, - models: { - 'model-with-definition-with-falsey-source-file': { - dataSource: 'the-db', - }, - }, - modelDefinitions: [ - { - definition: { - name: 'model-with-definition-with-falsey-source-file', + function(done) { + boot.compile({ + appRootDir: appdir.PATH, + models: { + 'model-with-definition-with-falsey-source-file': { + dataSource: 'the-db', }, - sourceFile: appdir.resolve('custom-models', - 'file-does-not-exist.js'), }, - ], - dataSources: dataSources, + modelDefinitions: [ + { + definition: { + name: 'model-with-definition-with-falsey-source-file', + }, + sourceFile: appdir.resolve('custom-models', + 'file-does-not-exist.js'), + }, + ], + dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name) + .to.equal('model-with-definition-with-falsey-source-file'); + expect(instructions.models[0].definition).not.to.equal(undefined); + expect(instructions.models[0].sourceFile).to.equal(undefined); + done(); + }); }); - expect(instruction.models[0].name) - .to.equal('model-with-definition-with-falsey-source-file'); - expect(instruction.models[0].definition).not.to.equal(undefined); - expect(instruction.models[0].sourceFile).to.equal(undefined); - }); it('does not set source file path if no source file supplied.', - function() { - var instruction = boot.compile({ - appRootDir: appdir.PATH, - models: { - 'model-with-definition-without-source-file-property': { - dataSource: 'the-db', - }, - }, - modelDefinitions: [ - { - definition: { - name: 'model-with-definition-without-source-file-property', + function(done) { + boot.compile({ + appRootDir: appdir.PATH, + models: { + 'model-with-definition-without-source-file-property': { + dataSource: 'the-db', }, - // sourceFile is not set }, - ], - dataSources: dataSources, + modelDefinitions: [ + { + definition: { + name: 'model-with-definition-without-source-file-property', + }, + // sourceFile is not set + }, + ], + dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models[0].name) + .to.equal('model-with-definition-without-source-file-property'); + expect(instructions.models[0].definition).not.to.equal(undefined); + expect(instructions.models[0].sourceFile).to.equal(undefined); + done(); + }); }); - expect(instruction.models[0].name) - .to.equal('model-with-definition-without-source-file-property'); - expect(instruction.models[0].definition).not.to.equal(undefined); - expect(instruction.models[0].sourceFile).to.equal(undefined); - }); - it('loads models defined in `models` only.', function() { - var instruction = boot.compile({ + it('loads models defined in `models` only.', function(done) { + boot.compile({ appRootDir: appdir.PATH, models: { 'some-model': { @@ -276,30 +334,37 @@ describe('compiler', function() { }, ], dataSources: dataSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.models.map(getNameProperty)) + .to.eql(['some-model']); + done(); }); - - expect(instruction.models.map(getNameProperty)) - .to.eql(['some-model']); }); }); }); - describe('from directory', function() { - it('loads config files', function() { - var instructions = boot.compile(SIMPLE_APP); + describe('from directory', function(done) { + it('loads config files', function(done) { + boot.compile(SIMPLE_APP, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'User', - config: { - dataSource: 'db', - }, - definition: undefined, - sourceFile: undefined, + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ + name: 'User', + config: { + dataSource: 'db', + }, + definition: undefined, + sourceFile: undefined, + }); + done(); }); }); - it('merges datasource configs from multiple files', function() { + it('merges datasource configs from multiple files', function(done) { appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { db: { local: 'applied' }, @@ -310,46 +375,58 @@ describe('compiler', function() { db: { env: 'applied' }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var db = instructions.dataSources.db; - expect(db).to.have.property('local', 'applied'); - expect(db).to.have.property('env', 'applied'); + var db = instructions.dataSources.db; + expect(db).to.have.property('local', 'applied'); + expect(db).to.have.property('env', 'applied'); - var expectedLoadOrder = ['local', 'env']; - var actualLoadOrder = Object.keys(db).filter(function(k) { - return expectedLoadOrder.indexOf(k) !== -1; + var expectedLoadOrder = ['local', 'env']; + var actualLoadOrder = Object.keys(db).filter(function(k) { + return expectedLoadOrder.indexOf(k) !== -1; + }); + + expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); + done(); }); - - expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); }); - it('supports .js for custom datasource config files', function() { + it('supports .js for custom datasource config files', function(done) { appdir.createConfigFilesSync(); appdir.writeFileSync('datasources.local.js', 'module.exports = { db: { fromJs: true } };'); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var db = instructions.dataSources.db; - expect(db).to.have.property('fromJs', true); + var db = instructions.dataSources.db; + expect(db).to.have.property('fromJs', true); + done(); + }); }); - it('merges new Object values', function() { + it('merges new Object values', function(done) { var objectValue = { key: 'value' }; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { db: { nested: objectValue }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var db = instructions.dataSources.db; - expect(db).to.have.property('nested'); - expect(db.nested).to.eql(objectValue); + var db = instructions.dataSources.db; + expect(db).to.have.property('nested'); + expect(db.nested).to.eql(objectValue); + done(); + }); }); - it('deeply merges Object values', function() { + it('deeply merges Object values', function(done) { appdir.createConfigFilesSync({}, { email: { transport: { @@ -366,12 +443,16 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); - var email = instructions.dataSources.email; - expect(email.transport.host).to.equal('mail.example.com'); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + var email = instructions.dataSources.email; + expect(email.transport.host).to.equal('mail.example.com'); + done(); + }); }); - it('deeply merges Array values of the same length', function() { + it('deeply merges Array values of the same length', function(done) { appdir.createConfigFilesSync({}, { rest: { operations: [ @@ -397,70 +478,124 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var rest = instructions.dataSources.rest; - expect(rest.operations[0].template).to.eql({ - method: 'POST', // the value from datasources.json - url: 'http://api.example.com', // overriden in datasources.local.json + var rest = instructions.dataSources.rest; + expect(rest.operations[0].template).to.eql({ + method: 'POST', // the value from datasources.json + url: 'http://api.example.com', // overriden in datasources.local.json + }); + done(); }); }); - it('merges Array properties', function() { + it('merges Array properties', function(done) { var arrayValue = ['value']; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { db: { nested: arrayValue }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var db = instructions.dataSources.db; - expect(db).to.have.property('nested'); - expect(db.nested).to.eql(arrayValue); + var db = instructions.dataSources.db; + expect(db).to.have.property('nested'); + expect(db.nested).to.eql(arrayValue); + done(); + }); }); - it('allows env specific model-config json', function() { + it('does not cache loaded values', function(done) { + 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 bootOptions = { + appRootDir: appdir.PATH, + env: 'development', + phases: ['load'], + }; + var productionBootOptions = { + appRootDir: appdir.PATH, + env: 'production', + phases: ['load'], + }; + boot.compile(bootOptions, function(err, context) { + var config = context.configurations.middleware; + expect(config['strong-error-handler'].params.debug, + 'debug in development').to.equal(true); + + boot.compile(productionBootOptions, function(err, context2) { + var config = context2.configurations.middleware; + expect(config['strong-error-handler'].params.debug, + 'debug in production').to.equal(false); + done(); + }); + }); + }); + + it('allows env specific model-config json', function(done) { appdir.createConfigFilesSync(); appdir.writeConfigFileSync('model-config.local.json', { foo: { dataSource: 'db' }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.have.property('name', 'foo'); + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.have.property('name', 'foo'); + done(); + }); }); - it('allows env specific model-config json to be merged', function() { + it('allows env specific model-config json to be merged', function(done) { appdir.createConfigFilesSync(null, null, { foo: { dataSource: 'mongo', public: false }}); appdir.writeConfigFileSync('model-config.local.json', { foo: { dataSource: 'db' }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.have.property('name', 'foo'); - expect(instructions.models[0].config).to.eql({ - dataSource: 'db', - public: false, + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.have.property('name', 'foo'); + expect(instructions.models[0].config).to.eql({ + dataSource: 'db', + public: false, + }); + done(); }); }); - it('allows env specific model-config js', function() { + it('allows env specific model-config js', function(done) { appdir.createConfigFilesSync(); appdir.writeFileSync('model-config.local.js', 'module.exports = { foo: { dataSource: \'db\' } };'); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.have.property('name', 'foo'); + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.have.property('name', 'foo'); + done(); + }); }); - it('refuses to merge Array properties of different length', function() { + it('refuses to merge Array properties of different length', function(done) { appdir.createConfigFilesSync({ nest: { array: [], @@ -477,11 +612,11 @@ describe('compiler', function() { }, }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/array values of different length.*nest\.array/); + expectCompileToThrow(/array values of different length.*nest\.array/, + done); }); - it('refuses to merge Array of different length in Array', function() { + it('refuses to merge Array of different length in Array', function(done) { appdir.createConfigFilesSync({ key: [[]], }); @@ -490,11 +625,10 @@ describe('compiler', function() { key: [['value']], }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/array values of different length.*key\[0\]/); + expectCompileToThrow(/array values of different length.*key\[0\]/, done); }); - it('returns full key of an incorrect Array value', function() { + it('returns full key of an incorrect Array value', function(done) { appdir.createConfigFilesSync({ toplevel: [ { @@ -511,11 +645,12 @@ describe('compiler', function() { ], }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/array values of different length.*toplevel\[0\]\.nested/); + expectCompileToThrow( + /array values of different length.*toplevel\[0\]\.nested/, + done); }); - it('refuses to merge incompatible object properties', function() { + it('refuses to merge incompatible object properties', function(done) { appdir.createConfigFilesSync({ key: [], }); @@ -523,11 +658,10 @@ describe('compiler', function() { key: {}, }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/incompatible types.*key/); + expectCompileToThrow(/incompatible types.*key/, done); }); - it('refuses to merge incompatible array items', function() { + it('refuses to merge incompatible array items', function(done) { appdir.createConfigFilesSync({ key: [[]], }); @@ -535,11 +669,10 @@ describe('compiler', function() { key: [{}], }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/incompatible types.*key\[0\]/); + expectCompileToThrow(/incompatible types.*key\[0\]/, done); }); - it('merges app configs from multiple files', function() { + it('merges app configs from multiple files', function(done) { appdir.createConfigFilesSync(); appdir.writeConfigFileSync('config.local.json', { cfgLocal: 'applied' }); @@ -548,32 +681,40 @@ describe('compiler', function() { appdir.writeConfigFileSync('config.' + env + '.json', { cfgEnv: 'applied' }); - var instructions = boot.compile(appdir.PATH); - var appConfig = instructions.config; + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + var appConfig = instructions.application; - expect(appConfig).to.have.property('cfgLocal', 'applied'); - expect(appConfig).to.have.property('cfgEnv', 'applied'); + expect(appConfig).to.have.property('cfgLocal', 'applied'); + expect(appConfig).to.have.property('cfgEnv', 'applied'); - var expectedLoadOrder = ['cfgLocal', 'cfgEnv']; - var actualLoadOrder = Object.keys(appConfig).filter(function(k) { - return expectedLoadOrder.indexOf(k) !== -1; + var expectedLoadOrder = ['cfgLocal', 'cfgEnv']; + var actualLoadOrder = Object.keys(appConfig).filter(function(k) { + return expectedLoadOrder.indexOf(k) !== -1; + }); + + expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); + done(); }); - - expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder); }); - it('supports .js for custom app config files', function() { + it('supports .js for custom app config files', function(done) { appdir.createConfigFilesSync(); appdir.writeFileSync('config.local.js', 'module.exports = { fromJs: true };'); - var instructions = boot.compile(appdir.PATH); - var appConfig = instructions.config; + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + var appConfig = instructions.application; - expect(appConfig).to.have.property('fromJs', true); + expect(appConfig).to.have.property('fromJs', true); + done(); + }); }); - it('supports `appConfigRootDir` option', function() { + it('supports `appConfigRootDir` option', function(done) { appdir.createConfigFilesSync({ port: 3000 }); var customDir = path.resolve(appdir.PATH, 'custom'); @@ -582,15 +723,19 @@ describe('compiler', function() { path.resolve(appdir.PATH, 'config.json'), path.resolve(customDir, 'config.json')); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, appConfigRootDir: path.resolve(appdir.PATH, 'custom'), - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.config).to.have.property('port'); + expect(instructions.application).to.have.property('port'); + done(); + }); }); - it('supports `dsRootDir` option', function() { + it('supports `dsRootDir` option', function(done) { appdir.createConfigFilesSync(); var customDir = path.resolve(appdir.PATH, 'custom'); @@ -599,291 +744,377 @@ describe('compiler', function() { path.resolve(appdir.PATH, 'datasources.json'), path.resolve(customDir, 'datasources.json')); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, dsRootDir: path.resolve(appdir.PATH, 'custom'), - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.dataSources).to.have.property('db'); + expect(instructions.dataSources).to.have.property('db'); + done(); + }); }); - it('supports `modelsRootDir` option', function() { + it('supports `modelsRootDir` option', function(done) { appdir.createConfigFilesSync(); appdir.writeConfigFileSync('custom/model-config.json', { foo: { dataSource: 'db' }, }); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, modelsRootDir: path.resolve(appdir.PATH, 'custom'), - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.have.property('name', 'foo'); + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.have.property('name', 'foo'); + done(); + }); }); - it('includes boot/*.js scripts', function() { + it('includes boot/*.js scripts', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile(appdir.PATH); - expect(instructions.files.boot).to.eql([initJs]); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); + }); }); - it('supports `bootDirs` option', function() { + it('supports `bootDirs` option', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootDirs: [path.dirname(initJs)], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('should resolve relative path in `bootDirs`', function() { + it('should resolve relative path in `bootDirs`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootDirs: ['./custom-boot'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('should resolve non-relative path in `bootDirs`', function() { + it('should resolve non-relative path in `bootDirs`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootDirs: ['custom-boot'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('ignores index.js in `bootDirs`', function() { + it('ignores index.js in `bootDirs`', function(done) { appdir.createConfigFilesSync(); appdir.writeFileSync('custom-boot/index.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootDirs: ['./custom-boot'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.have.length(0); + done(); }); - expect(instructions.files.boot).to.have.length(0); }); - it('prefers coffeescript over json in `appRootDir/bootDir`', function() { - appdir.createConfigFilesSync(); - var coffee = appdir.writeFileSync('./custom-boot/init.coffee', ''); - appdir.writeFileSync('./custom-boot/init.json', {}); + it('prefers coffeescript over json in `appRootDir/bootDir`', + function(done) { + appdir.createConfigFilesSync(); + var coffee = appdir.writeFileSync('./custom-boot/init.coffee', ''); + appdir.writeFileSync('./custom-boot/init.json', {}); - var instructions = boot.compile({ - appRootDir: appdir.PATH, - bootDirs: ['./custom-boot'], + boot.compile({ + appRootDir: appdir.PATH, + bootDirs: ['./custom-boot'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([coffee]); + done(); + }); }); - expect(instructions.files.boot).to.eql([coffee]); - }); it('prefers coffeescript over json in `bootDir` non-relative path', - function() { - appdir.createConfigFilesSync(); - var coffee = appdir.writeFileSync('custom-boot/init.coffee', - ''); - appdir.writeFileSync('custom-boot/init.json', ''); + function(done) { + appdir.createConfigFilesSync(); + var coffee = appdir.writeFileSync('custom-boot/init.coffee', + ''); + appdir.writeFileSync('custom-boot/init.json', ''); - var instructions = boot.compile({ - appRootDir: appdir.PATH, - bootDirs: ['custom-boot'], + boot.compile({ + appRootDir: appdir.PATH, + bootDirs: ['custom-boot'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([coffee]); + done(); + }); }); - expect(instructions.files.boot).to.eql([coffee]); - }); - it('supports `bootScripts` option', function() { + it('supports `bootScripts` option', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: [initJs], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('should remove duplicate scripts', function() { + it('should remove duplicate scripts', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootDirs: [path.dirname(initJs)], bootScripts: [initJs], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('should resolve relative path in `bootScripts`', function() { + it('should resolve relative path in `bootScripts`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: ['./custom-boot/init.js'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('should resolve non-relative path in `bootScripts`', function() { + it('should resolve non-relative path in `bootScripts`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: ['custom-boot/init.js'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('resolves missing extensions in `bootScripts`', function() { + it('resolves missing extensions in `bootScripts`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('custom-boot/init.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: ['./custom-boot/init'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); it('resolves missing extensions in `bootScripts` in module relative path', - function() { - appdir.createConfigFilesSync(); - var initJs = appdir.writeFileSync('node_modules/custom-boot/init.js', ''); + function(done) { + appdir.createConfigFilesSync(); + var initJs = appdir.writeFileSync( + 'node_modules/custom-boot/init.js', ''); - var instructions = boot.compile({ - appRootDir: appdir.PATH, - bootScripts: ['custom-boot/init'], + boot.compile({ + appRootDir: appdir.PATH, + bootScripts: ['custom-boot/init'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); + }); }); - expect(instructions.files.boot).to.eql([initJs]); - }); - it('resolves module relative path for `bootScripts`', function() { + it('resolves module relative path for `bootScripts`', function(done) { appdir.createConfigFilesSync(); var initJs = appdir.writeFileSync('node_modules/custom-boot/init.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: ['custom-boot/init.js'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([initJs]); + done(); }); - expect(instructions.files.boot).to.eql([initJs]); }); - it('explores `bootScripts` in app relative path', function() { + it('explores `bootScripts` in app relative path', function(done) { appdir.createConfigFilesSync(); var appJs = appdir.writeFileSync('./custom-boot/init.js', ''); appdir.writeFileSync('node_modules/custom-boot/init.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, bootScripts: ['custom-boot/init.js'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.bootScripts).to.eql([appJs]); + done(); }); - expect(instructions.files.boot).to.eql([appJs]); }); - it('ignores models/ subdirectory', function() { + it('ignores models/ subdirectory', function(done) { appdir.createConfigFilesSync(); appdir.writeFileSync('models/my-model.js', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.files).to.not.have.property('models'); + expect(instructions.bootScripts).to.not.have.property('models'); + done(); + }); }); - it('throws when models-config.json contains 1.x `properties`', function() { - appdir.createConfigFilesSync({}, {}, { - foo: { properties: { name: 'string' }}, + it('throws when models-config.json contains 1.x `properties`', + function(done) { + appdir.createConfigFilesSync({}, {}, { + foo: { properties: { name: 'string' }}, + }); + + expectCompileToThrow(/unsupported 1\.x format/, done); }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/unsupported 1\.x format/); - }); + it('throws when model-config.json contains 1.x `options.base`', + function(done) { + appdir.createConfigFilesSync({}, {}, { + Customer: { options: { base: 'User' }}, + }); - it('throws when model-config.json contains 1.x `options.base`', function() { - appdir.createConfigFilesSync({}, {}, { - Customer: { options: { base: 'User' }}, + expectCompileToThrow(/unsupported 1\.x format/, done); }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/unsupported 1\.x format/); - }); - - it('loads models from `./models`', function() { + it('loads models from `./models`', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('models/car.json', { name: 'Car' }); appdir.writeFileSync('models/car.js', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'Car', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ name: 'Car', - }, - sourceFile: path.resolve(appdir.PATH, 'models', 'car.js'), + config: { + dataSource: 'db', + }, + definition: { + name: 'Car', + }, + sourceFile: path.resolve(appdir.PATH, 'models', 'car.js'), + }); + done(); }); }); - it('loads coffeescript models from `./models`', function() { + it('loads coffeescript models from `./models`', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('models/car.json', { name: 'Car' }); appdir.writeFileSync('models/car.coffee', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'Car', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ name: 'Car', - }, - sourceFile: path.resolve(appdir.PATH, 'models', 'car.coffee'), + config: { + dataSource: 'db', + }, + definition: { + name: 'Car', + }, + sourceFile: path.resolve(appdir.PATH, 'models', 'car.coffee'), + }); + done(); }); }); - it('supports `modelSources` option', function() { + it('supports `modelSources` option', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); appdir.writeFileSync('custom-models/car.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, modelSources: ['./custom-models'], - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'Car', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ name: 'Car', - }, - sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js'), + config: { + dataSource: 'db', + }, + definition: { + name: 'Car', + }, + sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js'), + }); + done(); }); }); - it('supports `sources` option in `model-config.json`', function() { + it('supports `sources` option in `model-config.json`', function(done) { appdir.createConfigFilesSync({}, {}, { _meta: { sources: ['./custom-models'], @@ -893,148 +1124,183 @@ describe('compiler', function() { appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); appdir.writeFileSync('custom-models/car.js', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'Car', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ name: 'Car', - }, - sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js'), + config: { + dataSource: 'db', + }, + definition: { + name: 'Car', + }, + sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js'), + }); + done(); }); }); - it('supports sources relative to node_modules', function() { + it('supports sources relative to node_modules', function(done) { appdir.createConfigFilesSync({}, {}, { User: { dataSource: 'db' }, }); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, modelSources: [ 'loopback/common/models', 'loopback/common/dir-does-not-exist', ], - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0]).to.eql({ - name: 'User', - config: { - dataSource: 'db', - }, - definition: require('loopback/common/models/user.json'), - sourceFile: require.resolve('loopback/common/models/user.js'), + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ + name: 'User', + config: { + dataSource: 'db', + }, + definition: require('loopback/common/models/user.json'), + sourceFile: require.resolve('loopback/common/models/user.js'), + }); + done(); }); }); - it('resolves relative path in `modelSources` option', function() { + it('resolves relative path in `modelSources` option', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); var appJS = appdir.writeFileSync('custom-models/car.js', ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, modelSources: ['./custom-models'], - }); + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0].sourceFile).to.equal(appJS); + expect(instructions.models).to.have.length(1); + expect(instructions.models[0].sourceFile).to.equal(appJS); + done(); + }); }); - it('resolves module relative path in `modelSources` option', function() { - appdir.createConfigFilesSync({}, {}, { - Car: { dataSource: 'db' }, - }); - appdir.writeConfigFileSync('node_modules/custom-models/car.json', - { name: 'Car' }); - var appJS = appdir.writeFileSync('node_modules/custom-models/car.js', ''); + it('resolves module relative path in `modelSources` option', + function(done) { + appdir.createConfigFilesSync({}, {}, { + Car: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('node_modules/custom-models/car.json', + { name: 'Car' }); + var appJS = appdir.writeFileSync( + 'node_modules/custom-models/car.js', ''); - var instructions = boot.compile({ - appRootDir: appdir.PATH, - modelSources: ['custom-models'], - }); + boot.compile({ + appRootDir: appdir.PATH, + modelSources: ['custom-models'], + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.have.length(1); - expect(instructions.models[0].sourceFile).to.equal(appJS); - }); + expect(instructions.models).to.have.length(1); + expect(instructions.models[0].sourceFile).to.equal(appJS); + done(); + }); + }); it('resolves relative path in `sources` option in `model-config.json`', - function() { - appdir.createConfigFilesSync({}, {}, { - _meta: { - sources: ['./custom-models'], - }, - Car: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + _meta: { + sources: ['./custom-models'], + }, + Car: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); + var appJS = appdir.writeFileSync('custom-models/car.js', ''); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.models).to.have.length(1); + expect(instructions.models[0].sourceFile).to.equal(appJS); + done(); + }); }); - appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); - var appJS = appdir.writeFileSync('custom-models/car.js', ''); - - var instructions = boot.compile(appdir.PATH); - - expect(instructions.models).to.have.length(1); - expect(instructions.models[0].sourceFile).to.equal(appJS); - }); it('resolves module relative path in `sources` option in model-config.json', - function() { - appdir.createConfigFilesSync({}, {}, { - _meta: { - sources: ['custom-models'], - }, - Car: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + _meta: { + sources: ['custom-models'], + }, + Car: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('node_modules/custom-models/car.json', + { name: 'Car' }); + + var appJS = appdir.writeFileSync( + 'node_modules/custom-models/car.js', ''); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.models).to.have.length(1); + expect(instructions.models[0].sourceFile).to.equal(appJS); + done(); + }); }); - appdir.writeConfigFileSync('node_modules/custom-models/car.json', - { name: 'Car' }); - var appJS = appdir.writeFileSync('node_modules/custom-models/car.js', ''); - - var instructions = boot.compile(appdir.PATH); - - expect(instructions.models).to.have.length(1); - expect(instructions.models[0].sourceFile).to.equal(appJS); - }); - - it('handles model definitions with no code', function() { + it('handles model definitions with no code', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('models/car.json', { name: 'Car' }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.eql([{ - name: 'Car', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.eql([{ name: 'Car', - }, - sourceFile: undefined, - }]); + config: { + dataSource: 'db', + }, + definition: { + name: 'Car', + }, + sourceFile: undefined, + }]); + done(); + }); }); - it('excludes models not listed in `model-config.json`', function() { + it('excludes models not listed in `model-config.json`', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('models/car.json', { name: 'Car' }); appdir.writeConfigFileSync('models/bar.json', { name: 'Bar' }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var models = instructions.models.map(getNameProperty); - expect(models).to.eql(['Car']); + var models = instructions.models.map(getNameProperty); + expect(models).to.eql(['Car']); + done(); + }); }); - it('includes models used as Base models', function() { + it('includes models used as Base models', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); @@ -1046,15 +1312,19 @@ describe('compiler', function() { name: 'Vehicle', }); - var instructions = boot.compile(appdir.PATH); - var models = instructions.models; - var modelNames = models.map(getNameProperty); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + var models = instructions.models; + var modelNames = models.map(getNameProperty); - expect(modelNames).to.eql(['Vehicle', 'Car']); - expect(models[0].config).to.equal(undefined); + expect(modelNames).to.eql(['Vehicle', 'Car']); + expect(models[0].config).to.equal(undefined); + done(); + }); }); - it('excludes pre-built base models', function() { + it('excludes pre-built base models', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); @@ -1063,13 +1333,17 @@ describe('compiler', function() { base: 'Model', }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['Car']); + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['Car']); + done(); + }); }); - it('sorts models, base models first', function() { + it('sorts models, base models first', function(done) { appdir.createConfigFilesSync({}, {}, { Vehicle: { dataSource: 'db' }, FlyingCar: { dataSource: 'db' }, @@ -1087,13 +1361,17 @@ describe('compiler', function() { base: 'Car', }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['Vehicle', 'Car', 'FlyingCar']); + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['Vehicle', 'Car', 'FlyingCar']); + done(); + }); }); - it('detects circular Model dependencies', function() { + it('detects circular Model dependencies', function(done) { appdir.createConfigFilesSync({}, {}, { Vehicle: { dataSource: 'db' }, Car: { dataSource: 'db' }, @@ -1107,75 +1385,94 @@ describe('compiler', function() { base: 'Car', }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/cyclic dependency/i); + expectCompileToThrow(/cyclic dependency/i, done); }); - it('uses file name as default value for model name', function() { + it('uses file name as default value for model name', function(done) { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' }, }); appdir.writeConfigFileSync('models/car.json', {}); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['Car']); + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['Car']); + done(); + }); }); it('uses `OrderItem` as default model name for file with name `order-item`', - function() { - appdir.createConfigFilesSync({}, {}, { - OrderItem: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + OrderItem: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('models/order-item.json', {}); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['OrderItem']); + done(); + }); }); - appdir.writeConfigFileSync('models/order-item.json', {}); - - var instructions = boot.compile(appdir.PATH); - - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['OrderItem']); - }); it('uses `OrderItem` as default model name for file with name `order_item`', - function() { - appdir.createConfigFilesSync({}, {}, { - OrderItem: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + OrderItem: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('models/order_item.json', {}); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['OrderItem']); + done(); + }); }); - appdir.writeConfigFileSync('models/order_item.json', {}); - - var instructions = boot.compile(appdir.PATH); - - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['OrderItem']); - }); it('uses `OrderItem` as default model name for file with name `order item`', - function() { - appdir.createConfigFilesSync({}, {}, { - OrderItem: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + OrderItem: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('models/order item.json', {}); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['OrderItem']); + done(); + }); }); - appdir.writeConfigFileSync('models/order item.json', {}); - - var instructions = boot.compile(appdir.PATH); - - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['OrderItem']); - }); it('overrides `default model name` by `name` in model definition', - function() { - appdir.createConfigFilesSync({}, {}, { - overrideCar: { dataSource: 'db' }, + function(done) { + appdir.createConfigFilesSync({}, {}, { + overrideCar: { dataSource: 'db' }, + }); + appdir.writeConfigFileSync('models/car.json', { name: 'overrideCar' }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['overrideCar']); + done(); + }); }); - appdir.writeConfigFileSync('models/car.json', { name: 'overrideCar' }); - var instructions = boot.compile(appdir.PATH); - - var modelNames = instructions.models.map(getNameProperty); - expect(modelNames).to.eql(['overrideCar']); - }); - - it('overwrites model with same default name', function() { + it('overwrites model with same default name', function(done) { appdir.createConfigFilesSync({}, {}, { 'OrderItem': { dataSource: 'db' }, }); @@ -1194,24 +1491,28 @@ describe('compiler', function() { }); var appJS = appdir.writeFileSync('models/orderItem.js', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.eql([{ - name: 'OrderItem', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.eql([{ name: 'OrderItem', - properties: { - quantity: { type: 'number' }, + config: { + dataSource: 'db', }, - }, - sourceFile: appJS, - }]); + definition: { + name: 'OrderItem', + properties: { + quantity: { type: 'number' }, + }, + }, + sourceFile: appJS, + }]); + done(); + }); }); - it('overwrites model with same name in model definition', function() { + it('overwrites model with same name in model definition', function(done) { appdir.createConfigFilesSync({}, {}, { 'customOrder': { dataSource: 'db' }, }); @@ -1232,60 +1533,76 @@ describe('compiler', function() { }); var appJS = appdir.writeFileSync('models/order2.js', ''); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.models).to.eql([{ - name: 'customOrder', - config: { - dataSource: 'db', - }, - definition: { + expect(instructions.models).to.eql([{ name: 'customOrder', - properties: { - quantity: { type: 'number' }, + config: { + dataSource: 'db', }, - }, - sourceFile: appJS, - }]); + definition: { + name: 'customOrder', + properties: { + quantity: { type: 'number' }, + }, + }, + sourceFile: appJS, + }]); + done(); + }); }); - it('returns a new copy of JSON data', function() { + it('returns a new copy of JSON data', function(done) { appdir.createConfigFilesSync(); - var instructions = boot.compile(appdir.PATH); - instructions.config.modified = true; + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + instructions.application.modified = true; - instructions = boot.compile(appdir.PATH); - expect(instructions.config).to.not.have.property('modified'); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.application).to.not.have.property('modified'); + done(); + }); + }); }); describe('for mixins', function() { - describe(' - mixinDirs', function() { - function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs) { + describe(' - mixinDirs', function(done) { + function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs, done) { var appJS = appdir.writeFileSync(sourceFile, ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, mixinDirs: mixinDirs, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); }); - - expect(instructions.mixins[0].sourceFile).to.eql(appJS); } - it('supports `mixinDirs` option', function() { + it('supports `mixinDirs` option', function(done) { verifyMixinIsFoundViaMixinDirs('custom-mixins/other.js', - ['./custom-mixins']); + ['./custom-mixins'], done); }); - it('resolves relative path in `mixinDirs` option', function() { + it('resolves relative path in `mixinDirs` option', function(done) { verifyMixinIsFoundViaMixinDirs('custom-mixins/other.js', - ['./custom-mixins']); + ['./custom-mixins'], done); }); - it('resolves module relative path in `mixinDirs` option', function() { - verifyMixinIsFoundViaMixinDirs('node_modules/custom-mixins/other.js', - ['custom-mixins']); - }); + it('resolves module relative path in `mixinDirs` option', + function(done) { + verifyMixinIsFoundViaMixinDirs( + 'node_modules/custom-mixins/other.js', + ['custom-mixins'], done); + }); }); describe(' - mixinSources', function() { @@ -1299,35 +1616,39 @@ describe('compiler', function() { }); }); - function verifyMixinIsFoundViaMixinSources(sourceFile, mixinSources) { + function verifyMixinIsFoundViaMixinSources(sourceFile, mixinSources, + done) { var appJS = appdir.writeFileSync(sourceFile, ''); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, mixinSources: mixinSources, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); }); - - expect(instructions.mixins[0].sourceFile).to.eql(appJS); } - it('supports `mixinSources` option', function() { + it('supports `mixinSources` option', function(done) { verifyMixinIsFoundViaMixinSources('mixins/time-stamps.js', - ['./mixins']); + ['./mixins'], done); }); - it('resolves relative path in `mixinSources` option', function() { + it('resolves relative path in `mixinSources` option', function(done) { verifyMixinIsFoundViaMixinSources('custom-mixins/time-stamps.js', - ['./custom-mixins']); + ['./custom-mixins'], done); }); it('resolves module relative path in `mixinSources` option', - function() { - verifyMixinIsFoundViaMixinSources( - 'node_modules/custom-mixins/time-stamps.js', - ['custom-mixins']); - }); + function(done) { + verifyMixinIsFoundViaMixinSources( + 'node_modules/custom-mixins/time-stamps.js', + ['custom-mixins'], done); + }); - it('supports `mixins` option in `model-config.json`', function() { + it('supports `mixins` option in `model-config.json`', function(done) { appdir.createConfigFilesSync({}, {}, { _meta: { mixins: ['./custom-mixins'], @@ -1338,85 +1659,117 @@ describe('compiler', function() { }); var appJS = appdir.writeFileSync('custom-mixins/time-stamps.js', ''); - var instructions = boot.compile(appdir.PATH); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); }); - it('sets by default `mixinSources` to `mixins` directory', function() { - var appJS = appdir.writeFileSync('mixins/time-stamps.js', ''); - var instructions = boot.compile(appdir.PATH); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); - }); + it('sets by default `mixinSources` to `mixins` directory', + function(done) { + var appJS = appdir.writeFileSync('mixins/time-stamps.js', ''); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); + }); - it('loads only mixins used by models', function() { + it('loads only mixins used by models', function(done) { var appJS = appdir.writeFileSync('mixins/time-stamps.js', ''); appdir.writeFileSync('mixins/foo.js', ''); - var instructions = boot.compile(appdir.PATH); - expect(instructions.mixins).to.have.length(1); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins).to.have.length(1); + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); }); - it('loads mixins from model using mixin name in JSON file', function() { - var appJS = appdir.writeFileSync('mixins/time-stamps.js', ''); - appdir.writeConfigFileSync('mixins/time-stamps.json', { - name: 'Timestamping', - }); + it('loads mixins from model using mixin name in JSON file', + function(done) { + var appJS = appdir.writeFileSync('mixins/time-stamps.js', ''); + appdir.writeConfigFileSync('mixins/time-stamps.json', { + name: 'Timestamping', + }); - appdir.writeConfigFileSync('models/car.json', { - name: 'Car', - mixins: { 'Timestamping': {}}, - }); + appdir.writeConfigFileSync('models/car.json', { + name: 'Car', + mixins: { 'Timestamping': {}}, + }); - var instructions = boot.compile(appdir.PATH); - expect(instructions.mixins).to.have.length(1); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); - }); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins).to.have.length(1); + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); + }); it('loads mixin only once for dirs common to mixinDirs & mixinSources', - function() { - var appJS = appdir.writeFileSync('custom-mixins/time-stamps.js', ''); + function(done) { + var appJS = appdir.writeFileSync( + 'custom-mixins/time-stamps.js', ''); - var options = { - appRootDir: appdir.PATH, - mixinDirs: ['./custom-mixins'], - mixinSources: ['./custom-mixins'], - }; + var options = { + appRootDir: appdir.PATH, + mixinDirs: ['./custom-mixins'], + mixinSources: ['./custom-mixins'], + }; - var instructions = boot.compile(options); - expect(instructions.mixins).to.have.length(1); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); - }); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins).to.have.length(1); + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); + }); it('loads mixin from mixinSources, when it is also found in mixinDirs', - function() { - appdir.writeFileSync('mixinDir/time-stamps.js', ''); - var appJS = appdir.writeFileSync('mixinSource/time-stamps.js', ''); + function(done) { + appdir.writeFileSync('mixinDir/time-stamps.js', ''); + var appJS = appdir.writeFileSync('mixinSource/time-stamps.js', ''); - var options = { - appRootDir: appdir.PATH, - mixinDirs: ['./mixinDir'], - mixinSources: ['./mixinSource'], - }; + var options = { + appRootDir: appdir.PATH, + mixinDirs: ['./mixinDir'], + mixinSources: ['./mixinSource'], + }; - var instructions = boot.compile(options); - expect(instructions.mixins).to.have.length(1); - expect(instructions.mixins[0].sourceFile).to.eql(appJS); - }); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins).to.have.length(1); + expect(instructions.mixins[0].sourceFile).to.eql(appJS); + done(); + }); + }); - it('loads mixin from the most recent mixin definition', function() { - appdir.writeFileSync('mixins1/time-stamps.js', ''); - var mixins2 = appdir.writeFileSync('mixins2/time-stamps.js', ''); + it('loads mixin from the most recent mixin definition', + function(done) { + appdir.writeFileSync('mixins1/time-stamps.js', ''); + var mixins2 = appdir.writeFileSync('mixins2/time-stamps.js', ''); - var options = { - appRootDir: appdir.PATH, - mixinSources: ['./mixins1', './mixins2'], - }; + var options = { + appRootDir: appdir.PATH, + mixinSources: ['./mixins1', './mixins2'], + }; - var instructions = boot.compile(options); - expect(instructions.mixins).to.have.length(1); - expect(instructions.mixins[0].sourceFile).to.eql(mixins2); - }); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.mixins).to.have.length(1); + expect(instructions.mixins[0].sourceFile).to.eql(mixins2); + done(); + }); + }); }); describe('name normalization', function() { @@ -1431,129 +1784,166 @@ describe('compiler', function() { appdir.writeFileSync('custom-mixins/space name.js', ''); }); - it('supports classify', function() { + it('supports classify', function(done) { options.normalization = 'classify'; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps', - ]); + expect(mixinNames).to.eql([ + 'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps', + ]); + done(); + }); }); - it('supports dasherize', function() { + it('supports dasherize', function(done) { options.normalization = 'dasherize'; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'camel-case', 'foo', 'pascal-case', 'space-name', 'time-stamps', - ]); + expect(mixinNames).to.eql([ + 'camel-case', 'foo', 'pascal-case', 'space-name', 'time-stamps', + ]); + done(); + }); }); - it('supports custom function', function() { - var normalize = function(name) { return name.toUpperCase(); }; + it('supports custom function', function(done) { + var normalize = function(name) { + return name.toUpperCase(); + }; options.normalization = normalize; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'CAMELCASE', 'FOO', 'PASCALCASE', 'SPACE NAME', 'TIME-STAMPS', - ]); + expect(mixinNames).to.eql([ + 'CAMELCASE', 'FOO', 'PASCALCASE', 'SPACE NAME', 'TIME-STAMPS', + ]); + done(); + }); }); - it('supports none', function() { + it('supports none', function(done) { options.normalization = 'none'; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps', - ]); + expect(mixinNames).to.eql([ + 'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps', + ]); + done(); + }); }); - it('supports false', function() { + it('supports false', function(done) { options.normalization = false; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps', - ]); + expect(mixinNames).to.eql([ + 'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps', + ]); + done(); + }); }); - it('defaults to classify', function() { - var instructions = boot.compile(options); + it('defaults to classify', function(done) { + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var mixins = instructions.mixins; - var mixinNames = mixins.map(getNameProperty); + var mixins = instructions.mixins; + var mixinNames = mixins.map(getNameProperty); - expect(mixinNames).to.eql([ - 'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps', - ]); + expect(mixinNames).to.eql([ + 'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps', + ]); + done(); + }); }); - it('throws error for invalid normalization format', function() { + it('throws error for invalid normalization format', function(done) { options.normalization = 'invalidFormat'; - expect(function() { boot.compile(options); }) - .to.throw(/Invalid normalization format - "invalidFormat"/); + expectCompileToThrow(/Invalid normalization format - "invalidFormat"/, + options, done); }); }); - it('overrides default mixin name, by `name` in JSON', function() { + it('overrides default mixin name, by `name` in JSON', function(done) { appdir.writeFileSync('mixins/foo.js', ''); appdir.writeConfigFileSync('mixins/foo.json', { name: 'fooBar' }); - var options = { appRootDir: appdir.PATH, + var options = { + appRootDir: appdir.PATH, mixinDirs: ['./mixins'], }; - var instructions = boot.compile(options); + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.mixins[0].name).to.eql('fooBar'); + expect(instructions.mixins[0].name).to.eql('fooBar'); + done(); + }); }); - it('extends definition from JSON with same file name', function() { + it('extends definition from JSON with same file name', function(done) { var appJS = appdir.writeFileSync('custom-mixins/foo-bar.js', ''); appdir.writeConfigFileSync('custom-mixins/foo-bar.json', { - description: 'JSON file name same as JS file name' }); + description: 'JSON file name same as JS file name', + }); appdir.writeConfigFileSync('custom-mixins/FooBar.json', { - description: 'JSON file name same as normalized name of mixin' }); + description: 'JSON file name same as normalized name of mixin', + }); - var options = { appRootDir: appdir.PATH, + var options = { + appRootDir: appdir.PATH, mixinDirs: ['./custom-mixins'], - normalization: 'classify' }; - var instructions = boot.compile(options); + normalization: 'classify', + }; + boot.compile(options, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.mixins).to.eql([ - { - name: 'FooBar', - description: 'JSON file name same as JS file name', - sourceFile: appJS, - }, - ]); + expect(instructions.mixins).to.eql([ + { + name: 'FooBar', + description: 'JSON file name same as JS file name', + sourceFile: appJS, + }, + ]); + }); + done(); }); }); }); describe('for middleware', function() { - function testMiddlewareRegistration(middlewareId, sourceFile) { + function testMiddlewareRegistration(middlewareId, sourceFile, done) { var json = { - initial: { - }, - custom: { - }, + initial: {}, + custom: {}, }; json.custom[middlewareId] = { @@ -1562,19 +1952,23 @@ describe('compiler', function() { appdir.writeConfigFileSync('middleware.json', json); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware).to.eql({ - phases: ['initial', 'custom'], - middleware: [ - { - sourceFile: sourceFile, - config: { - phase: 'custom', - params: 'some-config-data', + expect(instructions.middleware).to.eql({ + phases: ['initial', 'custom'], + middleware: [ + { + sourceFile: sourceFile, + config: { + phase: 'custom', + params: 'some-config-data', + }, }, - }, - ], + ], + }); + done(); }); } @@ -1585,17 +1979,17 @@ describe('compiler', function() { 'loopback/server/middleware/url-not-found'); }); - it('emits middleware instructions', function() { + it('emits middleware instructions', function(done) { testMiddlewareRegistration('loopback/server/middleware/url-not-found', - sourceFileForUrlNotFound); + sourceFileForUrlNotFound, done); }); - it('emits middleware instructions for fragment', function() { + it('emits middleware instructions for fragment', function(done) { testMiddlewareRegistration('loopback#url-not-found', - sourceFileForUrlNotFound); + sourceFileForUrlNotFound, done); }); - it('supports `middlewareRootDir` option', function() { + it('supports `middlewareRootDir` option', function(done) { var middlewareJson = { initial: {}, custom: { @@ -1608,67 +2002,63 @@ describe('compiler', function() { fs.mkdirsSync(customDir); fs.writeJsonSync(path.resolve(customDir, 'middleware.json'), middlewareJson); - - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, - middlewareRootDir: path.resolve(appdir.PATH, 'custom'), - }); - - expect(instructions.middleware).to.eql({ - phases: ['initial', 'custom'], - middleware: [ - { - sourceFile: sourceFileForUrlNotFound, - config: { - phase: 'custom', - params: 'some-config-data', + middlewareRootDir: customDir, + }, function(err, context) { + var instructions = context.instructions; + expect(instructions.middleware).to.eql({ + phases: ['initial', 'custom'], + middleware: [ + { + sourceFile: sourceFileForUrlNotFound, + config: { + phase: 'custom', + params: 'some-config-data', + }, }, - }, - ], + ], + }); + done(); }); }); - it('fails when a module middleware cannot be resolved', function() { + it('fails when a module middleware cannot be resolved', function(done) { appdir.writeConfigFileSync('middleware.json', { final: { - 'loopback/path-does-not-exist': { }, + 'loopback/path-does-not-exist': {}, }, }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/path-does-not-exist/); + expectCompileToThrow(/path-does-not-exist/, done); }); it('does not fail when an optional middleware cannot be resolved', - function() { - appdir.writeConfigFileSync('middleware.json', { - final: { - 'loopback/path-does-not-exist': { - optional: 'this middleware is optional', - }, - }, - }); - - expect(function() { boot.compile(appdir.PATH); }) - .to.not.throw(); - }); - - it('fails when a module middleware fragment cannot be resolved', - function() { + function(done) { appdir.writeConfigFileSync('middleware.json', { final: { - 'loopback#path-does-not-exist': { }, + 'loopback/path-does-not-exist': { + optional: 'this middleware is optional', + }, }, }); - expect(function() { - boot.compile(appdir.PATH); - }) - .to.throw(/path-does-not-exist/); + expectCompileToNotThrow(done); + }); + + it('fails when a module middleware fragment cannot be resolved', + function(done) { + appdir.writeConfigFileSync('middleware.json', { + final: { + 'loopback#path-does-not-exist': {}, + }, + }); + + expectCompileToThrow(/path-does-not-exist/, done); }); it('does not fail when an optional middleware fragment cannot be resolved', - function() { + function(done) { appdir.writeConfigFileSync('middleware.json', { final: { 'loopback#path-does-not-exist': { @@ -1677,31 +2067,34 @@ describe('compiler', function() { }, }); - expect(function() { boot.compile(appdir.PATH); }) - .to.not.throw(); + expectCompileToNotThrow(done); }); - it('resolves paths relatively to appRootDir', function() { + it('resolves paths relatively to appRootDir', function(done) { appdir.writeFileSync('my-middleware.js', ''); appdir.writeConfigFileSync('./middleware.json', { routes: { // resolves to ./my-middleware.js - './my-middleware': { }, + './my-middleware': {}, }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware).to.eql({ - phases: ['routes'], - middleware: [{ - sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), - config: { phase: 'routes' }, - }], + expect(instructions.middleware).to.eql({ + phases: ['routes'], + middleware: [{ + sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), + config: { phase: 'routes' }, + }], + }); + done(); }); }); - it('merges config.params', function() { + it('merges config.params', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': { @@ -1722,14 +2115,18 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expectFirstMiddlewareParams(instructions).to.eql({ - key: 'custom value', + expectFirstMiddlewareParams(instructions).to.eql({ + key: 'custom value', + }); + done(); }); }); - it('merges config.enabled', function() { + it('merges config.enabled', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': { @@ -1748,39 +2145,47 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0].config) - .to.have.property('enabled', false); + expect(instructions.middleware.middleware[0].config) + .to.have.property('enabled', false); + done(); + }); }); - function verifyMiddlewareConfig() { - var instructions = boot.compile(appdir.PATH); + function verifyMiddlewareConfig(done) { + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware) - .to.eql([ - { - sourceFile: path.resolve(appdir.PATH, 'middleware'), - config: { - phase: 'routes', - params: { - key: 'initial value', + expect(instructions.middleware.middleware) + .to.eql([ + { + sourceFile: path.resolve(appdir.PATH, 'middleware'), + config: { + phase: 'routes', + params: { + key: 'initial value', + }, }, }, - }, - { - sourceFile: path.resolve(appdir.PATH, 'middleware'), - config: { - phase: 'routes', - params: { - key: 'custom value', + { + sourceFile: path.resolve(appdir.PATH, 'middleware'), + config: { + phase: 'routes', + params: { + key: 'custom value', + }, }, }, - }, - ]); + ]); + done(); + }); } - it('merges config.params array to array', function() { + it('merges config.params array to array', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': [{ @@ -1801,10 +2206,10 @@ describe('compiler', function() { }, }); - verifyMiddlewareConfig(); + verifyMiddlewareConfig(done); }); - it('merges config.params array to object', function() { + it('merges config.params array to object', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': { @@ -1825,10 +2230,10 @@ describe('compiler', function() { }, }); - verifyMiddlewareConfig(); + verifyMiddlewareConfig(done); }); - it('merges config.params object to array', function() { + it('merges config.params object to array', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': [{ @@ -1849,10 +2254,10 @@ describe('compiler', function() { }, }); - verifyMiddlewareConfig(); + verifyMiddlewareConfig(done); }); - it('merges config.params array to empty object', function() { + it('merges config.params array to empty object', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': {}, @@ -1869,23 +2274,27 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware) - .to.eql([ - { - sourceFile: path.resolve(appdir.PATH, 'middleware'), - config: { - phase: 'routes', - params: { - key: 'custom value', + expect(instructions.middleware.middleware) + .to.eql([ + { + sourceFile: path.resolve(appdir.PATH, 'middleware'), + config: { + phase: 'routes', + params: { + key: 'custom value', + }, }, }, - }, - ]); + ]); + }); + done(); }); - it('merges config.params array to array by name', function() { + it('merges config.params array to array by name', function(done) { appdir.writeConfigFileSync('./middleware.json', { routes: { './middleware': [{ @@ -1912,62 +2321,67 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware) - .to.eql([ - { - sourceFile: path.resolve(appdir.PATH, 'middleware'), - config: { - name: 'a', - phase: 'routes', - params: { - key: 'custom value', + expect(instructions.middleware.middleware) + .to.eql([ + { + sourceFile: path.resolve(appdir.PATH, 'middleware'), + config: { + name: 'a', + phase: 'routes', + params: { + key: 'custom value', + }, }, }, - }, - { - sourceFile: path.resolve(appdir.PATH, 'middleware'), - config: { - phase: 'routes', - params: { - key: '2nd value', + { + sourceFile: path.resolve(appdir.PATH, 'middleware'), + config: { + phase: 'routes', + params: { + key: '2nd value', + }, }, }, - }, - ]); + ]); + done(); + }); }); - it('flattens sub-phases', function() { + it('flattens sub-phases', function(done) { appdir.writeConfigFileSync('middleware.json', { - 'initial:after': { - }, + 'initial:after': {}, 'custom:before': { 'loopback/server/middleware/url-not-found': { params: 'some-config-data', }, }, - 'custom:after': { - - }, + 'custom:after': {}, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.phases, 'phases') - .to.eql(['initial', 'custom']); - expect(instructions.middleware.middleware, 'middleware') - .to.eql([{ - sourceFile: - require.resolve('loopback/server/middleware/url-not-found'), - config: { - phase: 'custom:before', - params: 'some-config-data', - }, - }]); + expect(instructions.middleware.phases, 'phases') + .to.eql(['initial', 'custom']); + expect(instructions.middleware.middleware, 'middleware') + .to.eql([{ + sourceFile: require.resolve( + 'loopback/server/middleware/url-not-found'), + config: { + phase: 'custom:before', + params: 'some-config-data', + }, + }]); + done(); + }); }); - it('supports multiple instances of the same middleware', function() { + it('supports multiple instances of the same middleware', function(done) { appdir.writeFileSync('my-middleware.js', ''); appdir.writeConfigFileSync('middleware.json', { 'final': { @@ -1982,74 +2396,89 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware) - .to.eql([ - { - sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), - config: { - phase: 'final', - params: 'first', + expect(instructions.middleware.middleware) + .to.eql([ + { + sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), + config: { + phase: 'final', + params: 'first', + }, }, - }, - { - sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), - config: { - phase: 'final', - params: 'second', + { + sourceFile: path.resolve(appdir.PATH, 'my-middleware.js'), + config: { + phase: 'final', + params: 'second', + }, }, - }, - ]); + ]); + done(); + }); }); - it('supports shorthand notation for middleware paths', function() { + it('supports shorthand notation for middleware paths', function(done) { appdir.writeConfigFileSync('middleware.json', { 'final': { 'loopback#url-not-found': {}, }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0].sourceFile) - .to.equal(require.resolve('loopback/server/middleware/url-not-found')); + expect(instructions.middleware.middleware[0].sourceFile).to.equal( + require.resolve('loopback/server/middleware/url-not-found')); + done(); + }); }); - it('supports shorthand notation for relative paths', function() { + it('supports shorthand notation for relative paths', function(done) { appdir.writeConfigFileSync('middleware.json', { 'routes': { - './middleware/index#myMiddleware': { - }, + './middleware/index#myMiddleware': {}, }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0].sourceFile) - .to.equal(path.resolve(appdir.PATH, + expect(instructions.middleware.middleware[0].sourceFile) + .to.equal(path.resolve(appdir.PATH, './middleware/index.js')); - expect(instructions.middleware.middleware[0]).have.property( - 'fragment', - 'myMiddleware'); + expect(instructions.middleware.middleware[0]).have.property( + 'fragment', + 'myMiddleware'); + done(); + }); }); it('supports shorthand notation when the fragment name matches a property', - function() { + function(done) { appdir.writeConfigFileSync('middleware.json', { 'final': { 'loopback#errorHandler': {}, }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', - pathWithoutIndex(require.resolve('loopback'))); - expect(instructions.middleware.middleware[0]).have.property( - 'fragment', - 'errorHandler'); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', + pathWithoutIndex(require.resolve('loopback'))); + expect(instructions.middleware.middleware[0]).have.property( + 'fragment', + 'errorHandler'); + done(); + }); }); it('resolves modules relative to appRootDir', function() { @@ -2064,14 +2493,18 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', - pathWithoutIndex(appdir.resolve(HANDLER_FILE))); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', + pathWithoutIndex(appdir.resolve(HANDLER_FILE))); + done(); + }); }); - it('prefers appRootDir over node_modules for middleware', function() { + it('prefers appRootDir over node_modules for middleware', function(done) { var appJS = appdir.writeFileSync('./my-middleware.js', ''); appdir.writeFileSync('node_modules/my-middleware.js', ''); appdir.writeConfigFileSync('middleware.json', { @@ -2080,31 +2513,40 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware).to.have.length(1); - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', appJS); + expect(instructions.middleware.middleware).to.have.length(1); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', appJS); + done(); + }); }); it('does not treat module relative path as `appRootDir` relative', - function() { - appdir.writeFileSync('./my-middleware.js', ''); - var moduleJS = appdir.writeFileSync('node_modules/my-middleware.js', ''); - appdir.writeConfigFileSync('middleware.json', { - 'routes': { - 'my-middleware': {}, - }, + function(done) { + appdir.writeFileSync('./my-middleware.js', ''); + var moduleJS = appdir.writeFileSync( + 'node_modules/my-middleware.js', ''); + appdir.writeConfigFileSync('middleware.json', { + 'routes': { + 'my-middleware': {}, + }, + }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.middleware.middleware).to.have.length(1); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', moduleJS); + done(); + }); }); - var instructions = boot.compile(appdir.PATH); - - expect(instructions.middleware.middleware).to.have.length(1); - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', moduleJS); - }); - - it('loads middleware from coffeescript in appRootdir', function() { + it('loads middleware from coffeescript in appRootdir', function(done) { var coffee = appdir.writeFileSync('my-middleware.coffee', ''); appdir.writeConfigFileSync('middleware.json', { 'routes': { @@ -2112,65 +2554,82 @@ describe('compiler', function() { }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', coffee); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', coffee); + done(); + }); }); it('loads coffeescript from middleware under node_modules', - function() { - var file = appdir.writeFileSync('node_modules/my-middleware/index.coffee', - ''); - appdir.writeFileSync('node_modules/my-middleware/index.json', ''); - appdir.writeConfigFileSync('middleware.json', { - 'routes': { - 'my-middleware': {}, - }, + function(done) { + var file = appdir.writeFileSync( + 'node_modules/my-middleware/index.coffee', + ''); + appdir.writeFileSync('node_modules/my-middleware/index.json', ''); + appdir.writeConfigFileSync('middleware.json', { + 'routes': { + 'my-middleware': {}, + }, + }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.middleware.middleware).to.have.length(1); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', pathWithoutIndex(file)); + done(); + }); }); - var instructions = boot.compile(appdir.PATH); - - expect(instructions.middleware.middleware).to.have.length(1); - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', pathWithoutIndex(file)); - }); - it('prefers coffeescript over json for relative middleware path', - function() { - var coffee = appdir.writeFileSync('my-middleware.coffee', ''); - appdir.writeFileSync('my-middleware.json', ''); - appdir.writeConfigFileSync('middleware.json', { - 'routes': { - './my-middleware': {}, - }, + function(done) { + var coffee = appdir.writeFileSync('my-middleware.coffee', ''); + appdir.writeFileSync('my-middleware.json', ''); + appdir.writeConfigFileSync('middleware.json', { + 'routes': { + './my-middleware': {}, + }, + }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.middleware.middleware).to.have.length(1); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', coffee); + done(); + }); }); - var instructions = boot.compile(appdir.PATH); - - expect(instructions.middleware.middleware).to.have.length(1); - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', coffee); - }); - it('prefers coffeescript over json for module relative middleware path', - function() { - var coffee = appdir.writeFileSync('node_modules/my-middleware.coffee', - ''); - appdir.writeFileSync('node_modules/my-middleware.json', ''); - appdir.writeConfigFileSync('middleware.json', { - 'routes': { - 'my-middleware': {}, - }, + function(done) { + var coffee = appdir.writeFileSync('node_modules/my-middleware.coffee', + ''); + appdir.writeFileSync('node_modules/my-middleware.json', ''); + appdir.writeConfigFileSync('middleware.json', { + 'routes': { + 'my-middleware': {}, + }, + }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.middleware.middleware).to.have.length(1); + expect(instructions.middleware.middleware[0]).have.property( + 'sourceFile', coffee); + done(); + }); }); - var instructions = boot.compile(appdir.PATH); - - expect(instructions.middleware.middleware).to.have.length(1); - expect(instructions.middleware.middleware[0]).have.property( - 'sourceFile', coffee); - }); - describe('config with relative paths in params', function() { var RELATIVE_PATH_PARAMS = [ '$!./here', @@ -2184,67 +2643,92 @@ describe('compiler', function() { }); }); - it('converts paths in top-level array items', function() { + it('converts paths in top-level array items', function(done) { givenMiddlewareEntrySync({ params: RELATIVE_PATH_PARAMS }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expectFirstMiddlewareParams(instructions) - .to.eql(absolutePathParams); + expectFirstMiddlewareParams(instructions) + .to.eql(absolutePathParams); + done(); + }); }); - it('converts paths in top-level object properties', function() { - givenMiddlewareEntrySync({ params: { - path: RELATIVE_PATH_PARAMS[0], - }}); - - var instructions = boot.compile(appdir.PATH); - - expectFirstMiddlewareParams(instructions) - .to.eql({ path: absolutePathParams[0] }); - }); - - it('converts path value when params is a string', function() { - givenMiddlewareEntrySync({ params: RELATIVE_PATH_PARAMS[0] }); - - var instructions = boot.compile(appdir.PATH); - - expectFirstMiddlewareParams(instructions) - .to.eql(absolutePathParams[0]); - }); - - it('converts paths in nested properties', function() { - givenMiddlewareEntrySync({ params: { - nestedObject: { + it('converts paths in top-level object properties', function(done) { + givenMiddlewareEntrySync({ + params: { path: RELATIVE_PATH_PARAMS[0], }, - nestedArray: RELATIVE_PATH_PARAMS, - }}); + }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - expectFirstMiddlewareParams(instructions) - .to.eql({ + expectFirstMiddlewareParams(instructions) + .to.eql({ path: absolutePathParams[0] }); + done(); + }); + }); + + it('converts path value when params is a string', function(done) { + givenMiddlewareEntrySync({ params: RELATIVE_PATH_PARAMS[0] }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expectFirstMiddlewareParams(instructions) + .to.eql(absolutePathParams[0]); + done(); + }); + }); + + it('converts paths in nested properties', function(done) { + givenMiddlewareEntrySync({ + params: { nestedObject: { - path: absolutePathParams[0], + path: RELATIVE_PATH_PARAMS[0], }, - nestedArray: absolutePathParams, + nestedArray: RELATIVE_PATH_PARAMS, + }, + }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expectFirstMiddlewareParams(instructions) + .to.eql({ + nestedObject: { + path: absolutePathParams[0], + }, + nestedArray: absolutePathParams, + }); + done(); + }); + }); + + it('does not convert values not starting with `./` or `../`', + function(done) { + var PARAMS = ['$!.somerc', '$!/root', '$!hello!']; + givenMiddlewareEntrySync({ params: PARAMS }); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expectFirstMiddlewareParams(instructions).to.eql(PARAMS); + done(); }); - }); - - it('does not convert values not starting with `./` or `../`', function() { - var PARAMS = ['$!.somerc', '$!/root', '$!hello!']; - givenMiddlewareEntrySync({ params: PARAMS }); - - var instructions = boot.compile(appdir.PATH); - - expectFirstMiddlewareParams(instructions).to.eql(PARAMS); - }); + }); }); }); describe('for components', function() { - it('loads component configs from multiple files', function() { + it('loads component configs from multiple files', function(done) { appdir.createConfigFilesSync(); appdir.writeConfigFileSync('component-config.json', { debug: { option: 'value' }, @@ -2258,20 +2742,24 @@ describe('compiler', function() { debug: { env: 'applied' }, }); - var instructions = boot.compile(appdir.PATH); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; - var component = instructions.components[0]; - expect(component).to.eql({ - sourceFile: require.resolve('debug'), - config: { - option: 'value', - local: 'applied', - env: 'applied', - }, + var component = instructions.components[0]; + expect(component).to.eql({ + sourceFile: require.resolve('debug'), + config: { + option: 'value', + local: 'applied', + env: 'applied', + }, + }); + done(); }); }); - it('supports `componentRootDir` option', function() { + it('supports `componentRootDir` option', function(done) { var componentJson = { debug: { option: 'value', @@ -2282,86 +2770,107 @@ describe('compiler', function() { fs.writeJsonSync( path.resolve(customDir, 'component-config.json'), componentJson); - var instructions = boot.compile({ + boot.compile({ appRootDir: appdir.PATH, componentRootDir: path.resolve(appdir.PATH, 'custom'), - }); - var component = instructions.components[0]; - expect(component).to.eql({ - sourceFile: require.resolve('debug'), - config: { - option: 'value', - }, + }, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + var component = instructions.components[0]; + expect(component).to.eql({ + sourceFile: require.resolve('debug'), + config: { + option: 'value', + }, + }); + done(); }); }); - it('loads component relative to appRootDir', function() { + it('loads component relative to appRootDir', function(done) { appdir.writeConfigFileSync('./component-config.json', { - './index': { }, + './index': {}, }); var appJS = appdir.writeConfigFileSync('index.js', ''); - var instructions = boot.compile(appdir.PATH); - expect(instructions.components[0]).have.property( - 'sourceFile', appJS - ); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.components[0]).have.property( + 'sourceFile', appJS + ); + done(); + }); }); - it('loads component relative to node modules', function() { + it('loads component relative to node modules', function(done) { appdir.writeConfigFileSync('component-config.json', { - 'mycomponent': { }, + 'mycomponent': {}, }); var js = appdir.writeConfigFileSync('node_modules/mycomponent/index.js', ''); - var instructions = boot.compile(appdir.PATH); - expect(instructions.components[0]).have.property( - 'sourceFile', js - ); + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + expect(instructions.components[0]).have.property( + 'sourceFile', js + ); + done(); + }); }); it('retains backward compatibility for non-relative path in `appRootDir`', - function() { - appdir.writeConfigFileSync('component-config.json', { - 'my-component/component.js': { }, - }); - appdir.writeConfigFileSync('./my-component/component.js', ''); + function(done) { + appdir.writeConfigFileSync('component-config.json', { + 'my-component/component.js': {}, + }); + appdir.writeConfigFileSync('./my-component/component.js', ''); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw('Cannot resolve path \"my-component/component.js\"'); - }); + expectCompileToThrow( + 'Cannot resolve path \"my-component/component.js\"', + done); + }); it('prefers coffeescript over json for relative path component', - function() { - appdir.writeConfigFileSync('component-config.json', { - './component': { }, + function(done) { + appdir.writeConfigFileSync('component-config.json', { + './component': {}, + }); + + var coffee = appdir.writeFileSync('component.coffee', ''); + appdir.writeFileSync('component.json', ''); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.components).to.have.length(1); + expect(instructions.components[0]).have.property( + 'sourceFile', coffee); + done(); + }); }); - var coffee = appdir.writeFileSync('component.coffee', ''); - appdir.writeFileSync('component.json', ''); - - var instructions = boot.compile(appdir.PATH); - - expect(instructions.components).to.have.length(1); - expect(instructions.components[0]).have.property( - 'sourceFile', coffee); - }); - it('prefers coffeescript over json for module relative component path', - function() { - appdir.writeConfigFileSync('component-config.json', { - 'component': { }, + function(done) { + appdir.writeConfigFileSync('component-config.json', { + 'component': {}, + }); + + var coffee = appdir.writeFileSync('node_modules/component.coffee', ''); + appdir.writeFileSync('node_modules/component.json', ''); + + boot.compile(appdir.PATH, function(err, context) { + if (err) return done(err); + var instructions = context.instructions; + + expect(instructions.components).to.have.length(1); + expect(instructions.components[0]).have.property( + 'sourceFile', coffee); + done(); + }); }); - - var coffee = appdir.writeFileSync('node_modules/component.coffee', ''); - appdir.writeFileSync('node_modules/component.json', ''); - - var instructions = boot.compile(appdir.PATH); - - expect(instructions.components).to.have.length(1); - expect(instructions.components[0]).have.property( - 'sourceFile', coffee); - }); }); }); @@ -2391,3 +2900,4 @@ function pathWithoutExtension(value) { function pathWithoutIndex(filePath) { return filePath.replace(/[\\\/]index\.[^.]+$/, ''); } + diff --git a/test/config-loader.test.js b/test/config-loader.test.js deleted file mode 100644 index 2faade0..0000000 --- a/test/config-loader.test.js +++ /dev/null @@ -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); - }); -}); diff --git a/test/executor.test.js b/test/executor.test.js index 5d55f43..fb3a262 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -8,7 +8,12 @@ var boot = require('../'); var path = require('path'); var loopback = require('loopback'); var assert = require('assert'); -var expect = require('chai').expect; + +var chai = require('chai'); +var dirtyChai = require('dirty-chai'); +var expect = chai.expect; +chai.use(dirtyChai); + var fs = require('fs-extra'); var sandbox = require('./helpers/sandbox'); var appdir = require('./helpers/appdir'); @@ -37,7 +42,7 @@ describe('executor', function() { }); var dummyInstructions = someInstructions({ - config: { + application: { port: 0, host: '127.0.0.1', restApiRoot: '/rest-api', @@ -62,17 +67,20 @@ describe('executor', function() { describe('when booting', function() { it('should set the `booting` flag during execution', function(done) { expect(app.booting).to.be.undefined(); - boot.execute(app, simpleAppInstructions(), function(err) { - expect(err).to.be.undefined(); - expect(process.bootingFlagSet).to.be.true(); - expect(app.booting).to.be.false(); - done(); + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, function(err) { + expect(err).to.not.exist; + expect(process.bootingFlagSet).to.be.true(); + expect(app.booting).to.be.false(); + done(); + }); }); }); it('should emit the `booted` event in the next tick', function(done) { boot.execute(app, dummyInstructions, function(err) { - expect(err).to.be.undefined(); + expect(err).to.not.exist; }); app.on('booted', function() { // This test fails with a timeout when the `booted` event has not been @@ -86,18 +94,21 @@ describe('executor', function() { }); }); - it('configures models', function() { - boot.execute(app, dummyInstructions); - assert(app.models); - assert(app.models.User); - assert.equal(app.models.User, app.registry.getModel('User'), - 'Boot should not have extended built-in User model'); - assertValidDataSource(app.models.User.dataSource); - assert.isFunc(app.models.User, 'find'); - assert.isFunc(app.models.User, 'create'); + it('configures models', function(done) { + boot.execute(app, dummyInstructions, function(err, context) { + if (err) return done(err); + assert(app.models); + assert(app.models.User); + assert.equal(app.models.User, app.registry.getModel('User'), + 'Boot should not have extended built-in User model'); + assertValidDataSource(app.models.User.dataSource); + assert.isFunc(app.models.User, 'find'); + assert.isFunc(app.models.User, 'create'); + done(); + }); }); - it('defines and customizes models', function() { + it('defines and customizes models', function(done) { appdir.writeFileSync('models/Customer.js', 'module.exports = ' + function(Customer) { Customer.settings._customized = 'Customer'; @@ -116,15 +127,18 @@ describe('executor', function() { sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), }, ], - })); + }), function(err, context) { + if (err) return done(err); - expect(app.models.Customer).to.exist(); - expect(app.models.Customer.settings._customized).to.be.equal('Customer'); - var UserModel = app.registry.getModel('User'); - expect(UserModel.settings._customized).to.equal('Base'); + expect(app.models.Customer).to.exist; + expect(app.models.Customer.settings._customized).to.be.equal('Customer'); + var UserModel = app.registry.getModel('User'); + expect(UserModel.settings._customized).to.equal('Base'); + done(); + }); }); - it('defines model without attaching it', function() { + it('defines model without attaching it', function(done) { boot.execute(app, someInstructions({ models: [ { @@ -145,46 +159,55 @@ describe('executor', function() { sourceFile: undefined, }, ], - })); - - expect(Object.keys(app.models)).to.eql(['Car']); + }), function(err, context) { + if (err) return done(err); + expect(Object.keys(app.models)).to.eql(['Car']); + done(); + }); }); - it('attaches models to data sources', function() { - boot.execute(app, dummyInstructions); - assert.equal(app.models.User.dataSource, app.dataSources.theDb); + it('attaches models to data sources', function(done) { + boot.execute(app, dummyInstructions, function(err, context) { + if (err) return done(err); + assert.equal(app.models.User.dataSource, app.dataSources.theDb); + done(); + }); }); - it('defines all models first before running the config phase', function() { - appdir.writeFileSync('models/Customer.js', 'module.exports = ' + - function(Customer/*, Base*/) { - Customer.on('attached', function() { - Customer._modelsWhenAttached = - Object.keys(Customer.modelBuilder.models); - }); - }.toString()); + it('defines all models first before running the config phase', + function(done) { + appdir.writeFileSync('models/Customer.js', 'module.exports = ' + + function(Customer/*, Base*/) { + Customer.on('attached', function() { + Customer._modelsWhenAttached = + Object.keys(Customer.modelBuilder.models); + }); + }.toString()); - boot.execute(app, someInstructions({ - models: [ - { - name: 'Customer', - config: { dataSource: 'db' }, - definition: { name: 'Customer' }, - sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), - }, - { - name: 'UniqueName', - config: { dataSource: 'db' }, - definition: { name: 'UniqueName' }, - sourceFile: undefined, - }, - ], - })); + boot.execute(app, someInstructions({ + models: [ + { + name: 'Customer', + config: { dataSource: 'db' }, + definition: { name: 'Customer' }, + sourceFile: path.resolve(appdir.PATH, 'models', 'Customer.js'), + }, + { + name: 'UniqueName', + config: { dataSource: 'db' }, + definition: { name: 'UniqueName' }, + sourceFile: undefined, + }, + ], + }), function(err, context) { + if (err) return done(err); + expect(app.models.Customer._modelsWhenAttached). + to.include('UniqueName'); + done(); + }); + }); - expect(app.models.Customer._modelsWhenAttached).to.include('UniqueName'); - }); - - it('defines models in the local app registry', function() { + it('defines models in the local app registry', function(done) { app = loopback({ localRegistry: true }); boot.execute(app, someInstructions({ models: [ @@ -195,48 +218,58 @@ describe('executor', function() { sourceFile: undefined, }, ], - })); - - expect(Object.keys(loopback.registry.modelBuilder.models), 'global models') - .to.not.contain('LocalCustomer'); - expect(Object.keys(app.registry.modelBuilder.models), 'local models') - .to.contain('LocalCustomer'); + }), function(err, context) { + if (err) return done(err); + expect(Object.keys(loopback.registry.modelBuilder.models), + 'global models') + .to.not.contain('LocalCustomer'); + expect(Object.keys(app.registry.modelBuilder.models), 'local models') + .to.contain('LocalCustomer'); + done(); + }); }); - it('throws on bad require() call inside boot script', function() { + it('throws on bad require() call inside boot script', function(done) { var file = appdir.writeFileSync('boot/badScript.js', 'require("doesnt-exist"); module.exports = {};'); - function doBoot() { - boot.execute(app, someInstructions({ files: { boot: [file] }})); - } - - expect(doBoot).to.throw(/Cannot find module \'doesnt-exist\'/); + boot.execute(app, someInstructions({ bootScripts: [file] }), + function(err) { + expect(err && err.message) + .to.match(/Cannot find module \'doesnt-exist\'/); + done(); + }); }); - it('instantiates data sources', function() { - boot.execute(app, dummyInstructions); - assert(app.dataSources); - assert(app.dataSources.theDb); - assertValidDataSource(app.dataSources.theDb); - assert(app.dataSources.TheDb); + it('instantiates data sources', function(done) { + boot.execute(app, dummyInstructions, function(err, context) { + if (err) return done(err); + assert(app.dataSources); + assert(app.dataSources.theDb); + assertValidDataSource(app.dataSources.theDb); + assert(app.dataSources.TheDb); + done(); + }); }); - it('does not call autoAttach', function() { - boot.execute(app, dummyInstructions); + it('does not call autoAttach', function(done) { + boot.execute(app, dummyInstructions, function(err, context) { + if (err) return done(err); - // loopback-datasource-juggler quirk: - // Model.dataSources has modelBuilder as the default value, - // therefore it's not enough to assert a false-y value - var actual = loopback.Email.dataSource instanceof loopback.DataSource ? - 'attached' : 'not attached'; - expect(actual).to.equal('not attached'); + // loopback-datasource-juggler quirk: + // Model.dataSources has modelBuilder as the default value, + // therefore it's not enough to assert a false-y value + var actual = loopback.Email.dataSource instanceof loopback.DataSource ? + 'attached' : 'not attached'; + expect(actual).to.equal('not attached'); + done(); + }); }); - it('skips definition of already defined LoopBack models', function() { + it('skips definition of already defined LoopBack models', function(done) { var builtinModel = { name: 'User', - definition: fs.readJsonFileSync( + definition: fs.readJsonSync( require.resolve('loopback/common/models/user.json') ), config: { dataSource: 'db' }, @@ -244,57 +277,55 @@ describe('executor', function() { }; builtinModel.definition.redefined = true; - boot.execute(app, someInstructions({ models: [builtinModel] })); - - expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true); + boot.execute(app, someInstructions({ models: [builtinModel] }), + function(err, context) { + if (err) return done(err); + expect(app.models.User.settings.redefined, + 'redefined').to.not.equal(true); + done(); + }); }); describe('with boot and models files', function() { - beforeEach(function() { - boot.execute(app, simpleAppInstructions()); + beforeEach(function(done) { + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, done); + }); }); afterEach(function() { delete process.bootFlags; }); - it('should run `boot/*` files', function(done) { + it('should run `boot/*` files', function() { // scripts are loaded by the order of file names expect(process.bootFlags).to.eql([ 'barLoaded', 'barSyncLoaded', 'fooLoaded', 'barStarted', + 'barFinished', + 'barSyncExecuted', ]); - - // bar finished happens in the next tick - // barSync executed after bar finished - setTimeout(function() { - expect(process.bootFlags).to.eql([ - 'barLoaded', - 'barSyncLoaded', - 'fooLoaded', - 'barStarted', - 'barFinished', - 'barSyncExecuted', - ]); - done(); - }, 10); }); }); describe('with boot with callback', function() { it('should run `boot/*` files asynchronously', function(done) { - boot.execute(app, simpleAppInstructions(), function() { - expect(process.bootFlags).to.eql([ - 'barLoaded', - 'barSyncLoaded', - 'fooLoaded', - 'barStarted', - 'barFinished', - 'barSyncExecuted', - ]); - done(); + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, function() { + expect(process.bootFlags).to.eql([ + 'barLoaded', + 'barSyncLoaded', + 'fooLoaded', + 'barStarted', + 'barFinished', + 'barSyncExecuted', + ]); + done(); + }); }); }); @@ -318,23 +349,30 @@ describe('executor', function() { }; }); - it('defines mixins from instructions - using `mixinDirs`', function() { - options.mixinDirs = ['./custom-mixins']; - boot(app, options); + it('defines mixins from instructions - using `mixinDirs`', + function(done) { + options.mixinDirs = ['./custom-mixins']; + boot(app, options, function(err) { + if (err) return done(err); + var modelBuilder = app.registry.modelBuilder; + var registry = modelBuilder.mixins.mixins; + expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); + done(); + }); + }); - var modelBuilder = app.registry.modelBuilder; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); - }); + it('defines mixins from instructions - using `mixinSources`', + function(done) { + options.mixinSources = ['./custom-mixins']; + boot(app, options, function(err) { + if (err) return done(err); - it('defines mixins from instructions - using `mixinSources`', function() { - options.mixinSources = ['./custom-mixins']; - boot(app, options); - - var modelBuilder = app.registry.modelBuilder; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); - }); + var modelBuilder = app.registry.modelBuilder; + var registry = modelBuilder.mixins.mixins; + expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); + done(); + }); + }); }); }); @@ -356,41 +394,51 @@ describe('executor', function() { delete process.env.npm_package_config_port; }); - function bootWithDefaults() { + function bootWithDefaults(done) { app = loopback(); boot.execute(app, someInstructions({ - config: { + application: { port: undefined, host: undefined, }, - })); + }), done); } - it('should apply env passed in option object', function() { - boot.execute(app, someInstructions({ env: 'custom_env' })); - expect(app.get('env')).to.equal('custom_env'); + it('should apply env passed in option object', function(done) { + boot.execute(app, someInstructions({ env: 'custom_env' }), function(err) { + if (err) return done(err); + expect(app.get('env')).to.equal('custom_env'); + done(); + }); }); - it('should honor host and port', function() { - function assertHonored(portKey, hostKey) { + it('should honor host and port', function(done) { + function assertHonored(portKey, hostKey, cb) { process.env[hostKey] = randomPort(); process.env[portKey] = randomHost(); - bootWithDefaults(); - assert.equal(app.get('port'), process.env[portKey], portKey); - assert.equal(app.get('host'), process.env[hostKey], hostKey); - delete process.env[portKey]; - delete process.env[hostKey]; + bootWithDefaults(function(err) { + if (err) return cb(err); + assert.equal(app.get('port'), process.env[portKey], portKey); + assert.equal(app.get('host'), process.env[hostKey], hostKey); + delete process.env[portKey]; + delete process.env[hostKey]; + cb(); + }); } - assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP'); - assertHonored('npm_config_port', 'npm_config_host'); - assertHonored('npm_package_config_port', 'npm_package_config_host'); - assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP'); - assertHonored('VCAP_APP_PORT', 'VCAP_APP_HOST'); - assertHonored('PORT', 'HOST'); + async.eachSeries([ + { port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_NODEJS_IP' }, + { port: 'npm_config_port', host: 'npm_config_host' }, + { port: 'npm_package_config_port', host: 'npm_package_config_host' }, + { port: 'OPENSHIFT_SLS_PORT', host: 'OPENSHIFT_SLS_IP' }, + { port: 'VCAP_APP_PORT', host: 'VCAP_APP_HOST' }, + { port: 'PORT', host: 'HOST' }, + ], function(config, cb) { + assertHonored(config.port, config.host, cb); + }, done); }); - it('should prioritize host sources', function() { + it('should prioritize host sources', function(done) { // jscs:disable requireCamelCaseOrUpperCaseIdentifiers /*eslint-disable camelcase*/ process.env.npm_config_host = randomHost(); @@ -400,12 +448,15 @@ describe('executor', function() { process.env.HOST = randomHost(); process.env.npm_package_config_host = randomHost(); - bootWithDefaults(); - assert.equal(app.get('host'), process.env.npm_config_host); - /*eslint-enable camelcase*/ + bootWithDefaults(function(err) { + if (err) return done(err); + assert.equal(app.get('host'), process.env.npm_config_host); + /*eslint-enable camelcase*/ + done(); + }); }); - it('should prioritize port sources', function() { + it('should prioritize port sources', function(done) { /*eslint-disable camelcase*/ process.env.npm_config_port = randomPort(); process.env.OPENSHIFT_SLS_PORT = randomPort(); @@ -414,9 +465,12 @@ describe('executor', function() { process.env.PORT = randomPort(); process.env.npm_package_config_port = randomPort(); - bootWithDefaults(); - assert.equal(app.get('port'), process.env.npm_config_port); - /*eslint-enable camelcase*/ + bootWithDefaults(function(err) { + if (err) return done(err); + assert.equal(app.get('port'), process.env.npm_config_port); + /*eslint-enable camelcase*/ + done(); + }); }); function randomHost() { @@ -427,25 +481,37 @@ describe('executor', function() { return Math.floor(Math.random() * 10000); } - it('should honor 0 for free port', function() { - boot.execute(app, someInstructions({ config: { port: 0 }})); - assert.equal(app.get('port'), 0); + it('should honor 0 for free port', function(done) { + boot.execute(app, someInstructions({ application: { port: 0 }}), + function(err) { + if (err) return done(err); + assert.equal(app.get('port'), 0); + done(); + }); }); - it('should default to port 3000', function() { - boot.execute(app, someInstructions({ config: { port: undefined }})); - assert.equal(app.get('port'), 3000); + it('should default to port 3000', function(done) { + boot.execute(app, someInstructions({ application: { port: undefined }}), + function(err) { + if (err) return done(err); + assert.equal(app.get('port'), 3000); + done(); + }); }); - it('should respect named pipes port values in ENV', function() { + it('should respect named pipes port values in ENV', function(done) { var NAMED_PORT = '\\.\\pipe\\test'; process.env.PORT = NAMED_PORT; - boot.execute(app, someInstructions({ config: { port: 3000 }})); - assert.equal(app.get('port'), NAMED_PORT); + boot.execute(app, someInstructions({ application: { port: 3000 }}), + function(err) { + if (err) return done(err); + assert.equal(app.get('port'), NAMED_PORT); + done(); + }); }); }); - describe('with middleware.json', function() { + describe('with middleware.json', function(done) { beforeEach(function() { delete process.env.restApiRoot; }); @@ -453,12 +519,14 @@ describe('executor', function() { it('should parse a simple config variable', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { path: '${restApiRoot}' } - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.path).to.equal(app.get('restApiRoot')); - done(); + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.path).to.equal(app.get('restApiRoot')); + done(); + }); }); }); @@ -466,134 +534,154 @@ describe('executor', function() { process.env.restApiRoot = '/url-from-env-var'; boot.execute(app, simpleMiddlewareConfig('routes', { path: '${restApiRoot}' } - )); - - supertest(app).get('/url-from-env-var').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.path).to.equal('/url-from-env-var'); - done(); + + supertest(app).get('/url-from-env-var').end(function(err, res) { + if (err) return done(err); + expect(res.body.path).to.equal('/url-from-env-var'); + done(); + }); }); }); it('dynamic variable from `env var` should have' + - ' precedence over app.get()', function(done) { + ' precedence over app.get()', function(done) { process.env.restApiRoot = '/url-from-env-var'; var bootInstructions; bootInstructions = simpleMiddlewareConfig('routes', { path: '${restApiRoot}' }); - bootInstructions.config = { restApiRoot: '/url-from-config' }; - boot.execute(app, someInstructions(bootInstructions)); - - supertest(app).get('/url-from-env-var').end(function(err, res) { + bootInstructions.application = { restApiRoot: '/url-from-config' }; + boot.execute(app, someInstructions(bootInstructions), function(err) { if (err) return done(err); - expect(app.get('restApiRoot')).to.equal('/url-from-config'); - expect(res.body.path).to.equal('/url-from-env-var'); - done(); + + supertest(app).get('/url-from-env-var').end(function(err, res) { + if (err) return done(err); + expect(app.get('restApiRoot')).to.equal('/url-from-config'); + expect(res.body.path).to.equal('/url-from-env-var'); + done(); + }); }); }); it('should parse multiple config variables', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { path: '${restApiRoot}', env: '${env}' } - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.path).to.equal(app.get('restApiRoot')); - expect(res.body.env).to.equal(app.get('env')); - done(); + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.path).to.equal(app.get('restApiRoot')); + expect(res.body.env).to.equal(app.get('env')); + done(); + }); }); }); it('should parse config variables in an array', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { paths: ['${restApiRoot}'] } - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.paths).to.eql( - [app.get('restApiRoot')] + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.paths).to.eql( + [app.get('restApiRoot')] ); - done(); + done(); + }); }); }); it('should parse config variables in an object', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { info: { path: '${restApiRoot}' }} - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.info).to.eql({ - path: app.get('restApiRoot'), + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.info).to.eql({ + path: app.get('restApiRoot'), + }); + done(); }); - done(); }); }); it('should parse config variables in a nested object', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { nested: { info: { path: '${restApiRoot}' }}} - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.nested).to.eql({ - info: { path: app.get('restApiRoot') }, + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.nested).to.eql({ + info: { path: app.get('restApiRoot') }, + }); + done(); }); - done(); }); }); it('should parse config variables with null values', function(done) { boot.execute(app, simpleMiddlewareConfig('routes', { nested: { info: { path: '${restApiRoot}', some: null }}} - )); - - supertest(app).get('/').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.nested).to.eql({ - info: { - path: app.get('restApiRoot'), - some: null, - }, + + supertest(app).get('/').end(function(err, res) { + if (err) return done(err); + expect(res.body.nested).to.eql({ + info: { + path: app.get('restApiRoot'), + some: null, + }, + }); + done(); }); - done(); }); }); it('should not parse invalid config variables', function(done) { - var invalidDataTypes = [undefined, function() {}]; + var invalidDataTypes = [undefined, function() { + }]; async.each(invalidDataTypes, function(invalidDataType, cb) { var config = simpleMiddlewareConfig('routes', { path: invalidDataType, }); - boot.execute(app, config); + boot.execute(app, config, function(err) { + if (err) return done(err); - supertest(app) - .get('/') - .end(function(err, res) { - expect(err).to.be.null(); - expect(res.body.path).to.be.undefined(); - cb(); - }); + supertest(app) + .get('/') + .end(function(err, res) { + expect(err).to.be.null(); + expect(res.body.path).to.be.undefined(); + cb(); + }); + }, cb); }, done); }); it('should parse valid config variables', function(done) { var config = simpleMiddlewareConfig('routes', { - props: ['a', '${vVar}', 1, true, function() {}, { x: 1, y: '${y}' }], + props: ['a', '${vVar}', 1, true, function() { + }, { x: 1, y: '${y}' }], }); - boot.execute(app, config); + boot.execute(app, config, function(err) { + if (err) return done(err); - supertest(app) - .get('/') - .end(function(err, res) { - expect(err).to.be.null(); - done(); - }); + supertest(app) + .get('/') + .end(function(err, res) { + expect(err).to.be.null(); + done(); + }); + }); }); it('should preserve object prototypes', function(done) { @@ -602,11 +690,13 @@ describe('executor', function() { // IMPORTANT we need more than one item to trigger the original issue [/^\/foobar/, /^\/another/], {}); - boot.execute(app, config); + boot.execute(app, config, function(err) { + if (err) return done(err); - supertest(app).get('/foobar') - .expect(200) - .end(done); + supertest(app).get('/foobar') + .expect(200) + .end(done); + }); }); }); @@ -619,12 +709,14 @@ describe('executor', function() { it('should parse a simple config variable', function(done) { boot.execute(app, simpleComponentConfig( { path: '${restApiRoot}' } - )); - - supertest(app).get('/component').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.path).to.equal(app.get('restApiRoot')); - done(); + + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.path).to.equal(app.get('restApiRoot')); + done(); + }); }); }); @@ -638,16 +730,18 @@ describe('executor', function() { ); // result should get value from config.json - bootInstructions.config['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG'; + bootInstructions.application['DYNAMIC_CONFIG'] = 'FOOBAR-CONFIG'; // result should get value from env var process.env.DYNAMIC_ENVVAR = 'FOOBAR-ENVVAR'; - boot.execute(app, bootInstructions); - supertest(app).get('/component').end(function(err, res) { + boot.execute(app, bootInstructions, function(err) { if (err) return done(err); - expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG'); - expect(res.body.fromEnvVar).to.equal('FOOBAR-ENVVAR'); - done(); + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.fromConfig).to.equal('FOOBAR-CONFIG'); + expect(res.body.fromEnvVar).to.equal('FOOBAR-ENVVAR'); + done(); + }); }); }); @@ -657,80 +751,94 @@ describe('executor', function() { path: '${restApiRoot}', isDynamic: '${' + key + '}', }); - bootInstructions.config[key] = 'should be overwritten'; + bootInstructions.application[key] = 'should be overwritten'; process.env[key] = 'successfully overwritten'; - boot.execute(app, bootInstructions); - supertest(app).get('/component').end(function(err, res) { + boot.execute(app, bootInstructions, function(err) { if (err) return done(err); - expect(res.body.isDynamic).to.equal('successfully overwritten'); - done(); + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.isDynamic).to.equal('successfully overwritten'); + done(); + }); }); }); it('should parse multiple config variables', function(done) { boot.execute(app, simpleComponentConfig( { path: '${restApiRoot}', env: '${env}' } - )); - - supertest(app).get('/component').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.path).to.equal(app.get('restApiRoot')); - expect(res.body.env).to.equal(app.get('env')); - done(); + + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.path).to.equal(app.get('restApiRoot')); + expect(res.body.env).to.equal(app.get('env')); + done(); + }); }); }); it('should parse config variables in an array', function(done) { boot.execute(app, simpleComponentConfig( { paths: ['${restApiRoot}'] } - )); - - supertest(app).get('/component').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.paths).to.eql( - [app.get('restApiRoot')] + + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.paths).to.eql( + [app.get('restApiRoot')] ); - done(); + done(); + }); }); }); it('should parse config variables in an object', function(done) { boot.execute(app, simpleComponentConfig( { info: { path: '${restApiRoot}' }} - )); - - supertest(app).get('/component').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.info).to.eql({ - path: app.get('restApiRoot'), + + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.info).to.eql({ + path: app.get('restApiRoot'), + }); + done(); }); - done(); }); }); it('should parse config variables in a nested object', function(done) { boot.execute(app, simpleComponentConfig( { nested: { info: { path: '${restApiRoot}' }}} - )); - - supertest(app).get('/component').end(function(err, res) { + ), function(err) { if (err) return done(err); - expect(res.body.nested).to.eql({ - info: { path: app.get('restApiRoot') }, + + supertest(app).get('/component').end(function(err, res) { + if (err) return done(err); + expect(res.body.nested).to.eql({ + info: { path: app.get('restApiRoot') }, + }); + done(); }); - done(); }); }); }); - it('calls function exported by boot/init.js', function() { + it('calls function exported by boot/init.js', function(done) { var file = appdir.writeFileSync('boot/init.js', 'module.exports = function(app) { app.fnCalled = true; };'); delete app.fnCalled; - boot.execute(app, someInstructions({ files: { boot: [file] }})); - expect(app.fnCalled, 'exported fn was called').to.be.true(); + boot.execute(app, someInstructions({ bootScripts: [file] }), + function(err) { + if (err) return done(err); + expect(app.fnCalled, 'exported fn was called').to.be.true(); + done(); + }); }); it('configures middleware', function(done) { @@ -771,16 +879,18 @@ describe('executor', function() { }, ], }, - })); + }), function(err) { + if (err) return done(err); - supertest(app) - .get('/') - .end(function(err, res) { - if (err) return done(err); - var names = (res.headers.names || '').split(','); - expect(names).to.eql(['initial', 'custom', 'routes']); - done(); - }); + supertest(app) + .get('/') + .end(function(err, res) { + if (err) return done(err); + var names = (res.headers.names || '').split(','); + expect(names).to.eql(['initial', 'custom', 'routes']); + done(); + }); + }); }); it('configures middleware using shortform', function(done) { @@ -797,34 +907,41 @@ describe('executor', function() { }, ], }, - })); + }), function(err) { + if (err) return done(err); - supertest(app) - .get('/') - .end(function(err, res) { - if (err) return done(err); - var EXPECTED_TEXT = '\n\n\n' + - ' \n simple-app\n' + - '\n\n

simple-app

\n' + - '\n'; - expect(normalizeEols(res.text)).to.eql(normalizeEols(EXPECTED_TEXT)); - done(); - }); + supertest(app) + .get('/') + .end(function(err, res) { + if (err) return done(err); + var EXPECTED_TEXT = '\n\n\n' + + ' \n simple-app\n' + + '\n\n

simple-app

\n' + + '\n'; + expect(normalizeEols(res.text)).to.eql(normalizeEols(EXPECTED_TEXT)); + done(); + }); + }); }); it('configures middleware (end-to-end)', function(done) { - boot.execute(app, simpleAppInstructions()); - - supertest(app) - .get('/') - .end(function(err, res) { + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, function(err) { if (err) return done(err); - expect(res.headers.names).to.equal('custom-middleware'); - done(); + + supertest(app) + .get('/') + .end(function(err, res) { + if (err) return done(err); + expect(res.headers.names).to.equal('custom-middleware'); + done(); + }); }); + }); }); - it('configures components', function() { + it('configures components', function(done) { appdir.writeConfigFileSync('component-config.json', { './components/test-component': { option: 'value', @@ -835,12 +952,15 @@ describe('executor', function() { 'module.exports = ' + 'function(app, options) { app.componentOptions = options; }'); - boot(app, appdir.PATH); + boot(app, appdir.PATH, function(err) { + if (err) return done(err); - expect(Object.keys(require.cache)).to.include( - appdir.resolve('components/test-component/index.js')); + expect(Object.keys(require.cache)).to.include( + appdir.resolve('components/test-component/index.js')); - expect(app.componentOptions).to.eql({ option: 'value' }); + expect(app.componentOptions).to.eql({ option: 'value' }); + done(); + }); }); it('disables component when configuration is not set', function() { @@ -852,10 +972,12 @@ describe('executor', function() { 'module.exports = ' + 'function(app, options) { app.componentOptions = options; }'); - boot(app, appdir.PATH); + boot(app, appdir.PATH, function(err) { + if (err) return done(err); - expect(Object.keys(require.cache)).to.not.include( - appdir.resolve('components/test-component/index.js')); + expect(Object.keys(require.cache)).to.not.include( + appdir.resolve('components/test-component/index.js')); + }); }); it('disable component if overrided by production configuration', function() { @@ -870,10 +992,12 @@ describe('executor', function() { 'module.exports = ' + 'function(app, options) { app.componentOptions = options; }'); - boot(app, { appRootDir: appdir.PATH, env: 'production' }); + boot(app, { appRootDir: appdir.PATH, env: 'production' }, function(err) { + if (err) return done(err); - expect(Object.keys(require.cache)).to.not.include( - appdir.resolve('components/test-component/index.js')); + expect(Object.keys(require.cache)).to.not.include( + appdir.resolve('components/test-component/index.js')); + }); }); it('configures middleware (that requires `this`)', function(done) { @@ -892,21 +1016,26 @@ describe('executor', function() { }, ], }, - })); + }), function(err) { + if (err) return done(err); - supertest(app) - .get('/') - .expect('passport', 'initialized', done); + supertest(app) + .get('/') + .expect('passport', 'initialized', done); + }); }); describe('when booting with env', function() { it('should set the `booting` flag during execution', function(done) { expect(app.booting).to.be.undefined(); - boot.execute(app, envAppInstructions(), function(err) { + envAppInstructions(function(err, context) { if (err) return done(err); - expect(app.booting).to.be.false(); - expect(process.bootFlags).to.not.have.property('barLoadedInTest'); - done(); + boot.execute(app, context.instructions, function(err) { + if (err) return done(err); + expect(app.booting).to.be.false(); + expect(process.bootFlags).to.not.have.property('barLoadedInTest'); + done(); + }); }); }); }); @@ -997,7 +1126,7 @@ describe('executor', function() { mydb: { host: '${DYNAMIC_HOST}' }, }; var bootInstructions = { - config: { DYNAMIC_HOST: '127.0.0.4' }, + application: { DYNAMIC_HOST: '127.0.0.4' }, dataSources: datasource, }; boot.execute(app, someInstructions(bootInstructions), function() { @@ -1014,7 +1143,7 @@ describe('executor', function() { mydb: { host: '${DYNAMIC_HOST}' }, }; var bootInstructions = { - config: { DYNAMIC_HOST: '127.0.0.3' }, + application: { DYNAMIC_HOST: '127.0.0.3' }, dataSources: datasource, }; boot.execute(app, someInstructions(bootInstructions), function() { @@ -1095,14 +1224,12 @@ assert.isFunc = function(obj, name) { function someInstructions(values) { var result = { - config: values.config || {}, + application: values.application || {}, models: values.models || [], dataSources: values.dataSources || { db: { connector: 'memory' }}, middleware: values.middleware || { phases: [], middleware: [] }, components: values.components || [], - files: { - boot: [], - }, + bootScripts: values.bootScripts || [], }; if (values.env) @@ -1116,18 +1243,18 @@ function someInstructions(values) { return result; } -function simpleAppInstructions() { +function simpleAppInstructions(done) { // Copy it so that require will happend again fs.copySync(SIMPLE_APP, appdir.PATH); - return boot.compile(appdir.PATH); + boot.compile(appdir.PATH, done); } -function envAppInstructions() { +function envAppInstructions(done) { fs.copySync(ENV_APP, appdir.PATH); - return boot.compile({ + boot.compile({ appRootDir: appdir.PATH, env: 'test', - }); + }, done); } function normalizeEols(str) { diff --git a/test/fixtures/browser-app-2/app.js b/test/fixtures/browser-app-2/app.js index d6ba5d3..15436b4 100644 --- a/test/fixtures/browser-app-2/app.js +++ b/test/fixtures/browser-app-2/app.js @@ -7,7 +7,6 @@ var loopback = require('loopback'); var boot = require('../../../'); var app = module.exports = loopback(); - boot(app, { appId: 'browserApp2', appRootDir: __dirname, diff --git a/test/fixtures/browser-app/app.js b/test/fixtures/browser-app/app.js index f2c4fb6..4ef4153 100644 --- a/test/fixtures/browser-app/app.js +++ b/test/fixtures/browser-app/app.js @@ -7,4 +7,4 @@ var loopback = require('loopback'); var boot = require('../../../'); var app = module.exports = loopback(); -boot(app); +boot(app, __dirname); diff --git a/test/fixtures/coffee-app/app.coffee b/test/fixtures/coffee-app/app.coffee index 2c0b8dd..534b35f 100644 --- a/test/fixtures/coffee-app/app.coffee +++ b/test/fixtures/coffee-app/app.coffee @@ -2,4 +2,4 @@ loopback = require 'loopback' boot = require '../../../' module.exports = client = loopback() -boot(client) +boot(client, __dirname) diff --git a/test/fixtures/simple-app/plugins/tracker.js b/test/fixtures/simple-app/plugins/tracker.js new file mode 100644 index 0000000..8df632a --- /dev/null +++ b/test/fixtures/simple-app/plugins/tracker.js @@ -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); +}; diff --git a/test/helpers/browser.js b/test/helpers/browser.js index 8b6f03d..7eb5a74 100644 --- a/test/helpers/browser.js +++ b/test/helpers/browser.js @@ -58,6 +58,8 @@ function createContext() { error: [], }, }, + + ArrayBuffer: ArrayBuffer, }; // `window` is used by loopback to detect browser runtime diff --git a/test/utils.test.js b/test/utils.test.js index d7b7dc8..f16191c 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var boot = require('../'); +var utils = require('../lib/utils'); var expect = require('chai').expect; var sandbox = require('./helpers/sandbox'); var appdir = require('./helpers/appdir'); @@ -16,7 +16,7 @@ describe('utils', function() { describe('fileExistsSync', function() { it('returns false when a file does not exist', function() { var doesNotExist = sandbox.resolve('does-not-exist.json'); - expect(boot.utils.fileExistsSync(doesNotExist)) + expect(utils.fileExistsSync(doesNotExist)) .to.equal(false); }); @@ -24,7 +24,7 @@ describe('utils', function() { var doesExist = appdir.writeConfigFileSync('does-exist.json', { exists: true, }); - expect(boot.utils.fileExistsSync(doesExist)) + expect(utils.fileExistsSync(doesExist)) .to.equal(true); }); });