Merge pull request #120 from PradnyaBaviskar/lb-boot-issue-79

Add support for mixinDirs
This commit is contained in:
Miroslav Bajtoš 2015-04-24 09:29:39 +02:00
commit 0e19f0a1b2
10 changed files with 317 additions and 5 deletions

View File

@ -111,10 +111,14 @@ var addInstructionsToBrowserify = require('./lib/bundler');
* of `{appRootDir}/middleware.json` * of `{appRootDir}/middleware.json`
* @property {Object} [components] Component configuration to use instead * @property {Object} [components] Component configuration to use instead
* of `{appRootDir}/component-config.json` * of `{appRootDir}/component-config.json`
* @property {Array.<String>} [mixinDirs] List of directories where to look
* for files containing model mixin definitions.
* @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
* on boot. * on boot.
* @property {String|Function|Boolean} [normalization] Mixin normalization
* format: false, 'none', 'classify', 'dasherize' - defaults to 'classify'.
* @end * @end
* @param {Function} [callback] Callback function. * @param {Function} [callback] Callback function.
* *

View File

@ -11,6 +11,7 @@ var cloneDeep = require('lodash').cloneDeep;
module.exports = function addInstructionsToBrowserify(instructions, bundler) { module.exports = function addInstructionsToBrowserify(instructions, bundler) {
bundleModelScripts(instructions, bundler); bundleModelScripts(instructions, bundler);
bundleMixinScripts(instructions, bundler);
bundleComponentScripts(instructions, bundler); bundleComponentScripts(instructions, bundler);
bundleOtherScripts(instructions, bundler); bundleOtherScripts(instructions, bundler);
bundleInstructions(instructions, bundler); bundleInstructions(instructions, bundler);
@ -26,6 +27,10 @@ function bundleModelScripts(instructions, bundler) {
bundleSourceFiles(instructions, 'models', bundler); bundleSourceFiles(instructions, 'models', bundler);
} }
function bundleMixinScripts(instructions, bundler) {
bundleSourceFiles(instructions, 'mixins', bundler);
}
function bundleComponentScripts(instructions, bundler) { function bundleComponentScripts(instructions, bundler) {
bundleSourceFiles(instructions, 'components', bundler); bundleSourceFiles(instructions, 'components', bundler);
} }

View File

@ -84,6 +84,10 @@ module.exports = function compile(options) {
var modelInstructions = buildAllModelInstructions( var modelInstructions = buildAllModelInstructions(
modelsRootDir, modelsConfig, modelSources); modelsRootDir, modelsConfig, modelSources);
var mixinDirs = options.mixinDirs || [];
var mixinInstructions = buildAllMixinInstructions(
appRootDir, mixinDirs, options);
// When executor passes the instruction to loopback methods, // When executor passes the instruction to loopback methods,
// loopback modifies the data. Since we are loading the data using `require`, // loopback modifies the data. Since we are loading the data using `require`,
// such change affects also code that calls `require` for the same file. // such change affects also code that calls `require` for the same file.
@ -93,6 +97,7 @@ module.exports = function compile(options) {
models: modelInstructions, models: modelInstructions,
middleware: middlewareInstructions, middleware: middlewareInstructions,
components: componentInstructions, components: componentInstructions,
mixins: mixinInstructions,
files: { files: {
boot: bootScripts boot: bootScripts
} }
@ -135,10 +140,11 @@ function assertIsValidModelConfig(config) {
* @private * @private
*/ */
function findScripts(dir) { function findScripts(dir, extensions) {
assert(dir, 'cannot require directory contents without directory name'); assert(dir, 'cannot require directory contents without directory name');
var files = tryReadDir(dir); var files = tryReadDir(dir);
extensions = extensions || _.keys(require.extensions);
// sort files in lowercase alpha for linux // sort files in lowercase alpha for linux
files.sort(function(a, b) { files.sort(function(a, b) {
@ -599,3 +605,69 @@ function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
var fixedFile = fixFileExtension(resolvedPath, files, false); var fixedFile = fixFileExtension(resolvedPath, files, false);
return (fixedFile === undefined ? resolvedPath : fixedFile); return (fixedFile === undefined ? resolvedPath : fixedFile);
} }
function buildAllMixinInstructions(appRootDir, mixinDirs, options) {
var extensions = _.without(_.keys(require.extensions),
_.keys(getExcludedExtensions()));
var files = options.mixins || [];
mixinDirs.forEach(function(dir) {
dir = tryResolveAppPath(appRootDir, dir);
if (!dir) {
debug('Skipping unknown module source dir %j', dir);
return;
}
files = files.concat(findScripts(dir, extensions));
});
var mixins = files.map(function(filepath) {
var dir = path.dirname(filepath);
var ext = path.extname(filepath);
var name = path.basename(filepath, ext);
var metafile = path.join(dir, name + FILE_EXTENSION_JSON);
name = normalizeMixinName(name, options);
var meta = {};
meta.name = name;
if (fs.existsSync(metafile)) {
// May overwrite name, not sourceFile
_.extend(meta, require(metafile));
}
meta.sourceFile = filepath;
return meta;
});
return mixins;
}
function normalizeMixinName(str, options) {
var normalization = options.normalization;
switch (normalization) {
case false:
case 'none': return str;
case undefined:
case 'classify':
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
str = str.replace(/(?:^|\s|-)\S/g, function(c) { return c.toUpperCase(); });
str = str.replace(/\s+/g, '');
return str;
case 'dasherize':
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
str = str.replace(/\s+/g, '-');
return str;
default:
if (typeof normalization === 'function') {
return normalization(str);
}
var err = new Error('Invalid normalization format - "' +
normalization + '"');
err.code = 'INVALID_NORMALIZATION_FORMAT';
throw err;
}
}

View File

@ -160,6 +160,7 @@ function setupDataSources(app, instructions) {
} }
function setupModels(app, instructions) { function setupModels(app, instructions) {
defineMixins(app, instructions);
defineModels(app, instructions); defineModels(app, instructions);
instructions.models.forEach(function(data) { instructions.models.forEach(function(data) {
@ -170,6 +171,26 @@ function setupModels(app, instructions) {
}); });
} }
function defineMixins(app, instructions) {
var modelBuilder = (app.registry || app.loopback).modelBuilder;
var BaseClass = app.loopback.Model;
var mixins = instructions.mixins || [];
if (!modelBuilder.mixins || !mixins.length) return;
mixins.forEach(function(obj) {
var mixin = require(obj.sourceFile);
if (typeof mixin === 'function' || mixin.prototype instanceof BaseClass) {
debug('Defining mixin %s', obj.name);
modelBuilder.mixins.define(obj.name, mixin); // TODO (name, mixin, meta)
} else {
debug('Skipping mixin file %s - `module.exports` is not a function' +
' or Loopback model', obj);
}
});
}
function defineModels(app, instructions) { function defineModels(app, instructions) {
var registry = app.registry || app.loopback; var registry = app.registry || app.loopback;
instructions.models.forEach(function(data) { instructions.models.forEach(function(data) {

View File

@ -38,7 +38,7 @@
"fs-extra": "^0.12.0", "fs-extra": "^0.12.0",
"jscs": "^1.7.3", "jscs": "^1.7.3",
"jshint": "^2.5.6", "jshint": "^2.5.6",
"loopback": "^2.5.0", "loopback": "^2.16.3",
"mocha": "^1.19.0", "mocha": "^1.19.0",
"supertest": "^0.14.0" "supertest": "^0.14.0"
} }

View File

@ -61,6 +61,27 @@ describe('browser support', function() {
}); });
}); });
it('loads mixins', function(done) {
var appDir = path.resolve(__dirname, './fixtures/browser-app');
var options = {
appRootDir: appDir,
mixinDirs: ['./mixins']
};
browserifyTestApp(options, function(err, bundlePath) {
if (err) return done(err);
var app = executeBundledApp(bundlePath);
var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['TimeStamps']);
expect(app.models.Customer.timeStampsMixin).to.eql(true);
done();
});
});
it('supports coffee-script files', function(done) { it('supports coffee-script files', function(done) {
// add coffee-script to require.extensions // add coffee-script to require.extensions
require('coffee-script/register'); require('coffee-script/register');
@ -82,7 +103,7 @@ describe('browser support', function() {
}); });
}); });
function browserifyTestApp(appDir, strategy, next) { function browserifyTestApp(options, strategy, next) {
// set default args // set default args
if (((typeof strategy) === 'function') && !next) { if (((typeof strategy) === 'function') && !next) {
next = strategy; next = strategy;
@ -91,9 +112,10 @@ function browserifyTestApp(appDir, strategy, next) {
if (!strategy) if (!strategy)
strategy = 'default'; strategy = 'default';
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
var b = compileStrategies[strategy](appDir); var b = compileStrategies[strategy](appDir);
boot.compileToBrowserify(appDir, b); boot.compileToBrowserify(options, b);
exportBrowserifyToFile(b, 'browser-app-bundle.js', next); exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
} }

View File

@ -1014,6 +1014,160 @@ describe('compiler', function() {
instructions = boot.compile(appdir.PATH); instructions = boot.compile(appdir.PATH);
expect(instructions.config).to.not.have.property('modified'); expect(instructions.config).to.not.have.property('modified');
}); });
describe('for mixins', function() {
function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs) {
var appJS = appdir.writeFileSync(sourceFile, '');
var instructions = boot.compile({
appRootDir: appdir.PATH,
mixinDirs: mixinDirs
});
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
}
it('supports `mixinDirs` option', function() {
verifyMixinIsFoundViaMixinDirs('mixins/other.js', ['./mixins']);
});
it('resolves relative path in `mixinDirs` option', function() {
verifyMixinIsFoundViaMixinDirs('custom-mixins/vehicle.js',
['./custom-mixins']);
});
it('resolves module relative path in `mixinDirs` option', function() {
verifyMixinIsFoundViaMixinDirs('node_modules/custom-mixins/vehicle.js',
['custom-mixins']);
});
describe('name normalization', function() {
var options;
beforeEach(function() {
options = { appRootDir: appdir.PATH, mixinDirs: ['./mixins'] };
appdir.writeFileSync('mixins/foo.js', '');
appdir.writeFileSync('mixins/time-stamps.js', '');
appdir.writeFileSync('mixins/camelCase.js', '');
appdir.writeFileSync('mixins/PascalCase.js', '');
appdir.writeFileSync('mixins/space name.js', '');
});
it('supports classify', function() {
options.normalization = 'classify';
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps'
]);
});
it('supports dasherize', function() {
options.normalization = 'dasherize';
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'camel-case', 'foo', 'pascal-case', 'space-name', 'time-stamps'
]);
});
it('supports custom function', function() {
var normalize = function(name) { return name.toUpperCase(); };
options.normalization = normalize;
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'CAMELCASE', 'FOO', 'PASCALCASE', 'SPACE NAME', 'TIME-STAMPS'
]);
});
it('supports none', function() {
options.normalization = 'none';
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps'
]);
});
it('supports false', function() {
options.normalization = false;
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps'
]);
});
it('defaults to classify', function() {
var instructions = boot.compile(options);
var mixins = instructions.mixins;
var mixinNames = mixins.map(getNameProperty);
expect(mixinNames).to.eql([
'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps'
]);
});
it('throws error for invalid normalization format', function() {
options.normalization = 'invalidFormat';
expect(function() { boot.compile(options); })
.to.throw(/Invalid normalization format - "invalidFormat"/);
});
});
it('overrides default mixin name, by `name` in JSON', function() {
appdir.writeFileSync('mixins/foo.js', '');
appdir.writeConfigFileSync('mixins/foo.json', {name: 'fooBar'});
var options = { appRootDir: appdir.PATH,
mixinDirs: ['./mixins']
};
var instructions = boot.compile(options);
expect(instructions.mixins[0].name).to.eql('fooBar');
});
it('extends definition from JSON with same file name', function() {
var appJS = appdir.writeFileSync('mixins/foo-bar.js', '');
appdir.writeConfigFileSync('mixins/foo-bar.json', {
description: 'JSON file name same as JS file name' });
appdir.writeConfigFileSync('mixins/FooBar.json', {
description: 'JSON file name same as normalized name of mixin' });
var options = { appRootDir: appdir.PATH,
mixinDirs: ['./mixins'],
normalization: 'classify' };
var instructions = boot.compile(options);
expect(instructions.mixins).to.eql([
{
name: 'FooBar',
description: 'JSON file name same as JS file name',
sourceFile: appJS
}
]);
});
});
}); });
describe('for middleware', function() { describe('for middleware', function() {

View File

@ -291,6 +291,34 @@ describe('executor', function() {
done(); done();
}); });
}); });
describe ('for mixins', function() {
it('defines mixins from instructions', function() {
appdir.writeFileSync('mixins/example.js',
'module.exports = ' +
'function(Model, options) {}');
appdir.writeFileSync('mixins/time-stamps.js',
'module.exports = ' +
'function(Model, options) {}');
appdir.writeConfigFileSync('mixins/time-stamps.json', {
name: 'Timestamping'
});
var options = {
appRootDir: appdir.PATH,
mixinDirs: ['./mixins']
};
boot(app, options);
var modelBuilder = app.registry.modelBuilder;
var registry = modelBuilder.mixins.mixins;
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
});
});
}); });
describe('with PaaS and npm env variables', function() { describe('with PaaS and npm env variables', function() {

View File

@ -0,0 +1,5 @@
module.exports = function(Model, options) {
Model.timeStampsMixin = true;
};

View File

@ -1,4 +1,5 @@
{ {
"name": "Customer", "name": "Customer",
"base": "User" "base": "User",
"mixins": {"TimeStamps": {} }
} }