diff --git a/lib/index.js b/lib/index.js index fc3357fd..f0e1e0e0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,4 @@ 'use strict'; -const util = require('util'); const fs = require('fs'); const path = require('path'); const os = require('os'); @@ -56,687 +55,668 @@ const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@'; * } * }; */ +class Generator extends EventEmitter { + constructor(args, options) { + super(); -const Generator = function (args, options) { - EventEmitter.call(this); + if (!Array.isArray(args)) { + options = args; + args = []; + } - if (!Array.isArray(args)) { - options = args; - args = []; - } + this.options = options || {}; + this._initOptions = _.clone(options); + this._args = args || []; + this._options = {}; + this._arguments = []; + this._composedWith = []; + this._transformStreams = []; + + this.option('help', { + type: Boolean, + alias: 'h', + description: 'Print the generator\'s options and usage' + }); - this.options = options || {}; - this._initOptions = _.clone(options); - this._args = args || []; - this._options = {}; - this._arguments = []; - this._composedWith = []; - this._transformStreams = []; - - this.option('help', { - type: Boolean, - alias: 'h', - description: 'Print the generator\'s options and usage' - }); - - this.option('skip-cache', { - type: Boolean, - description: 'Do not remember prompt answers', - default: false - }); - - this.option('skip-install', { - type: Boolean, - description: 'Do not automatically install dependencies', - default: false - }); - - // Checks required paramaters - assert(this.options.env, 'You must provide the environment object. Use env#create() to create a new generator.'); - assert(this.options.resolved, 'You must provide the resolved path value. Use env#create() to create a new generator.'); - this.env = this.options.env; - this.resolved = this.options.resolved; - - // Ensure the environment support features this yeoman-generator version require. - require('yeoman-environment').enforceUpdate(this.env); - - this.description = this.description || ''; - - this.async = function () { - return function () {}; - }; - - this.fs = FileEditor.create(this.env.sharedFs); - this.conflicter = new Conflicter(this.env.adapter, this.options.force); - - // Mirror the adapter log method on the generator. - // - // example: - // this.log('foo'); - // this.log.error('bar'); - this.log = this.env.adapter.log; - - // Determine the app root - this.contextRoot = this.env.cwd; - - let rootPath = findUp.sync('.yo-rc.json', { - cwd: this.env.cwd - }); - rootPath = rootPath ? path.dirname(rootPath) : this.env.cwd; - - if (rootPath !== this.env.cwd) { - this.log([ - '', - 'Just found a `.yo-rc.json` in a parent directory.', - 'Setting the project root at: ' + rootPath - ].join('\n')); - this.destinationRoot(rootPath); - } + this.option('skip-cache', { + type: Boolean, + description: 'Do not remember prompt answers', + default: false + }); - this.appname = this.determineAppname(); - this.config = this._getStorage(); - this._globalConfig = this._getGlobalStorage(); + this.option('skip-install', { + type: Boolean, + description: 'Do not automatically install dependencies', + default: false + }); - // Ensure source/destination path, can be configured from subclasses - this.sourceRoot(path.join(path.dirname(this.resolved), 'templates')); -}; + // Checks required paramaters + assert(this.options.env, 'You must provide the environment object. Use env#create() to create a new generator.'); + assert(this.options.resolved, 'You must provide the resolved path value. Use env#create() to create a new generator.'); + this.env = this.options.env; + this.resolved = this.options.resolved; -util.inherits(Generator, EventEmitter); + // Ensure the environment support features this yeoman-generator version require. + require('yeoman-environment').enforceUpdate(this.env); -// Mixin the actions modules -_.extend(Generator.prototype, require('./actions/install')); -_.extend(Generator.prototype, require('./actions/help')); -_.extend(Generator.prototype, require('./actions/spawn-command')); -Generator.prototype.user = require('./actions/user'); + this.description = this.description || ''; -/* - * Prompt user to answer questions. The signature of this method is the same as {@link https://github.com/SBoudrias/Inquirer.js Inquirer.js} - * - * On top of the Inquirer.js API, you can provide a `{cache: true}` property for - * every question descriptor. When set to true, Yeoman will store/fetch the - * user's answers as defaults. - * - * @param {array} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation} - * @return {Promise} - */ + this.async = () => () => {}; -Generator.prototype.prompt = function (questions) { - questions = promptSuggestion.prefillQuestions(this._globalConfig, questions); - questions = promptSuggestion.prefillQuestions(this.config, questions); + this.fs = FileEditor.create(this.env.sharedFs); + this.conflicter = new Conflicter(this.env.adapter, this.options.force); - return this.env.adapter.prompt(questions).then(answers => { - if (!this.options['skip-cache']) { - promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false); - promptSuggestion.storeAnswers(this.config, questions, answers, true); - } + // Mirror the adapter log method on the generator. + // + // example: + // this.log('foo'); + // this.log.error('bar'); + this.log = this.env.adapter.log; - return answers; - }); -}; + // Determine the app root + this.contextRoot = this.env.cwd; -/** - * Adds an option to the set of generator expected options, only used to - * generate generator usage. By default, generators get all the cli options - * parsed by nopt as a `this.options` hash object. - * - * ### Options: - * - * - `description` Description for the option - * - `type` Either Boolean, String or Number - * - `alias` Option name alias (example `-h` and --help`) - * - `default` Default value - * - `hide` Boolean whether to hide from help - * - * @param {String} name - * @param {Object} config - */ - -Generator.prototype.option = function (name, config) { - config = config || {}; + let rootPath = findUp.sync('.yo-rc.json', { + cwd: this.env.cwd + }); + rootPath = rootPath ? path.dirname(rootPath) : this.env.cwd; + + if (rootPath !== this.env.cwd) { + this.log([ + '', + 'Just found a `.yo-rc.json` in a parent directory.', + 'Setting the project root at: ' + rootPath + ].join('\n')); + this.destinationRoot(rootPath); + } - // Alias default to defaults for backward compatibility. - if ('defaults' in config) { - config.default = config.defaults; - } - config.description = config.description || config.desc; - - _.defaults(config, { - name, - description: 'Description for ' + name, - type: Boolean, - hide: false - }); - - // Check whether boolean option is invalid (starts with no-) - const boolOptionRegex = /^no-/; - if (config.type === Boolean && name.match(boolOptionRegex)) { - const simpleName = name.replace(boolOptionRegex, ''); - return this.emit('error', new Error([ - `Option name ${chalk.yellow(name)} cannot start with ${chalk.red('no-')}\n`, - `Option name prefixed by ${chalk.yellow('--no')} are parsed as implicit`, - ` boolean. To use ${chalk.yellow('--' + name)} as an option, use\n`, - chalk.cyan(` this.option('${simpleName}', {type: Boolean})`) - ].join(''))); - } + this.appname = this.determineAppname(); + this.config = this._getStorage(); + this._globalConfig = this._getGlobalStorage(); - if (this._options[name] === null || this._options[name] === undefined) { - this._options[name] = config; + // Ensure source/destination path, can be configured from subclasses + this.sourceRoot(path.join(path.dirname(this.resolved), 'templates')); } - this.parseOptions(); - return this; -}; - -/** - * Adds an argument to the class and creates an attribute getter for it. - * - * Arguments are different from options in several aspects. The first one - * is how they are parsed from the command line, arguments are retrieved - * based on their position. - * - * Besides, arguments are used inside your code as a property (`this.argument`), - * while options are all kept in a hash (`this.options`). - * - * ### Options: - * - * - `description` Description for the argument - * - `required` Boolean whether it is required - * - `optional` Boolean whether it is optional - * - `type` String, Number, Array, or Object - * - `default` Default value for this argument - * - * @param {String} name - * @param {Object} config - */ - -Generator.prototype.argument = function (name, config) { - config = config || {}; + /* + * Prompt user to answer questions. The signature of this method is the same as {@link https://github.com/SBoudrias/Inquirer.js Inquirer.js} + * + * On top of the Inquirer.js API, you can provide a `{cache: true}` property for + * every question descriptor. When set to true, Yeoman will store/fetch the + * user's answers as defaults. + * + * @param {array} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation} + * @return {Promise} + */ + prompt(questions) { + questions = promptSuggestion.prefillQuestions(this._globalConfig, questions); + questions = promptSuggestion.prefillQuestions(this.config, questions); + + return this.env.adapter.prompt(questions).then(answers => { + if (!this.options['skip-cache']) { + promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false); + promptSuggestion.storeAnswers(this.config, questions, answers, true); + } - // Alias default to defaults for backward compatibility. - if ('defaults' in config) { - config.default = config.defaults; + return answers; + }); } - config.description = config.description || config.desc; - - _.defaults(config, { - name, - required: config.default === null || config.default === undefined, - type: String - }); - - this._arguments.push(config); - - this.parseOptions(); - return this; -}; - -Generator.prototype.parseOptions = function () { - const minimistDef = { - string: [], - boolean: [], - alias: {}, - default: {} - }; - - _.each(this._options, option => { - if (option.type === Boolean) { - minimistDef.boolean.push(option.name); - if (!('default' in option) && !option.required) { - minimistDef.default[option.name] = EMPTY; - } - } else { - minimistDef.string.push(option.name); + + /** + * Adds an option to the set of generator expected options, only used to + * generate generator usage. By default, generators get all the cli options + * parsed by nopt as a `this.options` hash object. + * + * ### Options: + * + * - `description` Description for the option + * - `type` Either Boolean, String or Number + * - `alias` Option name alias (example `-h` and --help`) + * - `default` Default value + * - `hide` Boolean whether to hide from help + * + * @param {String} name + * @param {Object} config + */ + option(name, config) { + config = config || {}; + + // Alias default to defaults for backward compatibility. + if ('defaults' in config) { + config.default = config.defaults; } + config.description = config.description || config.desc; - if (option.alias) { - minimistDef.alias[option.alias] = option.name; + _.defaults(config, { + name, + description: 'Description for ' + name, + type: Boolean, + hide: false + }); + + // Check whether boolean option is invalid (starts with no-) + const boolOptionRegex = /^no-/; + if (config.type === Boolean && name.match(boolOptionRegex)) { + const simpleName = name.replace(boolOptionRegex, ''); + return this.emit('error', new Error([ + `Option name ${chalk.yellow(name)} cannot start with ${chalk.red('no-')}\n`, + `Option name prefixed by ${chalk.yellow('--no')} are parsed as implicit`, + ` boolean. To use ${chalk.yellow('--' + name)} as an option, use\n`, + chalk.cyan(` this.option('${simpleName}', {type: Boolean})`) + ].join(''))); } - // Only apply default values if we don't already have a value injected from - // the runner - if (option.name in this._initOptions) { - minimistDef.default[option.name] = this._initOptions[option.name]; - } else if (option.alias && option.alias in this._initOptions) { - minimistDef.default[option.name] = this._initOptions[option.alias]; - } else if ('default' in option) { - minimistDef.default[option.name] = option.default; + if (this._options[name] === null || this._options[name] === undefined) { + this._options[name] = config; } - }); - const parsedOpts = minimist(this._args, minimistDef); + this.parseOptions(); + return this; + } - // Parse options to the desired type - _.each(parsedOpts, (option, name) => { - // Manually set value as undefined if it should be. - if (option === EMPTY) { - parsedOpts[name] = undefined; - return; - } - if (this._options[name] && option !== undefined) { - parsedOpts[name] = this._options[name].type(option); + /** + * Adds an argument to the class and creates an attribute getter for it. + * + * Arguments are different from options in several aspects. The first one + * is how they are parsed from the command line, arguments are retrieved + * based on their position. + * + * Besides, arguments are used inside your code as a property (`this.argument`), + * while options are all kept in a hash (`this.options`). + * + * ### Options: + * + * - `description` Description for the argument + * - `required` Boolean whether it is required + * - `optional` Boolean whether it is optional + * - `type` String, Number, Array, or Object + * - `default` Default value for this argument + * + * @param {String} name + * @param {Object} config + */ + argument(name, config) { + config = config || {}; + + // Alias default to defaults for backward compatibility. + if ('defaults' in config) { + config.default = config.defaults; } - }); - - // Parse positional arguments to valid options - this._arguments.forEach((config, index) => { - let value; - if (index >= parsedOpts._.length) { - if (config.name in this._initOptions) { - value = this._initOptions[config.name]; - } else if ('default' in config) { - value = config.default; - } else { - return; - } - } else if (config.type === Array) { - value = parsedOpts._.slice(index, parsedOpts._.length); - } else { - value = config.type(parsedOpts._[index]); - } - - parsedOpts[config.name] = value; - }); + config.description = config.description || config.desc; - // Make the parsed options available to the instance - _.extend(this.options, parsedOpts); - this.args = parsedOpts._; - this.arguments = parsedOpts._; + _.defaults(config, { + name, + required: config.default === null || config.default === undefined, + type: String + }); - // Make sure required args are all present - this.checkRequiredArgs(); -}; + this._arguments.push(config); -Generator.prototype.checkRequiredArgs = function () { - // If the help option was provided, we don't want to check for required - // arguments, since we're only going to print the help message anyway. - if (this.options.help) { - return; + this.parseOptions(); + return this; } - // Bail early if it's not possible to have a missing required arg - if (this.args.length > this._arguments.length) { - return; - } + parseOptions() { + const minimistDef = { + string: [], + boolean: [], + alias: {}, + default: {} + }; + + _.each(this._options, option => { + if (option.type === Boolean) { + minimistDef.boolean.push(option.name); + if (!('default' in option) && !option.required) { + minimistDef.default[option.name] = EMPTY; + } + } else { + minimistDef.string.push(option.name); + } - this._arguments.forEach(function (config, position) { - // If the help option was not provided, check whether the argument was - // required, and whether a value was provided. - if (config.required && position >= this.args.length) { - return this.emit('error', new Error('Did not provide required argument ' + chalk.bold(config.name) + '!')); - } - }, this); -}; + if (option.alias) { + minimistDef.alias[option.alias] = option.name; + } -/** - * Runs the generator, scheduling prototype methods on a run queue. Method names - * will determine the order each method is run. Methods without special names - * will run in the default queue. - * - * Any method named `constructor` and any methods prefixed by a `_` won't be scheduled. - * - * You can also supply the arguments for the method to be invoked. If none are - * provided, the same values used to initialize the invoker are used to - * initialize the invoked. - * - * @param {Function} [cb] - */ + // Only apply default values if we don't already have a value injected from + // the runner + if (option.name in this._initOptions) { + minimistDef.default[option.name] = this._initOptions[option.name]; + } else if (option.alias && option.alias in this._initOptions) { + minimistDef.default[option.name] = this._initOptions[option.alias]; + } else if ('default' in option) { + minimistDef.default[option.name] = option.default; + } + }); -Generator.prototype.run = function (cb) { - cb = cb || function () {}; + const parsedOpts = minimist(this._args, minimistDef); - const self = this; - this._running = true; - this.emit('run'); + // Parse options to the desired type + _.each(parsedOpts, (option, name) => { + // Manually set value as undefined if it should be. + if (option === EMPTY) { + parsedOpts[name] = undefined; + return; + } + if (this._options[name] && option !== undefined) { + parsedOpts[name] = this._options[name].type(option); + } + }); - const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); - const validMethods = methods.filter(methodIsValid); - assert(validMethods.length, 'This Generator is empty. Add at least one method for it to run.'); + // Parse positional arguments to valid options + this._arguments.forEach((config, index) => { + let value; + if (index >= parsedOpts._.length) { + if (config.name in this._initOptions) { + value = this._initOptions[config.name]; + } else if ('default' in config) { + value = config.default; + } else { + return; + } + } else if (config.type === Array) { + value = parsedOpts._.slice(index, parsedOpts._.length); + } else { + value = config.type(parsedOpts._[index]); + } - this.env.runLoop.once('end', () => { - this.emit('end'); - cb(); - }); + parsedOpts[config.name] = value; + }); - // Ensure a prototype method is a candidate run by default - function methodIsValid(name) { - return name.charAt(0) !== '_' && name !== 'constructor'; - } + // Make the parsed options available to the instance + Object.assign(this.options, parsedOpts); + this.args = parsedOpts._; + this.arguments = parsedOpts._; - function addMethod(method, methodName, queueName) { - queueName = queueName || 'default'; - debug('Queueing ' + methodName + ' in ' + queueName); - self.env.runLoop.add(queueName, completed => { - debug('Running ' + methodName); - self.emit('method:' + methodName); - - runAsync(function () { - self.async = function () { - return this.async(); - }.bind(this); - - return method.apply(self, self.args); - })().then(completed).catch(err => { - debug('An error occured while running ' + methodName, err); - - // Ensure we emit the error event outside the promise context so it won't be - // swallowed when there's no listeners. - setImmediate(() => { - self.emit('error', err); - cb(err); - }); - }); - }); + // Make sure required args are all present + this.checkRequiredArgs(); } - function addInQueue(name) { - const item = Object.getPrototypeOf(self)[name]; - const queueName = self.env.runLoop.queueNames.indexOf(name) === -1 ? null : name; - - // Name points to a function; run it! - if (_.isFunction(item)) { - return addMethod(item, name, queueName); + checkRequiredArgs() { + // If the help option was provided, we don't want to check for required + // arguments, since we're only going to print the help message anyway. + if (this.options.help) { + return; } - // Not a queue hash; stop - if (!queueName) { + // Bail early if it's not possible to have a missing required arg + if (this.args.length > this._arguments.length) { return; } - // Run each queue items - _.each(item, (method, methodName) => { - if (!_.isFunction(method) || !methodIsValid(methodName)) { - return; + this._arguments.forEach((config, position) => { + // If the help option was not provided, check whether the argument was + // required, and whether a value was provided. + if (config.required && position >= this.args.length) { + return this.emit('error', new Error(`Did not provide required argument ${chalk.bold(config.name)}!`)); } - - addMethod(method, methodName, queueName); }); } - validMethods.forEach(addInQueue); - - const writeFiles = function () { - this.env.runLoop.add('conflicts', this._writeFiles.bind(this), { - once: 'write memory fs to disk' - }); - }.bind(this); - - this.env.sharedFs.on('change', writeFiles); - writeFiles(); - - // Add the default conflicts handling - this.env.runLoop.add('conflicts', done => { - this.conflicter.resolve(err => { - if (err) { - this.emit('error', err); - } - - done(); + /** + * Runs the generator, scheduling prototype methods on a run queue. Method names + * will determine the order each method is run. Methods without special names + * will run in the default queue. + * + * Any method named `constructor` and any methods prefixed by a `_` won't be scheduled. + * + * You can also supply the arguments for the method to be invoked. If none are + * provided, the same values used to initialize the invoker are used to + * initialize the invoked. + * + * @param {Function} [cb] + */ + run(cb) { + cb = cb || (() => {}); + + const self = this; + this._running = true; + this.emit('run'); + + const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); + const validMethods = methods.filter(methodIsValid); + assert(validMethods.length, 'This Generator is empty. Add at least one method for it to run.'); + + this.env.runLoop.once('end', () => { + this.emit('end'); + cb(); }); - }); - _.invokeMap(this._composedWith, 'run'); - return this; -}; - -/** - * 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