diff --git a/index.js b/index.js index 97e56dc..48b3e42 100644 --- a/index.js +++ b/index.js @@ -107,10 +107,14 @@ var addInstructionsToBrowserify = require('./lib/bundler'); * `production`; however the applications are free to use any names. * @property {Array.} [modelSources] List of directories where to look * for files containing model definitions. + * @property {Array.} [mixinSources] List of directories where to look + * for files containing model mixin definitions. - defaults to ['./mixins'] * @property {Array.} [bootDirs] List of directories where to look * for boot scripts. * @property {Array.} [bootScripts] List of script files to execute * on boot. + * @property {String|Function|Boolean} [normalization] Mixin normalization format + * can be false, 'none', 'classify', 'dasherize' - defaults to 'classify'. * @end * * @header boot(app, [options]) diff --git a/lib/compiler.js b/lib/compiler.js index d5d870a..e716b97 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -49,6 +49,7 @@ module.exports = function compile(options) { var bootScripts = options.bootScripts || []; bootDirs.forEach(function(dir) { + dir = path.resolve(dir); bootScripts = bootScripts.concat(findScripts(dir)); }); @@ -59,6 +60,9 @@ module.exports = function compile(options) { var modelInstructions = buildAllModelInstructions( modelsRootDir, modelsConfig, modelSources); + var mixinSources = options.mixinSources || modelsMeta.mixins || ['./mixins']; + var mixinInstructions = buildAllMixinInstructions(appRootDir, mixinSources, 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. @@ -66,12 +70,30 @@ module.exports = function compile(options) { config: appConfig, dataSources: dataSourcesConfig, models: modelInstructions, + mixins: mixinInstructions, files: { boot: bootScripts } }); }; +function buildAllMixinInstructions(appRootDir, mixinSources, options) { + var files = options.mixins || []; + mixinSources.forEach(function(dir) { + dir = path.resolve(appRootDir, dir); + files = files.concat(findScripts(dir)); + }); + + var mixins = {}; + files.forEach(function(filepath) { + var ext = path.extname(filepath); + var name = normalizeMixinName(path.basename(filepath, ext), options); + mixins[name] = filepath; + }); + + return mixins; +}; + function assertIsValidConfig(name, config) { if(config) { assert(typeof config === 'object', @@ -294,3 +316,19 @@ function loadModelDefinition(rootDir, jsonFile) { sourceFile: sourceFile }; } + +function normalizeMixinName(str, options) { + var normalization = options.normalization; + if (normalization === false || normalization === 'none') return str; + if (normalization === 'dasherize') { + str = String(str).replace(/[\W_]/g, ' ').toLowerCase(); + str = str.replace(/\s+/g, '-'); + } else if (typeof normalization === 'function') { + str = normalization(str); + } else { // classify + 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; +} diff --git a/lib/executor.js b/lib/executor.js index ceef61f..38ca1f2 100644 --- a/lib/executor.js +++ b/lib/executor.js @@ -135,6 +135,7 @@ function setupDataSources(app, instructions) { } function setupModels(app, instructions) { + defineMixins(app, instructions); defineModels(app, instructions); instructions.models.forEach(function(data) { @@ -145,6 +146,27 @@ function setupModels(app, instructions) { }); } +function defineMixins(app, instructions) { + var modelBuilder = app.loopback.modelBuilder; + var BaseClass = app.loopback.Model; + var mixins = instructions.mixins || {}; + var mixinNames = Object.keys(mixins); + if (modelBuilder.mixins && mixinNames.length > 0) { + mixinNames.forEach(function(name) { + var mixin = tryRequire(mixins[name]); + if (typeof mixin === 'function' || mixin.prototype instanceof BaseClass) { + var mixinName = mixin.name || mixin.mixinName || name; + debug('Configuring mixin %s', mixinName); + modelBuilder.mixins.define(mixinName, mixin); + } else { + debug('Skipping mixin file %s - `module.exports` is not a function' + + ' or Loopback model', + mixins[name]); + } + }); + } +} + function defineModels(app, instructions) { instructions.models.forEach(function(data) { var name = data.name; diff --git a/package.json b/package.json index 779f7d2..f9d2a52 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "browserify": "^4.1.8", "fs-extra": "^0.10.0", "jshint": "^2.5.6", - "loopback": "^1.5.0", + "loopback": "^2.2.0", "mocha": "^1.19.0", "must": "^0.12.0", "supertest": "^0.13.0" diff --git a/test/compiler.test.js b/test/compiler.test.js index 45e17ec..1e5ece6 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -6,6 +6,7 @@ var sandbox = require('./helpers/sandbox'); var appdir = require('./helpers/appdir'); var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app'); +var MIXIN_SOURCES = path.join(__dirname, 'fixtures', 'mixins'); describe('compiler', function() { beforeEach(sandbox.reset); @@ -620,6 +621,47 @@ describe('compiler', function() { instructions = boot.compile(appdir.PATH); expect(instructions.config).to.not.have.property('modified'); }); + + it('has a mixinSources option', function() { + var options = { appRootDir: SIMPLE_APP, mixinSources: [MIXIN_SOURCES] }; + var instructions = boot.compile(options); + expect(instructions.mixins).to.not.have.property('TimeStamps'); + expect(instructions.mixins).to.have.property('Other'); + }); + + it('supports mixin name normalization - classify (default)', function() { + var options = { appRootDir: SIMPLE_APP }; + var instructions = boot.compile(options); + expect(instructions.mixins).to.have.property('Example'); + expect(instructions.mixins).to.have.property('Foo'); + expect(instructions.mixins).to.have.property('TimeStamps'); + }); + + it('supports mixin name normalization - dasherize', function() { + var options = { appRootDir: SIMPLE_APP, normalization: 'dasherize' }; + var instructions = boot.compile(options); + expect(instructions.mixins).to.have.property('example'); + expect(instructions.mixins).to.have.property('foo'); + expect(instructions.mixins).to.have.property('time-stamps'); + }); + + it('supports mixin name normalization - custom function', function() { + var normalize = function(name) { return name.toUpperCase(); }; + var options = { appRootDir: SIMPLE_APP, normalization: normalize }; + var instructions = boot.compile(options); + expect(instructions.mixins).to.have.property('EXAMPLE'); + expect(instructions.mixins).to.have.property('FOO'); + expect(instructions.mixins).to.have.property('TIME-STAMPS'); + }); + + it('supports skip mixin name normalization - none', function() { + var options = { appRootDir: SIMPLE_APP, normalization: false }; + var instructions = boot.compile(options); + expect(instructions.mixins).to.have.property('example'); + expect(instructions.mixins).to.have.property('Foo'); + expect(instructions.mixins).to.have.property('time-stamps'); + }); + }); }); diff --git a/test/executor.test.js b/test/executor.test.js index 4a63929..513b1ab 100644 --- a/test/executor.test.js +++ b/test/executor.test.js @@ -193,6 +193,15 @@ describe('executor', function() { done(); }, 10); }); + + it('should define `mixins/*` files', function() { + if (app.loopback.datasourceJuggler && app.loopback.datasourceJuggler.mixins) { + var mixins = app.loopback.datasourceJuggler.mixins; + expect(mixins.registry).to.have.property('Example'); + expect(mixins.registry).to.have.property('TimeStamps'); + expect(mixins.registry).to.have.property('bar'); // mixinName + } + }); }); describe('with boot with callback', function() { @@ -217,7 +226,15 @@ describe('executor', function() { done(); }); }); - + + it('should define `mixins/*` files', function() { + var modelBuilder = app.loopback.modelBuilder; + var registry = modelBuilder.mixins.mixins; + expect(registry).to.have.property('Example'); + expect(registry).to.have.property('TimeStamps'); + expect(registry).to.have.property('bar'); // mixinName + }); + }); describe('with PaaS and npm env variables', function() { diff --git a/test/fixtures/mixins/other.js b/test/fixtures/mixins/other.js new file mode 100644 index 0000000..67744b8 --- /dev/null +++ b/test/fixtures/mixins/other.js @@ -0,0 +1,5 @@ +module.exports = function(Model, options) { + + Model.otherMixin = true; + +} \ No newline at end of file diff --git a/test/fixtures/simple-app/mixins/Foo.js b/test/fixtures/simple-app/mixins/Foo.js new file mode 100644 index 0000000..0123cc5 --- /dev/null +++ b/test/fixtures/simple-app/mixins/Foo.js @@ -0,0 +1,10 @@ +// When you create a named function, its name will be used +// as the mixin name - alternatively, set mixin.mixinName. + +var mixin = function bar(Model, options) { + + Model.barMixin = true; + +}; + +module.exports = mixin; \ No newline at end of file diff --git a/test/fixtures/simple-app/mixins/example.js b/test/fixtures/simple-app/mixins/example.js new file mode 100644 index 0000000..d583bef --- /dev/null +++ b/test/fixtures/simple-app/mixins/example.js @@ -0,0 +1,5 @@ +module.exports = function(Model, options) { + + Model.exampleMixin = true; + +} \ No newline at end of file diff --git a/test/fixtures/simple-app/mixins/time-stamps.js b/test/fixtures/simple-app/mixins/time-stamps.js new file mode 100644 index 0000000..6200848 --- /dev/null +++ b/test/fixtures/simple-app/mixins/time-stamps.js @@ -0,0 +1,5 @@ +module.exports = function(Model, options) { + + Model.timeStampsMixin = true; + +} \ No newline at end of file