From 6e36f0200598bb232f1bbba0b0233ace8bb1546a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Fri, 18 Aug 2017 12:42:54 +0200 Subject: [PATCH] Add support for ES6 style async boot scripts --- lib/plugins/boot-script.js | 31 ++-- test/executor.test.js | 150 +++++++++++++----- .../simple-app/boot/promise-callback.js | 15 ++ test/fixtures/simple-app/boot/promise.js | 21 +++ test/fixtures/simple-app/boot/reject.js | 14 ++ test/fixtures/simple-app/boot/thenable.js | 19 +++ test/fixtures/simple-app/boot/throw.js | 12 ++ 7 files changed, 211 insertions(+), 51 deletions(-) create mode 100644 test/fixtures/simple-app/boot/promise-callback.js 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/plugins/boot-script.js b/lib/plugins/boot-script.js index e5d0e3c..05bce38 100644 --- a/lib/plugins/boot-script.js +++ b/lib/plugins/boot-script.js @@ -81,23 +81,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); - var error; - try { - f.func(app); + var cb = function(err) { + debug('Async function %s %s', err ? 'failed' : 'finished', 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 = function() {}; + }; + 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); - } catch (err) { - debug('Sync function failed %s', f.path, err); - error = err; + done(); } - done(error); + } catch (err) { + debug('Sync function failed %s', f.path, err); + done(err); } }, callback); } diff --git a/test/executor.test.js b/test/executor.test.js index fcaa8e0..6ee89a4 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -306,9 +306,15 @@ describe('executor', function() { 'barLoaded', 'barSyncLoaded', 'fooLoaded', + 'promiseLoaded', + 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', + 'promiseStarted', + 'promiseFinished', + 'thenableStarted', + 'thenableFinished', ]); }); }); @@ -322,60 +328,132 @@ describe('executor', function() { 'barLoaded', 'barSyncLoaded', 'fooLoaded', + 'promiseLoaded', + 'thenableLoaded', 'barStarted', 'barFinished', 'barSyncExecuted', + 'promiseStarted', + 'promiseFinished', + 'thenableStarted', + 'thenableFinished', ]); 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) { + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, 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) { + simpleAppInstructions(function(err, context) { + if (err) return done(err); + boot.execute(app, context.instructions, function(err) { + expect(err).to.exist.and.be.an.instanceOf(Error) + .with.property('message', 'throw'); + done(); + }); + }); + }); + }); + + describe('with boot script returning a promise and calling callback', + function() { + before(function() { + process.promiseAndCallback = true; }); - it('defines mixins from instructions - using `mixinDirs`', - function(done) { - options.mixinDirs = ['./custom-mixins']; - boot(app, options, function(err) { - if (err) return done(err); - var modelBuilder = app.registry.modelBuilder; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); - done(); - }); - }); + after(function() { + delete process.promiseAndCallback; + }); - it('defines mixins from instructions - using `mixinSources`', - function(done) { - options.mixinSources = ['./custom-mixins']; - boot(app, options, function(err) { - if (err) return done(err); - - var modelBuilder = app.registry.modelBuilder; - var registry = modelBuilder.mixins.mixins; - expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); - done(); - }); + it('should only call the callback once', function(done) { + simpleAppInstructions(function(err, context) { + if (err) return done(err); + // Note: Mocha will fail this test if done() is called twice + boot.execute(app, context.instructions, 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', + }); + + options = { + appRootDir: appdir.PATH, + }; }); + + it('defines mixins from instructions - using `mixinDirs`', + function(done) { + options.mixinDirs = ['./custom-mixins']; + boot(app, options, function(err) { + if (err) return done(err); + var modelBuilder = app.registry.modelBuilder; + var registry = modelBuilder.mixins.mixins; + expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); + done(); + }); + }); + + it('defines mixins from instructions - using `mixinSources`', + function(done) { + options.mixinSources = ['./custom-mixins']; + boot(app, options, function(err) { + if (err) return done(err); + + var modelBuilder = app.registry.modelBuilder; + var registry = modelBuilder.mixins.mixins; + expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']); + done(); + }); + }); }); describe('with PaaS and npm env variables', function() { diff --git a/test/fixtures/simple-app/boot/promise-callback.js b/test/fixtures/simple-app/boot/promise-callback.js new file mode 100644 index 0000000..1fb7e80 --- /dev/null +++ b/test/fixtures/simple-app/boot/promise-callback.js @@ -0,0 +1,15 @@ +// 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 + +'use strict'; + +var Promise = require('bluebird'); + +module.exports = function(app, callback) { + callback(); + if (process.promiseAndCallback) { + return Promise.reject(); + } +}; diff --git a/test/fixtures/simple-app/boot/promise.js b/test/fixtures/simple-app/boot/promise.js new file mode 100644 index 0000000..38c3825 --- /dev/null +++ b/test/fixtures/simple-app/boot/promise.js @@ -0,0 +1,21 @@ +// 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 + +'use strict'; + +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..7978cc4 --- /dev/null +++ b/test/fixtures/simple-app/boot/reject.js @@ -0,0 +1,14 @@ +// 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 + +'use strict'; + +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..9164c67 --- /dev/null +++ b/test/fixtures/simple-app/boot/thenable.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 + +'use strict'; + +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..2f3de71 --- /dev/null +++ b/test/fixtures/simple-app/boot/throw.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 + +'use strict'; + +module.exports = function(app) { + if (process.throwError) { + throw new Error('throw'); + } +};