diff --git a/CHANGES.md b/CHANGES.md index 676b350..76810f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 ========================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7864057..7ab1d1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 [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. diff --git a/index.js b/index.js index 3f4dae2..5e82791 100644 --- a/index.js +++ b/index.js @@ -109,6 +109,8 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * for files containing model definitions. * @property {Object} [middleware] Middleware configuration to use instead * of `{appRootDir}/middleware.json` + * @property {Object} [components] Component configuration to use instead + * of `{appRootDir}/component-config.json` * @property {Array.} [bootDirs] List of directories where to look * for boot scripts. * @property {Array.} [bootScripts] List of script files to execute diff --git a/lib/bundler.js b/lib/bundler.js index dca9b9d..3445e26 100644 --- a/lib/bundler.js +++ b/lib/bundler.js @@ -11,6 +11,7 @@ var cloneDeep = require('lodash').cloneDeep; module.exports = function addInstructionsToBrowserify(instructions, bundler) { bundleModelScripts(instructions, bundler); + bundleComponentScripts(instructions, bundler); bundleOtherScripts(instructions, bundler); bundleInstructions(instructions, bundler); }; @@ -22,19 +23,27 @@ function bundleOtherScripts(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; }) .filter(function(f) { return !!f; }); - var modelToFileMapping = instructions.models + var instructionToFileMapping = instructions[type] .map(function(m) { return files.indexOf(m.sourceFile); }); - addScriptsToBundle('models', files, bundler); + addScriptsToBundle(type, files, bundler); // Update `sourceFile` properties with the new paths - modelToFileMapping.forEach(function(fileIx, modelIx) { + instructionToFileMapping.forEach(function(fileIx, sourceIx) { if (fileIx === -1) return; - instructions.models[modelIx].sourceFile = files[fileIx]; + instructions[type][sourceIx].sourceFile = files[fileIx]; }); } diff --git a/lib/compiler.js b/lib/compiler.js index 63e4c99..0a3ef7d 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -53,6 +53,12 @@ module.exports = function compile(options) { var middlewareInstructions = buildMiddlewareInstructions(middlewareRootDir, middlewareConfig); + var componentRootDir = appRootDir; // not configurable yet + 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')); @@ -81,6 +87,7 @@ module.exports = function compile(options) { dataSources: dataSourcesConfig, models: modelInstructions, middleware: middlewareInstructions, + components: componentInstructions, files: { 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] + }; + }); +} diff --git a/lib/config-loader.js b/lib/config-loader.js index e7d7095..2add670 100644 --- a/lib/config-loader.js +++ b/lib/config-loader.js @@ -44,6 +44,16 @@ 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 --*/ /** @@ -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) { for (var key in config) { var fullKey = keyPrefix ? keyPrefix + '.' + key : key; diff --git a/lib/executor.js b/lib/executor.js index af67b4b..0e151d5 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -16,6 +16,10 @@ var path = require('path'); */ module.exports = function execute(app, instructions, callback) { + callback = callback || function() {}; + + app.booting = true; + patchAppLoopback(app); assertLoopBackVersion(app); @@ -27,6 +31,7 @@ module.exports = function execute(app, instructions, callback) { 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 @@ -37,7 +42,15 @@ module.exports = function execute(app, instructions, callback) { function(done) { enableAnonymousSwagger(app, instructions); done(); - }], callback); + }], function(err) { + app.booting = false; + + if (err) return callback(err); + + app.emit('booted'); + + callback(); + }); }; 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) { runScripts(app, instructions.files.boot, callback); } diff --git a/package.json b/package.json index 82acc86..3852a5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-boot", - "version": "2.5.2", + "version": "2.6.0", "description": "Convention-based bootstrapper for LoopBack applications", "keywords": [ "StrongLoop", diff --git a/test/browser.test.js b/test/browser.test.js index 3acd166..79277e8 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -50,6 +50,10 @@ describe('browser support', function() { 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(); }); }); diff --git a/test/compiler.test.js b/test/compiler.test.js index c5a952d..e7407c7 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -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) { diff --git a/test/executor.test.js b/test/executor.test.js index c6fd891..2330266 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -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() { boot.execute(app, dummyInstructions); assert(app.models); @@ -414,6 +440,22 @@ describe('executor', function() { 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) { @@ -439,6 +481,7 @@ function someInstructions(values) { models: values.models || [], dataSources: values.dataSources || { db: { connector: 'memory' } }, middleware: values.middleware || { phases: [], middleware: [] }, + components: values.components || [], files: { boot: [] } diff --git a/test/fixtures/browser-app/component-config.json b/test/fixtures/browser-app/component-config.json new file mode 100644 index 0000000..3aa8175 --- /dev/null +++ b/test/fixtures/browser-app/component-config.json @@ -0,0 +1,5 @@ +{ + "./components/dummy-component": { + "option": "value" + } +} diff --git a/test/fixtures/browser-app/components/dummy-component.js b/test/fixtures/browser-app/components/dummy-component.js new file mode 100644 index 0000000..795a7a9 --- /dev/null +++ b/test/fixtures/browser-app/components/dummy-component.js @@ -0,0 +1,3 @@ +module.exports = function(app, options) { + app.dummyComponentOptions = options; +};