diff --git a/browser.js b/browser.js
new file mode 100644
index 0000000..ac49bc2
--- /dev/null
+++ b/browser.js
@@ -0,0 +1,24 @@
+var execute = require('./lib/executor');
+
+/**
+ * The browser version of `bootLoopBackApp`.
+ *
+ * When loopback-boot is loaded in browser, the module exports this
+ * function instead of `bootLoopBackApp`.
+ *
+ * The function expects the boot instructions to be included in
+ * the browser bundle, see `boot.compileToBrowserify`.
+ *
+ * @param {Object} app The loopback app to boot, as returned by `loopback()`.
+ *
+ * @header bootBrowserApp(app)
+ */
+
+exports = module.exports = function bootBrowserApp(app) {
+ // The name of the module containing instructions
+ // is hard-coded in lib/bundler
+ var instructions = require('loopback-boot#instructions');
+ execute(app, instructions);
+};
+
+exports.execute = execute;
diff --git a/docs.json b/docs.json
index c87c562..05f6402 100644
--- a/docs.json
+++ b/docs.json
@@ -5,9 +5,8 @@
"depth": 2
},
"index.js",
- "lib/compiler.js",
- "lib/executor.js",
+ "browser.js",
"docs/configuration.md",
- "docs/instructions.md"
+ "docs/browserify.md"
]
}
diff --git a/docs/browserify.md b/docs/browserify.md
new file mode 100644
index 0000000..9873026
--- /dev/null
+++ b/docs/browserify.md
@@ -0,0 +1,74 @@
+## Running in a browser
+
+The bootstrap process is implemented in two steps that can be called
+independently.
+
+### Build
+
+The first step loads all configuration files, merges values from additional
+config files like `app.local.js` and produces a set of instructions
+that can be used to boot the application.
+
+These instructions must be included in the browser bundle together
+with all configuration scripts from `models/` and `boot/`.
+
+Don't worry, you don't have to understand these details.
+Just call `boot.compileToBrowserify`, it will take care of everything for you.
+
+```js
+/*-- build file --*/
+var browserify = require('browserify');
+var boot = require('loopback-boot');
+
+var b = browserify({
+ basedir: appDir,
+});
+
+// add the main application file
+b.require('./app.js', { expose: 'loopback-app' });
+
+// add boot instructions
+boot.compileToBrowserify(appDir, b);
+
+// create the bundle
+var out = fs.createWriteStream('app.bundle.js');
+b.bundle().pipe(out);
+// handle out.on('error') and out.on('close')
+```
+
+### Run
+
+In the browser, the main application file should call loopback-boot
+to setup the loopback application by executing the instructions
+contained in the browser bundle:
+
+```js
+/*-- app.js --*/
+var loopback = require('loopback');
+var boot = require('loopback-boot');
+
+var app = module.exports = loopback();
+boot(app);
+```
+
+The app object created above can be accessed via `require('loopback-app')`,
+where `loopback-app` is the identifier used for the main app file in
+the browserify build shown above.
+
+Here is a simple example demonstrating the concept:
+
+```xml
+
+
+```
diff --git a/docs/instructions.md b/docs/instructions.md
deleted file mode 100644
index 8acfc8c..0000000
--- a/docs/instructions.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## Two-step boot
-
-The methods `compile` and `execute` can be used to split the bootstrap
-process into two steps, the first one run by a build script before calling
-`browserify`, the second one run in the browser by the browserified app.
-
-The first method - `compile` - loads all configuration files, applies any
-values specified in environmental variable and produces one JSON object
-containing all instructions needed by `execute` to bootstrap the application.
-
-```js
-{
- app: {
- /* application config from app.json & friends */
- },
- models: {
- /* model configuration from models.json */
- },
- dataSources: {
- /* datasources configuration from datasources.json & friends*/
- },
- files: {
- models: [
- '/project/models/customer.js',
- /* ... */
- ],
- boot: [
- '/project/boot/authentication.js',
- /* ... */
- ]
- }
-}
-```
diff --git a/index.js b/index.js
index 9c25987..fc0e2ef 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,7 @@
var ConfigLoader = require('./lib/config-loader');
var compile = require('./lib/compiler');
var execute = require('./lib/executor');
+var addInstructionsToBrowserify = require('./lib/bundler');
/**
* Initialize an application from an options object or
@@ -67,6 +68,20 @@ exports = module.exports = function bootLoopBackApp(app, options) {
execute(app, instructions);
};
+/**
+ * Compile boot instructions and add them to a browserify bundler.
+ * @param {Object|String} options as described in `bootLoopBackApp` above.
+ * @param {Object} bundler A browserify bundler created by `browserify()`.
+ *
+ * @header boot.compileToBrowserify(options, bundler)
+ */
+exports.compileToBrowserify = function(options, bundler) {
+ addInstructionsToBrowserify(compile(options), bundler);
+};
+
+//-- undocumented low-level API --//
+
exports.ConfigLoader = ConfigLoader;
exports.compile = compile;
exports.execute = execute;
+exports.addInstructionsToBrowserify = addInstructionsToBrowserify;
diff --git a/lib/bundler.js b/lib/bundler.js
new file mode 100644
index 0000000..d26b7fe
--- /dev/null
+++ b/lib/bundler.js
@@ -0,0 +1,59 @@
+var fs = require('fs');
+var path = require('path');
+var commondir = require('commondir');
+
+/**
+ * Add boot instructions to a browserify bundler.
+ * @param {Object} instructions Boot instructions.
+ * @param {Object} bundler A browserify object created by `browserify()`.
+ */
+
+module.exports = function addInstructionsToBrowserify(instructions, bundler) {
+ bundleScripts(instructions.files, bundler);
+ bundleInstructions(instructions, bundler);
+};
+
+function bundleScripts(files, bundler) {
+ for (var key in files) {
+ var list = files[key];
+ if (!list.length) continue;
+
+ var root = commondir(files[key].map(path.dirname));
+
+ for (var ix in list) {
+ var filepath = list[ix];
+
+ // Build a short unique id that does not expose too much
+ // information about the file system, but still preserves
+ // useful information about where is the file coming from.
+ var fileid = 'loopback-boot#' + key + '#' + path.relative(root, filepath);
+
+ // Add the file to the bundle.
+ bundler.require(filepath, { expose: fileid });
+
+ // Rewrite the instructions entry with the new id that will be
+ // used to load the file via `require(fileid)`.
+ list[ix] = fileid;
+ }
+ }
+}
+
+function bundleInstructions(instructions, bundler) {
+ var instructionsString = JSON.stringify(instructions, null, 2);
+
+ /* The following code does not work due to a bug in browserify
+ * https://github.com/substack/node-browserify/issues/771
+ var instructionsStream = require('resumer')()
+ .queue(instructionsString);
+ instructionsStream.path = 'boot-instructions';
+ b.require(instructionsStream, { expose: 'loopback-boot#instructions' });
+ */
+
+ // 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,
+ '..', 'node_modules', 'instructions.json');
+
+ fs.writeFileSync(instructionsFile, instructionsString, 'utf-8');
+ bundler.require(instructionsFile, { expose: 'loopback-boot#instructions' });
+}
diff --git a/package.json b/package.json
index dcd90a8..f0b3cf9 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"url": "https://github.com/loobpack/loopback-boot"
},
"main": "index.js",
+ "browser": "browser.js",
"scripts": {
"pretest": "jshint .",
"test": "mocha"
@@ -23,14 +24,16 @@
},
"dependencies": {
"underscore": "^1.6.0",
- "debug": "^0.8.1"
+ "debug": "^0.8.1",
+ "commondir": "0.0.1"
},
"devDependencies": {
"loopback": "^1.5.0",
"mocha": "^1.19.0",
"must": "^0.11.0",
"supertest": "^0.13.0",
- "fs-extra": "^0.9.1"
+ "fs-extra": "^0.9.1",
+ "browserify": "^4.1.8"
},
"peerDependencies": {
"loopback": "1.x || 2.x"
diff --git a/test/browser.test.js b/test/browser.test.js
new file mode 100644
index 0000000..31ddf94
--- /dev/null
+++ b/test/browser.test.js
@@ -0,0 +1,91 @@
+var boot = require('../');
+var fs = require('fs');
+var path = require('path');
+var expect = require('must');
+var browserify = require('browserify');
+var sandbox = require('./helpers/sandbox');
+var vm = require('vm');
+
+describe('browser support', function() {
+ it('has API for bundling and executing boot instructions', function(done) {
+ var appDir = path.resolve(__dirname, './fixtures/browser-app');
+
+ browserifyTestApp(appDir, function(err, bundlePath) {
+ if (err) return done(err);
+
+ var app = executeBundledApp(bundlePath);
+
+ // configured in fixtures/browser-app/boot/configure.js
+ expect(app.settings).to.have.property('custom-key', 'custom-value');
+
+ done();
+ });
+ });
+});
+
+function browserifyTestApp(appDir, next) {
+ var b = browserify({
+ basedir: appDir,
+ });
+ b.require('./app.js', { expose: 'browser-app' });
+
+ boot.compileToBrowserify(appDir, b);
+
+ var bundlePath = sandbox.resolve('browser-app-bundle.js');
+ var out = fs.createWriteStream(bundlePath);
+ b.bundle({ debug: true }).pipe(out);
+
+ out.on('error', function(err) { return next(err); });
+ out.on('close', function() {
+ next(null, bundlePath);
+ });
+}
+
+function executeBundledApp(bundlePath) {
+ var code = fs.readFileSync(bundlePath);
+ var context = createBrowserLikeContext();
+ vm.runInContext(code, context, bundlePath);
+ var app = vm.runInContext('require("browser-app")', context);
+
+ printContextLogs(context);
+
+ return app;
+}
+
+function createBrowserLikeContext() {
+ return vm.createContext({
+ // required by browserify
+ XMLHttpRequest: function() { throw new Error('not implemented'); },
+
+ // used by loopback to detect browser runtime
+ window: {},
+
+ // 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: []
+ },
+ }
+ });
+}
+
+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/app.js b/test/fixtures/browser-app/app.js
new file mode 100644
index 0000000..6a9b9cb
--- /dev/null
+++ b/test/fixtures/browser-app/app.js
@@ -0,0 +1,5 @@
+var loopback = require('loopback');
+var boot = require('../../../');
+
+var app = module.exports = loopback();
+boot(app);
diff --git a/test/fixtures/browser-app/boot/configure.js b/test/fixtures/browser-app/boot/configure.js
new file mode 100644
index 0000000..b159ba7
--- /dev/null
+++ b/test/fixtures/browser-app/boot/configure.js
@@ -0,0 +1,3 @@
+module.exports = function(app) {
+ app.set('custom-key', 'custom-value');
+};