Merge pull request #120 from PradnyaBaviskar/lb-boot-issue-79
Add support for mixinDirs
This commit is contained in:
commit
0e19f0a1b2
4
index.js
4
index.js
|
@ -111,10 +111,14 @@ var addInstructionsToBrowserify = require('./lib/bundler');
|
|||
* of `{appRootDir}/middleware.json`
|
||||
* @property {Object} [components] Component configuration to use instead
|
||||
* 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
|
||||
* for boot scripts.
|
||||
* @property {Array.<String>} [bootScripts] List of script files to execute
|
||||
* on boot.
|
||||
* @property {String|Function|Boolean} [normalization] Mixin normalization
|
||||
* format: false, 'none', 'classify', 'dasherize' - defaults to 'classify'.
|
||||
* @end
|
||||
* @param {Function} [callback] Callback function.
|
||||
*
|
||||
|
|
|
@ -11,6 +11,7 @@ var cloneDeep = require('lodash').cloneDeep;
|
|||
|
||||
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
|
||||
bundleModelScripts(instructions, bundler);
|
||||
bundleMixinScripts(instructions, bundler);
|
||||
bundleComponentScripts(instructions, bundler);
|
||||
bundleOtherScripts(instructions, bundler);
|
||||
bundleInstructions(instructions, bundler);
|
||||
|
@ -26,6 +27,10 @@ function bundleModelScripts(instructions, bundler) {
|
|||
bundleSourceFiles(instructions, 'models', bundler);
|
||||
}
|
||||
|
||||
function bundleMixinScripts(instructions, bundler) {
|
||||
bundleSourceFiles(instructions, 'mixins', bundler);
|
||||
}
|
||||
|
||||
function bundleComponentScripts(instructions, bundler) {
|
||||
bundleSourceFiles(instructions, 'components', bundler);
|
||||
}
|
||||
|
|
|
@ -84,6 +84,10 @@ module.exports = function compile(options) {
|
|||
var modelInstructions = buildAllModelInstructions(
|
||||
modelsRootDir, modelsConfig, modelSources);
|
||||
|
||||
var mixinDirs = options.mixinDirs || [];
|
||||
var mixinInstructions = buildAllMixinInstructions(
|
||||
appRootDir, mixinDirs, options);
|
||||
|
||||
// When executor passes the instruction to loopback methods,
|
||||
// loopback modifies the data. Since we are loading the data using `require`,
|
||||
// such change affects also code that calls `require` for the same file.
|
||||
|
@ -93,6 +97,7 @@ module.exports = function compile(options) {
|
|||
models: modelInstructions,
|
||||
middleware: middlewareInstructions,
|
||||
components: componentInstructions,
|
||||
mixins: mixinInstructions,
|
||||
files: {
|
||||
boot: bootScripts
|
||||
}
|
||||
|
@ -135,10 +140,11 @@ function assertIsValidModelConfig(config) {
|
|||
* @private
|
||||
*/
|
||||
|
||||
function findScripts(dir) {
|
||||
function findScripts(dir, extensions) {
|
||||
assert(dir, 'cannot require directory contents without directory name');
|
||||
|
||||
var files = tryReadDir(dir);
|
||||
extensions = extensions || _.keys(require.extensions);
|
||||
|
||||
// sort files in lowercase alpha for linux
|
||||
files.sort(function(a, b) {
|
||||
|
@ -599,3 +605,69 @@ function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
|
|||
var fixedFile = fixFileExtension(resolvedPath, files, false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,6 +160,7 @@ function setupDataSources(app, instructions) {
|
|||
}
|
||||
|
||||
function setupModels(app, instructions) {
|
||||
defineMixins(app, instructions);
|
||||
defineModels(app, instructions);
|
||||
|
||||
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) {
|
||||
var registry = app.registry || app.loopback;
|
||||
instructions.models.forEach(function(data) {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"fs-extra": "^0.12.0",
|
||||
"jscs": "^1.7.3",
|
||||
"jshint": "^2.5.6",
|
||||
"loopback": "^2.5.0",
|
||||
"loopback": "^2.16.3",
|
||||
"mocha": "^1.19.0",
|
||||
"supertest": "^0.14.0"
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
// add coffee-script to require.extensions
|
||||
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
|
||||
if (((typeof strategy) === 'function') && !next) {
|
||||
next = strategy;
|
||||
|
@ -91,9 +112,10 @@ function browserifyTestApp(appDir, strategy, next) {
|
|||
if (!strategy)
|
||||
strategy = 'default';
|
||||
|
||||
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
|
||||
var b = compileStrategies[strategy](appDir);
|
||||
|
||||
boot.compileToBrowserify(appDir, b);
|
||||
boot.compileToBrowserify(options, b);
|
||||
|
||||
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
|
||||
}
|
||||
|
|
|
@ -1014,6 +1014,160 @@ describe('compiler', function() {
|
|||
instructions = boot.compile(appdir.PATH);
|
||||
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() {
|
||||
|
|
|
@ -291,6 +291,34 @@ describe('executor', function() {
|
|||
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() {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = function(Model, options) {
|
||||
|
||||
Model.timeStampsMixin = true;
|
||||
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "Customer",
|
||||
"base": "User"
|
||||
"base": "User",
|
||||
"mixins": {"TimeStamps": {} }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue