Skip to content

Commit

Permalink
Merge pull request #485 from SBoudrias/composeWith
Browse files Browse the repository at this point in the history
Initial take at Base#composeWith()
  • Loading branch information
addyosmani committed Feb 19, 2014
2 parents feea0e0 + 2f8788a commit f468a48
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 14 deletions.
37 changes: 37 additions & 0 deletions lib/base.js
Expand Up @@ -116,6 +116,7 @@ var Base = module.exports = function Base(args, options) {
this._arguments = [];
this._options = [];
this._hooks = [];
this._composedWith = [];
this._conflicts = [];
this.appname = this.determineAppname();

Expand Down Expand Up @@ -379,6 +380,7 @@ Base.prototype.run = function run(args, cb) {
});

this.runHooks();
_.invoke(this._composedWith, 'run');

return this;
};
Expand Down Expand Up @@ -477,6 +479,41 @@ Base.prototype.defaultFor = function defaultFor(name) {
return this.options[name] || name;
};

/**
* Compose this generator with another one.
* @param {String} namespace The generator namespace to compose with
* @param {Object} options The options passed to the Generator
* @param {Object} [settings] Settings hash on the composition relation
* @param {string} [settings.local] Path to a locally stored generator
* @param {String} [settings.link="weak"] If "strong", the composition will occured
* even when the composition is initialized by
* the end user
* @return {this}
*
* @example <caption>Using a peerDependency generator</caption>
* this.composeWith('bootstrap', { options: { sass: true } });
*
* @example <caption>Using a direct dependency generator</caption>
* this.composeWith('bootstrap', { options: { sass: true } }, {
* local: require.resolve('generator-bootstrap/app/main.js');
* });
*/

Base.prototype.composeWith = function composeWith(namespace, options, settings) {
settings = settings || {};
var generator;
if (settings.local) {
var Generator = require(settings.local);
Generator.resolved = settings.local;
Generator.namespace = namespace;
generator = this.env.instantiate(Generator, options);
} else {
generator = this.env.create(namespace, options);
}
this._composedWith.push(generator);
return this;
};

/**
* Determine the root generator name (the one who's extending Base).
*/
Expand Down
30 changes: 22 additions & 8 deletions lib/env/index.js
Expand Up @@ -232,11 +232,6 @@ Environment.prototype.create = function create(namespace, options) {

var Generator = this.get(namespace);

var args = options.arguments || options.args || _.clone(this.arguments);
args = Array.isArray(args) ? args : args.split(' ');

var opts = options.options || _.clone(this.options);

if (!Generator) {
return this.error(
new Error(
Expand All @@ -248,10 +243,29 @@ Environment.prototype.create = function create(namespace, options) {
);
}

return this.instantiate(Generator, options);
};

/**
* Instantiate a Generator with metadatas
*
* @param {String} namespace
* @param {Object} options
* @param {Array|String} options.arguments Arguments to pass the instance
* @param {Object} options.options Options to pass the instance
*/

Environment.prototype.instantiate = function instantiate(Generator, options) {
options = options || {};

var args = options.arguments || options.args || _.clone(this.arguments);
args = Array.isArray(args) ? args : args.split(' ');

var opts = options.options || _.clone(this.options);

opts.env = this;
opts.name = name;
opts.resolved = Generator.resolved;
opts.namespace = namespace;
opts.resolved = Generator.resolved || 'unknown';
opts.namespace = Generator.namespace;
return new Generator(args, opts);
};

Expand Down
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -62,6 +62,7 @@
"gulp-jshint": "~1.4.0",
"jshint-stylish": "~0.1.4",
"gulp-jscs": "~0.3.0",
"gulp-istanbul": "~0.1.0"
"gulp-istanbul": "~0.1.0",
"mockery": "~1.4.0"
}
}
81 changes: 81 additions & 0 deletions test/base.js
Expand Up @@ -3,6 +3,11 @@
var fs = require('fs');
var path = require('path');
var sinon = require('sinon');
var mockery = require('mockery');
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false
});
var generators = require('..');
var yo = generators;
var helpers = generators.test;
Expand Down Expand Up @@ -390,6 +395,82 @@ describe('yeoman.generators.Base', function () {
});
});

describe('#composeWith()', function () {
beforeEach(function () {
this.dummy = new this.Dummy([], {
resolved: 'unknown',
namespace: 'dummy',
env: this.env,
'skip-install': true
});
this.spy = sinon.spy();
this.GenCompose = yo.generators.Base.extend({ exec: this.spy });
this.env.registerStub(this.GenCompose, 'composed:gen');
});

it('runs the composed generators', function (done) {
this.dummy.composeWith('composed:gen');
var runSpy = sinon.spy(this.dummy, 'run');
// I use a setTimeout here just to make sure composeWith() doesn't run the generator
setTimeout(function () {
this.dummy.run(function () {
assert(this.spy.calledAfter(runSpy));
done();
}.bind(this));
}.bind(this), 100);
});

it('pass options and arguments to the composed generators', function (done) {
this.dummy.composeWith('composed:gen', {
options: { foo: 'bar', 'skip-install': true },
arguments: ['foo']
});
this.dummy.run(function () {
assert.equal(this.spy.firstCall.thisValue.options.foo, 'bar');
assert.deepEqual(this.spy.firstCall.thisValue.args, ['foo']);
done();
}.bind(this));
});

describe('when passing a local path to a generator', function () {
beforeEach(function () {
this.spy = sinon.spy();
this.stubPath = path.join(__dirname, 'generator-dumb/main.js');
this.LocalDummy = yo.generators.Base.extend({ exec: this.spy });
mockery.registerMock(this.stubPath, this.LocalDummy);
});

it('runs the composed generator', function (done) {
this.dummy.composeWith('dumb', {}, { local: this.stubPath });
this.dummy.run(function () {
assert(this.LocalDummy.prototype.exec.called);
done();
}.bind(this));
});

it('pass options and arguments to the composed generators', function (done) {
this.dummy.composeWith('dumb', {
options: { foo: 'bar', 'skip-install': true },
arguments: ['foo']
}, { local: this.stubPath });
this.dummy.run(function () {
assert.equal(this.spy.firstCall.thisValue.options.foo, 'bar');
assert.deepEqual(this.spy.firstCall.thisValue.args, ['foo']);
done();
}.bind(this));
});

it('sets correct metadata on the Generator constructor', function (done) {
this.dummy.composeWith('dumb', {}, { local: this.stubPath });
this.dummy.run(function () {
assert.equal(this.spy.firstCall.thisValue.constructor.namespace, 'dumb');
assert.equal(this.spy.firstCall.thisValue.constructor.resolved, this.stubPath);
done();
}.bind(this));
});
});
});

describe('#desc()', function () {
it('update the internal description', function () {
this.dummy.desc('A new desc for this generator');
Expand Down
5 changes: 0 additions & 5 deletions test/env.js
Expand Up @@ -138,11 +138,6 @@ describe('Environment', function () {
assert.throws(this.env.create.bind(this.end, 'i:do:not:exist'));
});

it('add a name property on the options', function () {
assert.equal(this.env.create('stub').options.name, 'stub');
assert.equal(this.env.create('stub:foo:bar').options.name, 'bar');
});

it('add the env as property on the options', function () {
assert.equal(this.env.create('stub').options.env, this.env);
});
Expand Down

0 comments on commit f468a48

Please sign in to comment.