Skip to content

Commit

Permalink
Implement run with environment
Browse files Browse the repository at this point in the history
  • Loading branch information
mshima committed May 6, 2020
1 parent b8dbad6 commit 2bbd915
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Expand Up @@ -2,7 +2,8 @@
"extends": ["xo", "prettier"],
"env": {
"jest": true,
"node": true
"node": true,
"mocha": true
},
"rules": {
"prettier/prettier": [
Expand Down
1 change: 1 addition & 0 deletions lib/adapter.js
@@ -1,3 +1,4 @@
/* eslint-disable max-params */
'use strict';
var events = require('events');
var _ = require('lodash');
Expand Down
32 changes: 26 additions & 6 deletions lib/index.js
@@ -1,3 +1,4 @@
/* eslint-disable max-params */
/**
* Collection of unit test helpers. (mostly related to Mocha syntax)
* @module test/helpers
Expand Down Expand Up @@ -95,16 +96,17 @@ exports.testDirectory = function(dir, cb) {

/**
* Answer prompt questions for the passed-in generator
* @param {Generator} generator - a Yeoman generator
* @param {Generator|Environment} generator - a Yeoman generator or environment
* @param {Object} answers - an object where keys are the
* generators prompt names and values are the answers to
* the prompt questions
* @example
* mockPrompt(angular, {'bootstrap': 'Y', 'compassBoostrap': 'Y'});
*/

exports.mockPrompt = function(generator, answers, callback) {
var promptModule = generator.env.adapter.promptModule;
exports.mockPrompt = function(envOrGenerator, answers, callback) {
envOrGenerator = envOrGenerator.env || envOrGenerator;
var promptModule = envOrGenerator.adapter.promptModule;
answers = answers || {};
var DummyPrompt = adapter.DummyPrompt;

Expand All @@ -115,10 +117,11 @@ exports.mockPrompt = function(generator, answers, callback) {

/**
* Restore defaults prompts on a generator.
* @param {Generator} generator
* @param {Generator|Environment} generator or environment
*/
exports.restorePrompt = function(generator) {
generator.env.adapter.promptModule.restoreDefaultPrompts();
exports.restorePrompt = function(envOrGenerator) {
envOrGenerator = envOrGenerator.env || envOrGenerator;
envOrGenerator.adapter.promptModule.restoreDefaultPrompts();
};

/**
Expand Down Expand Up @@ -227,3 +230,20 @@ exports.run = function(GeneratorOrNamespace, settings, envOptions) {
var RunContext = require('./run-context');
return new RunContext(GeneratorOrNamespace, settings, envOptions);
};

/**
* Prepare a run context
* @param {String|Function} GeneratorOrNamespace - Generator constructor or namespace
* @return {RunContext}
*/

exports.create = function(GeneratorOrNamespace, settings, envOptions) {
var RunContext = require('./run-context');
const context = new RunContext(
GeneratorOrNamespace,
{ ...settings, runEnvironment: true },
envOptions
);

return context;
};
111 changes: 90 additions & 21 deletions lib/run-context.js
Expand Up @@ -35,6 +35,7 @@ const callbackWrapper = done => {
* @param {Boolean} [settings.tmpdir=true] - Automatically run this generator in a tmp dir
* @param {String} [settings.resolved] - File path to the generator (only used if Generator is a constructor)
* @param {String} [settings.namespace='gen:test'] - Namespace (only used if Generator is a constructor)
* @param {String} [settings.runEnvironment=false] - Require the run context to call run.
* @param {Object} [envOptions] - Options to be passed to environment.
* @return {this}
*/
Expand All @@ -50,15 +51,21 @@ function RunContext(Generator, settings, envOptions = {}) {
this.dependencies = [];
this.Generator = Generator;
this.inDirCallbacks = [];
this.settings = _.extend({ tmpdir: true, namespace: 'gen:test' }, settings);
this.lookups = [];
this.settings = _.extend(
{ tmpdir: true, namespace: 'gen:test', runEnvironment: false },
settings
);
this.envOptions = envOptions;

this.withOptions({
force: true,
skipCache: true,
skipInstall: true
});
setTimeout(this._run.bind(this), 10);
if (!this.settings.runEnvironment) {
setTimeout(this._run.bind(this), 10);
}
}

util.inherits(RunContext, EventEmitter);
Expand Down Expand Up @@ -88,12 +95,12 @@ RunContext.prototype._run = function() {
}

if (this._asyncHolds !== 0 || this.ran || this.completed) {
return;
return false;
}

if ((!this.dirReady && this.settings.tmpdir) || this.ran) {
if (!this.dirReady && this.settings.tmpdir) {
setTimeout(this._run.bind(this), 10);
return;
return false;
}

this.ran = true;
Expand All @@ -102,17 +109,30 @@ RunContext.prototype._run = function() {
this.inDirCallbacks.forEach(cb => cb(targetDirectory));
}

var namespace;
const testEnv = helpers.createTestEnv(this.envOptions.createEnv, this.envOptions);
const envOptions = { ...this.envOptions };
if (this.settings.runEnvironment) {
envOptions.newErrorHandler = true;
}

const testEnv = helpers.createTestEnv(this.envOptions.createEnv, envOptions);
this.env = this.envCB ? this.envCB(testEnv) || testEnv : testEnv;

this.lookups.forEach(lookup => {
this.env.lookup(lookup);
});

helpers.registerDependencies(this.env, this.dependencies);

let namespace;
if (typeof this.Generator === 'string') {
namespace = this.env.namespace(this.Generator);
if (namespace !== this.Generator) {
// Generator is a file path, it should be registered.
this.env.register(this.Generator);
if (this.settings.runEnvironment) {
namespace = this.Generator;
} else {
namespace = this.env.namespace(this.Generator);
if (namespace !== this.Generator) {
// Generator is a file path, it should be registered.
this.env.register(this.Generator);
}
}
} else {
namespace = this.settings.namespace;
Expand All @@ -124,39 +144,76 @@ RunContext.prototype._run = function() {
options: this.options
});

helpers.mockPrompt(this.generator, this.answers, this.promptCallback);
helpers.mockPrompt(this.env, this.answers, this.promptCallback);

if (this.localConfig) {
// Only mock local config when withLocalConfig was called
helpers.mockLocalConfig(this.generator, this.localConfig);
}

const endCallback = callbackWrapper(envOrGenerator => {
helpers.restorePrompt(envOrGenerator);
this.emit('end');
this.completed = true;
});

if (this.settings.runEnvironment) {
this.env.once('end', () => endCallback(this.env));
return true;
}

const callback = callbackWrapper(
function(err) {
helpers.restorePrompt(this.env);
this.emit('error', err);
}.bind(this)
);

this.generator.on('error', callback);
this.generator.once(
'end',
function() {
helpers.restorePrompt(this.generator);
this.emit('end');
this.completed = true;
}.bind(this)
);

this.generator.once('end', () => endCallback(this.generator));
this.emit('ready', this.generator);

this.generator.run().catch(callback);
return true;
};

/**
* Build the generator and the environment.
* @return {RunContext} this
*/
RunContext.prototype.build = function() {
if (!this._run()) {
throw new Error('The context is not ready');
}

return this;
};

/**
* Run the generator on the environment and returns the promise.
* @return {Promise} Promise
*/
RunContext.prototype.run = function() {
if (!this.settings.runEnvironment) {
throw new Error('Should be called with runEnvironment option');
}

if (!this.ran) {
this.build();
}

return this.env.runGenerator(this.generator);
};

/**
* Return a promise representing the generator run process
* @return {Promise} Promise resolved on end or rejected on error
*/
RunContext.prototype.toPromise = function() {
if (this.settings.runEnvironment) {
throw new Error('RunContext with runEnvironment uses promises by default');
}

return new Promise(
function(resolve, reject) {
this.on(
Expand Down Expand Up @@ -281,6 +338,18 @@ RunContext.prototype.withEnvironment = function(cb) {
return this;
};

/**
* Run lookup on the environment.
*
* @param {Object|Array} [lookups] - lookup to run.
* @return {this} run context instance.
*/
RunContext.prototype.withLookups = function(lookups) {
lookups = Array.isArray(lookups) ? lookups : [lookups];
this.lookups = this.lookups.concat(lookups);
return this;
};

/**
* Clean the directory used for tests inside inDir/inTmpDir
*/
Expand Down
4 changes: 3 additions & 1 deletion test/fixtures/generator-simple/app/index.js
Expand Up @@ -15,7 +15,9 @@ var Generator = require('yeoman-generator');
// in several methods.

class SimpleGenerator extends Generator {
exec() {}
exec() {
this.env.generatorTestExecuted = true;
}
}

SimpleGenerator.description =
Expand Down
28 changes: 28 additions & 0 deletions test/fixtures/generator-simple/composing/index.js
@@ -0,0 +1,28 @@
'use strict';
var Generator = require('yeoman-generator');

// Example of a simple generator.
//
// A raw function that is executed when this generator is resolved.
//
// It takes a list of arguments (usually CLI args) and a Hash of options
// (CLI options), the context of the function is a `new Generator.Base`
// object, which means that you can use the API as if you were extending
// `Base`.
//
// It works with simple generator. If you need to do a bit more complex
// stuff, extend from Generator.Base and defines your generator steps
// in several methods.

class SimpleGenerator extends Generator {
exec(toCompose) {
console.log(toCompose);
this.composeWith(toCompose);
}
}

SimpleGenerator.description =
'And add a custom description by adding a `description` property to your function.';
SimpleGenerator.usage = 'Usage can be used to customize the help output';

module.exports = SimpleGenerator;
27 changes: 27 additions & 0 deletions test/fixtures/generator-simple/throwing/index.js
@@ -0,0 +1,27 @@
'use strict';
var Generator = require('yeoman-generator');

// Example of a simple generator.
//
// A raw function that is executed when this generator is resolved.
//
// It takes a list of arguments (usually CLI args) and a Hash of options
// (CLI options), the context of the function is a `new Generator.Base`
// object, which means that you can use the API as if you were extending
// `Base`.
//
// It works with simple generator. If you need to do a bit more complex
// stuff, extend from Generator.Base and defines your generator steps
// in several methods.

class SimpleGenerator extends Generator {
exec() {
throw new Error('throwing error');
}
}

SimpleGenerator.description =
'And add a custom description by adding a `description` property to your function.';
SimpleGenerator.usage = 'Usage can be used to customize the help output';

module.exports = SimpleGenerator;

0 comments on commit 2bbd915

Please sign in to comment.