Skip to content

Commit

Permalink
Merge pull request #120 from PradnyaBaviskar/lb-boot-issue-79
Browse files Browse the repository at this point in the history
Add support for mixinDirs
  • Loading branch information
bajtos committed Apr 24, 2015
2 parents aed73a9 + 9bb9887 commit 0e19f0a
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 5 deletions.
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
5 changes: 5 additions & 0 deletions lib/bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
74 changes: 73 additions & 1 deletion lib/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -93,6 +97,7 @@ module.exports = function compile(options) {
models: modelInstructions,
middleware: middlewareInstructions,
components: componentInstructions,
mixins: mixinInstructions,
files: {
boot: bootScripts
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
21 changes: 21 additions & 0 deletions lib/executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function setupDataSources(app, instructions) {
}

function setupModels(app, instructions) {
defineMixins(app, instructions);
defineModels(app, instructions);

instructions.models.forEach(function(data) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
26 changes: 24 additions & 2 deletions test/browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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;
Expand All @@ -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);
}
Expand Down
154 changes: 154 additions & 0 deletions test/compiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 0e19f0a

Please sign in to comment.