Merge branch 'release/2.6.0' into production

This commit is contained in:
Miroslav Bajtoš 2015-01-08 08:31:28 +01:00
commit 33b3729b62
13 changed files with 177 additions and 8 deletions

View File

@ -1,3 +1,13 @@
2015-01-08, Version 2.6.0
=========================
* Add "booting" flag and emit "booted" event (Simon Ho)
* Configure components via `component-config.json` (Miroslav Bajtoš)
* Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham)
2014-12-19, Version 2.5.2 2014-12-19, Version 2.5.2
========================= =========================

View File

@ -14,7 +14,7 @@ Contributing to `loopback-boot` is easy. In a few simple steps:
* Adhere to code style outlined in the [Google C++ Style Guide][] and * Adhere to code style outlined in the [Google C++ Style Guide][] and
[Google Javascript Style Guide][]. [Google Javascript Style Guide][].
* Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback-boot) * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-boot)
* Submit a pull request through Github. * Submit a pull request through Github.

View File

@ -109,6 +109,8 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* for files containing model definitions. * for files containing model definitions.
* @property {Object} [middleware] Middleware configuration to use instead * @property {Object} [middleware] Middleware configuration to use instead
* of `{appRootDir}/middleware.json` * of `{appRootDir}/middleware.json`
* @property {Object} [components] Component configuration to use instead
* of `{appRootDir}/component-config.json`
* @property {Array.<String>} [bootDirs] List of directories where to look * @property {Array.<String>} [bootDirs] List of directories where to look
* for boot scripts. * for boot scripts.
* @property {Array.<String>} [bootScripts] List of script files to execute * @property {Array.<String>} [bootScripts] List of script files to execute

View File

@ -11,6 +11,7 @@ var cloneDeep = require('lodash').cloneDeep;
module.exports = function addInstructionsToBrowserify(instructions, bundler) { module.exports = function addInstructionsToBrowserify(instructions, bundler) {
bundleModelScripts(instructions, bundler); bundleModelScripts(instructions, bundler);
bundleComponentScripts(instructions, bundler);
bundleOtherScripts(instructions, bundler); bundleOtherScripts(instructions, bundler);
bundleInstructions(instructions, bundler); bundleInstructions(instructions, bundler);
}; };
@ -22,19 +23,27 @@ function bundleOtherScripts(instructions, bundler) {
} }
function bundleModelScripts(instructions, bundler) { function bundleModelScripts(instructions, bundler) {
var files = instructions.models bundleSourceFiles(instructions, 'models', bundler);
}
function bundleComponentScripts(instructions, bundler) {
bundleSourceFiles(instructions, 'components', bundler);
}
function bundleSourceFiles(instructions, type, bundler) {
var files = instructions[type]
.map(function(m) { return m.sourceFile; }) .map(function(m) { return m.sourceFile; })
.filter(function(f) { return !!f; }); .filter(function(f) { return !!f; });
var modelToFileMapping = instructions.models var instructionToFileMapping = instructions[type]
.map(function(m) { return files.indexOf(m.sourceFile); }); .map(function(m) { return files.indexOf(m.sourceFile); });
addScriptsToBundle('models', files, bundler); addScriptsToBundle(type, files, bundler);
// Update `sourceFile` properties with the new paths // Update `sourceFile` properties with the new paths
modelToFileMapping.forEach(function(fileIx, modelIx) { instructionToFileMapping.forEach(function(fileIx, sourceIx) {
if (fileIx === -1) return; if (fileIx === -1) return;
instructions.models[modelIx].sourceFile = files[fileIx]; instructions[type][sourceIx].sourceFile = files[fileIx];
}); });
} }

View File

@ -53,6 +53,12 @@ module.exports = function compile(options) {
var middlewareInstructions = var middlewareInstructions =
buildMiddlewareInstructions(middlewareRootDir, middlewareConfig); buildMiddlewareInstructions(middlewareRootDir, middlewareConfig);
var componentRootDir = appRootDir; // not configurable yet
var componentConfig = options.components ||
ConfigLoader.loadComponents(componentRootDir, env);
var componentInstructions =
buildComponentInstructions(componentRootDir, componentConfig);
// require directories // require directories
var bootDirs = options.bootDirs || []; // precedence var bootDirs = options.bootDirs || []; // precedence
bootDirs = bootDirs.concat(path.join(appRootDir, 'boot')); bootDirs = bootDirs.concat(path.join(appRootDir, 'boot'));
@ -81,6 +87,7 @@ module.exports = function compile(options) {
dataSources: dataSourcesConfig, dataSources: dataSourcesConfig,
models: modelInstructions, models: modelInstructions,
middleware: middlewareInstructions, middleware: middlewareInstructions,
components: componentInstructions,
files: { files: {
boot: bootScripts boot: bootScripts
} }
@ -471,3 +478,20 @@ function resolveMiddlewareParams(rootDir, params) {
} }
}); });
} }
function buildComponentInstructions(rootDir, componentConfig) {
return Object.keys(componentConfig).map(function(name) {
var sourceFile;
if (name.indexOf('./') === 0 || name.indexOf('../') === 0) {
// Relative path
sourceFile = path.resolve(rootDir, name);
} else {
sourceFile = require.resolve(name);
}
return {
sourceFile: sourceFile,
config: componentConfig[name]
};
});
}

View File

