Merge branch 'release/1.0.0' into production
This commit is contained in:
commit
e0ccb67886
|
@ -13,3 +13,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
checkstyle.xml
|
checkstyle.xml
|
||||||
loopback-boot-*.tgz
|
loopback-boot-*.tgz
|
||||||
|
/test/sandbox/
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
coverage/
|
coverage/
|
||||||
|
test/sandbox/
|
||||||
|
|
|
@ -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.
|
16
README.md
16
README.md
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
LoopBack Boot is a convention-based bootstrapper for LoopBack applications.
|
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)
|
* [Creating a LoopBack application](http://docs.strongloop.com/display/DOC/Creating+a+LoopBack+application)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -11,5 +12,16 @@ LoopBack Boot is a convention-based bootstrapper for LoopBack applications.
|
||||||
|
|
||||||
## Usage
|
## 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.
|
||||||
|
|
|
@ -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;
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"title": "Bootstrap API",
|
||||||
|
"depth": 2
|
||||||
|
},
|
||||||
|
"index.js",
|
||||||
|
"browser.js",
|
||||||
|
"docs/configuration.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>
|
||||||
|
```
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -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;
|
|
@ -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' });
|
||||||
|
}
|
|
@ -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.<String>} 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 [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.<String>} 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.<String>} files
|
||||||
|
* @returns {Array.<Object>}
|
||||||
|
*/
|
||||||
|
function loadConfigFiles(files) {
|
||||||
|
return files.map(function(f) {
|
||||||
|
var config = require(f);
|
||||||
|
Object.defineProperty(config, '_filename', {
|
||||||
|
enumerable: false,
|
||||||
|
value: f
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge multiple configuration objects into a single one.
|
||||||
|
* @param {Array.<Object>} configObjects
|
||||||
|
* @param {function(target:Object, config:Object, filename:String)} mergeFn
|
||||||
|
*/
|
||||||
|
function mergeConfigurations(configObjects, mergeFn) {
|
||||||
|
var result = configObjects.shift() || {};
|
||||||
|
while(configObjects.length) {
|
||||||
|
var next = configObjects.shift();
|
||||||
|
mergeFn(result, next, next._filename);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeDataSourceConfig(target, config, fileName) {
|
||||||
|
for (var ds in target) {
|
||||||
|
var err = applyCustomConfig(target[ds], config[ds]);
|
||||||
|
if (err) {
|
||||||
|
throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAppConfig(target, config, fileName) {
|
||||||
|
var err = applyCustomConfig(target, config);
|
||||||
|
if (err) {
|
||||||
|
throw new Error('Cannot apply ' + fileName + ': ' + err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCustomConfig(target, config) {
|
||||||
|
for (var key in config) {
|
||||||
|
var value = config[key];
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return 'override for the option `' + key + '` is not a value type.';
|
||||||
|
}
|
||||||
|
target[key] = value;
|
||||||
|
}
|
||||||
|
return null; // no error
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to read a config file with .json extension
|
||||||
|
* @param cwd Dirname of the file
|
||||||
|
* @param fileName Name of the file without extension
|
||||||
|
* @returns {Object|undefined} Content of the file, undefined if not found.
|
||||||
|
*/
|
||||||
|
function tryReadJsonConfig(cwd, fileName) {
|
||||||
|
try {
|
||||||
|
return require(path.join(cwd, fileName + '.json'));
|
||||||
|
} catch(e) {
|
||||||
|
if(e.code !== 'MODULE_NOT_FOUND') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
var assert = require('assert');
|
||||||
|
var _ = require('underscore');
|
||||||
|
var loopback = require('loopback');
|
||||||
|
var debug = require('debug')('loopback:boot:executor');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute bootstrap instructions gathered by `boot.compile`.
|
||||||
|
*
|
||||||
|
* @options {Object} app The loopback app to boot.
|
||||||
|
* @options {Object} instructions Boot instructions.
|
||||||
|
*
|
||||||
|
* @header boot.execute(instructions)
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = function execute(app, instructions) {
|
||||||
|
setHost(app, instructions);
|
||||||
|
setPort(app, instructions);
|
||||||
|
setApiRoot(app, instructions);
|
||||||
|
applyAppConfig(app, instructions);
|
||||||
|
|
||||||
|
setupDataSources(app, instructions);
|
||||||
|
setupModels(app, instructions);
|
||||||
|
autoAttach();
|
||||||
|
|
||||||
|
runBootScripts(app, instructions);
|
||||||
|
|
||||||
|
enableAnonymousSwagger(app, instructions);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setHost(app, instructions) {
|
||||||
|
//jshint camelcase:false
|
||||||
|
var host =
|
||||||
|
process.env.npm_config_host ||
|
||||||
|
process.env.OPENSHIFT_SLS_IP ||
|
||||||
|
process.env.OPENSHIFT_NODEJS_IP ||
|
||||||
|
process.env.HOST ||
|
||||||
|
instructions.app.host ||
|
||||||
|
process.env.npm_package_config_host ||
|
||||||
|
app.get('host');
|
||||||
|
|
||||||
|
if(host !== undefined) {
|
||||||
|
assert(typeof host === 'string', 'app.host must be a string');
|
||||||
|
app.set('host', host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPort(app, instructions) {
|
||||||
|
//jshint camelcase:false
|
||||||
|
var port = _.find([
|
||||||
|
process.env.npm_config_port,
|
||||||
|
process.env.OPENSHIFT_SLS_PORT,
|
||||||
|
process.env.OPENSHIFT_NODEJS_PORT,
|
||||||
|
process.env.PORT,
|
||||||
|
instructions.app.port,
|
||||||
|
process.env.npm_package_config_port,
|
||||||
|
app.get('port'),
|
||||||
|
3000
|
||||||
|
], _.isFinite);
|
||||||
|
|
||||||
|
if(port !== undefined) {
|
||||||
|
var portType = typeof port;
|
||||||
|
assert(portType === 'string' || portType === 'number',
|
||||||
|
'app.port must be a string or number');
|
||||||
|
app.set('port', port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setApiRoot(app, instructions) {
|
||||||
|
var restApiRoot =
|
||||||
|
instructions.app.restApiRoot ||
|
||||||
|
app.get('restApiRoot') ||
|
||||||
|
'/api';
|
||||||
|
|
||||||
|
assert(restApiRoot !== undefined, 'app.restBasePath is required');
|
||||||
|
assert(typeof restApiRoot === 'string',
|
||||||
|
'app.restApiRoot must be a string');
|
||||||
|
assert(/^\//.test(restApiRoot),
|
||||||
|
'app.restApiRoot must start with "/"');
|
||||||
|
app.set('restApiRoot', restApiRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAppConfig(app, instructions) {
|
||||||
|
var appConfig = instructions.app;
|
||||||
|
for(var configKey in appConfig) {
|
||||||
|
var cur = app.get(configKey);
|
||||||
|
if(cur === undefined || cur === null) {
|
||||||
|
app.set(configKey, appConfig[configKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDataSources(app, instructions) {
|
||||||
|
forEachKeyedObject(instructions.dataSources, function(key, obj) {
|
||||||
|
app.dataSource(key, obj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupModels(app, instructions) {
|
||||||
|
forEachKeyedObject(instructions.models, function(key, obj) {
|
||||||
|
app.model(key, obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
runScripts(app, instructions.files.models);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEachKeyedObject(obj, fn) {
|
||||||
|
if(typeof obj !== 'object') return;
|
||||||
|
|
||||||
|
Object.keys(obj).forEach(function(key) {
|
||||||
|
fn(key, obj[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function runScripts(app, list) {
|
||||||
|
if (!list || !list.length) return;
|
||||||
|
list.forEach(function(filepath) {
|
||||||
|
var exports = tryRequire(filepath);
|
||||||
|
if (isFunctionNotModelCtor(exports))
|
||||||
|
exports(app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFunctionNotModelCtor(fn) {
|
||||||
|
return typeof fn === 'function' &&
|
||||||
|
!(fn.prototype instanceof loopback.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryRequire(modulePath) {
|
||||||
|
try {
|
||||||
|
return require.apply(this, arguments);
|
||||||
|
} catch(e) {
|
||||||
|
if(e.code === 'MODULE_NOT_FOUND') {
|
||||||
|
debug('Warning: cannot require %s - module not found.', modulePath);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
console.error('failed to require "%s"', modulePath);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated, will be removed soon
|
||||||
|
function autoAttach() {
|
||||||
|
try {
|
||||||
|
loopback.autoAttach();
|
||||||
|
} catch(e) {
|
||||||
|
if(e.name === 'AssertionError') {
|
||||||
|
console.warn(e);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runBootScripts(app, instructions) {
|
||||||
|
runScripts(app, instructions.files.boot);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableAnonymousSwagger(app, instructions) {
|
||||||
|
// disable token requirement for swagger, if available
|
||||||
|
var swagger = app.remotes().exports.swagger;
|
||||||
|
if (!swagger) return;
|
||||||
|
|
||||||
|
var appConfig = instructions.app;
|
||||||
|
var requireTokenForSwagger = appConfig.swagger &&
|
||||||
|
appConfig.swagger.requireToken;
|
||||||
|
swagger.requireToken = requireTokenForSwagger || false;
|
||||||
|
}
|
20
package.json
20
package.json
|
@ -13,11 +13,29 @@
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "Dual MIT/StrongLoop",
|
"name": "Dual MIT/StrongLoop",
|
||||||
"url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE"
|
"url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": "^1.6.0",
|
||||||
|
"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",
|
||||||
|
"browserify": "^4.1.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"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,220 @@
|
||||||
|
var boot = require('../');
|
||||||
|
var fs = require('fs-extra');
|
||||||
|
var path = require('path');
|
||||||
|
var assert = require('assert');
|
||||||
|
var expect = require('must');
|
||||||
|
var sandbox = require('./helpers/sandbox');
|
||||||
|
var appdir = require('./helpers/appdir');
|
||||||
|
|
||||||
|
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||||
|
|
||||||
|
describe('compiler', function() {
|
||||||
|
beforeEach(sandbox.reset);
|
||||||
|
beforeEach(appdir.init);
|
||||||
|
|
||||||
|
describe('from options', function() {
|
||||||
|
var options, instructions, appConfig;
|
||||||
|
beforeEach(function() {
|
||||||
|
options = {
|
||||||
|
app: {
|
||||||
|
port: 3000,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
restApiRoot: '/rest-api',
|
||||||
|
foo: {bar: 'bat'},
|
||||||
|
baz: true
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
'foo-bar-bat-baz': {
|
||||||
|
options: {
|
||||||
|
plural: 'foo-bar-bat-bazzies'
|
||||||
|
},
|
||||||
|
dataSource: 'the-db'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataSources: {
|
||||||
|
'the-db': {
|
||||||
|
connector: 'memory',
|
||||||
|
defaultForType: 'db'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
instructions = boot.compile(options);
|
||||||
|
appConfig = instructions.app;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has port setting', function() {
|
||||||
|
expect(appConfig).to.have.property('port', 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has host setting', function() {
|
||||||
|
expect(appConfig).to.have.property('host', '127.0.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has restApiRoot setting', function() {
|
||||||
|
expect(appConfig).to.have.property('restApiRoot', '/rest-api');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has other settings', function() {
|
||||||
|
expect(appConfig).to.have.property('baz', true);
|
||||||
|
expect(appConfig.foo, 'appConfig.foo').to.eql({
|
||||||
|
bar: 'bat'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has models definition', function() {
|
||||||
|
expect(instructions.models).to.eql(options.models);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has datasources definition', function() {
|
||||||
|
expect(instructions.dataSources).to.eql(options.dataSources);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('from directory', function() {
|
||||||
|
it('loads config files', function() {
|
||||||
|
var instructions = boot.compile(SIMPLE_APP);
|
||||||
|
assert(instructions.models.foo);
|
||||||
|
assert(instructions.models.foo.dataSource);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges datasource configs from multiple files', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeConfigFileSync('datasources.local.json', {
|
||||||
|
db: { local: 'applied' }
|
||||||
|
});
|
||||||
|
|
||||||
|
var env = process.env.NODE_ENV || 'development';
|
||||||
|
appdir.writeConfigFileSync('datasources.' + env + '.json', {
|
||||||
|
db: { env: 'applied' }
|
||||||
|
});
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
var db = instructions.dataSources.db;
|
||||||
|
expect(db).to.have.property('local', 'applied');
|
||||||
|
expect(db).to.have.property('env', 'applied');
|
||||||
|
|
||||||
|
var expectedLoadOrder = ['local', 'env'];
|
||||||
|
var actualLoadOrder = Object.keys(db).filter(function(k) {
|
||||||
|
return expectedLoadOrder.indexOf(k) !== -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports .js for custom datasource config files', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeFileSync('datasources.local.js',
|
||||||
|
'module.exports = { db: { fromJs: true } };');
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
var db = instructions.dataSources.db;
|
||||||
|
expect(db).to.have.property('fromJs', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to merge Object properties', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeConfigFileSync('datasources.local.json', {
|
||||||
|
db: { nested: { key: 'value' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(function() { boot.compile(appdir.PATH); })
|
||||||
|
.to.throw(/`nested` is not a value type/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses to merge Array properties', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeConfigFileSync('datasources.local.json', {
|
||||||
|
db: { nested: ['value'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(function() { boot.compile(appdir.PATH); })
|
||||||
|
.to.throw(/`nested` is not a value type/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges app configs from multiple files', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
|
||||||
|
appdir.writeConfigFileSync('app.local.json', { cfgLocal: 'applied' });
|
||||||
|
|
||||||
|
var env = process.env.NODE_ENV || 'development';
|
||||||
|
appdir.writeConfigFileSync('app.' + env + '.json', { cfgEnv: 'applied' });
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
var appConfig = instructions.app;
|
||||||
|
|
||||||
|
expect(appConfig).to.have.property('cfgLocal', 'applied');
|
||||||
|
expect(appConfig).to.have.property('cfgEnv', 'applied');
|
||||||
|
|
||||||
|
var expectedLoadOrder = ['cfgLocal', 'cfgEnv'];
|
||||||
|
var actualLoadOrder = Object.keys(appConfig).filter(function(k) {
|
||||||
|
return expectedLoadOrder.indexOf(k) !== -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(actualLoadOrder, 'load order').to.eql(expectedLoadOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports .js for custom app config files', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeFileSync('app.local.js',
|
||||||
|
'module.exports = { fromJs: true };');
|
||||||
|
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
var appConfig = instructions.app;
|
||||||
|
|
||||||
|
expect(appConfig).to.have.property('fromJs', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports `dsRootDir` option', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
|
||||||
|
var customDir = path.resolve(appdir.PATH, 'custom');
|
||||||
|
fs.mkdirsSync(customDir);
|
||||||
|
fs.renameSync(
|
||||||
|
path.resolve(appdir.PATH, 'datasources.json'),
|
||||||
|
path.resolve(customDir, 'datasources.json'));
|
||||||
|
|
||||||
|
var instructions = boot.compile({
|
||||||
|
appRootDir: appdir.PATH,
|
||||||
|
dsRootDir: path.resolve(appdir.PATH, 'custom')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instructions.dataSources).to.have.property('db');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports `modelsRootDir` option', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeConfigFileSync('custom/models.json', {
|
||||||
|
foo: { dataSource: 'db' }
|
||||||
|
});
|
||||||
|
|
||||||
|
var fooJs = appdir.writeFileSync('custom/models/foo.js', '');
|
||||||
|
|
||||||
|
var instructions = boot.compile({
|
||||||
|
appRootDir: appdir.PATH,
|
||||||
|
modelsRootDir: path.resolve(appdir.PATH, 'custom')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(instructions.models).to.have.property('foo');
|
||||||
|
expect(instructions.files.models).to.eql([fooJs]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes boot/*.js scripts', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
var initJs = appdir.writeFileSync('boot/init.js',
|
||||||
|
'module.exports = function(app) { app.fnCalled = true; };');
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
expect(instructions.files.boot).to.eql([initJs]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports models/ subdirectires that are not require()able', function() {
|
||||||
|
appdir.createConfigFilesSync();
|
||||||
|
appdir.writeFileSync('models/test/model.test.js',
|
||||||
|
'throw new Error("should not been called");');
|
||||||
|
var instructions = boot.compile(appdir.PATH);
|
||||||
|
|
||||||
|
expect(instructions.files.models).to.eql([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,239 @@
|
||||||
|
var boot = require('../');
|
||||||
|
var path = require('path');
|
||||||
|
var loopback = require('loopback');
|
||||||
|
var assert = require('assert');
|
||||||
|
var expect = require('must');
|
||||||
|
var sandbox = require('./helpers/sandbox');
|
||||||
|
var appdir = require('./helpers/appdir');
|
||||||
|
|
||||||
|
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||||
|
|
||||||
|
var app;
|
||||||
|
|
||||||
|
|
||||||
|
describe('executor', function() {
|
||||||
|
beforeEach(sandbox.reset);
|
||||||
|
|
||||||
|
beforeEach(appdir.init);
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
app = loopback();
|
||||||
|
});
|
||||||
|
|
||||||
|
var dummyInstructions = someInstructions({
|
||||||
|
app: {
|
||||||
|
port: 3000,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
restApiRoot: '/rest-api',
|
||||||
|
foo: { bar: 'bat' },
|
||||||
|
baz: true
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
'foo-bar-bat-baz': {
|
||||||
|
options: {
|
||||||
|
plural: 'foo-bar-bat-bazzies'
|
||||||
|
},
|
||||||
|
dataSource: 'the-db'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataSources: {
|
||||||
|
'the-db': {
|
||||||
|
connector: 'memory',
|
||||||
|
defaultForType: 'db'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('instantiates models', function() {
|
||||||
|
boot.execute(app, dummyInstructions);
|
||||||
|
assert(app.models);
|
||||||
|
assert(app.models.FooBarBatBaz);
|
||||||
|
assert(app.models.fooBarBatBaz);
|
||||||
|
assertValidDataSource(app.models.FooBarBatBaz.dataSource);
|
||||||
|
assert.isFunc(app.models.FooBarBatBaz, 'find');
|
||||||
|
assert.isFunc(app.models.FooBarBatBaz, 'create');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attaches models to data sources', function() {
|
||||||
|
boot.execute(app, dummyInstructions);
|
||||||
|
assert.equal(app.models.FooBarBatBaz.dataSource, app.dataSources.theDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('instantiates data sources', function() {
|
||||||
|
boot.execute(app, dummyInstructions);
|
||||||
|
assert(app.dataSources);
|
||||||
|
assert(app.dataSources.theDb);
|
||||||
|
assertValidDataSource(app.dataSources.theDb);
|
||||||
|
assert(app.dataSources.TheDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with boot and models files', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
boot.execute(app, simpleAppInstructions());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run `boot/*` files', function() {
|
||||||
|
assert(process.loadedFooJS);
|
||||||
|
delete process.loadedFooJS;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run `models/*` files', function() {
|
||||||
|
assert(process.loadedBarJS);
|
||||||
|
delete process.loadedBarJS;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with PaaS and npm env variables', function() {
|
||||||
|
function bootWithDefaults() {
|
||||||
|
app = loopback();
|
||||||
|
boot.execute(app, someInstructions({
|
||||||
|
app: {
|
||||||
|
port: undefined,
|
||||||
|
host: undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should honor host and port', function() {
|
||||||
|
function assertHonored(portKey, hostKey) {
|
||||||
|
process.env[hostKey] = randomPort();
|
||||||
|
process.env[portKey] = randomHost();
|
||||||
|
bootWithDefaults();
|
||||||
|
assert.equal(app.get('port'), process.env[portKey], portKey);
|
||||||
|
assert.equal(app.get('host'), process.env[hostKey], hostKey);
|
||||||
|
delete process.env[portKey];
|
||||||
|
delete process.env[hostKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_NODEJS_IP');
|
||||||
|
assertHonored('npm_config_port', 'npm_config_host');
|
||||||
|
assertHonored('npm_package_config_port', 'npm_package_config_host');
|
||||||
|
assertHonored('OPENSHIFT_SLS_PORT', 'OPENSHIFT_SLS_IP');
|
||||||
|
assertHonored('PORT', 'HOST');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize sources', function() {
|
||||||
|
/*jshint camelcase:false */
|
||||||
|
process.env.npm_config_host = randomHost();
|
||||||
|
process.env.OPENSHIFT_SLS_IP = randomHost();
|
||||||
|
process.env.OPENSHIFT_NODEJS_IP = randomHost();
|
||||||
|
process.env.HOST = randomHost();
|
||||||
|
process.env.npm_package_config_host = randomHost();
|
||||||
|
|
||||||
|
bootWithDefaults();
|
||||||
|
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||||
|
|
||||||
|
delete process.env.npm_config_host;
|
||||||
|
delete process.env.OPENSHIFT_SLS_IP;
|
||||||
|
delete process.env.OPENSHIFT_NODEJS_IP;
|
||||||
|
delete process.env.HOST;
|
||||||
|
delete process.env.npm_package_config_host;
|
||||||
|
|
||||||
|
process.env.npm_config_port = randomPort();
|
||||||
|
process.env.OPENSHIFT_SLS_PORT = randomPort();
|
||||||
|
process.env.OPENSHIFT_NODEJS_PORT = randomPort();
|
||||||
|
process.env.PORT = randomPort();
|
||||||
|
process.env.npm_package_config_port = randomPort();
|
||||||
|
|
||||||
|
bootWithDefaults();
|
||||||
|
assert.equal(app.get('host'), process.env.npm_config_host);
|
||||||
|
assert.equal(app.get('port'), process.env.npm_config_port);
|
||||||
|
|
||||||
|
delete process.env.npm_config_port;
|
||||||
|
delete process.env.OPENSHIFT_SLS_PORT;
|
||||||
|
delete process.env.OPENSHIFT_NODEJS_PORT;
|
||||||
|
delete process.env.PORT;
|
||||||
|
delete process.env.npm_package_config_port;
|
||||||
|
});
|
||||||
|
|
||||||
|
function randomHost() {
|
||||||
|
return Math.random().toString().split('.')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomPort() {
|
||||||
|
return Math.floor(Math.random() * 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should honor 0 for free port', function() {
|
||||||
|
boot.execute(app, someInstructions({ app: { port: 0 } }));
|
||||||
|
assert.equal(app.get('port'), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to port 3000', function() {
|
||||||
|
boot.execute(app, someInstructions({ app: { port: undefined } }));
|
||||||
|
assert.equal(app.get('port'), 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls function exported by models/model.js', function() {
|
||||||
|
var file = appdir.writeFileSync('models/model.js',
|
||||||
|
'module.exports = function(app) { app.fnCalled = true; };');
|
||||||
|
|
||||||
|
delete app.fnCalled;
|
||||||
|
boot.execute(app, someInstructions({ files: { models: [ file ] } }));
|
||||||
|
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls function exported by boot/init.js', function() {
|
||||||
|
var file = appdir.writeFileSync('boot/init.js',
|
||||||
|
'module.exports = function(app) { app.fnCalled = true; };');
|
||||||
|
|
||||||
|
delete app.fnCalled;
|
||||||
|
boot.execute(app, someInstructions({ files: { boot: [ file ] } }));
|
||||||
|
expect(app.fnCalled, 'exported fn was called').to.be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call Model ctor exported by models/model.json', function() {
|
||||||
|
var file = appdir.writeFileSync('models/model.js',
|
||||||
|
'var loopback = require("loopback");\n' +
|
||||||
|
'module.exports = loopback.Model.extend("foo");\n' +
|
||||||
|
'module.exports.prototype._initProperties = function() {\n' +
|
||||||
|
' global.fnCalled = true;\n' +
|
||||||
|
'};');
|
||||||
|
|
||||||
|
delete global.fnCalled;
|
||||||
|
boot.execute(app, someInstructions({ files: { models: [ file ] } }));
|
||||||
|
expect(global.fnCalled, 'exported fn was called').to.be.undefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function assertValidDataSource(dataSource) {
|
||||||
|
// has methods
|
||||||
|
assert.isFunc(dataSource, 'createModel');
|
||||||
|
assert.isFunc(dataSource, 'discoverModelDefinitions');
|
||||||
|
assert.isFunc(dataSource, 'discoverSchema');
|
||||||
|
assert.isFunc(dataSource, 'enableRemote');
|
||||||
|
assert.isFunc(dataSource, 'disableRemote');
|
||||||
|
assert.isFunc(dataSource, 'defineOperation');
|
||||||
|
assert.isFunc(dataSource, 'operations');
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.isFunc = function (obj, name) {
|
||||||
|
assert(obj, 'cannot assert function ' + name +
|
||||||
|
' on object that does not exist');
|
||||||
|
assert(typeof obj[name] === 'function', name + ' is not a function');
|
||||||
|
};
|
||||||
|
|
||||||
|
function someInstructions(values) {
|
||||||
|
var result = {
|
||||||
|
app: values.app || {},
|
||||||
|
models: values.models || {},
|
||||||
|
dataSources: values.dataSources || {},
|
||||||
|
files: {
|
||||||
|
models: [],
|
||||||
|
boot: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (values.files) {
|
||||||
|
for (var k in values.files)
|
||||||
|
result.files[k] = values.files[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function simpleAppInstructions() {
|
||||||
|
return boot.compile(SIMPLE_APP);
|
||||||
|
}
|
|
@ -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');
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"port": 3000,
|
||||||
|
"host": "127.0.0.1"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
this is not a js file!
|
|
@ -0,0 +1 @@
|
||||||
|
process.loadedFooJS = true;
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"db": {
|
||||||
|
"connector": "memory"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"foo": {
|
||||||
|
"dataSource": "db"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
process.loadedBarJS = true;
|
|
@ -0,0 +1,11 @@
|
||||||
|
var loopback = require('loopback');
|
||||||
|
|
||||||
|
// bootLoopBackApp() calls loopback.autoAttach
|
||||||
|
// which attempts to attach all models to default datasources
|
||||||
|
// one of those models is Email which requires 'email' datasource
|
||||||
|
loopback.setDefaultDataSourceForType('mail', {
|
||||||
|
connector: loopback.Mail,
|
||||||
|
transports: [
|
||||||
|
{type: 'STUB'}
|
||||||
|
]
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
var path = require('path');
|
||||||
|
var fs = require('fs-extra');
|
||||||
|
var extend = require('util')._extend;
|
||||||
|
var sandbox = require('./sandbox');
|
||||||
|
|
||||||
|
var appdir = exports;
|
||||||
|
|
||||||
|
var PATH = appdir.PATH = null;
|
||||||
|
|
||||||
|
appdir.init = function(cb) {
|
||||||
|
// Node's module loader has a very aggressive caching, therefore
|
||||||
|
// we can't reuse the same path for multiple tests
|
||||||
|
// The code here is used to generate a random string
|
||||||
|
require('crypto').randomBytes(5, function(err, buf) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
var randomStr = buf.toString('hex');
|
||||||
|
PATH = appdir.PATH = sandbox.resolve(randomStr);
|
||||||
|
cb(null, appdir.PATH);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
appdir.createConfigFilesSync = function(appConfig, dataSources, models) {
|
||||||
|
appConfig = extend({
|
||||||
|
}, appConfig);
|
||||||
|
appdir.writeConfigFileSync ('app.json', appConfig);
|
||||||
|
|
||||||
|
dataSources = extend({
|
||||||
|
db: {
|
||||||
|
connector: 'memory',
|
||||||
|
defaultForType: 'db'
|
||||||
|
}
|
||||||
|
}, dataSources);
|
||||||
|
appdir.writeConfigFileSync ('datasources.json', dataSources);
|
||||||
|
|
||||||
|
models = extend({
|
||||||
|
}, models);
|
||||||
|
appdir.writeConfigFileSync ('models.json', models);
|
||||||
|
};
|
||||||
|
|
||||||
|
appdir.writeConfigFileSync = function(name, json) {
|
||||||
|
return appdir.writeFileSync(name, JSON.stringify(json, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
appdir.writeFileSync = function(name, content) {
|
||||||
|
var filePath = path.resolve(PATH, name);
|
||||||
|
fs.mkdirsSync(path.dirname(filePath));
|
||||||
|
fs.writeFileSync(filePath, content, 'utf-8');
|
||||||
|
return filePath;
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
var fs = require('fs-extra');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var sandbox = exports;
|
||||||
|
sandbox.PATH = path.join(__dirname, '..', 'sandbox');
|
||||||
|
|
||||||
|
sandbox.reset = function() {
|
||||||
|
fs.removeSync(sandbox.PATH);
|
||||||
|
fs.mkdirsSync(sandbox.PATH);
|
||||||
|
};
|
||||||
|
|
||||||
|
sandbox.resolve = function() {
|
||||||
|
var args = Array.prototype.slice.apply(arguments);
|
||||||
|
args.unshift(sandbox.PATH);
|
||||||
|
return path.resolve.apply(path.resolve, args);
|
||||||
|
};
|
Loading…
Reference in New Issue