Merge pull request #6 from strongloop/feature/browserify-support
Implement compileToBrowserify and bootBrowserApp
This commit is contained in:
commit
0647c95e99
|
@ -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;
|
|
@ -5,9 +5,8 @@
|
||||||
"depth": 2
|
"depth": 2
|
||||||
},
|
},
|
||||||
"index.js",
|
"index.js",
|
||||||
"lib/compiler.js",
|
"browser.js",
|
||||||
"lib/executor.js",
|
|
||||||
"docs/configuration.md",
|
"docs/configuration.md",
|
||||||
"docs/instructions.md"
|
"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
|
||||||
|
<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>
|
||||||
|
```
|
|
@ -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',
|
|
||||||
/* ... */
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
15
index.js
15
index.js
|
@ -1,6 +1,7 @@
|
||||||
var ConfigLoader = require('./lib/config-loader');
|
var ConfigLoader = require('./lib/config-loader');
|
||||||
var compile = require('./lib/compiler');
|
var compile = require('./lib/compiler');
|
||||||
var execute = require('./lib/executor');
|
var execute = require('./lib/executor');
|
||||||
|
var addInstructionsToBrowserify = require('./lib/bundler');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize an application from an options object or
|
* Initialize an application from an options object or
|
||||||
|
@ -67,6 +68,20 @@ exports = module.exports = function bootLoopBackApp(app, options) {
|
||||||
execute(app, instructions);
|
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.ConfigLoader = ConfigLoader;
|
||||||
exports.compile = compile;
|
exports.compile = compile;
|
||||||
exports.execute = execute;
|
exports.execute = execute;
|
||||||
|
exports.addInstructionsToBrowserify = addInstructionsToBrowserify;
|
||||||
|
|
|
@ -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' });
|
||||||
|
}
|
|
@ -13,6 +13,7 @@
|
||||||
"url": "https://github.com/loobpack/loopback-boot"
|
"url": "https://github.com/loobpack/loopback-boot"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
"browser": "browser.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pretest": "jshint .",
|
"pretest": "jshint .",
|
||||||
"test": "mocha"
|
"test": "mocha"
|
||||||
|
@ -23,14 +24,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"underscore": "^1.6.0",
|
"underscore": "^1.6.0",
|
||||||
"debug": "^0.8.1"
|
"debug": "^0.8.1",
|
||||||
|
"commondir": "0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"loopback": "^1.5.0",
|
"loopback": "^1.5.0",
|
||||||
"mocha": "^1.19.0",
|
"mocha": "^1.19.0",
|
||||||
"must": "^0.11.0",
|
"must": "^0.11.0",
|
||||||
"supertest": "^0.13.0",
|
"supertest": "^0.13.0",
|
||||||
"fs-extra": "^0.9.1"
|
"fs-extra": "^0.9.1",
|
||||||
|
"browserify": "^4.1.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"loopback": "1.x || 2.x"
|
"loopback": "1.x || 2.x"
|
||||||
|
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
var loopback = require('loopback');
|
||||||
|
var boot = require('../../../');
|
||||||
|
|
||||||
|
var app = module.exports = loopback();
|
||||||
|
boot(app);
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = function(app) {
|
||||||
|
app.set('custom-key', 'custom-value');
|
||||||
|
};
|
Loading…
Reference in New Issue