Skip to content

Commit 0e19f0a

Browse files
committed
Merge pull request #120 from PradnyaBaviskar/lb-boot-issue-79
Add support for mixinDirs
2 parents aed73a9 + 9bb9887 commit 0e19f0a

File tree

10 files changed

+317
-5
lines changed

10 files changed

+317
-5
lines changed

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,14 @@ var addInstructionsToBrowserify = require('./lib/bundler');
111111
* of `{appRootDir}/middleware.json`
112112
* @property {Object} [components] Component configuration to use instead
113113
* of `{appRootDir}/component-config.json`
114+
* @property {Array.<String>} [mixinDirs] List of directories where to look
115+
* for files containing model mixin definitions.
114116
* @property {Array.<String>} [bootDirs] List of directories where to look
115117
* for boot scripts.
116118
* @property {Array.<String>} [bootScripts] List of script files to execute
117119
* on boot.
120+
* @property {String|Function|Boolean} [normalization] Mixin normalization
121+
* format: false, 'none', 'classify', 'dasherize' - defaults to 'classify'.
118122
* @end
119123
* @param {Function} [callback] Callback function.
120124
*

lib/bundler.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var cloneDeep = require('lodash').cloneDeep;
1111

1212
module.exports = function addInstructionsToBrowserify(instructions, bundler) {
1313
bundleModelScripts(instructions, bundler);
14+
bundleMixinScripts(instructions, bundler);
1415
bundleComponentScripts(instructions, bundler);
1516
bundleOtherScripts(instructions, bundler);
1617
bundleInstructions(instructions, bundler);
@@ -26,6 +27,10 @@ function bundleModelScripts(instructions, bundler) {
2627
bundleSourceFiles(instructions, 'models', bundler);
2728
}
2829

30+
function bundleMixinScripts(instructions, bundler) {
31+
bundleSourceFiles(instructions, 'mixins', bundler);
32+
}
33+
2934
function bundleComponentScripts(instructions, bundler) {
3035
bundleSourceFiles(instructions, 'components', bundler);
3136
}

lib/compiler.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ module.exports = function compile(options) {
8484
var modelInstructions = buildAllModelInstructions(
8585
modelsRootDir, modelsConfig, modelSources);
8686

87+
var mixinDirs = options.mixinDirs || [];
88+
var mixinInstructions = buildAllMixinInstructions(
89+
appRootDir, mixinDirs, options);
90+
8791
// When executor passes the instruction to loopback methods,
8892
// loopback modifies the data. Since we are loading the data using `require`,
8993
// such change affects also code that calls `require` for the same file.
@@ -93,6 +97,7 @@ module.exports = function compile(options) {
9397
models: modelInstructions,
9498
middleware: middlewareInstructions,
9599
components: componentInstructions,
100+
mixins: mixinInstructions,
96101
files: {
97102
boot: bootScripts
98103
}
@@ -135,10 +140,11 @@ function assertIsValidModelConfig(config) {
135140
* @private
136141
*/
137142

138-
function findScripts(dir) {
143+
function findScripts(dir, extensions) {
139144
assert(dir, 'cannot require directory contents without directory name');
140145

141146
var files = tryReadDir(dir);
147+
extensions = extensions || _.keys(require.extensions);
142148

143149
// sort files in lowercase alpha for linux
144150
files.sort(function(a, b) {
@@ -599,3 +605,69 @@ function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
599605
var fixedFile = fixFileExtension(resolvedPath, files, false);
600606
return (fixedFile === undefined ? resolvedPath : fixedFile);
601607
}
608+
609+
function buildAllMixinInstructions(appRootDir, mixinDirs, options) {
610+
var extensions = _.without(_.keys(require.extensions),
611+
_.keys(getExcludedExtensions()));
612+
var files = options.mixins || [];
613+
614+
mixinDirs.forEach(function(dir) {
615+
dir = tryResolveAppPath(appRootDir, dir);
616+
if (!dir) {
617+
debug('Skipping unknown module source dir %j', dir);
618+
return;
619+
}
620+
files = files.concat(findScripts(dir, extensions));
621+
});
622+
623+
var mixins = files.map(function(filepath) {
624+
var dir = path.dirname(filepath);
625+
var ext = path.extname(filepath);
626+
var name = path.basename(filepath, ext);
627+
var metafile = path.join(dir, name + FILE_EXTENSION_JSON);
628+
629+
name = normalizeMixinName(name, options);
630+
var meta = {};
631+
meta.name = name;
632+
if (fs.existsSync(metafile)) {
633+
// May overwrite name, not sourceFile
634+
_.extend(meta, require(metafile));
635+
}
636+
meta.sourceFile = filepath;
637+
return meta;
638+
});
639+
640+
return mixins;
641+
}
642+
643+
function normalizeMixinName(str, options) {
644+
var normalization = options.normalization;
645+
switch (normalization) {
646+
case false:
647+
case 'none': return str;
648+
649+
case undefined:
650+
case 'classify':
651+
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
652+
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
653+
str = str.replace(/(?:^|\s|-)\S/g, function(c) { return c.toUpperCase(); });
654+
str = str.replace(/\s+/g, '');
655+
return str;
656+
657+
case 'dasherize':
658+
str = String(str).replace(/([A-Z]+)/g, ' $1').trim();
659+
str = String(str).replace(/[\W_]/g, ' ').toLowerCase();
660+
str = str.replace(/\s+/g, '-');
661+
return str;
662+
663+
default:
664+
if (typeof normalization === 'function') {
665+
return normalization(str);
666+
}
667+
668+
var err = new Error('Invalid normalization format - "' +
669+
normalization + '"');
670+
err.code = 'INVALID_NORMALIZATION_FORMAT';
671+
throw err;
672+
}
673+
}

lib/executor.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function setupDataSources(app, instructions) {
160160
}
161161

162162
function setupModels(app, instructions) {
163+
defineMixins(app, instructions);
163164
defineModels(app, instructions);
164165

165166
instructions.models.forEach(function(data) {
@@ -170,6 +171,26 @@ function setupModels(app, instructions) {
170171
});
171172
}
172173

174+
function defineMixins(app, instructions) {
175+
var modelBuilder = (app.registry || app.loopback).modelBuilder;
176+
var BaseClass = app.loopback.Model;
177+
var mixins = instructions.mixins || [];
178+
179+
if (!modelBuilder.mixins || !mixins.length) return;
180+
181+
mixins.forEach(function(obj) {
182+
var mixin = require(obj.sourceFile);
183+
184+
if (typeof mixin === 'function' || mixin.prototype instanceof BaseClass) {
185+
debug('Defining mixin %s', obj.name);
186+
modelBuilder.mixins.define(obj.name, mixin); // TODO (name, mixin, meta)
187+
} else {
188+
debug('Skipping mixin file %s - `module.exports` is not a function' +
189+
' or Loopback model', obj);
190+
}
191+
});
192+
}
193+
173194
function defineModels(app, instructions) {
174195
var registry = app.registry || app.loopback;
175196
instructions.models.forEach(function(data) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"fs-extra": "^0.12.0",
3939
"jscs": "^1.7.3",
4040
"jshint": "^2.5.6",
41-
"loopback": "^2.5.0",
41+
"loopback": "^2.16.3",
4242
"mocha": "^1.19.0",
4343
"supertest": "^0.14.0"
4444
}

test/browser.test.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ describe('browser support', function() {
6161
});
6262
});
6363

64+
it('loads mixins', function(done) {
65+
var appDir = path.resolve(__dirname, './fixtures/browser-app');
66+
var options = {
67+
appRootDir: appDir,
68+
mixinDirs: ['./mixins']
69+
};
70+
71+
browserifyTestApp(options, function(err, bundlePath) {
72+
if (err) return done(err);
73+
74+
var app = executeBundledApp(bundlePath);
75+
76+
var modelBuilder = app.registry.modelBuilder;
77+
var registry = modelBuilder.mixins.mixins;
78+
expect(Object.keys(registry)).to.eql(['TimeStamps']);
79+
expect(app.models.Customer.timeStampsMixin).to.eql(true);
80+
81+
done();
82+
});
83+
});
84+
6485
it('supports coffee-script files', function(done) {
6586
// add coffee-script to require.extensions
6687
require('coffee-script/register');
@@ -82,7 +103,7 @@ describe('browser support', function() {
82103
});
83104
});
84105

85-
function browserifyTestApp(appDir, strategy, next) {
106+
function browserifyTestApp(options, strategy, next) {
86107
// set default args
87108
if (((typeof strategy) === 'function') && !next) {
88109
next = strategy;
@@ -91,9 +112,10 @@ function browserifyTestApp(appDir, strategy, next) {
91112
if (!strategy)
92113
strategy = 'default';
93114

115+
var appDir = typeof(options) === 'object' ? options.appRootDir : options;
94116
var b = compileStrategies[strategy](appDir);
95117

96-
boot.compileToBrowserify(appDir, b);
118+
boot.compileToBrowserify(options, b);
97119

98120
exportBrowserifyToFile(b, 'browser-app-bundle.js', next);
99121
}

test/compiler.test.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,160 @@ describe('compiler', function() {
10141014
instructions = boot.compile(appdir.PATH);
10151015
expect(instructions.config).to.not.have.property('modified');
10161016
});
1017+
1018+
describe('for mixins', function() {
1019+
function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs) {
1020+
var appJS = appdir.writeFileSync(sourceFile, '');
1021+
1022+
var instructions = boot.compile({
1023+
appRootDir: appdir.PATH,
1024+
mixinDirs: mixinDirs
1025+
});
1026+
1027+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1028+
}
1029+
1030+
it('supports `mixinDirs` option', function() {
1031+
verifyMixinIsFoundViaMixinDirs('mixins/other.js', ['./mixins']);
1032+
});
1033+
1034+
it('resolves relative path in `mixinDirs` option', function() {
1035+
verifyMixinIsFoundViaMixinDirs('custom-mixins/vehicle.js',
1036+
['./custom-mixins']);
1037+
});
1038+
1039+
it('resolves module relative path in `mixinDirs` option', function() {
1040+
verifyMixinIsFoundViaMixinDirs('node_modules/custom-mixins/vehicle.js',
1041+
['custom-mixins']);
1042+
});
1043+
1044+
describe('name normalization', function() {
1045+
var options;
1046+
beforeEach(function() {
1047+
options = { appRootDir: appdir.PATH, mixinDirs: ['./mixins'] };
1048+
1049+
appdir.writeFileSync('mixins/foo.js', '');
1050+
appdir.writeFileSync('mixins/time-stamps.js', '');
1051+
appdir.writeFileSync('mixins/camelCase.js', '');
1052+
appdir.writeFileSync('mixins/PascalCase.js', '');
1053+
appdir.writeFileSync('mixins/space name.js', '');
1054+
});
1055+
1056+
it('supports classify', function() {
1057+
options.normalization = 'classify';
1058+
var instructions = boot.compile(options);
1059+
1060+
var mixins = instructions.mixins;
1061+
var mixinNames = mixins.map(getNameProperty);
1062+
1063+
expect(mixinNames).to.eql([
1064+
'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps'
1065+
]);
1066+
});
1067+
1068+
it('supports dasherize', function() {
1069+
options.normalization = 'dasherize';
1070+
var instructions = boot.compile(options);
1071+
1072+
var mixins = instructions.mixins;
1073+
var mixinNames = mixins.map(getNameProperty);
1074+
1075+
expect(mixinNames).to.eql([
1076+
'camel-case', 'foo', 'pascal-case', 'space-name', 'time-stamps'
1077+
]);
1078+
});
1079+
1080+
it('supports custom function', function() {
1081+
var normalize = function(name) { return name.toUpperCase(); };
1082+
options.normalization = normalize;
1083+
var instructions = boot.compile(options);
1084+
1085+
var mixins = instructions.mixins;
1086+
var mixinNames = mixins.map(getNameProperty);
1087+
1088+
expect(mixinNames).to.eql([
1089+
'CAMELCASE', 'FOO', 'PASCALCASE', 'SPACE NAME', 'TIME-STAMPS'
1090+
]);
1091+
});
1092+
1093+
it('supports none', function() {
1094+
options.normalization = 'none';
1095+
var instructions = boot.compile(options);
1096+
1097+
var mixins = instructions.mixins;
1098+
var mixinNames = mixins.map(getNameProperty);
1099+
1100+
expect(mixinNames).to.eql([
1101+
'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps'
1102+
]);
1103+
});
1104+
1105+
it('supports false', function() {
1106+
options.normalization = false;
1107+
var instructions = boot.compile(options);
1108+
1109+
var mixins = instructions.mixins;
1110+
var mixinNames = mixins.map(getNameProperty);
1111+
1112+
expect(mixinNames).to.eql([
1113+
'camelCase', 'foo', 'PascalCase', 'space name', 'time-stamps'
1114+
]);
1115+
});
1116+
1117+
it('defaults to classify', function() {
1118+
var instructions = boot.compile(options);
1119+
1120+
var mixins = instructions.mixins;
1121+
var mixinNames = mixins.map(getNameProperty);
1122+
1123+
expect(mixinNames).to.eql([
1124+
'CamelCase', 'Foo', 'PascalCase', 'SpaceName', 'TimeStamps'
1125+
]);
1126+
});
1127+
1128+
it('throws error for invalid normalization format', function() {
1129+
options.normalization = 'invalidFormat';
1130+
1131+
expect(function() { boot.compile(options); })
1132+
.to.throw(/Invalid normalization format - "invalidFormat"/);
1133+
});
1134+
});
1135+
1136+
it('overrides default mixin name, by `name` in JSON', function() {
1137+
appdir.writeFileSync('mixins/foo.js', '');
1138+
appdir.writeConfigFileSync('mixins/foo.json', {name: 'fooBar'});
1139+
1140+
var options = { appRootDir: appdir.PATH,
1141+
mixinDirs: ['./mixins']
1142+
};
1143+
var instructions = boot.compile(options);
1144+
1145+
expect(instructions.mixins[0].name).to.eql('fooBar');
1146+
});
1147+
1148+
it('extends definition from JSON with same file name', function() {
1149+
var appJS = appdir.writeFileSync('mixins/foo-bar.js', '');
1150+
1151+
appdir.writeConfigFileSync('mixins/foo-bar.json', {
1152+
description: 'JSON file name same as JS file name' });
1153+
appdir.writeConfigFileSync('mixins/FooBar.json', {
1154+
description: 'JSON file name same as normalized name of mixin' });
1155+
1156+
var options = { appRootDir: appdir.PATH,
1157+
mixinDirs: ['./mixins'],
1158+
normalization: 'classify' };
1159+
var instructions = boot.compile(options);
1160+
1161+
expect(instructions.mixins).to.eql([
1162+
{
1163+
name: 'FooBar',
1164+
description: 'JSON file name same as JS file name',
1165+
sourceFile: appJS
1166+
}
1167+
]);
1168+
});
1169+
1170+
});
10171171
});
10181172

10191173
describe('for middleware', function() {

0 commit comments

Comments
 (0)