diff --git a/.gitignore b/.gitignore
index 9fad0c2..bff8718 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@
node_modules
checkstyle.xml
loopback-boot-*.tgz
+/test/sandbox/
diff --git a/.jshintignore b/.jshintignore
index 25fbf5a..d8f8557 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1,2 +1,3 @@
node_modules/
coverage/
+test/sandbox/
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..c62e362
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,14 @@
+## Changes in version 1.0
+
+ - New options: `modelsRootDir`, `dsRootDir`
+
+ - Load configuration from files, support dynamic (scripted) options
+
+ ```sh
+ app.json, app.local.*, app.{env}.*
+ datasources.json, datasources.local.*, datasources.{env}.*
+ ```
+
+ - Scripts in `models/` and `boot/` can export `function(app)`,
+ this function is then called by the bootstrapper. The existing code
+ using `var app = require('../app')` will continue to work.
diff --git a/README.md b/README.md
index 699be92..55b0e4d 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@
LoopBack Boot is a convention-based bootstrapper for LoopBack applications.
-**For full documentation, see the official StrongLoop documentation**:
+**For full documentation, see the official StrongLoop documentation:**
+
* [Creating a LoopBack application](http://docs.strongloop.com/display/DOC/Creating+a+LoopBack+application)
## Installation
@@ -11,5 +12,16 @@ LoopBack Boot is a convention-based bootstrapper for LoopBack applications.
## Usage
-TBD
+```js
+var loopback = require('loopback');
+var boot = require('loopback-boot');
+var app = loopback();
+boot(app, __dirname);
+
+app.use(loopback.rest());
+app.listen();
+```
+
+See [API docs](http://apidocs.strongloop.com/loopback-boot/) for
+complete API reference.
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
new file mode 100644
index 0000000..05f6402
--- /dev/null
+++ b/docs.json
@@ -0,0 +1,12 @@
+{
+ "content": [
+ {
+ "title": "Bootstrap API",
+ "depth": 2
+ },
+ "index.js",
+ "browser.js",
+ "docs/configuration.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/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..082e959
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,50 @@
+## Configuration and conventions
+
+### Model Definitions
+
+The following is example JSON for two `Model` definitions:
+"dealership" and "location".
+
+```js
+{
+ "dealership": {
+ // a reference, by name, to a dataSource definition
+ "dataSource": "my-db",
+ // the options passed to Model.extend(name, properties, options)
+ "options": {
+ "relations": {
+ "cars": {
+ "type": "hasMany",
+ "model": "Car",
+ "foreignKey": "dealerId"
+ }
+ }
+ },
+ // the properties passed to Model.extend(name, properties, options)
+ "properties": {
+ "id": {"id": true},
+ "name": "String",
+ "zip": "Number",
+ "address": "String"
+ }
+ },
+ "car": {
+ "dataSource": "my-db"
+ "properties": {
+ "id": {
+ "type": "String",
+ "required": true,
+ "id": true
+ },
+ "make": {
+ "type": "String",
+ "required": true
+ },
+ "model": {
+ "type": "String",
+ "required": true
+ }
+ }
+ }
+}
+```
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..fc0e2ef
--- /dev/null
+++ b/index.js
@@ -0,0 +1,87 @@
+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
+ * a set of JSON and JavaScript files.
+ *
+ * This function takes an optional argument that is either a string
+ * or an object.
+ *
+ * If the argument is a string, then it sets the application root directory
+ * based on the string value. Then it:
+ *
+ * 1. Creates DataSources from the `datasources.json` file in the application
+ * root directory.
+ *
+ * 2. Creates Models from the `models.json` file in the application
+ * root directory.
+ *
+ * If the argument is an object, then it looks for `model`, `dataSources`,
+ * and `appRootDir` properties of the object.
+ * If the object has no `appRootDir` property then it sets the current working
+ * directory as the application root directory.
+ * Then it:
+ *
+ * 1. Creates DataSources from the `options.dataSources` object.
+ *
+ * 2. Creates Models from the `options.models` object.
+ *
+ * In both cases, the function loads JavaScript files in the `/models` and
+ * `/boot` subdirectories of the application root directory with `require()`.
+ *
+ * **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
+ * files may result in models being **undefined** due to race conditions.
+ * To avoid this when using `app.boot()` make sure all models are passed
+ * as part of the `models` definition.
+ *
+ * Throws an error if the config object is not valid or if boot fails.
+ *
+ * @param app LoopBack application created by `loopback()`.
+ * @options {String|Object} options Boot options; If String, this is
+ * the application root directory; if object, has below properties.
+ * @property {String} appRootDir Directory to use when loading JSON and
+ * JavaScript files (optional).
+ * Defaults to the current directory (`process.cwd()`).
+ * @property {Object} models Object containing `Model` definitions (optional).
+ * @property {Object} dataSources Object containing `DataSource`
+ * definitions (optional).
+ * @property {String} modelsRootDir Directory to use when loading `models.json`
+ * and `models/*.js`. Defaults to `appRootDir`.
+ * @property {String} datasourcesRootDir Directory to use when loading
+ * `datasources.json`. Defaults to `appRootDir`.
+ * @property {String} env Environment type, defaults to `process.env.NODE_ENV`
+ * or `development`. Common values are `development`, `staging` and
+ * `production`; however the applications are free to use any names.
+ * @end
+ *
+ * @header bootLoopBackApp(app, [options])
+ */
+
+exports = module.exports = function bootLoopBackApp(app, options) {
+ // backwards compatibility with loopback's app.boot
+ options.env = options.env || app.get('env');
+
+ var instructions = compile(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/lib/compiler.js b/lib/compiler.js
new file mode 100644
index 0000000..ac35dab
--- /dev/null
+++ b/lib/compiler.js
@@ -0,0 +1,126 @@
+var assert = require('assert');
+var fs = require('fs');
+var path = require('path');
+var ConfigLoader = require('./config-loader');
+var debug = require('debug')('loopback:boot:compiler');
+
+/**
+ * Gather all bootstrap-related configuration data and compile it into
+ * a single object containing instruction for `boot.execute`.
+ *
+ * @options {String|Object} options Boot options; If String, this is
+ * the application root directory; if object, has the properties
+ * described in `bootLoopBackApp` options above.
+ * @return {Object}
+ *
+ * @header boot.compile(options)
+ */
+
+module.exports = function compile(options) {
+ options = options || {};
+
+ if(typeof options === 'string') {
+ options = { appRootDir: options };
+ }
+
+ var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
+ var env = options.env || process.env.NODE_ENV || 'development';
+
+ var appConfig = options.app || ConfigLoader.loadAppConfig(appRootDir, env);
+ assertIsValidConfig('app', appConfig);
+
+ var modelsRootDir = options.modelsRootDir || appRootDir;
+ var modelsConfig = options.models ||
+ ConfigLoader.loadModels(modelsRootDir, env);
+ assertIsValidConfig('model', modelsConfig);
+
+ var dsRootDir = options.dsRootDir || appRootDir;
+ var dataSourcesConfig = options.dataSources ||
+ ConfigLoader.loadDataSources(dsRootDir, env);
+ assertIsValidConfig('data source', dataSourcesConfig);
+
+ // require directories
+ var modelsScripts = findScripts(path.join(modelsRootDir, 'models'));
+ var bootScripts = findScripts(path.join(appRootDir, 'boot'));
+
+ return {
+ app: appConfig,
+ dataSources: dataSourcesConfig,
+ models: modelsConfig,
+ files: {
+ models: modelsScripts,
+ boot: bootScripts
+ }
+ };
+};
+
+function assertIsValidConfig(name, config) {
+ if(config) {
+ assert(typeof config === 'object',
+ name + ' config must be a valid JSON object');
+ }
+}
+
+/**
+ * Find all javascript files (except for those prefixed with _)
+ * and all directories.
+ * @param {String} dir Full path of the directory to enumerate.
+ * @return {Array.} A list of absolute paths to pass to `require()`.
+ * @private
+ */
+
+function findScripts(dir) {
+ assert(dir, 'cannot require directory contents without directory name');
+
+ var files = tryReadDir(dir);
+
+ // sort files in lowercase alpha for linux
+ files.sort(function(a, b) {
+ a = a.toLowerCase();
+ b = b.toLowerCase();
+
+ if (a < b) {
+ return -1;
+ } else if (b < a) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ var results = [];
+ files.forEach(function(filename) {
+ // ignore index.js and files prefixed with underscore
+ if ((filename === 'index.js') || (filename[0] === '_')) {
+ return;
+ }
+
+ var filepath = path.resolve(path.join(dir, filename));
+ var ext = path.extname(filename);
+ var stats = fs.statSync(filepath);
+
+ // only require files supported by require.extensions (.txt .md etc.)
+ if (stats.isFile()) {
+ if (ext in require.extensions)
+ results.push(filepath);
+ else
+ debug('Skipping file %s - unknown extension', filepath);
+ } else {
+ try {
+ path.join(require.resolve(filepath));
+ } catch(err) {
+ debug('Skipping directory %s - %s', filepath, err.code || err);
+ }
+ }
+ });
+
+ return results;
+}
+
+function tryReadDir() {
+ try {
+ return fs.readdirSync.apply(fs, arguments);
+ } catch(e) {
+ return [];
+ }
+}
diff --git a/lib/config-loader.js b/lib/config-loader.js
new file mode 100644
index 0000000..7cafb35
--- /dev/null
+++ b/lib/config-loader.js
@@ -0,0 +1,154 @@
+var fs = require('fs');
+var path = require('path');
+
+var ConfigLoader = exports;
+
+/**
+ * Load application config from `app.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadAppConfig = function(rootDir, env) {
+ return loadNamed(rootDir, env, 'app', mergeAppConfig);
+};
+
+/**
+ * Load data-sources config from `datasources.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadDataSources = function(rootDir, env) {
+ return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig);
+};
+
+/**
+ * Load models config from `models.json` and friends.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @returns {Object}
+ */
+ConfigLoader.loadModels = function(rootDir, env) {
+ /*jshint unused:false */
+ return tryReadJsonConfig(rootDir, 'models') || {};
+};
+
+/*-- Implementation --*/
+
+/**
+ * Load named configuration.
+ * @param {String} rootDir Directory where to look for files.
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @param {String} name
+ * @param {function(target:Object, config:Object, filename:String)} mergeFn
+ * @returns {Object}
+ */
+function loadNamed(rootDir, env, name, mergeFn) {
+ var files = findConfigFiles(rootDir, env, name);
+ var configs = loadConfigFiles(files);
+ return mergeConfigurations(configs, mergeFn);
+}
+
+/**
+ * Search `appRootDir` for all files containing configuration for `name`.
+ * @param {String} appRootDir
+ * @param {String} env Environment, usually `process.env.NODE_ENV`
+ * @param {String} name
+ * @returns {Array.} Array of absolute file paths.
+ */
+function findConfigFiles(appRootDir, env, name) {
+ var master = ifExists(name + '.json');
+ if (!master) return [];
+
+ var candidates = [
+ master,
+ ifExistsWithAnyExt(name + '.local'),
+ ifExistsWithAnyExt(name + '.' + env)
+ ];
+
+ return candidates.filter(function(c) { return c !== undefined; });
+
+ function ifExists(fileName) {
+ var filepath = path.resolve(appRootDir, fileName);
+ return fs.existsSync(filepath) ? filepath : undefined;
+ }
+
+ function ifExistsWithAnyExt(fileName) {
+ return ifExists(fileName + '.js') || ifExists(fileName + '.json');
+ }
+}
+
+/**
+ * Load configuration files into an array of objects.
+ * Attach non-enumerable `_filename` property to each object.
+ * @param {Array.} files
+ * @returns {Array.