Implement compileToBrowserify and bootBrowserApp

Hide `compile` and `execute` and provide a better API for browserified
applications:

 - `boot.compileToBrowserify(options, bundler)` calls `compile` under
   the hood and adds all instructions and scripts to the bundler.

 - `bootBrowserApp(app)` is exported by loopback-boot when the module
   is loaded in a browser, the function loads the instructions as
   bundled by `compileToBrowserify`.

This new API hides all implementation details from the user and makes
it easy to add loopback-boot to any build script.
This commit is contained in:
Miroslav Bajtoš 2014-06-03 14:08:34 +02:00
parent f3207f6451
commit 5f85971d11
10 changed files with 278 additions and 38 deletions

24
browser.js Normal file
View File

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

View File

@ -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"
]
}

74
docs/browserify.md Normal file
View File

@ -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
<script src="app.bundle.js"></script>
<script>
var app = require('loopback-app');
var User = app.models.User;
User.login({ email: 'test@example.com', password: '12345', function(err, res) {
if (err) {
console.error('Login failed: ', err);
} else {
console.log('Logged in.');
}
});
</script>
```

View File

@ -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',
/* ... */
]
}
}
```

View File

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

59
lib/bundler.js Normal file
View File

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

View File

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

91
test/browser.test.js Normal file
View File

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

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

@ -0,0 +1,5 @@
var loopback = require('loopback');
var boot = require('../../../');
var app = module.exports = loopback();
boot(app);

View File

@ -0,0 +1,3 @@
module.exports = function(app) {
app.set('custom-key', 'custom-value');
};