Merge pull request #106 from strongloop/feature/multi-app-browserify-support
Support for multiple apps in browserified bundle.
This commit is contained in:
commit
416738b679
|
@ -12,7 +12,7 @@
|
||||||
*.swo
|
*.swo
|
||||||
*.iml
|
*.iml
|
||||||
node_modules
|
node_modules
|
||||||
generated-instructions.json
|
generated-instructions*.json
|
||||||
checkstyle.xml
|
checkstyle.xml
|
||||||
loopback-boot-*.tgz
|
loopback-boot-*.tgz
|
||||||
/test/sandbox/
|
/test/sandbox/
|
||||||
|
|
12
browser.js
12
browser.js
|
@ -10,14 +10,22 @@ var execute = require('./lib/executor');
|
||||||
* the browser bundle, see `boot.compileToBrowserify`.
|
* the browser bundle, see `boot.compileToBrowserify`.
|
||||||
*
|
*
|
||||||
* @param {Object} app The loopback app to boot, as returned by `loopback()`.
|
* @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)
|
* @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
|
// The name of the module containing instructions
|
||||||
// is hard-coded in lib/bundler
|
// is hard-coded in lib/bundler
|
||||||
var instructions = require('loopback-boot#instructions');
|
var instructions = require(moduleName);
|
||||||
execute(app, instructions);
|
execute(app, instructions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
3
index.js
3
index.js
|
@ -132,6 +132,9 @@ exports = module.exports = function bootLoopBackApp(app, options, callback) {
|
||||||
/**
|
/**
|
||||||
* Compile boot instructions and add them to a browserify bundler.
|
* Compile boot instructions and add them to a browserify bundler.
|
||||||
* @param {Object|String} options as described in `bootLoopBackApp` above.
|
* @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()`.
|
* @param {Object} bundler A browserify bundler created by `browserify()`.
|
||||||
*
|
*
|
||||||
* @header boot.compileToBrowserify(options, bundler)
|
* @header boot.compileToBrowserify(options, bundler)
|
||||||
|
|
|
@ -91,11 +91,19 @@ function bundleInstructions(instructions, bundler) {
|
||||||
b.require(instructionsStream, { expose: 'loopback-boot#instructions' });
|
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.
|
// Write the instructions to a file in our node_modules folder.
|
||||||
// The location should not really matter as long as it is .gitignore-ed
|
// The location should not really matter as long as it is .gitignore-ed
|
||||||
var instructionsFile = path.resolve(__dirname,
|
var instructionsFile = path.resolve(__dirname,
|
||||||
'..', 'generated-instructions.json');
|
'..', 'generated-' + instructionId + '.json');
|
||||||
|
|
||||||
fs.writeFileSync(instructionsFile, instructionsString, 'utf-8');
|
fs.writeFileSync(instructionsFile, instructionsString, 'utf-8');
|
||||||
bundler.require(instructionsFile, { expose: 'loopback-boot#instructions' });
|
|
||||||
|
var moduleName = 'loopback-boot#' + instructionId;
|
||||||
|
bundler.require(instructionsFile, { expose: moduleName });
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports = function compile(options) {
|
||||||
// When executor passes the instruction to loopback methods,
|
// When executor passes the instruction to loopback methods,
|
||||||
// loopback modifies the data. Since we are loading the data using `require`,
|
// loopback modifies the data. Since we are loading the data using `require`,
|
||||||
// such change affects also code that calls `require` for the same file.
|
// such change affects also code that calls `require` for the same file.
|
||||||
return cloneDeep({
|
var instructions = {
|
||||||
config: appConfig,
|
config: appConfig,
|
||||||
dataSources: dataSourcesConfig,
|
dataSources: dataSourcesConfig,
|
||||||
models: modelInstructions,
|
models: modelInstructions,
|
||||||
|
@ -94,7 +94,12 @@ module.exports = function compile(options) {
|
||||||
files: {
|
files: {
|
||||||
boot: bootScripts
|
boot: bootScripts
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (options.appId)
|
||||||
|
instructions.appId = options.appId;
|
||||||
|
|
||||||
|
return cloneDeep(instructions);
|
||||||
};
|
};
|
||||||
|
|
||||||
function assertIsValidConfig(name, config) {
|
function assertIsValidConfig(name, config) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
var boot = require('../');
|
var boot = require('../');
|
||||||
|
var exportBrowserifyToFile = require('./helpers/browserify').exportToSandbox;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
var browserify = require('browserify');
|
var browserify = require('browserify');
|
||||||
var sandbox = require('./helpers/sandbox');
|
var sandbox = require('./helpers/sandbox');
|
||||||
var vm = require('vm');
|
var vm = require('vm');
|
||||||
|
var createBrowserLikeContext = require('./helpers/browser').createContext;
|
||||||
|
var printContextLogs = require('./helpers/browser').printContextLogs;
|
||||||
|
|
||||||
var compileStrategies = {
|
var compileStrategies = {
|
||||||
'default': function(appDir) {
|
'default': function(appDir) {
|
||||||
|
@ -92,14 +95,7 @@ function browserifyTestApp(appDir, strategy, next) {
|
||||||
|
|
||||||
boot.compileToBrowserify(appDir, b);
|
boot.compileToBrowserify(appDir, b);
|
||||||
|
|
||||||
var bundlePath = sandbox.resolve('browser-app-bundle.js');
|
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
||||||
var out = fs.createWriteStream(bundlePath);
|
|
||||||
b.bundle().pipe(out);
|
|
||||||
|
|
||||||
out.on('error', function(err) { return next(err); });
|
|
||||||
out.on('close', function() {
|
|
||||||
next(null, bundlePath);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeBundledApp(bundlePath) {
|
function executeBundledApp(bundlePath) {
|
||||||
|
@ -112,61 +108,3 @@ function executeBundledApp(bundlePath) {
|
||||||
|
|
||||||
return app;
|
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
var loopback = require('loopback');
|
||||||
|
var boot = require('../../../');
|
||||||
|
|
||||||
|
var app = module.exports = loopback();
|
||||||
|
|
||||||
|
boot(app, {
|
||||||
|
appId: 'browserApp2',
|
||||||
|
appRootDir: __dirname
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"db": {
|
||||||
|
"connector": "remote"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"sources": [
|
||||||
|
"./models",
|
||||||
|
"loopback/common/models"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Robot": {
|
||||||
|
"dataSource": "db"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = function(Robot) {
|
||||||
|
Robot.settings._customized = 'Robot';
|
||||||
|
Robot.base.settings._customized = 'Robot';
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "Robot",
|
||||||
|
"base": "PersistedModel"
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue