From 51326091b20cea96f9841f3163e80588e1c360cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sat, 29 Jul 2017 13:48:28 +0200 Subject: [PATCH] Add support for ES6 style async boot scripts --- lib/executor.js | 29 +++--- package.json | 1 + test/executor.test.js | 114 ++++++++++++++++------ test/fixtures/simple-app/boot/promise.js | 19 ++++ test/fixtures/simple-app/boot/reject.js | 12 +++ test/fixtures/simple-app/boot/thenable.js | 17 ++++ test/fixtures/simple-app/boot/throw.js | 10 ++ 7 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 test/fixtures/simple-app/boot/promise.js create mode 100644 test/fixtures/simple-app/boot/reject.js create mode 100644 test/fixtures/simple-app/boot/thenable.js create mode 100644 test/fixtures/simple-app/boot/throw.js diff --git a/lib/executor.js b/lib/executor.js index a025cf1..0351aa6 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -305,17 +305,24 @@ function runScripts(app, list, callback) { 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(); + var cb = function(err) { + debug('Async function finished %s', f.path); + done(err); + // Make sure done() isn't called twice, e.g. if a script returns a + // thenable object and also calls the passed callback. + cb = null; + }; + try { + var result = f.func(app, cb); + if (result && typeof result.then === 'function') { + result.then(function() { cb(); }, cb); + } else if (f.func.length < 2) { + debug('Sync function finished %s', f.path); + done(); + } + } catch (err) { + debug('Sync function failed %s', f.path, err); + done(err); } }, callback); } diff --git a/package.json b/package.json index 7589117..540cfb2 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "browserify": "^4.1.8", + "bluebird": "^3.1.1", "chai": "^1.10.0", "coffee-script": "^1.8.0", "coffeeify": "^0.7.0", diff --git a/test/executor.test.js b/test/executor.test.js index bdaf3e3..1123d3a 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -265,6 +265,8 @@ describe('executor', function() { 'barLoaded', 'barSyncLoaded', 'fooLoaded', + 'promiseLoaded', + 'thenableLoaded', 'barStarted', ]); @@ -275,9 +277,15 @@ describe('executor', function() { 'barLoaded', 'barSyncLoaded', 'fooLoaded', + 'promiseLoaded', + 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', + 'promiseStarted', + 'promiseFinished', + 'thenableStarted', + 'thenableFinished', 'umdLoaded', ]); done(); @@ -292,9 +300,15 @@ describe('executor', function() { 'barLoaded', 'barSyncLoaded', 'fooLoaded', + 'promiseLoaded', + 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', + 'promiseStarted', + 'promiseFinished', + 'thenableStarted', + 'thenableFinished', 'umdLoaded', ]); done(); @@ -305,7 +319,7 @@ describe('executor', function() { function(done) { var options = { app: app, - appRootDir: path.join(__dirname, './fixtures/simple-app'), + appRootDir: SIMPLE_APP, scriptExtensions: ['.customjs', '.customjs2'], }; boot.execute(app, boot.compile(options), function(err) { @@ -317,44 +331,84 @@ describe('executor', function() { done(); }); }); + }); - describe('for mixins', function() { - var options; - beforeEach(function() { - appdir.writeFileSync('custom-mixins/example.js', - 'module.exports = ' + - 'function(Model, options) {}'); + describe('with boot script returning a rejected promise', function() { + before(function() { + // Tell simple-app/boot/reject.js to return a rejected promise + process.rejectPromise = true; + }); - appdir.writeFileSync('custom-mixins/time-stamps.js', - 'module.exports = ' + - 'function(Model, options) {}'); + after(function() { + delete process.rejectPromise; + }); - appdir.writeConfigFileSync('custom-mixins/time-stamps.json', { - name: 'Timestamping', - }); + it('receives rejected promise as callback error', + function(done) { + boot.execute(app, simpleAppInstructions(), function(err) { + expect(err).to.exist.and.be.an.instanceOf(Error) + .with.property('message', 'reject'); + done(); + }); + }); + }); - options = { - appRootDir: appdir.PATH, - }; + describe('with boot script throwing an error', function() { + before(function() { + // Tell simple-app/boot/throw.js to throw an error + process.throwError = true; + }); + + after(function() { + delete process.throwError; + }); + + it('receives thrown error as callback errors', + function(done) { + boot.execute(app, simpleAppInstructions(), function(err) { + expect(err).to.exist.and.be.an.instanceOf(Error) + .with.property('message', 'throw'); + done(); + }); + }); + }); + + describe('for mixins', function() { + var options; + beforeEach(function() { + appdir.writeFileSync('custom-mixins/example.js', + 'module.exports = ' + + 'function(Model, options) {}'); + + appdir.writeFileSync('custom-mixins/time-stamps.js', + 'module.exports = ' + + 'function(Model, options) {}'); + + appdir.writeConfigFileSync('custom-mixins/time-stamps.json', { + name: 'Timestamping', }); - it('defines mixins from instructions - using `mixinDirs`', function() { - options.mixinDirs = ['./custom-mixins']; - boot(app, options); + options = { + appRootDir: appdir.PATH, + }; + }); - var modelBuilder = app.registry.modelBuilder; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); - }); + it('defines mixins from instructions - using `mixinDirs`', function() { + options.mixinDirs = ['./custom-mixins']; + boot(app, options); - 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']); - }); + 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']); }); }); diff --git a/test/fixtures/simple-app/boot/promise.js b/test/fixtures/simple-app/boot/promise.js new file mode 100644 index 0000000..e8bd9ce --- /dev/null +++ b/test/fixtures/simple-app/boot/promise.js @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2017. 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 Promise = require('bluebird'); + +process.bootFlags.push('promiseLoaded'); +module.exports = function(app) { + process.bootFlags.push('promiseStarted'); + return Promise.resolve({ + then: function(onFulfill, onReject) { + process.nextTick(function() { + process.bootFlags.push('promiseFinished'); + onFulfill(); + }); + }, + }); +}; diff --git a/test/fixtures/simple-app/boot/reject.js b/test/fixtures/simple-app/boot/reject.js new file mode 100644 index 0000000..bf47d40 --- /dev/null +++ b/test/fixtures/simple-app/boot/reject.js @@ -0,0 +1,12 @@ +// Copyright IBM Corp. 2017. 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 Promise = require('bluebird'); + +module.exports = function(app) { + if (process.rejectPromise) { + return Promise.reject(new Error('reject')); + } +}; diff --git a/test/fixtures/simple-app/boot/thenable.js b/test/fixtures/simple-app/boot/thenable.js new file mode 100644 index 0000000..8746b31 --- /dev/null +++ b/test/fixtures/simple-app/boot/thenable.js @@ -0,0 +1,17 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: loopback-boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +process.bootFlags.push('thenableLoaded'); +module.exports = function(app) { + process.bootFlags.push('thenableStarted'); + return { + then: function(onFulfill, onReject) { + process.nextTick(function() { + process.bootFlags.push('thenableFinished'); + onFulfill(); + }); + }, + }; +}; diff --git a/test/fixtures/simple-app/boot/throw.js b/test/fixtures/simple-app/boot/throw.js new file mode 100644 index 0000000..f0d2545 --- /dev/null +++ b/test/fixtures/simple-app/boot/throw.js @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2017. All Rights Reserved. +// Node module: loopback-boot +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = function(app) { + if (process.throwError) { + throw new Error('throw'); + } +};