From f98d2cb89c1cabedce15c8eaec6b65e4622ba687 Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Mon, 4 Aug 2014 10:33:03 +0200 Subject: [PATCH 01/16] Implemented modelSources, bootDirs and bootScripts options --- index.js | 3 +++ lib/compiler.js | 10 +++++++-- test/browser.test.js | 2 +- test/compiler.test.js | 47 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index f6d8cbd..f7f0953 100644 --- a/index.js +++ b/index.js @@ -66,6 +66,9 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * `production`; however the applications are free to use any names. * @property {Array.} [modelSources] List of directories where to look * for files containing model definitions. + * @property {Array.} [bootDirs] List of directories where to look + * for boot scripts. + * @property {Array.} [bootScripts] List of script files to execute on boot. * @end * * @header boot(app, [options]) diff --git a/lib/compiler.js b/lib/compiler.js index e75ff6b..bca4a85 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -42,12 +42,18 @@ module.exports = function compile(options) { assertIsValidConfig('data source', dataSourcesConfig); // require directories - var bootScripts = findScripts(path.join(appRootDir, 'boot')); + var bootDirs = options.bootDirs || []; // precedence + bootDirs = bootDirs.concat(path.join(appRootDir, 'boot')); + + var bootScripts = options.bootScripts || []; + bootDirs.forEach(function(dir) { + bootScripts = bootScripts.concat(findScripts(dir)); + }); var modelsMeta = modelsConfig._meta || {}; delete modelsConfig._meta; - var modelSources = modelsMeta.sources || ['./models']; + var modelSources = options.modelSources || modelsMeta.sources || ['./models']; var modelInstructions = buildAllModelInstructions( modelsRootDir, modelsConfig, modelSources); diff --git a/test/browser.test.js b/test/browser.test.js index 1df93f9..1d5c660 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -37,7 +37,7 @@ function browserifyTestApp(appDir, next) { var bundlePath = sandbox.resolve('browser-app-bundle.js'); var out = fs.createWriteStream(bundlePath); b.bundle({ debug: true }).pipe(out); - + out.on('error', function(err) { return next(err); }); out.on('close', function() { next(null, bundlePath); diff --git a/test/compiler.test.js b/test/compiler.test.js index 5b6e450..69b782d 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -218,6 +218,28 @@ describe('compiler', function() { var instructions = boot.compile(appdir.PATH); expect(instructions.files.boot).to.eql([initJs]); }); + + it('supports `bootDirs` option', function() { + appdir.createConfigFilesSync(); + var initJs = appdir.writeFileSync('custom-boot/init.js', + 'module.exports = function(app) { app.fnCalled = true; };'); + var instructions = boot.compile({ + appRootDir: appdir.PATH, + bootDirs: [path.dirname(initJs)] + }); + expect(instructions.files.boot).to.eql([initJs]); + }); + + it('supports `bootScripts` option', function() { + appdir.createConfigFilesSync(); + var initJs = appdir.writeFileSync('custom-boot/init.js', + 'module.exports = function(app) { app.fnCalled = true; };'); + var instructions = boot.compile({ + appRootDir: appdir.PATH, + bootScripts: [initJs] + }); + expect(instructions.files.boot).to.eql([initJs]); + }); it('ignores models/ subdirectory', function() { appdir.createConfigFilesSync(); @@ -267,6 +289,31 @@ describe('compiler', function() { sourceFile: path.resolve(appdir.PATH, 'models', 'car.js') }); }); + + it('supports `sources` option', function() { + appdir.createConfigFilesSync({}, {}, { + Car: { dataSource: 'db' } + }); + appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); + appdir.writeFileSync('custom-models/car.js', ''); + + var instructions = boot.compile({ + appRootDir: appdir.PATH, + modelSources: ['./custom-models'] + }); + + expect(instructions.models).to.have.length(1); + expect(instructions.models[0]).to.eql({ + name: 'Car', + config: { + dataSource: 'db' + }, + definition: { + name: 'Car' + }, + sourceFile: path.resolve(appdir.PATH, 'custom-models', 'car.js') + }); + }); it('supports `sources` option in `model-config.json`', function() { appdir.createConfigFilesSync({}, {}, { From 217f0ce32fbc2c1610d73d0322edeaa795f3e40b Mon Sep 17 00:00:00 2001 From: Fabien Franzen Date: Mon, 4 Aug 2014 10:35:13 +0200 Subject: [PATCH 02/16] Fix typo --- test/compiler.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compiler.test.js b/test/compiler.test.js index 69b782d..ee712d1 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -290,7 +290,7 @@ describe('compiler', function() { }); }); - it('supports `sources` option', function() { + it('supports `modelSources` option', function() { appdir.createConfigFilesSync({}, {}, { Car: { dataSource: 'db' } }); From 1ae0167b7cabca3e3c2d03f000d173b3e9fcfce9 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Aug 2014 10:12:35 -0400 Subject: [PATCH 03/16] documentation fix --- lib/config-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config-loader.js b/lib/config-loader.js index a28be28..d9b503f 100644 --- a/lib/config-loader.js +++ b/lib/config-loader.js @@ -4,7 +4,7 @@ var path = require('path'); var ConfigLoader = exports; /** - * Load application config from `app.json` and friends. + * 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} From edb1f1fdce20a7a13b3dcd15934e212c931c8697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 18 Aug 2014 14:26:11 +0200 Subject: [PATCH 04/16] index: fix jshint error Wrap a too long line in a jsdoc comment. --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index f7f0953..772a321 100644 --- a/index.js +++ b/index.js @@ -68,7 +68,8 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * for files containing model definitions. * @property {Array.} [bootDirs] List of directories where to look * for boot scripts. - * @property {Array.} [bootScripts] List of script files to execute on boot. + * @property {Array.} [bootScripts] List of script files to execute + * on boot. * @end * * @header boot(app, [options]) From 93bfc3a63a2bb0aba1ccbafd3b70d4195a5b53e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 18 Aug 2014 14:35:19 +0200 Subject: [PATCH 05/16] test: increase timeout for browserify --- test/browser.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser.test.js b/test/browser.test.js index 1d5c660..3a6c94b 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -7,6 +7,8 @@ var sandbox = require('./helpers/sandbox'); var vm = require('vm'); describe('browser support', function() { + this.timeout(60000); // 60s to give browserify enough time to finish + it('has API for bundling and executing boot instructions', function(done) { var appDir = path.resolve(__dirname, './fixtures/browser-app'); From 34de5932029553daa667039b8bee3348f8126821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 18 Aug 2014 14:49:02 +0200 Subject: [PATCH 06/16] test: add `global.navigator` for browser tests The debug module uses `navigator.userAgent` to detect whether the browser support colors in console logs. --- test/browser.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/browser.test.js b/test/browser.test.js index 3a6c94b..d4c3846 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -70,6 +70,9 @@ function createBrowserLikeContext() { // used by `debug` module document: { documentElement: { style: {} } }, + // used by `debug` module + navigator: { userAgent: 'sandbox' }, + // used by crypto-browserify & friends Int32Array: Int32Array, DataView: DataView, From 0169f22cd001fb7ff808627c00d87029befc51af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 19 Aug 2014 09:26:57 +0200 Subject: [PATCH 07/16] test: ensure sandbox dir is present Fix test/browser.test.js failing on CI due to sandbox dir not present on the first run. --- test/browser.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/browser.test.js b/test/browser.test.js index d4c3846..869b451 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -9,6 +9,8 @@ var vm = require('vm'); describe('browser support', function() { this.timeout(60000); // 60s to give browserify enough time to finish + beforeEach(sandbox.reset); + it('has API for bundling and executing boot instructions', function(done) { var appDir = path.resolve(__dirname, './fixtures/browser-app'); From e97403339506b10b9432d61acdf54b08d57f3d5e Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Wed, 1 Oct 2014 18:06:20 -0700 Subject: [PATCH 08/16] Update contribution guidelines Replace commit signing process with https://cla.strongloop.com/ --- CONTRIBUTING.md | 57 ++++++------------------------------------------- 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a43e851..7864057 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,65 +1,24 @@ - ### Contributing ### -Thank you for your interest in `loopback`, an open source project +Thank you for your interest in `loopback-boot`, an open source project administered by StrongLoop. -Contributing to loopback is easy. In a few simple steps: +Contributing to `loopback-boot` is easy. In a few simple steps: - * Ensure that your effort is aligned with the project’s roadmap by + * Ensure that your effort is aligned with the project's roadmap by talking to the maintainers, especially if you are going to spend a - lot of time on it. This project is currently maintained by - [@ritch](https://github.com/ritch), [@raymondfeng](https://github.com/raymondfeng), - and [@bajtos](https://github.com/bajtos). The preferred channel of communication - is [LoopBack Forum](https://groups.google.com/forum/#!forum/loopbackjs) or - [Github Issues](https://github.com/strongloop/loopback/issues). + lot of time on it. * Make something better or fix a bug. - * Adhere to code style outlined in the + * Adhere to code style outlined in the [Google C++ Style Guide][] and [Google Javascript Style Guide][]. - * [Sign your patches](#signing-patches) to indicate that your are - making your contribution available under the terms of the - [Contributor License Agreement](#contributor-license-agreement). + * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback-boot) * Submit a pull request through Github. -### Signing patches ### - -Like many open source projects, we need a contributor license agreement -from you before we can merge in your changes. - -In summary, by submitting your code, you are granting us a right to use -that code under the terms of this Agreement, including providing it to -others. You are also certifying that you wrote it, and that you are -allowed to license it to us. You are not giving up your copyright in -your work. The license does not change your rights to use your own -contributions for any other purpose. - -Contributor License Agreements are important because they define the -chain of ownership of a piece of software. Some companies won't allow -the use of free software without clear agreements around code ownership. -That's why many open source projects collect similar agreements from -contributors. The CLA here is based on the Apache CLA. - -To signify your agreement to these terms, add the following line to the -bottom of your commit message. Use your real name and an actual e-mail -address. - -``` -Signed-off-by: Random J Developer -``` - -Alternatively you can use the git command line to automatically add this -line, as follows: - -``` -$ git commit -sm "Replace rainbows by unicorns" -``` - - ### Contributor License Agreement ### ``` @@ -188,7 +147,5 @@ $ git commit -sm "Replace rainbows by unicorns" inaccurate in any respect. Email us at callback@strongloop.com. ``` - +[Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml [Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml -[license]: LICENSE - From ed0880d00f8c6a48afd6daca79e1c8ff07d6ef14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 8 Oct 2014 12:08:13 +0200 Subject: [PATCH 09/16] package: Add `jshint` to `devDependencies` Remove dependency on a globally installed jshint instance and fix `npm test` on machines without a global jshint. --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8fbb725..9348dc3 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,12 @@ "underscore": "^1.6.0" }, "devDependencies": { + "browserify": "^4.1.8", + "fs-extra": "^0.10.0", + "jshint": "^2.5.6", "loopback": "^1.5.0", "mocha": "^1.19.0", "must": "^0.12.0", - "supertest": "^0.13.0", - "fs-extra": "^0.10.0", - "browserify": "^4.1.8" + "supertest": "^0.13.0" } } From abda37fee9b42bd4ed3d8173ac845e9f0928a659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 8 Oct 2014 15:26:23 +0200 Subject: [PATCH 10/16] gitignore: add Idea's *.iml files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bff8718..ec7a8f1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ *.pid *.swp *.swo +*.iml node_modules checkstyle.xml loopback-boot-*.tgz From e1d870dced2bb5db82ca12933bed14760b322cdb Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 13 Aug 2014 22:07:10 -0700 Subject: [PATCH 11/16] config-loader: deeply merge Array and Object vals --- lib/config-loader.js | 24 ++++++++++++-- test/compiler.test.js | 72 ++++++++++++++++++++++++++++++++++++++---- test/helpers/appdir.js | 10 ++++++ 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/config-loader.js b/lib/config-loader.js index d9b503f..f43fac7 100644 --- a/lib/config-loader.js +++ b/lib/config-loader.js @@ -129,10 +129,28 @@ function mergeAppConfig(target, config, fileName) { function applyCustomConfig(target, config) { for (var key in config) { var value = config[key]; - if (typeof value === 'object') { - return 'override for the option `' + key + '` is not a value type.'; + if (target[key]) { + if (Array.isArray(target[key]) && Array.isArray(value)) { + if (target[key].length == value.length) { + for (var valueIdx in value) { + if (typeof value[valueIdx] === 'object') { + applyCustomConfig(target[key][valueIdx], value[valueIdx]); + } else { + target[key][valueIdx] = value[valueIdx]; + } + } + } else { + return 'override for the option `' + key + + '` is an array and lengths mismatch.'; + } + } else if (typeof target[key] === 'object' && typeof value === 'object') { + applyCustomConfig(target[key], value); + } else { + target[key] = value; + } + } else { + target[key] = value; } - target[key] = value; } return null; // no error } diff --git a/test/compiler.test.js b/test/compiler.test.js index ee712d1..d5c18cc 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -125,24 +125,82 @@ describe('compiler', function() { expect(db).to.have.property('fromJs', true); }); - it('refuses to merge Object properties', function() { + it('merges Object properties', function() { + var nestedValue = { key: 'value' }; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { - db: { nested: { key: 'value' } } + db: { nested: nestedValue } }); - expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/`nested` is not a value type/); + var instructions = boot.compile(appdir.PATH); + + var db = instructions.dataSources.db; + expect(db).to.have.property('nested'); + expect(db.nested).to.eql(nestedValue); }); - it('refuses to merge Array properties', function() { + it('merges nested Object properties', function() { + var nestedValue = 'http://api.test.com'; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { - db: { nested: ['value'] } + rest: { + operations: [ + { + template: { + url: nestedValue + } + } + ] + } + }); + + var instructions = boot.compile(appdir.PATH); + + var rest = instructions.dataSources.rest; + expect(rest).to.have.property('operations'); + expect(rest.operations[0]).to.have.property('template'); + expect(rest.operations[0].template).to.have.property('url'); + expect(rest.operations[0].template.method).to.eql('POST'); + expect(rest.operations[0].template.url).to.eql(nestedValue); + }); + + it('merges Array properties', function() { + var nestedValue = ['value']; + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('datasources.local.json', { + db: { nested: nestedValue } + }); + + var instructions = boot.compile(appdir.PATH); + + var db = instructions.dataSources.db; + expect(db).to.have.property('nested'); + expect(db.nested).to.eql(nestedValue); + }); + + it('errors on mismatched arrays', function() { + var nestedValue = 'http://api.test.com'; + appdir.createConfigFilesSync(); + appdir.writeConfigFileSync('datasources.local.json', { + rest: { + operations: [ + { + template: { + url: nestedValue + } + }, + { + template: { + method: 'GET', + url: nestedValue + } + } + ] + } }); expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/`nested` is not a value type/); + .to.throw(/an array and lengths mismatch/); }); it('merges app configs from multiple files', function() { diff --git a/test/helpers/appdir.js b/test/helpers/appdir.js index 1a31be5..579e675 100644 --- a/test/helpers/appdir.js +++ b/test/helpers/appdir.js @@ -28,6 +28,16 @@ appdir.createConfigFilesSync = function(appConfig, dataSources, models) { db: { connector: 'memory', defaultForType: 'db' + }, + rest: { + connector: 'rest', + operations: [ + { + template: { + method: 'POST' + } + } + ] } }, dataSources); appdir.writeConfigFileSync ('datasources.json', dataSources); From f0836719c9a0f483fd49af64da5d909d42c72653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 8 Oct 2014 17:12:02 +0200 Subject: [PATCH 12/16] compiler: improve merging of Arrays and Objects Add more unit-tests to cover various edge cases. Fix issues discovered by these new tests. --- lib/config-loader.js | 84 ++++++++++++++++------- test/compiler.test.js | 150 ++++++++++++++++++++++++++++++++--------- test/helpers/appdir.js | 10 --- 3 files changed, 176 insertions(+), 68 deletions(-) diff --git a/lib/config-loader.js b/lib/config-loader.js index f43fac7..17834eb 100644 --- a/lib/config-loader.js +++ b/lib/config-loader.js @@ -112,7 +112,7 @@ function mergeConfigurations(configObjects, mergeFn) { function mergeDataSourceConfig(target, config, fileName) { for (var ds in target) { - var err = applyCustomConfig(target[ds], config[ds]); + var err = mergeObjects(target[ds], config[ds]); if (err) { throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err); } @@ -120,41 +120,73 @@ function mergeDataSourceConfig(target, config, fileName) { } function mergeAppConfig(target, config, fileName) { - var err = applyCustomConfig(target, config); + var err = mergeObjects(target, config); if (err) { throw new Error('Cannot apply ' + fileName + ': ' + err); } } -function applyCustomConfig(target, config) { +function mergeObjects(target, config, keyPrefix) { for (var key in config) { - var value = config[key]; - if (target[key]) { - if (Array.isArray(target[key]) && Array.isArray(value)) { - if (target[key].length == value.length) { - for (var valueIdx in value) { - if (typeof value[valueIdx] === 'object') { - applyCustomConfig(target[key][valueIdx], value[valueIdx]); - } else { - target[key][valueIdx] = value[valueIdx]; - } - } - } else { - return 'override for the option `' + key + - '` is an array and lengths mismatch.'; - } - } else if (typeof target[key] === 'object' && typeof value === 'object') { - applyCustomConfig(target[key], value); - } else { - target[key] = value; - } - } else { - target[key] = value; - } + 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 (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'; +} + /** * Try to read a config file with .json extension * @param cwd Dirname of the file diff --git a/test/compiler.test.js b/test/compiler.test.js index d5c18cc..73d812b 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -125,29 +125,62 @@ describe('compiler', function() { expect(db).to.have.property('fromJs', true); }); - it('merges Object properties', function() { - var nestedValue = { key: 'value' }; + it('merges new Object values', function() { + var objectValue = { key: 'value' }; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { - db: { nested: nestedValue } + db: { nested: objectValue } }); var instructions = boot.compile(appdir.PATH); var db = instructions.dataSources.db; expect(db).to.have.property('nested'); - expect(db.nested).to.eql(nestedValue); + expect(db.nested).to.eql(objectValue); }); - it('merges nested Object properties', function() { - var nestedValue = 'http://api.test.com'; - appdir.createConfigFilesSync(); + it('deeply merges Object values', function() { + appdir.createConfigFilesSync({}, { + email: { + transport: { + host: 'localhost' + } + } + }); + + appdir.writeConfigFileSync('datasources.local.json', { + email: { + transport: { + host: 'mail.example.com' + } + } + }); + + var instructions = boot.compile(appdir.PATH); + var email = instructions.dataSources.email; + expect(email.transport.host).to.equal('mail.example.com'); + }); + + it('deeply merges Array values of the same length', function() { + appdir.createConfigFilesSync({}, { + rest: { + operations: [ + { + template: { + method: 'POST', + url: 'http://localhost:12345' + } + } + ] + } + + }); appdir.writeConfigFileSync('datasources.local.json', { rest: { operations: [ { template: { - url: nestedValue + url: 'http://api.example.com' } } ] @@ -157,50 +190,103 @@ describe('compiler', function() { var instructions = boot.compile(appdir.PATH); var rest = instructions.dataSources.rest; - expect(rest).to.have.property('operations'); - expect(rest.operations[0]).to.have.property('template'); - expect(rest.operations[0].template).to.have.property('url'); - expect(rest.operations[0].template.method).to.eql('POST'); - expect(rest.operations[0].template.url).to.eql(nestedValue); + expect(rest.operations[0].template).to.eql({ + method: 'POST', // the value from datasources.json + url: 'http://api.example.com' // overriden in datasources.local.json + }); }); it('merges Array properties', function() { - var nestedValue = ['value']; + var arrayValue = ['value']; appdir.createConfigFilesSync(); appdir.writeConfigFileSync('datasources.local.json', { - db: { nested: nestedValue } + db: { nested: arrayValue } }); var instructions = boot.compile(appdir.PATH); var db = instructions.dataSources.db; expect(db).to.have.property('nested'); - expect(db.nested).to.eql(nestedValue); + expect(db.nested).to.eql(arrayValue); }); - it('errors on mismatched arrays', function() { - var nestedValue = 'http://api.test.com'; - appdir.createConfigFilesSync(); - appdir.writeConfigFileSync('datasources.local.json', { - rest: { - operations: [ + it('refuses to merge Array properties of different length', function() { + appdir.createConfigFilesSync({ + nest: { + array: [] + } + }); + + appdir.writeConfigFileSync('config.local.json', { + nest: { + array: [ { - template: { - url: nestedValue - } - }, - { - template: { - method: 'GET', - url: nestedValue - } + key: 'value' } ] } }); expect(function() { boot.compile(appdir.PATH); }) - .to.throw(/an array and lengths mismatch/); + .to.throw(/array values of different length.*nest\.array/); + }); + + it('refuses to merge Array of different length in Array', function() { + appdir.createConfigFilesSync({ + key: [[]] + }); + + appdir.writeConfigFileSync('config.local.json', { + key: [['value']] + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/array values of different length.*key\[0\]/); + }); + + it('returns full key of an incorrect Array value', function() { + appdir.createConfigFilesSync({ + toplevel: [ + { + nested: [] + } + ] + }); + + appdir.writeConfigFileSync('config.local.json', { + toplevel: [ + { + nested: [ 'value' ] + } + ] + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/array values of different length.*toplevel\[0\]\.nested/); + }); + + it('refuses to merge incompatible object properties', function() { + appdir.createConfigFilesSync({ + key: [] + }); + appdir.writeConfigFileSync('config.local.json', { + key: {} + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/incompatible types.*key/); + }); + + it('refuses to merge incompatible array items', function() { + appdir.createConfigFilesSync({ + key: [[]] + }); + appdir.writeConfigFileSync('config.local.json', { + key: [{}] + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/incompatible types.*key\[0\]/); }); it('merges app configs from multiple files', function() { diff --git a/test/helpers/appdir.js b/test/helpers/appdir.js index 579e675..1a31be5 100644 --- a/test/helpers/appdir.js +++ b/test/helpers/appdir.js @@ -28,16 +28,6 @@ appdir.createConfigFilesSync = function(appConfig, dataSources, models) { db: { connector: 'memory', defaultForType: 'db' - }, - rest: { - connector: 'rest', - operations: [ - { - template: { - method: 'POST' - } - } - ] } }, dataSources); appdir.writeConfigFileSync ('datasources.json', dataSources); From 18121a4208c95eba1f0aaa2874f398b171733906 Mon Sep 17 00:00:00 2001 From: johnsoftek Date: Tue, 30 Sep 2014 00:46:13 +1000 Subject: [PATCH 13/16] Custom rootDir for app config --- index.js | 40 +++++++++++++++++++++++++++++++++++++--- lib/compiler.js | 4 +++- test/compiler.test.js | 17 +++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 772a321..f135260 100644 --- a/index.js +++ b/index.js @@ -23,15 +23,49 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * 2. Configures Models from the `model-config.json` file in the application * root directory. * + * 3. Configures the LoopBack Application object from the `config.json` file + * in the application root directory. These properties can be accessed + * using app.get('propname'). + * * If the argument is an object, then it looks for `models`, `dataSources`, - * and `appRootDir` properties of the object. + * 'config', modelRootDir, dsRootDir, appConfigRootDir and `appRootDir` + * properties of the object. * If the object has no `appRootDir` property then it sets the current working * directory as the application root directory. + * The execution environment, {env}, is establised from, in order, + * 'options.env', + * Process.env.NODE_ENV, + * the literal 'development' + * * Then it: * - * 1. Creates DataSources from the `options.dataSources` object. + * 1. Creates DataSources from the `options.dataSources` object, if provided; + * otherwise, it searches for the files + * 'datasources.json', + * 'datasources.local.js' or 'datasources.local.json' (only one), + * 'datasources.{env}.js' or 'datasources.{env}.json' (only one) + * in the directory designated by 'options.dsRootDir', if present, or the + * application root directory. It merges the data source definitions from + * the files found. * - * 2. Configures Models from the `options.models` object. + * 2. Creates Models from the `options.models` object, if provided; + * otherwise, it searches for the files + * 'model-config.json', + * 'model-config.local.js' or 'model-config.local.json' (only one), + * 'model-config.{env}.js' or 'model-config.{env}.json' (only one) + * in the directory designated by 'options.modelsRootDir', if present, or + * the application root directory. It merges the model definitions from the + * files found. + * + * 3. Configures the Application object from the `options.config` object, + * if provided; + * otherwise, it searches for the files + * 'config.json', + * 'config.local.js' or 'config.local.json' (only one), + * 'config.{env}.js' or 'config.{env}.json' (only one) + * in the directory designated by 'options.appConfigRootDir', if present, or + * the application root directory. It merges the properties from the files + * found. * * In both cases, the function loads JavaScript files in the * `/boot` subdirectory of the application root directory with `require()`. diff --git a/lib/compiler.js b/lib/compiler.js index bca4a85..d5d870a 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -28,7 +28,9 @@ module.exports = function compile(options) { var appRootDir = options.appRootDir = options.appRootDir || process.cwd(); var env = options.env || process.env.NODE_ENV || 'development'; - var appConfig = options.config || ConfigLoader.loadAppConfig(appRootDir, env); + var appConfigRootDir = options.appConfigRootDir || appRootDir; + var appConfig = options.config || + ConfigLoader.loadAppConfig(appConfigRootDir, env); assertIsValidConfig('app', appConfig); var modelsRootDir = options.modelsRootDir || appRootDir; diff --git a/test/compiler.test.js b/test/compiler.test.js index ee712d1..aa6182f 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -179,6 +179,23 @@ describe('compiler', function() { expect(appConfig).to.have.property('fromJs', true); }); + it('supports `appConfigRootDir` option', function() { + appdir.createConfigFilesSync({port:3000}); + + var customDir = path.resolve(appdir.PATH, 'custom'); + fs.mkdirsSync(customDir); + fs.renameSync( + path.resolve(appdir.PATH, 'config.json'), + path.resolve(customDir, 'config.json')); + + var instructions = boot.compile({ + appRootDir: appdir.PATH, + appConfigRootDir: path.resolve(appdir.PATH, 'custom') + }); + + expect(instructions.config).to.have.property('port'); + }); + it('supports `dsRootDir` option', function() { appdir.createConfigFilesSync(); From d54e2b54d074f031a6193b27096b3ea815d09ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 9 Oct 2014 16:34:39 +0200 Subject: [PATCH 14/16] Clean up jsdoc comments. - Fix formatting to improve the way how the text is rendered on http://apidocs.strongloop.com/loopback-boot/ - Add `@property` entry for `options.appConfigRootDir`. --- index.js | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index f135260..4651138 100644 --- a/index.js +++ b/index.js @@ -25,44 +25,49 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * * 3. Configures the LoopBack Application object from the `config.json` file * in the application root directory. These properties can be accessed - * using app.get('propname'). + * using `app.get('propname')`. * * If the argument is an object, then it looks for `models`, `dataSources`, - * 'config', modelRootDir, dsRootDir, appConfigRootDir and `appRootDir` + * 'config', `modelsRootDir`, `dsRootDir`, `appConfigRootDir` and `appRootDir` * properties of the object. + * * If the object has no `appRootDir` property then it sets the current working * directory as the application root directory. - * The execution environment, {env}, is establised from, in order, - * 'options.env', - * Process.env.NODE_ENV, - * the literal 'development' + * + * The execution environment, {env}, is established from, in order, + * - `options.env` + * - `process.env.NODE_ENV`, + * - the literal `development`. * * Then it: * - * 1. Creates DataSources from the `options.dataSources` object, if provided; - * otherwise, it searches for the files - * 'datasources.json', - * 'datasources.local.js' or 'datasources.local.json' (only one), - * 'datasources.{env}.js' or 'datasources.{env}.json' (only one) + * 1. Creates DataSources from the `options.dataSources` object, if provided; + * otherwise, it searches for the files + * - `datasources.json`, + * - `datasources.local.js` or `datasources.local.json` (only one), + * - `datasources.{env}.js` or `datasources.{env}.json` (only one) + * * in the directory designated by 'options.dsRootDir', if present, or the * application root directory. It merges the data source definitions from * the files found. * - * 2. Creates Models from the `options.models` object, if provided; + * 2. Creates Models from the `options.models` object, if provided; * otherwise, it searches for the files - * 'model-config.json', - * 'model-config.local.js' or 'model-config.local.json' (only one), - * 'model-config.{env}.js' or 'model-config.{env}.json' (only one) + * - `model-config.json`, + * - `model-config.local.js` or `model-config.local.json` (only one), + * - `model-config.{env}.js` or `model-config.{env}.json` (only one) + * * in the directory designated by 'options.modelsRootDir', if present, or * the application root directory. It merges the model definitions from the * files found. * - * 3. Configures the Application object from the `options.config` object, + * 3. Configures the Application object from the `options.config` object, * if provided; * otherwise, it searches for the files - * 'config.json', - * 'config.local.js' or 'config.local.json' (only one), - * 'config.{env}.js' or 'config.{env}.json' (only one) + * - `config.json`, + * - `config.local.js` or `config.local.json` (only one), + * - `config.{env}.js` or `config.{env}.json` (only one) + * * in the directory designated by 'options.appConfigRootDir', if present, or * the application root directory. It merges the properties from the files * found. @@ -89,6 +94,8 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * @property {String} [appRootDir] Directory to use when loading JSON and * JavaScript files. * Defaults to the current directory (`process.cwd()`). + * @property {String} [appConfigRootDir] Directory to use when loading + * `config.json`. Defaults to `appRootDir`. * @property {Object} [models] Object containing `Model` configurations. * @property {Object} [dataSources] Object containing `DataSource` definitions. * @property {String} [modelsRootDir] Directory to use when loading From 94cb4d6342a17fbaab999c27634651a73f90386d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 9 Oct 2014 12:18:36 -0700 Subject: [PATCH 15/16] Add support for async boot scripts --- .jshintrc | 1 + index.js | 4 +- lib/executor.js | 51 ++++++++++++++++---- package.json | 1 + test/executor.test.js | 60 ++++++++++++++++++++++-- test/fixtures/simple-app/boot/bar.js | 8 ++++ test/fixtures/simple-app/boot/barSync.js | 5 ++ test/fixtures/simple-app/boot/foo.js | 2 +- 8 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/simple-app/boot/bar.js create mode 100644 test/fixtures/simple-app/boot/barSync.js diff --git a/.jshintrc b/.jshintrc index 569b12d..3f23a40 100644 --- a/.jshintrc +++ b/.jshintrc @@ -12,6 +12,7 @@ "newcap": true, "nonew": true, "sub": true, +"unused": "vars", "globals": { "describe": true, "it": true, diff --git a/index.js b/index.js index 772a321..dd84419 100644 --- a/index.js +++ b/index.js @@ -75,12 +75,12 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * @header boot(app, [options]) */ -exports = module.exports = function bootLoopBackApp(app, options) { +exports = module.exports = function bootLoopBackApp(app, options, callback) { // backwards compatibility with loopback's app.boot options.env = options.env || app.get('env'); var instructions = compile(options); - execute(app, instructions); + execute(app, instructions, callback); }; /** diff --git a/lib/executor.js b/lib/executor.js index d04e833..ceef61f 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -2,6 +2,7 @@ var assert = require('assert'); var _ = require('underscore'); var semver = require('semver'); var debug = require('debug')('loopback:boot:executor'); +var async = require('async'); /** * Execute bootstrap instructions gathered by `boot.compile`. @@ -12,7 +13,7 @@ var debug = require('debug')('loopback:boot:executor'); * @header boot.execute(instructions) */ -module.exports = function execute(app, instructions) { +module.exports = function execute(app, instructions, callback) { patchAppLoopback(app); assertLoopBackVersion(app); @@ -24,9 +25,16 @@ module.exports = function execute(app, instructions) { setupDataSources(app, instructions); setupModels(app, instructions); - runBootScripts(app, instructions); - - enableAnonymousSwagger(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(); + }], callback); }; function patchAppLoopback(app) { @@ -176,13 +184,36 @@ function forEachKeyedObject(obj, fn) { }); } -function runScripts(app, list) { - if (!list || !list.length) return; +function runScripts(app, list, callback) { + list = list || []; + var functions = []; list.forEach(function(filepath) { + debug('Requiring script %s', filepath); var exports = tryRequire(filepath); - if (typeof exports === 'function') - exports(app); + if (typeof exports === 'function') { + debug('Exported function detected %s', filepath); + functions.push({ + path: filepath, + func: exports + }); + } }); + + 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 tryRequire(modulePath) { @@ -198,8 +229,8 @@ function tryRequire(modulePath) { } } -function runBootScripts(app, instructions) { - runScripts(app, instructions.files.boot); +function runBootScripts(app, instructions, callback) { + runScripts(app, instructions.files.boot, callback); } function enableAnonymousSwagger(app, instructions) { diff --git a/package.json b/package.json index 8fbb725..2c2c8cb 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE" }, "dependencies": { + "async": "~0.9.0", "commondir": "0.0.1", "debug": "^1.0.4", "lodash.clonedeep": "^2.4.1", diff --git a/test/executor.test.js b/test/executor.test.js index 8c53715..4a63929 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -3,6 +3,7 @@ var path = require('path'); var loopback = require('loopback'); var assert = require('assert'); var expect = require('must'); +var fs = require('fs-extra'); var sandbox = require('./helpers/sandbox'); var appdir = require('./helpers/appdir'); @@ -161,13 +162,62 @@ describe('executor', function() { describe('with boot and models files', function() { beforeEach(function() { + process.bootFlags = process.bootFlags || []; boot.execute(app, simpleAppInstructions()); }); - it('should run `boot/*` files', function() { - assert(process.loadedFooJS); - delete process.loadedFooJS; + afterEach(function() { + delete process.bootFlags; }); + + it('should run `boot/*` files', function(done) { + // scripts are loaded by the order of file names + expect(process.bootFlags).to.eql([ + 'barLoaded', + 'barSyncLoaded', + 'fooLoaded', + 'barStarted' + ]); + + // 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() { + beforeEach(function() { + process.bootFlags = process.bootFlags || []; + }); + + afterEach(function() { + delete process.bootFlags; + }); + + 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(); + }); + }); + }); describe('with PaaS and npm env variables', function() { @@ -299,5 +349,7 @@ function someInstructions(values) { } function simpleAppInstructions() { - return boot.compile(SIMPLE_APP); + // Copy it so that require will happend again + fs.copySync(SIMPLE_APP, appdir.PATH); + return boot.compile(appdir.PATH); } diff --git a/test/fixtures/simple-app/boot/bar.js b/test/fixtures/simple-app/boot/bar.js new file mode 100644 index 0000000..4e732a1 --- /dev/null +++ b/test/fixtures/simple-app/boot/bar.js @@ -0,0 +1,8 @@ +process.bootFlags.push('barLoaded'); +module.exports = function(app, callback) { + process.bootFlags.push('barStarted'); + process.nextTick(function() { + process.bootFlags.push('barFinished'); + callback(); + }); +}; diff --git a/test/fixtures/simple-app/boot/barSync.js b/test/fixtures/simple-app/boot/barSync.js new file mode 100644 index 0000000..64c0b0b --- /dev/null +++ b/test/fixtures/simple-app/boot/barSync.js @@ -0,0 +1,5 @@ +process.bootFlags.push('barSyncLoaded'); +module.exports = function(app) { + process.bootFlags.push('barSyncExecuted'); +}; + diff --git a/test/fixtures/simple-app/boot/foo.js b/test/fixtures/simple-app/boot/foo.js index 7e74863..6641f03 100644 --- a/test/fixtures/simple-app/boot/foo.js +++ b/test/fixtures/simple-app/boot/foo.js @@ -1 +1 @@ -process.loadedFooJS = true; +process.bootFlags.push('fooLoaded'); \ No newline at end of file From 68593c8100a991ed38cf99bd55d6e694e2ccbace Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 9 Oct 2014 12:22:34 -0700 Subject: [PATCH 16/16] Bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c062599..779f7d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-boot", - "version": "2.0.0", + "version": "2.1.0", "description": "Convention-based bootstrapper for LoopBack applications", "keywords": [ "StrongLoop", @@ -25,7 +25,7 @@ "dependencies": { "async": "~0.9.0", "commondir": "0.0.1", - "debug": "^1.0.4", + "debug": "^2.0.0", "lodash.clonedeep": "^2.4.1", "semver": "^2.3.0", "toposort": "^0.2.10",