Skip to content

Commit 77c8c60

Browse files
support 'mixinsources' option
- By default looks for mixinsources in directory - Loads only mixins used through models
1 parent 0e19f0a commit 77c8c60

File tree

4 files changed

+249
-43
lines changed

4 files changed

+249
-43
lines changed

lib/compiler.js

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ module.exports = function compile(options) {
8585
modelsRootDir, modelsConfig, modelSources);
8686

8787
var mixinDirs = options.mixinDirs || [];
88+
var mixinSources = options.mixinSources || modelsMeta.mixins || ['./mixins'];
8889
var mixinInstructions = buildAllMixinInstructions(
89-
appRootDir, mixinDirs, options);
90+
appRootDir, mixinDirs, mixinSources, options, modelInstructions);
9091

9192
// When executor passes the instruction to loopback methods,
9293
// loopback modifies the data. Since we are loading the data using `require`,
@@ -606,21 +607,59 @@ function resolveAppScriptPath(rootDir, relativePath, resolveOptions) {
606607
return (fixedFile === undefined ? resolvedPath : fixedFile);
607608
}
608609

609-
function buildAllMixinInstructions(appRootDir, mixinDirs, options) {
610+
function buildAllMixinInstructions(appRootDir, mixinDirs, mixinSources, options,
611+
modelInstructions) {
610612
var extensions = _.without(_.keys(require.extensions),
611613
_.keys(getExcludedExtensions()));
612-
var files = options.mixins || [];
613614

614-
mixinDirs.forEach(function(dir) {
615+
// load mixins from `options.mixins`
616+
var sourceFiles = options.mixins || [];
617+
var instructionsFromMixins = loadMixins(sourceFiles, options);
618+
619+
// load mixins from `options.mixinDirs`
620+
sourceFiles = findMixinDefinition(appRootDir, mixinDirs, extensions);
621+
if (sourceFiles === undefined) return;
622+
var instructionsFromMixinDirs = loadMixins(sourceFiles, options);
623+
624+
/* If `mixinDirs` and `mixinSources` have any directories in common,
625+
* then remove the common directories from `mixinSources` */
626+
mixinSources = _.difference(mixinSources, mixinDirs);
627+
628+
// load mixins from `options.mixinSources`
629+
sourceFiles = findMixinDefinition(appRootDir, mixinSources, extensions);
630+
if (sourceFiles === undefined) return;
631+
var instructionsFromMixinSources = loadMixins(sourceFiles, options);
632+
633+
// Fetch unique list of mixin names, used in models
634+
var modelMixins = fetchMixinNamesUsedInModelInstructions(modelInstructions);
635+
modelMixins = _.unique(modelMixins);
636+
637+
// Filter-in only mixins, that are used in models
638+
instructionsFromMixinSources = filterMixinInstructionsUsingWhitelist(
639+
instructionsFromMixinSources, modelMixins);
640+
641+
var mixins = concatMixinInstructions(instructionsFromMixins,
642+
instructionsFromMixinDirs);
643+
mixins = concatMixinInstructions(mixins, instructionsFromMixinSources);
644+
return mixins;
645+
}
646+
647+
function findMixinDefinition(appRootDir, sourceDirs, extensions) {
648+
var files = [];
649+
sourceDirs.forEach(function(dir) {
615650
dir = tryResolveAppPath(appRootDir, dir);
616651
if (!dir) {
617652
debug('Skipping unknown module source dir %j', dir);
618-
return;
653+
return undefined;
619654
}
620655
files = files.concat(findScripts(dir, extensions));
621656
});
657+
return files;
658+
}
622659

623-
var mixins = files.map(function(filepath) {
660+
function loadMixins(sourceFiles, options) {
661+
var mixinInstructions = {};
662+
sourceFiles.forEach(function(filepath) {
624663
var dir = path.dirname(filepath);
625664
var ext = path.extname(filepath);
626665
var name = path.basename(filepath, ext);
@@ -634,9 +673,33 @@ function buildAllMixinInstructions(appRootDir, mixinDirs, options) {
634673
_.extend(meta, require(metafile));
635674
}
636675
meta.sourceFile = filepath;
637-
return meta;
676+
mixinInstructions[meta.name] = meta;
638677
});
639678

679+
return _.values(mixinInstructions);
680+
}
681+
682+
function fetchMixinNamesUsedInModelInstructions(modelInstructions) {
683+
return _.flatten(modelInstructions
684+
.map(function(model) {
685+
return model.definition && model.definition.mixins ?
686+
Object.keys(model.definition.mixins) : [];
687+
}));
688+
}
689+
690+
function filterMixinInstructionsUsingWhitelist(instructions, includeMixins) {
691+
return instructions
692+
.filter(function(m) {
693+
return (includeMixins.indexOf(m.name) === -1 ? false : true);
694+
});
695+
}
696+
697+
function concatMixinInstructions(rejectionSet, inclusionSet) {
698+
var mixins = _.reject(rejectionSet, function(instruction) {
699+
return (_.findWhere(inclusionSet,
700+
{name: instruction.name}) !== undefined);
701+
});
702+
mixins = mixins.concat(inclusionSet);
640703
return mixins;
641704
}
642705

test/browser.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ describe('browser support', function() {
6464
it('loads mixins', function(done) {
6565
var appDir = path.resolve(__dirname, './fixtures/browser-app');
6666
var options = {
67-
appRootDir: appDir,
68-
mixinDirs: ['./mixins']
67+
appRootDir: appDir
6968
};
7069

7170
browserifyTestApp(options, function(err, bundlePath) {

test/compiler.test.js

Lines changed: 160 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,41 +1016,175 @@ describe('compiler', function() {
10161016
});
10171017

10181018
describe('for mixins', function() {
1019-
function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs) {
1020-
var appJS = appdir.writeFileSync(sourceFile, '');
1019+
describe(' - mixinDirs', function() {
1020+
function verifyMixinIsFoundViaMixinDirs(sourceFile, mixinDirs) {
1021+
var appJS = appdir.writeFileSync(sourceFile, '');
10211022

1022-
var instructions = boot.compile({
1023-
appRootDir: appdir.PATH,
1024-
mixinDirs: mixinDirs
1023+
var instructions = boot.compile({
1024+
appRootDir: appdir.PATH,
1025+
mixinDirs: mixinDirs
1026+
});
1027+
1028+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1029+
}
1030+
1031+
it('supports `mixinDirs` option', function() {
1032+
verifyMixinIsFoundViaMixinDirs('custom-mixins/other.js',
1033+
['./custom-mixins']);
10251034
});
10261035

1027-
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1028-
}
1036+
it('resolves relative path in `mixinDirs` option', function() {
1037+
verifyMixinIsFoundViaMixinDirs('custom-mixins/other.js',
1038+
['./custom-mixins']);
1039+
});
10291040

1030-
it('supports `mixinDirs` option', function() {
1031-
verifyMixinIsFoundViaMixinDirs('mixins/other.js', ['./mixins']);
1041+
it('resolves module relative path in `mixinDirs` option', function() {
1042+
verifyMixinIsFoundViaMixinDirs('node_modules/custom-mixins/other.js',
1043+
['custom-mixins']);
1044+
});
10321045
});
10331046

1034-
it('resolves relative path in `mixinDirs` option', function() {
1035-
verifyMixinIsFoundViaMixinDirs('custom-mixins/vehicle.js',
1036-
['./custom-mixins']);
1037-
});
1047+
describe(' - mixinSources', function() {
1048+
beforeEach(function() {
1049+
appdir.createConfigFilesSync({}, {}, {
1050+
Car: { dataSource: 'db' }
1051+
});
1052+
appdir.writeConfigFileSync('models/car.json', {
1053+
name: 'Car',
1054+
mixins: {'TimeStamps': {} }
1055+
});
1056+
});
1057+
1058+
function verifyMixinIsFoundViaMixinSources(sourceFile, mixinSources) {
1059+
var appJS = appdir.writeFileSync(sourceFile, '');
1060+
1061+
var instructions = boot.compile({
1062+
appRootDir: appdir.PATH,
1063+
mixinSources: mixinSources
1064+
});
1065+
1066+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1067+
}
1068+
1069+
it('supports `mixinSources` option', function() {
1070+
verifyMixinIsFoundViaMixinSources('mixins/time-stamps.js',
1071+
['./mixins']);
1072+
});
1073+
1074+
it('resolves relative path in `mixinSources` option', function() {
1075+
verifyMixinIsFoundViaMixinSources('custom-mixins/time-stamps.js',
1076+
['./custom-mixins']);
1077+
});
1078+
1079+
it('resolves module relative path in `mixinSources` option',
1080+
function() {
1081+
verifyMixinIsFoundViaMixinSources(
1082+
'node_modules/custom-mixins/time-stamps.js',
1083+
['custom-mixins']);
1084+
});
1085+
1086+
it('supports `mixins` option in `model-config.json`', function() {
1087+
appdir.createConfigFilesSync({}, {}, {
1088+
_meta: {
1089+
mixins: ['./custom-mixins']
1090+
},
1091+
Car: {
1092+
dataSource: 'db'
1093+
}
1094+
});
1095+
1096+
var appJS = appdir.writeFileSync('custom-mixins/time-stamps.js', '');
1097+
var instructions = boot.compile(appdir.PATH);
1098+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1099+
});
10381100

1039-
it('resolves module relative path in `mixinDirs` option', function() {
1040-
verifyMixinIsFoundViaMixinDirs('node_modules/custom-mixins/vehicle.js',
1041-
['custom-mixins']);
1101+
it('sets by default `mixinSources` to `mixins` directory', function() {
1102+
var appJS = appdir.writeFileSync('mixins/time-stamps.js', '');
1103+
var instructions = boot.compile(appdir.PATH);
1104+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1105+
});
1106+
1107+
it('loads only mixins used by models', function() {
1108+
var appJS = appdir.writeFileSync('mixins/time-stamps.js', '');
1109+
appdir.writeFileSync('mixins/foo.js', '');
1110+
1111+
var instructions = boot.compile(appdir.PATH);
1112+
expect(instructions.mixins).to.have.length(1);
1113+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1114+
});
1115+
1116+
it('loads mixins from model using mixin name in JSON file', function() {
1117+
var appJS = appdir.writeFileSync('mixins/time-stamps.js', '');
1118+
appdir.writeConfigFileSync('mixins/time-stamps.json', {
1119+
name: 'Timestamping'
1120+
});
1121+
1122+
appdir.writeConfigFileSync('models/car.json', {
1123+
name: 'Car',
1124+
mixins: {'Timestamping': {} }
1125+
});
1126+
1127+
var instructions = boot.compile(appdir.PATH);
1128+
expect(instructions.mixins).to.have.length(1);
1129+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1130+
});
1131+
1132+
it('loads mixin only once for dirs common to mixinDirs & mixinSources',
1133+
function() {
1134+
var appJS = appdir.writeFileSync('custom-mixins/time-stamps.js', '');
1135+
1136+
var options = {
1137+
appRootDir: appdir.PATH,
1138+
mixinDirs: ['./custom-mixins'],
1139+
mixinSources: ['./custom-mixins']
1140+
};
1141+
1142+
var instructions = boot.compile(options);
1143+
expect(instructions.mixins).to.have.length(1);
1144+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1145+
});
1146+
1147+
it('loads mixin from mixinSources, when it is also found in mixinDirs',
1148+
function() {
1149+
appdir.writeFileSync('mixinDir/time-stamps.js', '');
1150+
var appJS = appdir.writeFileSync('mixinSource/time-stamps.js', '');
1151+
1152+
var options = {
1153+
appRootDir: appdir.PATH,
1154+
mixinDirs: ['./mixinDir'],
1155+
mixinSources: ['./mixinSource']
1156+
};
1157+
1158+
var instructions = boot.compile(options);
1159+
expect(instructions.mixins).to.have.length(1);
1160+
expect(instructions.mixins[0].sourceFile).to.eql(appJS);
1161+
});
1162+
1163+
it('loads mixin from the most recent mixin definition', function() {
1164+
appdir.writeFileSync('mixins1/time-stamps.js', '');
1165+
var mixins2 = appdir.writeFileSync('mixins2/time-stamps.js', '');
1166+
1167+
var options = {
1168+
appRootDir: appdir.PATH,
1169+
mixinSources: ['./mixins1', './mixins2']
1170+
};
1171+
1172+
var instructions = boot.compile(options);
1173+
expect(instructions.mixins).to.have.length(1);
1174+
expect(instructions.mixins[0].sourceFile).to.eql(mixins2);
1175+
});
10421176
});
10431177

10441178
describe('name normalization', function() {
10451179
var options;
10461180
beforeEach(function() {
1047-
options = { appRootDir: appdir.PATH, mixinDirs: ['./mixins'] };
1181+
options = { appRootDir: appdir.PATH, mixinDirs: ['./custom-mixins'] };
10481182

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', '');
1183+
appdir.writeFileSync('custom-mixins/foo.js', '');
1184+
appdir.writeFileSync('custom-mixins/time-stamps.js', '');
1185+
appdir.writeFileSync('custom-mixins/camelCase.js', '');
1186+
appdir.writeFileSync('custom-mixins/PascalCase.js', '');
1187+
appdir.writeFileSync('custom-mixins/space name.js', '');
10541188
});
10551189

10561190
it('supports classify', function() {
@@ -1146,15 +1280,15 @@ describe('compiler', function() {
11461280
});
11471281

11481282
it('extends definition from JSON with same file name', function() {
1149-
var appJS = appdir.writeFileSync('mixins/foo-bar.js', '');
1283+
var appJS = appdir.writeFileSync('custom-mixins/foo-bar.js', '');
11501284

1151-
appdir.writeConfigFileSync('mixins/foo-bar.json', {
1285+
appdir.writeConfigFileSync('custom-mixins/foo-bar.json', {
11521286
description: 'JSON file name same as JS file name' });
1153-
appdir.writeConfigFileSync('mixins/FooBar.json', {
1287+
appdir.writeConfigFileSync('custom-mixins/FooBar.json', {
11541288
description: 'JSON file name same as normalized name of mixin' });
11551289

11561290
var options = { appRootDir: appdir.PATH,
1157-
mixinDirs: ['./mixins'],
1291+
mixinDirs: ['./custom-mixins'],
11581292
normalization: 'classify' };
11591293
var instructions = boot.compile(options);
11601294

@@ -1166,7 +1300,6 @@ describe('compiler', function() {
11661300
}
11671301
]);
11681302
});
1169-
11701303
});
11711304
});
11721305

test/executor.test.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -293,28 +293,39 @@ describe('executor', function() {
293293
});
294294

295295
describe ('for mixins', function() {
296-
it('defines mixins from instructions', function() {
297-
appdir.writeFileSync('mixins/example.js',
296+
var options;
297+
beforeEach(function() {
298+
appdir.writeFileSync('custom-mixins/example.js',
298299
'module.exports = ' +
299300
'function(Model, options) {}');
300301

301-
appdir.writeFileSync('mixins/time-stamps.js',
302+
appdir.writeFileSync('custom-mixins/time-stamps.js',
302303
'module.exports = ' +
303304
'function(Model, options) {}');
304305

305-
appdir.writeConfigFileSync('mixins/time-stamps.json', {
306+
appdir.writeConfigFileSync('custom-mixins/time-stamps.json', {
306307
name: 'Timestamping'
307308
});
308309

309-
var options = {
310-
appRootDir: appdir.PATH,
311-
mixinDirs: ['./mixins']
310+
options = {
311+
appRootDir: appdir.PATH
312312
};
313+
});
313314

315+
it('defines mixins from instructions - using `mixinDirs`', function() {
316+
options.mixinDirs = ['./custom-mixins'];
314317
boot(app, options);
315318

316319
var modelBuilder = app.registry.modelBuilder;
320+
var registry = modelBuilder.mixins.mixins;
321+
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
322+
});
317323

324+
it('defines mixins from instructions - using `mixinSources`', function() {
325+
options.mixinSources = ['./custom-mixins'];
326+
boot(app, options);
327+
328+
var modelBuilder = app.registry.modelBuilder;
318329
var registry = modelBuilder.mixins.mixins;
319330
expect(Object.keys(registry)).to.eql(['Example', 'Timestamping']);
320331
});

0 commit comments

Comments
 (0)