diff --git a/.gitignore b/.gitignore index 46ffc56..6e610ce 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.swo *.iml node_modules -generated-instructions.json +generated-instructions*.json checkstyle.xml loopback-boot-*.tgz /test/sandbox/ diff --git a/browser.js b/browser.js index 7346c4a..becbec6 100644 --- a/browser.js +++ b/browser.js @@ -10,14 +10,22 @@ var execute = require('./lib/executor'); * the browser bundle, see `boot.compileToBrowserify`. * * @param {Object} app The loopback app to boot, as returned by `loopback()`. + * @param {Object|string} [options] options as described in + * `boot.compileToBrowserify`. * * @header boot(app) */ -exports = module.exports = function bootBrowserApp(app) { +exports = module.exports = function bootBrowserApp(app, options) { + // Only using options.id to identify the browserified bundle to load for + // this application. If no Id was provided, load the default bundle. + var moduleName = 'loopback-boot#instructions'; + if (options && typeof options === 'object' && options.appId) + moduleName += '-' + options.appId; + // The name of the module containing instructions // is hard-coded in lib/bundler - var instructions = require('loopback-boot#instructions'); + var instructions = require(moduleName); execute(app, instructions); }; diff --git a/index.js b/index.js index 5e82791..cf8d162 100644 --- a/index.js +++ b/index.js @@ -132,6 +132,9 @@ exports = module.exports = function bootLoopBackApp(app, options, callback) { /** * Compile boot instructions and add them to a browserify bundler. * @param {Object|String} options as described in `bootLoopBackApp` above. + * @property {String} [appId] Application identifier used to load the correct + * boot configuration when building multiple applications using browserify. + * @end * @param {Object} bundler A browserify bundler created by `browserify()`. * * @header boot.compileToBrowserify(options, bundler) diff --git a/lib/bundler.js b/lib/bundler.js index 40d8ef8..a1a8a1c 100644 --- a/lib/bundler.js +++ b/lib/bundler.js @@ -91,11 +91,19 @@ function bundleInstructions(instructions, bundler) { b.require(instructionsStream, { expose: 'loopback-boot#instructions' }); */ + var instructionId = 'instructions'; + // Create an unique instruction identifier using the application ID. + // This is only useful when multiple loopback applications are being bundled + // together. + if (instructions.appId) + instructionId += '-' + instructions.appId; + // Write the instructions to a file in our node_modules folder. // The location should not really matter as long as it is .gitignore-ed var instructionsFile = path.resolve(__dirname, - '..', 'generated-instructions.json'); - + '..', 'generated-' + instructionId + '.json'); fs.writeFileSync(instructionsFile, instructionsString, 'utf-8'); - bundler.require(instructionsFile, { expose: 'loopback-boot#instructions' }); + + var moduleName = 'loopback-boot#' + instructionId; + bundler.require(instructionsFile, { expose: moduleName }); } diff --git a/lib/compiler.js b/lib/compiler.js index 5958b03..1ea3040 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -85,7 +85,7 @@ module.exports = function compile(options) { // When executor passes the instruction to loopback methods, // loopback modifies the data. Since we are loading the data using `require`, // such change affects also code that calls `require` for the same file. - return cloneDeep({ + var instructions = { config: appConfig, dataSources: dataSourcesConfig, models: modelInstructions, @@ -94,7 +94,12 @@ module.exports = function compile(options) { files: { boot: bootScripts } - }); + }; + + if (options.appId) + instructions.appId = options.appId; + + return cloneDeep(instructions); }; function assertIsValidConfig(name, config) { diff --git a/test/browser.multiapp.test.js b/test/browser.multiapp.test.js new file mode 100644 index 0000000..fa734e0 --- /dev/null +++ b/test/browser.multiapp.test.js @@ -0,0 +1,102 @@ +var boot = require('../'); +var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox; +var fs = require('fs'); +var path = require('path'); +var expect = require('chai').expect; +var browserify = require('browserify'); +var sandbox = require('./helpers/sandbox'); +var vm = require('vm'); +var createBrowserLikeContext = require('./helpers/browser').createContext; +var printContextLogs = require('./helpers/browser').printContextLogs; + +describe('browser support for multiple apps', function() { + this.timeout(60000); // 60s to give browserify enough time to finish + + beforeEach(sandbox.reset); + + it('has API for bundling and booting multiple apps', function(done) { + var app1Dir = path.resolve(__dirname, './fixtures/browser-app'); + var app2Dir = path.resolve(__dirname, './fixtures/browser-app-2'); + + var apps = [ + { + appDir: app1Dir, + appFile: './app.js', + moduleName: 'browser-app' + }, + { + appDir: app2Dir, + appFile: './app.js', + moduleName: 'browser-app2', + appId: 'browserApp2' + } + ]; + + browserifyTestApps(apps, function(err, bundlePath) { + if (err) return done(err); + + var bundledApps = executeBundledApps(bundlePath, apps); + var app1 = bundledApps.defaultApp; + var app2 = bundledApps.browserApp2; + + expect(app1.settings).to.have.property('custom-key', 'custom-value'); + expect(Object.keys(app1.models)).to.include('Customer'); + expect(Object.keys(app1.models)).to.not.include('Robot'); + expect(app1.models.Customer.settings).to.have.property('_customized', + 'Customer'); + + expect(Object.keys(app2.models)).to.include('Robot'); + expect(Object.keys(app2.models)).to.not.include('Customer'); + + done(); + }); + }); +}); + +function browserifyTestApps(apps, next) { + var b = browserify({ + basedir: appDir, + debug: true + }); + + for (var i in apps) { + var appDir = apps[i].appDir; + var appFile = apps[i].appFile; + var moduleName = apps[i].moduleName; + var appId = apps[i].appId; + + appFile = path.join(appDir, appFile); + b.require(appFile, {expose: moduleName}); + + var opts = appDir; + if (appId) { + opts = { + appId: appId, + appRootDir: appDir + }; + } + boot.compileToBrowserify(opts, b); + } + + exportBrowserifyToFile(b, 'browser-app-bundle.js', next); +} + +function executeBundledApps(bundlePath, apps) { + var code = fs.readFileSync(bundlePath); + var context = createBrowserLikeContext(); + vm.runInContext(code, context, bundlePath); + + var script = 'var apps = {};\n'; + for (var i in apps) { + var moduleName = apps[i].moduleName; + var id = apps[i].appId || 'defaultApp'; + script += 'apps.' + id + ' = require("' + moduleName + '");\n'; + } + script += 'apps;\n'; + + var appsInContext = vm.runInContext(script, context); + + printContextLogs(context); + + return appsInContext; +} diff --git a/test/browser.test.js b/test/browser.test.js index 79277e8..19dd334 100644 --- a/test/browser.test.js +++ b/test/browser.test.js @@ -1,10 +1,13 @@ var boot = require('../'); +var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox; var fs = require('fs'); var path = require('path'); var expect = require('chai').expect; var browserify = require('browserify'); var sandbox = require('./helpers/sandbox'); var vm = require('vm'); +var createBrowserLikeContext = require('./helpers/browser').createContext; +var printContextLogs = require('./helpers/browser').printContextLogs; var compileStrategies = { 'default': function(appDir) { @@ -92,14 +95,7 @@ function browserifyTestApp(appDir, strategy, next) { boot.compileToBrowserify(appDir, b); - var bundlePath = sandbox.resolve('browser-app-bundle.js'); - var out = fs.createWriteStream(bundlePath); - b.bundle().pipe(out); - - out.on('error', function(err) { return next(err); }); - out.on('close', function() { - next(null, bundlePath); - }); + exportBrowserifyToFile(b, 'browser-app-bundle.js', next); } function executeBundledApp(bundlePath) { @@ -112,61 +108,3 @@ function executeBundledApp(bundlePath) { return app; } - -function createBrowserLikeContext() { - var context = { - // required by browserify - XMLHttpRequest: function() { throw new Error('not implemented'); }, - - localStorage: { - // used by `debug` module - debug: process.env.DEBUG - }, - - // used by DataSource.prototype.ready - setTimeout: setTimeout, - - // used by `debug` module - document: { documentElement: { style: {} } }, - - // used by `debug` module - navigator: { userAgent: 'sandbox' }, - - // used by crypto-browserify & friends - Int32Array: Int32Array, - DataView: DataView, - - // allow the browserified code to log messages - // call `printContextLogs(context)` to print the accumulated messages - console: { - log: function() { - this._logs.log.push(Array.prototype.slice.call(arguments)); - }, - warn: function() { - this._logs.warn.push(Array.prototype.slice.call(arguments)); - }, - error: function() { - this._logs.error.push(Array.prototype.slice.call(arguments)); - }, - _logs: { - log: [], - warn: [], - error: [] - }, - } - }; - - // `window` is used by loopback to detect browser runtime - context.window = context; - - return vm.createContext(context); -} - -function printContextLogs(context) { - for (var k in context.console._logs) { - var items = context.console._logs[k]; - for (var ix in items) { - console[k].apply(console, items[ix]); - } - } -} diff --git a/test/fixtures/browser-app-2/app.js b/test/fixtures/browser-app-2/app.js new file mode 100644 index 0000000..f33937a --- /dev/null +++ b/test/fixtures/browser-app-2/app.js @@ -0,0 +1,9 @@ +var loopback = require('loopback'); +var boot = require('../../../'); + +var app = module.exports = loopback(); + +boot(app, { + appId: 'browserApp2', + appRootDir: __dirname +}); diff --git a/test/fixtures/browser-app-2/datasources.json b/test/fixtures/browser-app-2/datasources.json new file mode 100644 index 0000000..618fd1f --- /dev/null +++ b/test/fixtures/browser-app-2/datasources.json @@ -0,0 +1,5 @@ +{ + "db": { + "connector": "remote" + } +} diff --git a/test/fixtures/browser-app-2/model-config.json b/test/fixtures/browser-app-2/model-config.json new file mode 100644 index 0000000..371c52d --- /dev/null +++ b/test/fixtures/browser-app-2/model-config.json @@ -0,0 +1,11 @@ +{ + "_meta": { + "sources": [ + "./models", + "loopback/common/models" + ] + }, + "Robot": { + "dataSource": "db" + } +} diff --git a/test/fixtures/browser-app-2/models/robot.js b/test/fixtures/browser-app-2/models/robot.js new file mode 100644 index 0000000..545c357 --- /dev/null +++ b/test/fixtures/browser-app-2/models/robot.js @@ -0,0 +1,4 @@ +module.exports = function(Robot) { + Robot.settings._customized = 'Robot'; + Robot.base.settings._customized = 'Robot'; +}; diff --git a/test/fixtures/browser-app-2/models/robot.json b/test/fixtures/browser-app-2/models/robot.json new file mode 100644 index 0000000..8767645 --- /dev/null +++ b/test/fixtures/browser-app-2/models/robot.json @@ -0,0 +1,4 @@ +{ + "name": "Robot", + "base": "PersistedModel" +} diff --git a/test/helpers/browser.js b/test/helpers/browser.js new file mode 100644 index 0000000..bf3e562 --- /dev/null +++ b/test/helpers/browser.js @@ -0,0 +1,61 @@ +var vm = require('vm'); + +function createContext() { + var context = { + // required by browserify + XMLHttpRequest: function() { throw new Error('not implemented'); }, + + localStorage: { + // used by `debug` module + debug: process.env.DEBUG + }, + + // used by DataSource.prototype.ready + setTimeout: setTimeout, + + // used by `debug` module + document: { documentElement: { style: {} } }, + + // used by `debug` module + navigator: { userAgent: 'sandbox' }, + + // used by crypto-browserify & friends + Int32Array: Int32Array, + DataView: DataView, + + // allow the browserified code to log messages + // call `printContextLogs(context)` to print the accumulated messages + console: { + log: function() { + this._logs.log.push(Array.prototype.slice.call(arguments)); + }, + warn: function() { + this._logs.warn.push(Array.prototype.slice.call(arguments)); + }, + error: function() { + this._logs.error.push(Array.prototype.slice.call(arguments)); + }, + _logs: { + log: [], + warn: [], + error: [] + }, + } + }; + + // `window` is used by loopback to detect browser runtime + context.window = context; + + return vm.createContext(context); +} +exports.createContext = createContext; + +function printContextLogs(context) { + for (var k in context.console._logs) { + var items = context.console._logs[k]; + for (var ix in items) { + console[k].apply(console, items[ix]); + } + } +} +exports.printContextLogs = printContextLogs; diff --git a/test/helpers/browserify.js b/test/helpers/browserify.js new file mode 100644 index 0000000..02e1fbe --- /dev/null +++ b/test/helpers/browserify.js @@ -0,0 +1,16 @@ +var fs = require('fs'); +var sandbox = require('./sandbox'); + +function exportToSandbox(b, fileName, callback) { + var bundlePath = sandbox.resolve(fileName); + var out = fs.createWriteStream(bundlePath); + b.bundle().pipe(out); + + out.on('error', function(err) { + return callback(err); + }); + out.on('close', function() { + callback(null, bundlePath); + }); +} +exports.exportToSandbox = exportToSandbox;