Merge branch 'release/2.4.0' into production

This commit is contained in:
Miroslav Bajtoš 2014-11-27 19:23:47 +01:00
commit d3145f5745
21 changed files with 896 additions and 78 deletions

24
.jscsrc Normal file
View File

@ -0,0 +1,24 @@
{
"preset": "google",
"requireCurlyBraces": [
"do",
"try",
"catch"
],
"disallowSpacesInsideObjectBrackets": null,
"requireSpaceAfterLineComment": true,
"maximumLineLength": {
"value": 80,
"allowRegex": true
},
"validateJSDoc": {
"checkParamNames": false,
"checkRedundantParams": false,
"requireParamTypes": true
},
"excludeFiles": [
"node_modules/**",
"coverage/**",
"test/sandbox/**"
]
}

View File

@ -1,24 +1,21 @@
{ {
"node": true, "node": true,
"browser": true, "browser": true,
"camelcase" : true,
"eqnull" : true, "eqnull" : true,
"indent": 2, "indent": 2,
"undef": true, "undef": true,
"unused": true, "unused": true,
"quotmark": "single", "quotmark": "single",
"maxlen": 80,
"trailing": true,
"newcap": true, "newcap": true,
"nonew": true, "nonew": true,
"sub": true, "sub": true,
"unused": "vars", "unused": "vars",
"globals": { "globals": {
"describe": true, "describe": false,
"it": true, "it": false,
"before": true, "before": false,
"beforeEach": true, "beforeEach": false,
"after": true, "after": false,
"afterEach": true "afterEach": false
} }
} }

View File

@ -1,14 +1,194 @@
## Changes in version 1.0 2014-11-27, Version 2.4.0
=========================
- New options: `modelsRootDir`, `dsRootDir` * Implement shorthand notation for middleware paths (Raymond Feng)
- Load configuration from files, support dynamic (scripted) options * Load middleware and phases from `middleware.json` (Miroslav Bajtoš)
```sh * Add jscs style check, fix violations found (Miroslav Bajtoš)
app.json, app.local.*, app.{env}.*
datasources.json, datasources.local.*, datasources.{env}.*
```
- Scripts in `models/` and `boot/` can export `function(app)`, * Clean up .jshintrc (Miroslav Bajtoš)
this function is then called by the bootstrapper. The existing code
using `var app = require('../app')` will continue to work. * Use `chai` instead of `must` (Miroslav Bajtoš)
2014-11-10, Version 2.3.1
=========================
* Bump version (Raymond Feng)
* Fix the test for built-in models on Windows (Raymond Feng)
* Fix jsdoc (Raymond Feng)
2014-10-27, Version 2.3.0
=========================
* compiler: fix coding style violations (Miroslav Bajtoš)
* support coffee-script models and client code (bitmage)
2014-10-22, Version 2.2.0
=========================
* compiler: support module-relative model sources (Miroslav Bajtoš)
* Skip definitions of built-in loopback models (Miroslav Bajtoš)
* package: update dependency versions (Miroslav Bajtoš)
* Use loopback 2.x in unit tests. (Miroslav Bajtoš)
2014-10-09, Version 2.1.0
=========================
* Bump version (Raymond Feng)
* Add support for async boot scripts (Raymond Feng)
* Clean up jsdoc comments. (Miroslav Bajtoš)
* Custom rootDir for app config (johnsoftek)
* compiler: improve merging of Arrays and Objects (Miroslav Bajtoš)
* config-loader: deeply merge Array and Object vals (Shelby Sanders)
* gitignore: add Idea's *.iml files (Miroslav Bajtoš)
* package: Add `jshint` to `devDependencies` (Miroslav Bajtoš)
* Update contribution guidelines (Ryan Graham)
* test: ensure sandbox dir is present (Miroslav Bajtoš)
* test: add `global.navigator` for browser tests (Miroslav Bajtoš)
* test: increase timeout for browserify (Miroslav Bajtoš)
* index: fix jshint error (Miroslav Bajtoš)
* documentation fix (Alex)
* Fix typo (Fabien Franzen)
* Implemented modelSources, bootDirs and bootScripts options (Fabien Franzen)
2014-07-22, Version 2.0.0
=========================
* executor: remove `Base` arg from model function (Miroslav Bajtoš)
* package: update dependency versions (Miroslav Bajtoš)
2014-07-17, Version v2.0.0-beta3
================================
* v2.0.0-beta3 (Miroslav Bajtoš)
* compiler: return a clone of instructions (Miroslav Bajtoš)
* test: export Int32Array and DataView for browser (Miroslav Bajtoš)
* v2.0.0-beta2 (Miroslav Bajtoš)
* Rename `models.json` to `model-config.json` (Miroslav Bajtoš)
* Remove non-API docs. (Rand McKinney)
* 2.0.0-beta1 (Miroslav Bajtoš)
* test: fix jshint warnings (Miroslav Bajtoš)
* compiler: fix references to loopback (Miroslav Bajtoš)
* Rename `app.json` to `config.json` (Miroslav Bajtoš)
* compiler: Sort models topologically (Miroslav Bajtoš)
* executor: Split model boot into two phases (Miroslav Bajtoš)
* compiler: Move model-sources cfg to models.json (Miroslav Bajtoš)
* package: Bump up the version to 2.0.0-dev (Miroslav Bajtoš)
* Rework model configuration (Miroslav Bajtoš)
* Remove auto-attach. (Miroslav Bajtoš)
* Change models.json to configure existing models (Miroslav Bajtoš)
2014-07-17, Version 1.1.1
=========================
* compiler: return a clone of instructions (Miroslav Bajtoš)
* Remove README from API docs (Rand McKinney)
2014-07-17, Version 2.0.0-beta2
===============================
* test: export Int32Array and DataView for browser (Miroslav Bajtoš)
* v2.0.0-beta2 (Miroslav Bajtoš)
* Rename `models.json` to `model-config.json` (Miroslav Bajtoš)
* Remove non-API docs. (Rand McKinney)
2014-06-26, Version 2.0.0-beta1
===============================
* 2.0.0-beta1 (Miroslav Bajtoš)
* test: fix jshint warnings (Miroslav Bajtoš)
* compiler: fix references to loopback (Miroslav Bajtoš)
* Rename `app.json` to `config.json` (Miroslav Bajtoš)
* compiler: Sort models topologically (Miroslav Bajtoš)
* executor: Split model boot into two phases (Miroslav Bajtoš)
* compiler: Move model-sources cfg to models.json (Miroslav Bajtoš)
* package: Bump up the version to 2.0.0-dev (Miroslav Bajtoš)
* Rework model configuration (Miroslav Bajtoš)
* Remove auto-attach. (Miroslav Bajtoš)
* Change models.json to configure existing models (Miroslav Bajtoš)
2014-06-26, Version 1.1.0
=========================
* docs: move hand-written content to README.md (Miroslav Bajtoš)
* executor: remove direct reference to loopback (Miroslav Bajtoš)
* Update link to doc (Rand McKinney)
* package: Fix repository url (Miroslav Bajtoš)
* Drop peer dep on loopback; add a runtime check (Miroslav Bajtoš)
* Wrap too long lines (Miroslav Bajtoš)
* Add disclaimer to JSDoc and small correction. (crandmck)
2014-06-05, Version 1.0.0
=========================
* First release!

