Support for multiple apps in browserified bundle.

This commit is contained in:
Krishna Raman 2015-03-12 18:03:35 -07:00
parent 61798455f8
commit 311b892a0f
14 changed files with 248 additions and 74 deletions

2
.gitignore vendored
View File

@ -12,7 +12,7 @@
*.swo
*.iml
node_modules
generated-instructions.json
generated-instructions*.json
checkstyle.xml
loopback-boot-*.tgz
/test/sandbox/

View File

@ -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);
};

View File

@ -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)

View File

@ -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 });
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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]);
}
}
}

9
test/fixtures/browser-app-2/app.js vendored Normal file
View File

@ -0,0 +1,9 @@
var loopback = require('loopback');
var boot = require('../../../');
var app = module.exports = loopback();
boot(app, {
appId: 'browserApp2',
appRootDir: __dirname
});

View File

@ -0,0 +1,5 @@
{
"db": {
"connector": "remote"
}
}

View File

@ -0,0 +1,11 @@
{
"_meta": {
"sources": [
"./models",
"loopback/common/models"
]
},
"Robot": {
"dataSource": "db"
}
}

View File

@ -0,0 +1,4 @@
module.exports = function(Robot) {
Robot.settings._customized = 'Robot';
Robot.base.settings._customized = 'Robot';
};

View File

@ -0,0 +1,4 @@
{
"name": "Robot",
"base": "PersistedModel"
}

61
test/helpers/browser.js Normal file
View File

@ -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;

View File

@ -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;