@ -44,6 +44,16 @@ ConfigLoader.loadMiddleware = function(rootDir, env) {
return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig); 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 --*/ /*-- Implementation --*/
/** /**
@ -162,6 +172,15 @@ function mergePhaseConfig(target, config, phase) {
} }
} }
function mergeComponentConfig(target, config, fileName) {
for (var c in target) {
var err = mergeObjects(target[c], config[c]);
if (err) {
throw new Error('Cannot apply ' + fileName + ' to `' + c + '`: ' + err);
}
}
}
function mergeObjects(target, config, keyPrefix) { function mergeObjects(target, config, keyPrefix) {
for (var key in config) { for (var key in config) {
var fullKey = keyPrefix ? keyPrefix + '.' + key : key; var fullKey = keyPrefix ? keyPrefix + '.' + key : key;

View File

@ -16,6 +16,10 @@ var path = require('path');
*/ */
module.exports = function execute(app, instructions, callback) { module.exports = function execute(app, instructions, callback) {
callback = callback || function() {};
app.booting = true;
patchAppLoopback(app); patchAppLoopback(app);
assertLoopBackVersion(app); assertLoopBackVersion(app);
@ -27,6 +31,7 @@ module.exports = function execute(app, instructions, callback) {
setupDataSources(app, instructions); setupDataSources(app, instructions);
setupModels(app, instructions); setupModels(app, instructions);
setupMiddleware(app, instructions); setupMiddleware(app, instructions);
setupComponents(app, instructions);
// Run the boot scripts in series synchronously or asynchronously // Run the boot scripts in series synchronously or asynchronously
// Please note async supports both styles // Please note async supports both styles
@ -37,7 +42,15 @@ module.exports = function execute(app, instructions, callback) {
function(done) { function(done) {
enableAnonymousSwagger(app, instructions); enableAnonymousSwagger(app, instructions);
done(); done();
}], callback); }], function(err) {
app.booting = false;
if (err) return callback(err);
app.emit('booted');
callback();
});
}; };
function patchAppLoopback(app) { function patchAppLoopback(app) {
@ -282,6 +295,14 @@ function setupMiddleware(app, instructions) {
}); });
} }
function setupComponents(app, instructions) {
instructions.components.forEach(function(data) {
debug('Configuring component %j', data.sourceFile);
var configFn = require(data.sourceFile);
configFn(app, data.config);
});
}
function runBootScripts(app, instructions, callback) { function runBootScripts(app, instructions, callback) {
runScripts(app, instructions.files.boot, callback); runScripts(app, instructions.files.boot, callback);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-boot", "name": "loopback-boot",
"version": "2.5.2", "version": "2.6.0",
"description": "Convention-based bootstrapper for LoopBack applications", "description": "Convention-based bootstrapper for LoopBack applications",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",

View File

@ -50,6 +50,10 @@ describe('browser support', function() {
expect(app.models.Customer.settings) expect(app.models.Customer.settings)
.to.have.property('_customized', 'Customer'); .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(); done();
}); });
}); });

View File

@ -1038,6 +1038,35 @@ describe('compiler', function() {
}); });
}); });
}); });
describe('for components', function() {
it('loads component configs from multiple files', function() {
appdir.createConfigFilesSync();
appdir.writeConfigFileSync('component-config.json', {
debug: { option: 'value' }
});
appdir.writeConfigFileSync('component-config.local.json', {
debug: { local: 'applied' }
});
var env = process.env.NODE_ENV || 'development';
appdir.writeConfigFileSync('component-config.' + env + '.json', {
debug: { env: 'applied' }
});
var instructions = boot.compile(appdir.PATH);
var component = instructions.components[0];
expect(component).to.eql({
sourceFile: require.resolve('debug'),
config: {
option: 'value',
local: 'applied',
env: 'applied'
}
});
});
});
}); });
function getNameProperty(obj) { function getNameProperty(obj) {

View File

@ -52,6 +52,32 @@ describe('executor', function() {
} }
}); });
describe('when booting', function() {
it('should set the booting status', function(done) {
expect(app.booting).to.be.undefined();
boot.execute(app, dummyInstructions, function(err) {
expect(err).to.be.undefined();
expect(app.booting).to.be.false();
done();
});
});
it('should emit the `booted` event', function(done) {
app.on('booted', function() {
// This test fails with a timeout when the `booted` event has not been
// emitted correctly
done();
});
boot.execute(app, dummyInstructions, function(err) {
expect(err).to.be.undefined();
});
});
it('should work when called synchronously', function() {
boot.execute(app, dummyInstructions);
});
});
it('configures models', function() { it('configures models', function() {
boot.execute(app, dummyInstructions); boot.execute(app, dummyInstructions);
assert(app.models); assert(app.models);
@ -414,6 +440,22 @@ describe('executor', function() {
done(); done();
}); });
}); });
it('configures components', function() {
appdir.writeConfigFileSync('component-config.json', {
'./components/test-component': {
option: 'value'
}
});
appdir.writeFileSync('components/test-component/index.js',
'module.exports = ' +
'function(app, options) { app.componentOptions = options; }');
boot(app, appdir.PATH);
expect(app.componentOptions).to.eql({ option: 'value' });
});
}); });
function assertValidDataSource(dataSource) { function assertValidDataSource(dataSource) {
@ -439,6 +481,7 @@ function someInstructions(values) {
models: values.models || [], models: values.models || [],
dataSources: values.dataSources || { db: { connector: 'memory' } }, dataSources: values.dataSources || { db: { connector: 'memory' } },
middleware: values.middleware || { phases: [], middleware: [] }, middleware: values.middleware || { phases: [], middleware: [] },
components: values.components || [],
files: { files: {
boot: [] boot: []
} }

View File

@ -0,0 +1,5 @@
{
"./components/dummy-component": {
"option": "value"
}
}

View File

@ -0,0 +1,3 @@
module.exports = function(app, options) {
app.dummyComponentOptions = options;
};