View File

@ -107,6 +107,8 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* `production`; however the applications are free to use any names. * `production`; however the applications are free to use any names.
* @property {Array.<String>} [modelSources] List of directories where to look * @property {Array.<String>} [modelSources] List of directories where to look
* for files containing model definitions. * for files containing model definitions.
* @property {Object} [middleware] Middleware configuration to use instead
* of `{appRootDir}/middleware.json`
* @property {Array.<String>} [bootDirs] List of directories where to look * @property {Array.<String>} [bootDirs] List of directories where to look
* for boot scripts. * for boot scripts.
* @property {Array.<String>} [bootScripts] List of script files to execute * @property {Array.<String>} [bootScripts] List of script files to execute
@ -136,7 +138,7 @@ exports.compileToBrowserify = function(options, bundler) {
addInstructionsToBrowserify(compile(options), bundler); addInstructionsToBrowserify(compile(options), bundler);
}; };
//-- undocumented low-level API --// /*-- undocumented low-level API --*/
exports.ConfigLoader = ConfigLoader; exports.ConfigLoader = ConfigLoader;
exports.compile = compile; exports.compile = compile;

View File

@ -1,6 +1,7 @@
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var commondir = require('commondir'); var commondir = require('commondir');
var cloneDeep = require('lodash.clonedeep');
/** /**
* Add boot instructions to a browserify bundler. * Add boot instructions to a browserify bundler.
@ -60,6 +61,17 @@ function addScriptsToBundle(name, list, bundler) {
} }
function bundleInstructions(instructions, bundler) { function bundleInstructions(instructions, bundler) {
instructions = cloneDeep(instructions);
var hasMiddleware = instructions.middleware.phases.length ||
instructions.middleware.middleware.length;
if (hasMiddleware) {
console.warn(
'Discarding middleware instructions,' +
' loopback client does not support middleware.');
}
delete instructions.middleware;
var instructionsString = JSON.stringify(instructions, null, 2); var instructionsString = JSON.stringify(instructions, null, 2);
/* The following code does not work due to a bug in browserify /* The following code does not work due to a bug in browserify

View File

@ -22,7 +22,7 @@ var Module = require('module');
module.exports = function compile(options) { module.exports = function compile(options) {
options = options || {}; options = options || {};
if(typeof options === 'string') { if (typeof options === 'string') {
options = { appRootDir: options }; options = { appRootDir: options };
} }
@ -44,6 +44,14 @@ module.exports = function compile(options) {
ConfigLoader.loadDataSources(dsRootDir, env); ConfigLoader.loadDataSources(dsRootDir, env);
assertIsValidConfig('data source', dataSourcesConfig); assertIsValidConfig('data source', dataSourcesConfig);
// not configurable yet
var middlewareRootDir = appRootDir;
var middlewareConfig = options.middleware ||
ConfigLoader.loadMiddleware(middlewareRootDir, env);
var middlewareInstructions =
buildMiddlewareInstructions(middlewareRootDir, middlewareConfig);
// require directories // require directories
var bootDirs = options.bootDirs || []; // precedence var bootDirs = options.bootDirs || []; // precedence
bootDirs = bootDirs.concat(path.join(appRootDir, 'boot')); bootDirs = bootDirs.concat(path.join(appRootDir, 'boot'));
@ -67,6 +75,7 @@ module.exports = function compile(options) {
config: appConfig, config: appConfig,
dataSources: dataSourcesConfig, dataSources: dataSourcesConfig,
models: modelInstructions, models: modelInstructions,
middleware: middlewareInstructions,
files: { files: {
boot: bootScripts boot: bootScripts
} }
@ -74,7 +83,7 @@ module.exports = function compile(options) {
}; };
function assertIsValidConfig(name, config) { function assertIsValidConfig(name, config) {
if(config) { if (config) {
assert(typeof config === 'object', assert(typeof config === 'object',
name + ' config must be a valid JSON object'); name + ' config must be a valid JSON object');
} }
@ -143,7 +152,7 @@ function findScripts(dir) {
} else { } else {
try { try {
path.join(require.resolve(filepath)); path.join(require.resolve(filepath));
} catch(err) { } catch (err) {
debug('Skipping directory %s - %s', filepath, err.code || err); debug('Skipping directory %s - %s', filepath, err.code || err);
} }
} }
@ -155,7 +164,7 @@ function findScripts(dir) {
function tryReadDir() { function tryReadDir() {
try { try {
return fs.readdirSync.apply(fs, arguments); return fs.readdirSync.apply(fs, arguments);
} catch(e) { } catch (e) {
return []; return [];
} }
} }
@ -309,7 +318,9 @@ function loadModelDefinition(rootDir, jsonFile, allFiles) {
var basename = path.basename(jsonFile, path.extname(jsonFile)); var basename = path.basename(jsonFile, path.extname(jsonFile));
// find a matching file with a supported extension like `.js` or `.coffee` // find a matching file with a supported extension like `.js` or `.coffee`
var base, ext, validFileType; var base;
var ext;
var validFileType;
var sourceFile = allFiles var sourceFile = allFiles
.filter(function(f) { .filter(function(f) {
ext = path.extname(f); ext = path.extname(f);
@ -336,3 +347,104 @@ function loadModelDefinition(rootDir, jsonFile, allFiles) {
sourceFile: sourceFile sourceFile: sourceFile
}; };
} }
function buildMiddlewareInstructions(rootDir, config) {
var phasesNames = Object.keys(config);
var middlewareList = [];
phasesNames.forEach(function(phase) {
var phaseConfig = config[phase];
Object.keys(phaseConfig).forEach(function(middleware) {
var allConfigs = phaseConfig[middleware];
if (!Array.isArray(allConfigs))
allConfigs = [allConfigs];
allConfigs.forEach(function(config) {
var resolved = resolveMiddlewarePath(rootDir, middleware);
var middlewareConfig = cloneDeep(config);
middlewareConfig.phase = phase;
var item = {
sourceFile: resolved.sourceFile,
config: middlewareConfig
};
if (resolved.fragment) {
item.fragment = resolved.fragment;
}
middlewareList.push(item);
});
});
});
var flattenedPhaseNames = phasesNames
.map(function getBaseName(name) {
return name.replace(/:[^:]+$/, '');
})
.filter(function differsFromPreviousItem(value, ix, source) {
// Skip duplicate entries. That happens when
// `name:before` and `name:after` are both translated to `name`
return ix === 0 || value !== source[ix - 1];
});
return {
phases: flattenedPhaseNames,
middleware: middlewareList
};
}
function resolveMiddlewarePath(rootDir, middleware) {
var resolved = {};
var segments = middleware.split('#');
var pathName = segments[0];
var fragment = segments[1];
if (fragment) {
resolved.fragment = fragment;
}
if (pathName.indexOf('./') === 0 || pathName.indexOf('../') === 0) {
// Relative path
pathName = path.resolve(rootDir, pathName);
}
if (!fragment) {
resolved.sourceFile = require.resolve(pathName);
return resolved;
}
var err;
// Try to require the module and check if <module>.<fragment> is a valid
// function
var m = require(pathName);
if (typeof m[fragment] === 'function') {
resolved.sourceFile = require.resolve(pathName);
return resolved;
}
/*
* module/server/middleware/fragment
* module/middleware/fragment
*/
var candidates = [
pathName + '/server/middleware/' + fragment,
pathName + '/middleware/' + fragment,
// TODO: [rfeng] Should we support the following flavors?
// pathName + '/lib/' + fragment,
// pathName + '/' + fragment
];
for (var ix in candidates) {
try {
resolved.sourceFile = require.resolve(candidates[ix]);
delete resolved.fragment;
return resolved;
}
catch (e) {
// Report the error for the first candidate when no candidate matches
if (!err) err = e;
}
}
throw err;
}

