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..deeded6 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -27,6 +27,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 @@ -282,6 +283,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/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..e94c289 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -414,6 +414,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 +455,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; +};