diff --git a/Gruntfile.js b/Gruntfile.js index 83d9140..1aee4f5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -64,7 +64,32 @@ module.exports = function(grunt) { // Whenever the "test" task is run, first clean the "tmp" dir, then run this // plugin's task(s), then test the result. - grunt.registerTask('test', ['clean', 'loopback_angular', 'nodeunit']); + grunt.registerTask('test:sync', ['clean', 'loopback_angular', 'nodeunit']); + + // need a special config and fixture to test the async case + grunt.registerTask( + 'test:async', + 'Configure the fixture and options for the async test and run it.', + function() { + + grunt.config.set('loopback_angular', { + services: { + options: { + input: 'test/fixtures/app-async.js', + appReadyEvent: 'ready', + output: 'tmp/async_app' + } + } + }); + + grunt.config.set('nodeunit', { + tests: ['test/async/*_test.js'] + }); + + grunt.task.run('clean', 'loopback_angular', 'nodeunit'); + }); + + grunt.registerTask('test', ['test:sync', 'test:async']); // By default, lint and run all tests. grunt.registerTask('default', ['jshint', 'test']); diff --git a/README.md b/README.md index 7eb69c6..b5b8786 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,80 @@ See the official [LoopBack AngularJS SDK documentation](http://docs.strongloop.com/display/DOC/AngularJS+JavaScript+SDK#AngularJSJavaScriptSDK-Gruntplugin) for the description of tasks provided by this Grunt plugin. +## Async loading of LoopBack App + +If your LoopBack app (i.e. the `input` option for this grunt task) needs to perform asynchronous operations before this task runs (like fetch a set of metadata that you want to introspect to build your models), you must export and specify an `appReady` method that registers one or more callback functions and calls them when it is done building/configuring LoopBack. + +EXAMPLE: your `app.js` might look something like this... + +``` +var loopback = require('loopback'), + app = module.exports = loopback(); + +//do something asyncronously that gets me some model metadata I can use later +var myAsyncThing = require('my-async-model-building-module'); + +myAsyncThing.run(function(metadata) { + + // Configure LoopBack models and datasources + app.boot(__dirname); + + //use the model metadata to introspectively register new models + myAsyncThing.buildModels(metadata); + + // other configuration as scaffolded by `slc lb project` + app.use(loopback.favicon()); + // etc. + app.enableAuth(); + + //app API is ready to go -- time to alert anyone who cares (lookin' at you grunt-loopback-angular) + app.emit('ready'); + + if(require.main === module) { + //start the http server + } +}); + +``` + +And then your Gruntfile would look like this… + +``` +module.exports = function(grunt) { + grunt.initConfig({ + loopback_angular: { + services: { + options: { + input: './app.js', + appReadyEvent: 'ready', + output: '../client/app/scripts/lb-services.js' + } + } + }, + docular: { + groups: [ + { + groupTitle: 'LoopBack', + groupId: 'loopback', + sections: [ + { + id: 'lbServices', + title: 'LoopBack Services', + scripts: [ '../client/app/scripts/lb-services.js' ] + } + ] + } + ] + } + }); + // Load the plugin that provides the "loopback-angular" and "grunt-docular" tasks. + grunt.loadNpmTasks('grunt-loopback-angular'); + grunt.loadNpmTasks('grunt-docular'); + // Default task(s). + grunt.registerTask('default', ['loopback_angular', 'docular']); +}; +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/tasks/loopback_angular.js b/tasks/loopback_angular.js index c8a6d77..7cd8498 100644 --- a/tasks/loopback_angular.js +++ b/tasks/loopback_angular.js @@ -24,6 +24,10 @@ module.exports = function(grunt) { function runTask() { /*jshint validthis:true */ + // need this to accomodate an input file that builds the loopback + // interface asynchronously + var done = this.async(); + // Merge task-specific and/or target-specific options with these defaults. var options = this.options({ ngModuleName: 'lbServices', @@ -50,16 +54,27 @@ module.exports = function(grunt) { grunt.fail.warn(err); } - options.apiUrl = options.apiUrl || app.get('restApiRoot') || '/api'; + var completeTask = function() { + options.apiUrl = options.apiUrl || app.get('restApiRoot') || '/api'; - grunt.log.writeln('Generating %j for the API endpoint %j', - options.ngModuleName, - options.apiUrl); + grunt.log.writeln('Generating %j for the API endpoint %j', + options.ngModuleName, + options.apiUrl); - var script = generator.services(app, options.ngModuleName, options.apiUrl); + var script = generator.services(app, options.ngModuleName, options.apiUrl); - grunt.file.write(options.output, script); + grunt.file.write(options.output, script); - grunt.log.ok('Generated Angular services file %j', options.output); + grunt.log.ok('Generated Angular services file %j', options.output); + + done(); + }; + + if (options.appReadyEvent) { + //register with the app's ready event + app.on(options.appReadyEvent, completeTask); + } else { + completeTask(); + } } }; diff --git a/test/async/loopback_angular_async_test.js b/test/async/loopback_angular_async_test.js new file mode 100644 index 0000000..b80d57b --- /dev/null +++ b/test/async/loopback_angular_async_test.js @@ -0,0 +1,43 @@ +'use strict'; + +var grunt = require('grunt'); +var parse = require('loopback-angular/parse-helper'); + +/* + ======== A Handy Little Nodeunit Reference ======== + https://github.com/caolan/nodeunit + + Test methods: + test.expect(numAssertions) + test.done() + Test assertions: + test.ok(value, [message]) + test.equal(actual, expected, [message]) + test.notEqual(actual, expected, [message]) + test.deepEqual(actual, expected, [message]) + test.notDeepEqual(actual, expected, [message]) + test.strictEqual(actual, expected, [message]) + test.notStrictEqual(actual, expected, [message]) + test.throws(block, [error], [message]) + test.doesNotThrow(block, [error], [message]) + test.ifError(value) +*/ + +// Note: the tests are depending on targets configured in Gruntfile.js + +exports.loopback_angular = { + setUp: function(done) { + // setup here if necessary + done(); + }, + + async_app: function(test) { + var script = grunt.file.read('tmp/async_app'); + + // the value "lbservices" is the --module-name default + test.equal(parse.moduleName(script), 'lbServices'); + // the value "/rest-api-root" is hard-coded in fixtures/app.js + test.equal(parse.baseUrl(script), '/rest-api-root'); + test.done(); + } +}; diff --git a/test/fixtures/app-async.js b/test/fixtures/app-async.js new file mode 100644 index 0000000..5c77b67 --- /dev/null +++ b/test/fixtures/app-async.js @@ -0,0 +1,21 @@ +var loopback = require('loopback'); +var app = loopback(); + +module.exports = app; + +//simulate the case where there are async operations that the app does before it's ready to have its lb-services generated +process.nextTick(function() { + + // Setup default datasources for autoAttach() + app.dataSource('db', { connector: 'memory', defaultForType: 'db' }); + app.dataSource('mail', { connector: 'mail', defaultForType: 'mail' }); + + // Attach all built-in models + loopback.autoAttach(); + + // Configure REST API path + app.set('restApiRoot', '/rest-api-root'); + + //let anything that cares (require'ing code) know the app is ready + app.emit('ready'); +}); \ No newline at end of file