View File

@ -34,6 +34,16 @@ ConfigLoader.loadModels = function(rootDir, env) {
return tryReadJsonConfig(rootDir, 'model-config') || {}; return tryReadJsonConfig(rootDir, 'model-config') || {};
}; };
/**
* Load middleware config from `middleware.json` and friends.
* @param {String} rootDir Directory where to look for files.
* @param {String} env Environment, usually `process.env.NODE_ENV`
* @returns {Object}
*/
ConfigLoader.loadMiddleware = function(rootDir, env) {
return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig);
};
/*-- Implementation --*/ /*-- Implementation --*/
/** /**
@ -103,7 +113,7 @@ function loadConfigFiles(files) {
*/ */
function mergeConfigurations(configObjects, mergeFn) { function mergeConfigurations(configObjects, mergeFn) {
var result = configObjects.shift() || {}; var result = configObjects.shift() || {};
while(configObjects.length) { while (configObjects.length) {
var next = configObjects.shift(); var next = configObjects.shift();
mergeFn(result, next, next._filename); mergeFn(result, next, next._filename);
} }
@ -126,6 +136,32 @@ function mergeAppConfig(target, config, fileName) {
} }
} }
function mergeMiddlewareConfig(target, config, fileName) {
var err;
for (var phase in config) {
if (phase in target) {
err = mergePhaseConfig(target[phase], config[phase], phase);
} else {
err = 'The phase "' + phase + '" is not defined in the main config.';
}
if (err)
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}
function mergePhaseConfig(target, config, phase) {
var err;
for (var middleware in config) {
if (middleware in target) {
err = mergeObjects(target[middleware], config[middleware]);
} else {
err = 'The middleware "' + middleware + '" in phase "' + phase + '"' +
'is not defined in the main config.';
}
if (err) return err;
}
}
function mergeObjects(target, config, keyPrefix) { function mergeObjects(target, config, keyPrefix) {
for (var key in config) { for (var key in config) {
var fullKey = keyPrefix ? keyPrefix + '.' + key : key; var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
@ -163,7 +199,7 @@ function mergeArrays(target, config, keyPrefix) {
} }
// Use for(;;) to iterate over undefined items, for(in) would skip them. // Use for(;;) to iterate over undefined items, for(in) would skip them.
for (var ix=0; ix < target.length; ix++) { for (var ix = 0; ix < target.length; ix++) {
var fullKey = keyPrefix + '[' + ix + ']'; var fullKey = keyPrefix + '[' + ix + ']';
var err = mergeSingleItemOrProperty(target, config, ix, fullKey); var err = mergeSingleItemOrProperty(target, config, ix, fullKey);
if (err) return err; if (err) return err;
@ -189,15 +225,15 @@ function hasCompatibleType(origValue, newValue) {
/** /**
* Try to read a config file with .json extension * Try to read a config file with .json extension
* @param cwd Dirname of the file * @param {string} cwd Dirname of the file
* @param fileName Name of the file without extension * @param {string} fileName Name of the file without extension
* @returns {Object|undefined} Content of the file, undefined if not found. * @returns {Object|undefined} Content of the file, undefined if not found.
*/ */
function tryReadJsonConfig(cwd, fileName) { function tryReadJsonConfig(cwd, fileName) {
try { try {
return require(path.join(cwd, fileName + '.json')); return require(path.join(cwd, fileName + '.json'));
} catch(e) { } catch (e) {
if(e.code !== 'MODULE_NOT_FOUND') { if (e.code !== 'MODULE_NOT_FOUND') {
throw e; throw e;
} }
} }

View File

@ -26,6 +26,7 @@ module.exports = function execute(app, instructions, callback) {
setupDataSources(app, instructions); setupDataSources(app, instructions);
setupModels(app, instructions); setupModels(app, instructions);
setupMiddleware(app, instructions);
// Run the boot scripts in series synchronously or asynchronously // Run the boot scripts in series synchronously or asynchronously
// Please note async supports both styles // Please note async supports both styles
@ -45,10 +46,10 @@ function patchAppLoopback(app) {
// patch the app object to make loopback-boot work with older versions too // patch the app object to make loopback-boot work with older versions too
try { try {
app.loopback = require('loopback'); app.loopback = require('loopback');
} catch(err) { } catch (err) {
if (err.code === 'MODULE_NOT_FOUND') { if (err.code === 'MODULE_NOT_FOUND') {
console.error( console.error(
'When using loopback-boot with loopback <1.9, '+ 'When using loopback-boot with loopback <1.9, ' +
'the loopback module must be available for `require(\'loopback\')`.'); 'the loopback module must be available for `require(\'loopback\')`.');
} }
throw err; throw err;
@ -69,7 +70,7 @@ function assertLoopBackVersion(app) {
} }
function setHost(app, instructions) { function setHost(app, instructions) {
//jshint camelcase:false // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var host = var host =
process.env.npm_config_host || process.env.npm_config_host ||
process.env.OPENSHIFT_SLS_IP || process.env.OPENSHIFT_SLS_IP ||
@ -79,14 +80,14 @@ function setHost(app, instructions) {
process.env.npm_package_config_host || process.env.npm_package_config_host ||
app.get('host'); app.get('host');
if(host !== undefined) { if (host !== undefined) {
assert(typeof host === 'string', 'app.host must be a string'); assert(typeof host === 'string', 'app.host must be a string');
app.set('host', host); app.set('host', host);
} }
} }
function setPort(app, instructions) { function setPort(app, instructions) {
//jshint camelcase:false // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var port = _.find([ var port = _.find([
process.env.npm_config_port, process.env.npm_config_port,
process.env.OPENSHIFT_SLS_PORT, process.env.OPENSHIFT_SLS_PORT,
@ -98,7 +99,7 @@ function setPort(app, instructions) {
3000 3000
], _.isFinite); ], _.isFinite);
if(port !== undefined) { if (port !== undefined) {
var portType = typeof port; var portType = typeof port;
assert(portType === 'string' || portType === 'number', assert(portType === 'string' || portType === 'number',
'app.port must be a string or number'); 'app.port must be a string or number');
@ -122,9 +123,9 @@ function setApiRoot(app, instructions) {
function applyAppConfig(app, instructions) { function applyAppConfig(app, instructions) {
var appConfig = instructions.config; var appConfig = instructions.config;
for(var configKey in appConfig) { for (var configKey in appConfig) {
var cur = app.get(configKey); var cur = app.get(configKey);
if(cur === undefined || cur === null) { if (cur === undefined || cur === null) {
app.set(configKey, appConfig[configKey]); app.set(configKey, appConfig[configKey]);
} }
} }
@ -198,7 +199,7 @@ function isBuiltinLoopBackModel(app, data) {
} }
function forEachKeyedObject(obj, fn) { function forEachKeyedObject(obj, fn) {
if(typeof obj !== 'object') return; if (typeof obj !== 'object') return;
Object.keys(obj).forEach(function(key) { Object.keys(obj).forEach(function(key) {
fn(key, obj[key]); fn(key, obj[key]);
@ -240,8 +241,8 @@ function runScripts(app, list, callback) {
function tryRequire(modulePath) { function tryRequire(modulePath) {
try { try {
return require.apply(this, arguments); return require.apply(this, arguments);
} catch(e) { } catch (e) {
if(e.code === 'MODULE_NOT_FOUND') { if (e.code === 'MODULE_NOT_FOUND') {
debug('Warning: cannot require %s - module not found.', modulePath); debug('Warning: cannot require %s - module not found.', modulePath);
return undefined; return undefined;
} }
@ -250,6 +251,37 @@ function tryRequire(modulePath) {
} }
} }
function setupMiddleware(app, instructions) {
if (!instructions.middleware) {
// the browserified client does not support middleware
return;
}
// Phases can be empty
var phases = instructions.middleware.phases || [];
assert(Array.isArray(phases),
'instructions.middleware.phases must be an array');
var middleware = instructions.middleware.middleware;
assert(Array.isArray(middleware),
'instructions.middleware.middleware must be an object');
debug('Defining middleware phases %j', phases);
app.defineMiddlewarePhases(phases);
middleware.forEach(function(data) {
debug('Configuring middleware %j%s', data.sourceFile,
data.fragment ? ('#' + data.fragment) : '');
var factory = require(data.sourceFile);
if (data.fragment) {
factory = factory[data.fragment];
}
assert(typeof factory === 'function',
'Middleware factory must be a function');
app.middlewareFromConfig(factory, data.config);
});
}
function runBootScripts(app, instructions, callback) { function runBootScripts(app, instructions, callback) {
runScripts(app, instructions.files.boot, callback); runScripts(app, instructions.files.boot, callback);
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "loopback-boot", "name": "loopback-boot",
"version": "2.3.1", "version": "2.4.0",
"description": "Convention-based bootstrapper for LoopBack applications", "description": "Convention-based bootstrapper for LoopBack applications",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",
@ -15,7 +15,7 @@
"main": "index.js", "main": "index.js",
"browser": "browser.js", "browser": "browser.js",
"scripts": { "scripts": {
"pretest": "jshint .", "pretest": "jscs . && jshint .",
"test": "mocha" "test": "mocha"
}, },
"license": { "license": {
@ -32,15 +32,15 @@
"underscore": "^1.6.0" "underscore": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"browserify": "^6.1.0",
"fs-extra": "^0.12.0",
"browserify": "^4.1.8", "browserify": "^4.1.8",
"chai": "^1.10.0",
"coffee-script": "^1.8.0", "coffee-script": "^1.8.0",
"coffeeify": "^0.7.0", "coffeeify": "^0.7.0",
"fs-extra": "^0.12.0",
"jscs": "^1.7.3",
"jshint": "^2.5.6", "jshint": "^2.5.6",
"loopback": "^2.5.0", "loopback": "^2.5.0",
"mocha": "^1.19.0", "mocha": "^1.19.0",
"must": "^0.12.0",
"supertest": "^0.14.0" "supertest": "^0.14.0"
} }
} }

View File

@ -1,7 +1,7 @@
var boot = require('../'); var boot = require('../');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var expect = require('must'); var expect = require('chai').expect;
var browserify = require('browserify'); var browserify = require('browserify');
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var vm = require('vm'); var vm = require('vm');
@ -76,7 +76,7 @@ describe('browser support', function() {
}); });
function browserifyTestApp(appDir, strategy, next) { function browserifyTestApp(appDir, strategy, next) {
//set default args // set default args
if (((typeof strategy) === 'function') && !next) { if (((typeof strategy) === 'function') && !next) {
next = strategy; next = strategy;
strategy = undefined; strategy = undefined;
@ -91,7 +91,7 @@ function browserifyTestApp(appDir, strategy, next) {
var bundlePath = sandbox.resolve('browser-app-bundle.js'); var bundlePath = sandbox.resolve('browser-app-bundle.js');
var out = fs.createWriteStream(bundlePath); var out = fs.createWriteStream(bundlePath);
b.bundle().pipe(out); b.bundle().pipe(out);
out.on('error', function(err) { return next(err); }); out.on('error', function(err) { return next(err); });
out.on('close', function() { out.on('close', function() {
next(null, bundlePath); next(null, bundlePath);

View File

@ -1,7 +1,7 @@
var boot = require('../'); var boot = require('../');
var fs = require('fs-extra'); var fs = require('fs-extra');
var path = require('path'); var path = require('path');
var expect = require('must'); var expect = require('chai').expect;
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir'); var appdir = require('./helpers/appdir');
@ -12,7 +12,10 @@ describe('compiler', function() {
beforeEach(appdir.init); beforeEach(appdir.init);
describe('from options', function() { describe('from options', function() {
var options, instructions, appConfig; var options;
var instructions;
var appConfig;
beforeEach(function() { beforeEach(function() {
options = { options = {
config: { config: {
@ -256,7 +259,7 @@ describe('compiler', function() {
appdir.writeConfigFileSync('config.local.json', { appdir.writeConfigFileSync('config.local.json', {
toplevel: [ toplevel: [
{ {
nested: [ 'value' ] nested: ['value']
} }
] ]
}); });
@ -337,7 +340,7 @@ describe('compiler', function() {
appConfigRootDir: path.resolve(appdir.PATH, 'custom') appConfigRootDir: path.resolve(appdir.PATH, 'custom')
}); });
expect(instructions.config).to.have.property('port'); expect(instructions.config).to.have.property('port');
}); });
it('supports `dsRootDir` option', function() { it('supports `dsRootDir` option', function() {
@ -379,7 +382,7 @@ describe('compiler', function() {
var instructions = boot.compile(appdir.PATH); var instructions = boot.compile(appdir.PATH);
expect(instructions.files.boot).to.eql([initJs]); expect(instructions.files.boot).to.eql([initJs]);
}); });
it('supports `bootDirs` option', function() { it('supports `bootDirs` option', function() {
appdir.createConfigFilesSync(); appdir.createConfigFilesSync();
var initJs = appdir.writeFileSync('custom-boot/init.js', var initJs = appdir.writeFileSync('custom-boot/init.js',
@ -390,7 +393,7 @@ describe('compiler', function() {
}); });
expect(instructions.files.boot).to.eql([initJs]); expect(instructions.files.boot).to.eql([initJs]);
}); });
it('supports `bootScripts` option', function() { it('supports `bootScripts` option', function() {
appdir.createConfigFilesSync(); appdir.createConfigFilesSync();
var initJs = appdir.writeFileSync('custom-boot/init.js', var initJs = appdir.writeFileSync('custom-boot/init.js',
@ -475,19 +478,19 @@ describe('compiler', function() {
sourceFile: path.resolve(appdir.PATH, 'models', 'car.coffee') sourceFile: path.resolve(appdir.PATH, 'models', 'car.coffee')
}); });
}); });
it('supports `modelSources` option', function() { it('supports `modelSources` option', function() {
appdir.createConfigFilesSync({}, {}, { appdir.createConfigFilesSync({}, {}, {
Car: { dataSource: 'db' } Car: { dataSource: 'db' }
}); });
appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' }); appdir.writeConfigFileSync('custom-models/car.json', { name: 'Car' });
appdir.writeFileSync('custom-models/car.js', ''); appdir.writeFileSync('custom-models/car.js', '');
var instructions = boot.compile({ var instructions = boot.compile({
appRootDir: appdir.PATH, appRootDir: appdir.PATH,
modelSources: ['./custom-models'] modelSources: ['./custom-models']
}); });
expect(instructions.models).to.have.length(1); expect(instructions.models).to.have.length(1);
expect(instructions.models[0]).to.eql({ expect(instructions.models[0]).to.eql({
name: 'Car', name: 'Car',
@ -670,6 +673,288 @@ describe('compiler', function() {
expect(instructions.config).to.not.have.property('modified'); expect(instructions.config).to.not.have.property('modified');
}); });
}); });
describe('for middleware', function() {
function testMiddlewareRegistration(middlewareId, sourceFile) {
var json = {
initial: {
},
custom: {
}
};
json.custom[middlewareId] = {
params: 'some-config-data'
};
appdir.writeConfigFileSync('middleware.json', json);
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware).to.eql({
phases: ['initial', 'custom'],
middleware: [
{
sourceFile: sourceFile,
config: {
phase: 'custom',
params: 'some-config-data'
}
}
]
});
}
var sourceFileForUrlNotFound;
beforeEach(function() {
fs.copySync(SIMPLE_APP, appdir.PATH);
sourceFileForUrlNotFound = require.resolve(
'loopback/server/middleware/url-not-found');
});
it('emits middleware instructions', function() {
testMiddlewareRegistration('loopback/server/middleware/url-not-found',
sourceFileForUrlNotFound);
});
it('emits middleware instructions for fragment', function() {
testMiddlewareRegistration('loopback#url-not-found',
sourceFileForUrlNotFound);
});
it('fails when a module middleware cannot be resolved', function() {
appdir.writeConfigFileSync('middleware.json', {
final: {
'loopback/path-does-not-exist': { }
}
});
expect(function() { boot.compile(appdir.PATH); })
.to.throw(/path-does-not-exist/);
});
it('fails when a module middleware fragment cannot be resolved',
function() {
appdir.writeConfigFileSync('middleware.json', {
final: {
'loopback#path-does-not-exist': { }
}
});
expect(function() {
boot.compile(appdir.PATH);
})
.to.throw(/path-does-not-exist/);
});
it('resolves paths relatively to appRootDir', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
// resolves to ./middleware.json
'./middleware': { }
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware).to.eql({
phases: ['routes'],
middleware: [{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: { phase: 'routes' }
}]
});
});
it('merges config.params', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
'./middleware': {
params: {
key: 'initial value'
}
}
}
});
appdir.writeConfigFileSync('./middleware.local.json', {
routes: {
'./middleware': {
params: {
key: 'custom value'
}
}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].config.params).to.eql({
key: 'custom value'
});
});
it('merges config.enabled', function() {
appdir.writeConfigFileSync('./middleware.json', {
routes: {
'./middleware': {
params: {
key: 'initial value'
}
}
}
});
appdir.writeConfigFileSync('./middleware.local.json', {
routes: {
'./middleware': {
enabled: false
}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].config)
.to.have.property('enabled', false);
});
it('flattens sub-phases', function() {
appdir.writeConfigFileSync('middleware.json', {
'initial:after': {
},
'custom:before': {
'loopback/server/middleware/url-not-found': {
params: 'some-config-data'
}
},
'custom:after': {
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.phases, 'phases')
.to.eql(['initial', 'custom']);
expect(instructions.middleware.middleware, 'middleware')
.to.eql([{
sourceFile:
require.resolve('loopback/server/middleware/url-not-found'),
config: {
phase: 'custom:before',
params: 'some-config-data'
}
}]);
});
it('supports multiple instances of the same middleware', function() {
appdir.writeConfigFileSync('middleware.json', {
'final': {
'./middleware': [
{
params: 'first'
},
{
params: 'second'
}
]
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware)
.to.eql([
{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: {
phase: 'final',
params: 'first'
}
},
{
sourceFile: path.resolve(appdir.PATH, 'middleware.json'),
config: {
phase: 'final',
params: 'second'
}
}
]);
});
it('supports shorthand notation for middleware paths', function() {
appdir.writeConfigFileSync('middleware.json', {
'final': {
'loopback#url-not-found': {}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].sourceFile)
.to.equal(require.resolve('loopback/server/middleware/url-not-found'));
});
it('supports shorthand notation for relative paths', function() {
appdir.writeConfigFileSync('middleware.json', {
'routes': {
'./middleware/index#myMiddleware': {
}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0].sourceFile)
.to.equal(path.resolve(appdir.PATH,
'./middleware/index.js'));
expect(instructions.middleware.middleware[0]).have.property(
'fragment',
'myMiddleware');
});
it('supports shorthand notation when the fragment name matches a property',
function() {
appdir.writeConfigFileSync('middleware.json', {
'final': {
'loopback#errorHandler': {}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0]).have.property(
'sourceFile',
require.resolve('loopback'));
expect(instructions.middleware.middleware[0]).have.property(
'fragment',
'errorHandler');
});
// FIXME: [rfeng] The following test is disabled until
// https://github.com/strongloop/loopback-boot/issues/73 is fixed
it.skip('resolves modules relative to appRootDir', function() {
var HANDLER_FILE = 'node_modules/handler/index.js';
appdir.writeFileSync(
HANDLER_FILE,
'module.exports = function(req, res, next) { next(); }');
appdir.writeConfigFileSync('middleware.json', {
'initial': {
'handler': {}
}
});
var instructions = boot.compile(appdir.PATH);
expect(instructions.middleware.middleware[0]).have.property(
'sourceFile',
appdir.resolve(HANDLER_FILE));
});
});
}); });
function getNameProperty(obj) { function getNameProperty(obj) {

View File

@ -2,10 +2,11 @@ var boot = require('../');
var path = require('path'); var path = require('path');
var loopback = require('loopback'); var loopback = require('loopback');
var assert = require('assert'); var assert = require('assert');
var expect = require('must'); var expect = require('chai').expect;
var fs = require('fs-extra'); var fs = require('fs-extra');
var sandbox = require('./helpers/sandbox'); var sandbox = require('./helpers/sandbox');
var appdir = require('./helpers/appdir'); var appdir = require('./helpers/appdir');
var supertest = require('supertest');
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
@ -18,6 +19,13 @@ describe('executor', function() {
beforeEach(function() { beforeEach(function() {
app = loopback(); app = loopback();
// process.bootFlags is used by simple-app/boot/*.js scripts
process.bootFlags = [];
});
afterEach(function() {
delete process.bootFlags;
}); });
var dummyInstructions = someInstructions({ var dummyInstructions = someInstructions({
@ -171,14 +179,13 @@ describe('executor', function() {
}; };
builtinModel.definition.redefined = true; builtinModel.definition.redefined = true;
boot.execute(app, someInstructions({ models: [ builtinModel ] })); boot.execute(app, someInstructions({ models: [builtinModel] }));
expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true); expect(app.models.User.settings.redefined, 'redefined').to.not.equal(true);
}); });
describe('with boot and models files', function() { describe('with boot and models files', function() {
beforeEach(function() { beforeEach(function() {
process.bootFlags = process.bootFlags || [];
boot.execute(app, simpleAppInstructions()); boot.execute(app, simpleAppInstructions());
}); });
@ -212,14 +219,6 @@ describe('executor', function() {
}); });
describe('with boot with callback', function() { describe('with boot with callback', function() {
beforeEach(function() {
process.bootFlags = process.bootFlags || [];
});
afterEach(function() {
delete process.bootFlags;
});
it('should run `boot/*` files asynchronously', function(done) { it('should run `boot/*` files asynchronously', function(done) {
boot.execute(app, simpleAppInstructions(), function() { boot.execute(app, simpleAppInstructions(), function() {
expect(process.bootFlags).to.eql([ expect(process.bootFlags).to.eql([
@ -233,7 +232,6 @@ describe('executor', function() {
done(); done();
}); });
}); });
}); });
describe('with PaaS and npm env variables', function() { describe('with PaaS and npm env variables', function() {
@ -266,7 +264,7 @@ describe('executor', function() {
}); });
it('should prioritize sources', function() { it('should prioritize sources', function() {
/*jshint camelcase:false */ // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
process.env.npm_config_host = randomHost(); process.env.npm_config_host = randomHost();
process.env.OPENSHIFT_SLS_IP = randomHost(); process.env.OPENSHIFT_SLS_IP = randomHost();
process.env.OPENSHIFT_NODEJS_IP = randomHost(); process.env.OPENSHIFT_NODEJS_IP = randomHost();
@ -323,11 +321,100 @@ describe('executor', function() {
'module.exports = function(app) { app.fnCalled = true; };'); 'module.exports = function(app) { app.fnCalled = true; };');
delete app.fnCalled; delete app.fnCalled;
boot.execute(app, someInstructions({ files: { boot: [ file ] } })); boot.execute(app, someInstructions({ files: { boot: [file] } }));
expect(app.fnCalled, 'exported fn was called').to.be.true(); expect(app.fnCalled, 'exported fn was called').to.be.true();
}); });
});
it('configures middleware', function(done) {
var pushNamePath = require.resolve('./helpers/push-name-middleware');
boot.execute(app, someInstructions({
middleware: {
phases: ['initial', 'custom'],
middleware: [
{
sourceFile: pushNamePath,
config: {
phase: 'initial',
params: 'initial'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'custom',
params: 'custom'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'routes',
params: 'routes'
}
},
{
sourceFile: pushNamePath,
config: {
phase: 'routes',
enabled: false,
params: 'disabled'
}
}
]
}
}));
supertest(app)
.get('/')
.end(function(err, res) {
if (err) return done(err);
var names = (res.headers.names || '').split(',');
expect(names).to.eql(['initial', 'custom', 'routes']);
done();
});
});
it('configures middleware using shortform', function(done) {
boot.execute(app, someInstructions({
middleware: {
middleware: [
{
sourceFile: require.resolve('loopback'),
fragment: 'static',
config: {
phase: 'files',
params: path.join(__dirname, './fixtures/simple-app/client/')
}
}
]
}
}));
supertest(app)
.get('/')
.end(function(err, res) {
if (err) return done(err);
expect(res.text).to.eql('<!DOCTYPE html>\n<html>\n<head lang="en">\n' +
' <meta charset="UTF-8">\n <title>simple-app</title>\n' +
'</head>\n<body>\n<h1>simple-app</h1>\n</body>\n</html>');
done();
});
});
it('configures middleware (end-to-end)', function(done) {
boot.execute(app, simpleAppInstructions());
supertest(app)
.get('/')
.end(function(err, res) {
if (err) return done(err);
expect(res.headers.names).to.equal('custom-middleware');
done();
});
});
});
function assertValidDataSource(dataSource) { function assertValidDataSource(dataSource) {
// has methods // has methods
@ -340,7 +427,7 @@ function assertValidDataSource(dataSource) {
assert.isFunc(dataSource, 'operations'); assert.isFunc(dataSource, 'operations');
} }
assert.isFunc = function (obj, name) { assert.isFunc = function(obj, name) {
assert(obj, 'cannot assert function ' + name + assert(obj, 'cannot assert function ' + name +
' on object that does not exist'); ' on object that does not exist');
assert(typeof obj[name] === 'function', name + ' is not a function'); assert(typeof obj[name] === 'function', name + ' is not a function');
@ -351,6 +438,7 @@ function someInstructions(values) {
config: values.config || {}, config: values.config || {},
models: values.models || [], models: values.models || [],
dataSources: values.dataSources || { db: { connector: 'memory' } }, dataSources: values.dataSources || { db: { connector: 'memory' } },
middleware: values.middleware || { phases: [], middleware: [] },
files: { files: {
boot: [] boot: []
} }

View File

@ -2,4 +2,3 @@ process.bootFlags.push('barSyncLoaded');
module.exports = function(app) { module.exports = function(app) {
process.bootFlags.push('barSyncExecuted'); process.bootFlags.push('barSyncExecuted');
}; };

View File

@ -1 +1 @@
process.bootFlags.push('fooLoaded'); process.bootFlags.push('fooLoaded');

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>simple-app</title>
</head>
<body>
<h1>simple-app</h1>
</body>
</html>

View File

@ -0,0 +1,7 @@
{
"initial": {
"../../helpers/push-name-middleware": {
"params": "custom-middleware"
}
}
}

View File

@ -0,0 +1,8 @@
exports.myMiddleware = function(name) {
return function(req, res, next) {
req._names = req._names || [];
req._names.push(name);
res.setHeader('names', req._names.join(','));
next();
};
};

View File

@ -0,0 +1,7 @@
/**
* Exporting a middleware as a property of the main module
*/
exports.myMiddleware = function(req, res, next) {
res.setHeader('X-MY-MIDDLEWARE', 'myMiddleware');
next();
};

View File

@ -0,0 +1,7 @@
{
"name": "my-module",
"version": "1.0.0",
"description": "my-module",
"main": "index.js",
"license": "MIT"
}

View File

@ -42,8 +42,12 @@ appdir.writeConfigFileSync = function(name, json) {
}; };
appdir.writeFileSync = function(name, content) { appdir.writeFileSync = function(name, content) {
var filePath = path.resolve(PATH, name); var filePath = this.resolve(name);
fs.mkdirsSync(path.dirname(filePath)); fs.mkdirsSync(path.dirname(filePath));
fs.writeFileSync(filePath, content, 'utf-8'); fs.writeFileSync(filePath, content, 'utf-8');
return filePath; return filePath;
}; };
appdir.resolve = function(name) {
return path.resolve(PATH, name);
};

View File

@ -0,0 +1,8 @@
module.exports = function(name) {
return function(req, res, next) {
req._names = req._names || [];
req._names.push(name);
res.setHeader('names', req._names.join(','));
next();
};
};