'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); const EventEmitter = require('events'); const assert = require('assert'); const _ = require('lodash'); const findUp = require('find-up'); const semver = require('semver'); const readPkgUp = require('read-pkg-up'); const chalk = require('chalk'); const makeDir = require('make-dir'); const minimist = require('minimist'); const runAsync = require('run-async'); const through = require('through2'); const createDebug = require('debug'); const Conflicter = require('./util/conflicter'); const Storage = require('./util/storage'); const promptSuggestion = require('./util/prompt-suggestion'); const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@'; const debug = createDebug('yeoman:generator'); const ENV_VER_WITH_VER_API = '2.9.0'; // Ensure a prototype method is a candidate run by default const methodIsValid = function(name) { return !['_', '#'].includes(name.charAt(0)) && name !== 'constructor'; }; // New runWithOptions should take precedence if exists. const runGenerator = generator => { if (generator.runWithOptions) { generator.runWithOptions(); } else { generator.run(); } }; /** * Queue options. * @typedef {Object} QueueOptions * @property {string} [queueName] - Name of the queue. * @property {boolean} [once] - Execute only once by namespace and taskName. * @property {boolean} [run] - Run the queue if not running yet. */ /** * Task options. * @typedef {QueueOptions} TaskOptions * @property {Function} [reject] - Reject callback. */ /** * Priority object. * @typedef {QueueOptions} Priority * @property {string} priorityName - Name of the priority. * @property {string} [before] - The queue which this priority should be added before. */ /** * Complete Task object. * @typedef {TaskOptions} Task * @property {WrappedMethod} method - Function to be queued. * @property {string} taskName - Name of the task. */ /** * RunAsync creates a promise and executes wrappedMethod inside the promise. * It replaces async property of the wrappedMethod's context with one RunAsync provides. * async() simulates an async function by creating a callback. * * It supports promises/async and sync functions. * - Promises/async: forward resolve/reject from the runAsync promise to the * promise returned by the wrappedMethod. * - Sync functions: resolves with the returned value. * Can be a promise for chaining * - Sync functions with callback (done = this.async()) calls: * Reject with done(rejectValue) first argument * Resolve with done(undefined, resolveValue) second argument * - Callback must called when 'async()' was called inside a sync function. * - Callback can be ignored when 'async()' was called inside a async function. * @typedef {Function} WrappedMethod */ class Generator extends EventEmitter { // If for some reason environment adds more queues, we should use or own for stability. static get queues() { return [ 'initializing', 'prompting', 'configuring', 'default', 'writing', 'conflicts', 'install', 'end' ]; } /* eslint-disable complexity */ /** * @classdesc The `Generator` class provides the common API shared by all generators. * It define options, arguments, file, prompt, log, API, etc. * * It mixes into its prototype all the methods found in the `actions/` mixins. * * Every generator should extend this base class. * * @constructor * @mixes actions/help * @mixes actions/install * @mixes actions/spawn-command * @mixes actions/user * @mixes actions/fs * @mixes nodejs/EventEmitter * * @param {string[]} args - Provide arguments at initialization * @param {Object} options - Provide options at initialization * @param {Priority[]} [options.customPriorities] - Custom priorities * * @property {Object} env - the current Environment being run * @property {String} resolved - the path to the current generator * @property {String} description - Used in `--help` output * @property {String} appname - The application name * @property {Storage} config - `.yo-rc` config file manager * @property {Object} fs - An instance of {@link https://github.com/SBoudrias/mem-fs-editor Mem-fs-editor} * @property {Function} log - Output content through Interface Adapter * * @example * const Generator = require('yeoman-generator'); * module.exports = class extends Generator { * writing() { * this.fs.write(this.destinationPath('index.js'), 'const foo = 1;'); * } * }; */ constructor(args, options) { super(); if (!Array.isArray(args)) { options = args; args = []; } options = options || {}; this.options = options; this._initOptions = _.clone(options); this._args = args || []; this._options = {}; this._arguments = []; this._prompts = []; this._composedWith = []; this._transformStreams = []; this._namespace = this.options.namespace; this._namespaceId = this.options.namespaceId; 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 }); this.option('force-install', { type: Boolean, description: 'Fail on install dependencies error', default: false }); this.option('ask-answered', { type: Boolean, description: 'Show prompts for already configured options', default: false }); this.resolved = this.options.resolved || __dirname; this.description = this.description || ''; if (this.options.help) { this.options.localConfigOnly = true; } this.env = this.options.env; // Make sure we have a full featured environment. try { const Environment = require('yeoman-environment'); if (!this.env) { this.env = Environment.createEnv(); } else if (Object.getPrototypeOf(this.env) === Object.prototype) { debug('Converting env from a simple object to an Environment'); const env = Environment.createEnv(); Object.assign(env, this.env); this.env = env; } else { Environment.enforceUpdate(this.env); } } catch (error) { const env = this.env; if (!env) { throw new Error('This generator requires an environment.'); } if (Object.getPrototypeOf(this.env) === Object.prototype) { console.log( 'Current Environment is a plain object, some features can be missing' ); } // Ensure the environment support features this yeoman-generator version require. if (!env.adapter || !env.runLoop || !env.sharedFs) { throw new Error( "Current environment doesn't provides some necessary feature this generator needs" ); } } this.fs = require('mem-fs-editor').create(this.env.sharedFs); // Place holder for run-async callback. this.async = () => () => {}; this.conflicter = new Conflicter(this.env.adapter, this.options.force, { bail: this.options.bail, ignoreWhitespace: this.options.whitespace, skipRegenerate: this.options.skipRegenerate, dryRun: this.options.dryRun }); // Mirror the adapter log method on the generator. // // example: // this.log('foo'); // this.log.error('bar'); this.log = this.env.adapter.log; // Add convenience debug object this._debug = createDebug(this.options.namespace || 'yeoman:unknownnamespace'); // Determine the app root this.contextRoot = this.env.cwd; this._destinationRoot = this.options.destinationRoot; if (this.options.localConfigOnly) { debug('Using local configurations only'); } else if (!this._destinationRoot) { 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.appname = this.determineAppname(); this.config = this._getStorage(); if (this._namespaceId && this._namespaceId.generator) { this.generatorConfig = this.config.createStorage(`:${this._namespaceId.generator}`); if (this._namespaceId.instanceId) { this.instanceConfig = this.generatorConfig.createStorage( `#${this._namespaceId.instanceId}` ); } } this._globalConfig = this._getGlobalStorage(); // Ensure source/destination path, can be configured from subclasses this.sourceRoot(path.join(path.dirname(this.resolved), 'templates')); // Queues map: generator's queue name => grouped-queue's queue name (custom name) this._queues = {}; // Add original queues. Generator.queues.forEach(queue => { this._queues[queue] = { priorityName: queue, queueName: queue }; }); // Add custom queues if (Array.isArray(this.options.customPriorities)) { this.registerPriorities(this.options.customPriorities); } this.compose = this.options.compose; // Expose utilities for dependency-less generators. this._ = _; } /** * Register priorities for this generator * * @param {Object[]} priorities - Priorities * @param {String} priorities.priorityName - Priority name * @param {String} priorities.before - The new priority will be queued before the `before` priority. * @param {String} [priorities.queueName] - Name to be used at grouped-queue */ registerPriorities(priorities) { const customPriorities = priorities.map(customPriority => { // Keep backward compatibility with name const newPriority = { priorityName: customPriority.name, ...customPriority }; delete newPriority.name; return newPriority; }); // Sort customPriorities, a referenced custom queue must be added before the one that reference it. customPriorities.sort((a, b) => { if (a.priorityName === b.priorityName) { throw new Error(`Duplicate custom queue ${a.name}`); } if (a.priorityName === b.before) { return -1; } if (b.priorityName === a.before) { return 1; } return 0; }); // Add queue to runLoop customPriorities.forEach(customQueue => { customQueue.queueName = customQueue.queueName || `${this.options.namespace}#${customQueue.priorityName}`; debug(`Registering custom queue ${customQueue.queueName}`); this._queues[customQueue.priorityName] = customQueue; if (this.env.runLoop.queueNames.includes(customQueue.queueName)) { return; } // Backwards compatibilitiy with grouped-queue < 1.0.0 if (!this.env.runLoop.addSubQueue) { let SubQueue; try { SubQueue = require('grouped-queue/lib/subqueue'); } catch (error) { throw new Error( "The running environment doesn't have the necessary features to run this generator. Update it and run again." ); } this.env.runLoop.addSubQueue = function(name, before) { if (this.__queues__[name]) { // Sub-queue already exists return; } if (!before) { // Add at last place. this.__queues__[name] = new SubQueue(); this.queueNames.push(name); return; } if (!this.__queues__[before] || _.indexOf(this.queueNames, before) === -1) { throw new Error('sub-queue ' + before + ' not found'); } const current = this.__queues__; const currentNames = Object.keys(current); // Recreate the queue with new order. this.__queues__ = {}; currentNames.forEach(currentName => { if (currentName === before) { this.__queues__[name] = new SubQueue(); } this.__queues__[currentName] = current[currentName]; }); // Recreate queueNames this.queueNames = Object.keys(this.__queues__); }; } let beforeQueue = customQueue.before ? this._queues[customQueue.before].queueName : undefined; this.env.runLoop.addSubQueue(customQueue.queueName, beforeQueue); }); } checkEnvironmentVersion(packageDependency, version) { if (version === undefined) { version = packageDependency; packageDependency = 'yeoman-environment'; } version = version || ENV_VER_WITH_VER_API; const returnError = currentVersion => { return new Error( `This generator (${this.options.namespace}) requires ${packageDependency} at least ${version}, current version is ${currentVersion}` ); }; if (!this.env.getVersion) { if (!this.options.ignoreVersionCheck) { throw returnError(`less than ${ENV_VER_WITH_VER_API}`); } console.warn( `It's not possible to check version with running Environment less than ${ENV_VER_WITH_VER_API}` ); console.warn('Some features may be missing'); if (semver.lte(version, '2.8.1')) { return undefined; } return false; } let runningVersion = this.env.getVersion(packageDependency); if (runningVersion !== undefined && semver.lte(version, runningVersion)) { return true; } if (this.options.ignoreVersionCheck) { console.warn( `Current ${packageDependency} is not compatible with current generator, min required: ${version} current version: ${runningVersion}. Some features may be missing.` ); return false; } throw returnError(runningVersion); } /** * Convenience debug method * * @param {any} args parameters to be passed to debug */ debug(...args) { this._debug(...args); } /** * Register stored config prompts and optional option alternative. * * @param {Inquirer|Inquirer[]} questions - Inquirer question or questions. * @param {Object|Boolean} [questions.exportOption] - Additional data to export this question as an option. * @param {Storage|String} [question.storage=this.config] - Storage to store the answers. */ registerConfigPrompts(questions) { questions = Array.isArray(questions) ? questions : [questions]; const getOptionTypeFromInquirerType = type => { if (type === 'number') { return Number; } if (type === 'confirm') { return Boolean; } if (type === 'checkbox') { return Array; } return String; }; questions.forEach(q => { const question = { ...q }; if (q.exportOption) { let option = typeof q.exportOption === 'boolean' ? {} : q.exportOption; this.option({ name: q.name, type: getOptionTypeFromInquirerType(q.type), description: q.message, ...option, storage: q.storage || this.config }); } this._prompts.push(question); }); } /** * 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 `{store: true}` property for * every question descriptor. When set to true, Yeoman will store/fetch the * user's answers as defaults. * * @param {object|object[]} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation} * @param {Storage|String} [questions.storage] Storage object or name (generator property) to be used by the question to store/fetch the response. * @param {Storage|String} [storage] Storage object or name (generator property) to be used by default to store/fetch responses. * @return {Promise} prompt promise */ prompt(questions, storage) { const checkInquirer = () => { if (this.inquireSupportsPrefilled === undefined) { this.checkEnvironmentVersion(); this.inquireSupportsPrefilled = this.checkEnvironmentVersion('inquirer', '7.1.0'); } }; if (storage !== undefined) { checkInquirer(); } const storageForQuestion = {}; const getAnswerFromStorage = question => { let questionStorage = question.storage || storage; questionStorage = typeof questionStorage === 'string' ? this[questionStorage] : questionStorage; if (questionStorage) { checkInquirer(); const name = question.name; storageForQuestion[name] = questionStorage; const value = questionStorage.getPath(name); if (value !== undefined) { question.default = value; } return [name, value]; } return undefined; }; if (!Array.isArray(questions)) { questions = [questions]; } // Shows the prompt even if the answer already exists. questions.forEach(question => { if (question.askAnswered === undefined) { question.askAnswered = this.options.askAnswered === true; } }); // Pre-fill answers with storage values. const answers = {}; questions .map(getAnswerFromStorage) .filter(a => a) .forEach(([key, value]) => { answers[key] = value; }); questions = promptSuggestion.prefillQuestions(this._globalConfig, questions); questions = promptSuggestion.prefillQuestions(this.config, questions); return this.env.adapter.prompt(questions, answers).then(answers => { Object.entries(storageForQuestion).forEach(([name, questionStorage]) => { const answer = answers[name] === undefined ? null : answers[name]; questionStorage.setPath(name, answer); }); if (!this.options['skip-cache'] && !this.options.skipCache) { promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false); if (!this.options.skipLocalCache) { promptSuggestion.storeAnswers(this.config, questions, answers, true); } } return answers; }); } /** * 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. * * @param {String} [name] - Option name * @param {Object} config - Option options * @param {any} config.type - Either Boolean, String or Number * @param {string} [config.description] - Description for the option * @param {any} [config.default] - Default value * @param {any} [config.alias] - Option name alias (example `-h` and --help`) * @param {any} [config.hide] - Boolean whether to hide from help * @param {Storage} [config.storage] - Storage to persist the option * @return {this} This generator */ option(name, config) { if (Array.isArray(name)) { name.forEach(option => { this.option(option); }); return; } if (typeof name === 'object') { config = name; name = config.name; } config = config || {}; // 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('') ) ); } if (this._options[name] === null || this._options[name] === undefined) { this._options[name] = config; } this.parseOptions(); if (config.storage && this.options[name] !== undefined) { const storage = typeof config.storage === 'string' ? this[config.storage] : config.storage; storage.set(name, this.options[name]); } 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`). * * * @param {String} name - Argument name * @param {Object} config - Argument options * @param {any} config.type - String, Number, Array, or Object * @param {string} [config.description] - Description for the argument * @param {boolean} [config.required] - required` Boolean whether it is required * @param {boolean} [config.optional] - Boolean whether it is optional * @param {any} [config.default] - Default value for this argument * @return {this} This generator */ argument(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; _.defaults(config, { name, required: config.default === null || config.default === undefined, type: String }); this._arguments.push(config); this.parseOptions(); return this; } 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); } if (option.alias) { minimistDef.alias[option.alias] = option.name; } // 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; } }); const parsedOpts = minimist(this._args, minimistDef); // 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); } }); // 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; }); // Make the parsed options available to the instance Object.assign(this.options, parsedOpts); this.args = parsedOpts._; this.arguments = parsedOpts._; // Make sure required args are all present this.checkRequiredArgs(); } 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; } // Bail early if it's not possible to have a missing required arg if (this.args.length > this._arguments.length) { 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)}!`) ); } }); } /** * Schedule methods on a run queue. * * @param {Function|Object} method: Method to be scheduled or object with function properties. * @param {String} [methodName]: Name of the method (task) to be scheduled. * @param {String} [queueName]: Name of the queue to be scheduled on. * @param {Function} [reject]: Reject callback. */ queueMethod(method, methodName, queueName, reject) { if (typeof queueName === 'function') { reject = queueName; queueName = 'default'; } else { queueName = queueName || 'default'; } if (!_.isFunction(method)) { if (typeof methodName === 'function') { reject = methodName; methodName = undefined; } this.queueTaskGroup(method, { queueName: methodName, reject }); return; } this.queueTask({ method, taskName: methodName, queueName, reject }); } /** * Schedule methods on a run queue. * * @param {Object} taskGroup: Object containing tasks. * @param {TaskOptions} [taskOptions]: options. */ queueTaskGroup(taskGroup, taskOptions) { const self = this; // Run each queue items _.each(taskGroup, (newMethod, newMethodName) => { if (!_.isFunction(newMethod) || !methodIsValid(newMethodName)) return; self.queueTask({ ...taskOptions, method: newMethod, taskName: newMethodName }); }); } /** * @private * Schedule a generator's method on a run queue. * * @param {String} name: The method name to schedule. * @param {TaskOptions} [taskOptions]: options. */ queueOwnTask(name, taskOptions = {}) { const property = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), name); const item = property.value ? property.value : property.get.call(this); const priority = this._queues[name]; taskOptions = { ...priority, cancellable: true, run: false, ...taskOptions }; // Name points to a function; run it! if (typeof item === 'function') { taskOptions.taskName = name; taskOptions.method = item; this.queueTask(taskOptions); return; } // Not a queue hash; stop if (!priority) { return; } this.queueTaskGroup(item, taskOptions); } /** * @private * Schedule every generator's methods on a run queue. * * @param {TaskOptions} [taskOptions]: options. */ queueOwnTasks(taskOptions) { this._running = true; this._taskStatus = { cancelled: false, timestamp: new Date() }; const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); const validMethods = methods.filter(methodIsValid); if (validMethods.length === 0 && this._prompts.length === 0) { const error = new Error( 'This Generator is empty. Add at least one method for it to run.' ); this.emit('error', error); throw error; } if (this._prompts.length > 0) { this.queueTask({ method: () => this.prompt(this._prompts, this.config), taskName: 'Prompt registered questions', queueName: 'prompting', cancellable: true }); if (validMethods.length === 0) { this.queueTask({ method: () => { this.renderTemplate(); }, taskName: 'Empty generator: copy templates', queueName: 'writing', cancellable: true }); } } validMethods.forEach(methodName => this.queueOwnTask(methodName, taskOptions)); } /** * Schedule tasks on a run queue. * * @param {Task} task: Task to be queued. */ queueTask(task) { const { generatorReject, reject, queueName = 'default', taskName: methodName, method } = task; const once = task.once ? methodName : undefined; const priority = Object.entries(this._queues).find( ([_, opts]) => opts.queueName === queueName ); const priorityName = priority ? priority[0] : undefined; const self = this; const runLoop = this.env.runLoop; let namespace = ''; if (self.options && self.options.namespace) { namespace = self.options.namespace; } // Task status allows to ignore (cancel) current queued tasks. // Each queueOwnTasks (complete run) create a new taskStatus. const taskStatus = this._taskStatus || {}; debug( `Queueing ${namespace}#${methodName} with options %o`, _.omit(task, ['method']) ); runLoop.add( queueName, // Run-queue's done(continue), pause continueQueue => { debug(`Running ${namespace}#${methodName}`); self.emit(`method:${methodName}`); const taskCancelled = task.cancellable && taskStatus.cancelled; if (taskCancelled) { continueQueue(); return; } runAsync(function() { self.async = () => this.async(); self.runningState = { namespace, queueName, methodName }; return method.apply(self, self.args); })() .then(function() { delete self.runningState; const eventName = `done$${namespace || 'unknownnamespace'}#${methodName}`; debug(`Emiting event ${eventName}`); self.env.emit(eventName, { namespace, generator: self, queueName, priorityName }); continueQueue(); }) .catch(err => { debug(`An error occured while running ${namespace}#${methodName}`, err); if (reject) { debug('Rejecting task promise, queue will continue normally'); reject(err); continueQueue(); return; } delete self.runningState; // Ensure we emit the error event outside the promise context so it won't be // swallowed when there's no listeners. setImmediate(() => { if (generatorReject) { generatorReject(err); } self.emit('error', err); }); }); }, { once, run: task.run } ); } /** * Ignore cancellable tasks. */ cancelCancellableTasks() { this._running = false; // Task status references is registered at each running task this._taskStatus.cancelled = true; // Create a new task status. delete this._taskStatus; } /** * Start the generator again. * * @param {Object} [options]: options. */ startOver(options = {}) { this.cancelCancellableTasks(); Object.assign(this.options, options); this.queueOwnTasks(); } /** * 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. * * @param {Function} [cb] Deprecated: prefer to use the promise interface * @return {Promise} Resolved once the process finish */ run(cb) { const promise = this.runWithOptions({ withOptions: false }); // Maintain backward compatibility with the callback function if (_.isFunction(cb)) { return promise.then(cb, cb); } return promise; } /** * Alternative implementation of run() with a different api. * Api for run is stable for old generators. * * 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. * * @private * @param {Object} [options] Options. * @param {Boolean} [options.forwardErrorToEnvironment=true] Handle errors and forward the error on environment. * @param {Boolean} [options.usePromise=true] Register an error handler to reject the returned promise. * @return {Promise} Resolved once the queue is cleared. */ runWithOptions(options = { withOptions: true }) { // Precedence is the arg, then this.options let { forwardErrorToEnvironment = this.options.forwardErrorToEnvironment, usePromise = this.options.usePromise } = options; if (usePromise === undefined && forwardErrorToEnvironment !== undefined) { // Options usePromise is recommended for forwardErrorToEnvironment usePromise = forwardErrorToEnvironment; } else if (usePromise === undefined) { // Use default config for the method run or runWithOptions. usePromise = options.withOptions; } const promise = new Promise((resolve, reject) => { this.debug('Generator is starting'); this.emit('run'); /* * Adding a error listener breaks workaround that throws an error on a scheduled callback. * Since there is no error handler on the callback, the error will be treated by node.js * and the process will be terminated. */ if (usePromise) { // Add an error listener to reject the promise this.on('error', reject); } this.env.runLoop.once('end', () => { this.debug('Generator has ended'); this.emit('end'); resolve(); }); this.queueOwnTasks({ generatorReject: usePromise ? undefined : reject }); this.queueBasicTasks(); this._composedWith.forEach(runGenerator); this._composedWith = []; if (typeof this.env.runLoop.start === 'function') { this.env.runLoop.start(); } }); // For composed generators, otherwise error will not be catched. if (forwardErrorToEnvironment) { return promise.catch(err => { this.env.emit('error', err); }); } return promise; } /** * Queue generator's basic tasks, only once execution is required for each environment. */ queueBasicTasks() { const writeFiles = () => { this.env.runLoop.add('conflicts', this._writeFiles.bind(this), { once: 'write memory fs to disk' }); }; this.env.sharedFs.on('change', writeFiles); writeFiles(); // Add the default conflicts handling this.env.runLoop.add('conflicts', done => { this.conflicter.resolve(err => { if (err) { return this.emit('error', err); } done(); }); }); } /** * Compose this generator with another one. * @param {String|Object|Array} generator The path to the generator module or an object (see examples) * @param {Object} [options] The options passed to the Generator * @param {boolean} [returnNewGenerator] Returns the created generator instead of returning this. * @return {this|Object} This generator or the composed generator when returnNewGenerator=true * * @example