You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1523 lines
46 KiB
1523 lines
46 KiB
'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 <caption>Using a peerDependency generator</caption> |
|
* this.composeWith('bootstrap', { sass: true }); |
|
* |
|
* @example <caption>Using a direct dependency generator</caption> |
|
* this.composeWith(require.resolve('generator-bootstrap/app/main.js'), { sass: true }); |
|
* |
|
* @example <caption>Passing a Generator class</caption> |
|
* this.composeWith({ Generator: MyGenerator, path: '../generator-bootstrap/app/main.js' }, { sass: true }); |
|
*/ |
|
composeWith(generator, options, returnNewGenerator = false) { |
|
if (typeof options === 'boolean') { |
|
returnNewGenerator = options; |
|
options = {}; |
|
} |
|
|
|
const returnCompose = ret => (returnNewGenerator ? ret : this); |
|
|
|
let instantiatedGenerator; |
|
|
|
if (Array.isArray(generator)) { |
|
const generators = generator.map(gen => |
|
this.composeWith(gen, options, returnNewGenerator) |
|
); |
|
return returnCompose(generators); |
|
} |
|
|
|
const instantiate = (Generator, path) => { |
|
if (path === 'unknown') { |
|
Generator.resolved = path; |
|
} else { |
|
Generator.resolved = require.resolve(path); |
|
} |
|
|
|
Generator.namespace = this.env.namespace(path); |
|
|
|
return this.env.instantiate(Generator, { |
|
options, |
|
arguments: options.arguments |
|
}); |
|
}; |
|
|
|
options = options || {}; |
|
|
|
// Pass down the default options so they're correctly mirrored down the chain. |
|
options = _.extend( |
|
{ |
|
skipInstall: this.options.skipInstall || this.options['skip-install'], |
|
'skip-install': this.options.skipInstall || this.options['skip-install'], |
|
skipCache: this.options.skipCache || this.options['skip-cache'], |
|
'skip-cache': this.options.skipCache || this.options['skip-cache'], |
|
forceInstall: this.options.forceInstall || this.options['force-install'], |
|
'force-install': this.options.forceInstall || this.options['force-install'], |
|
skipLocalCache: this.options.skipLocalCache, |
|
destinationRoot: this._destinationRoot |
|
}, |
|
options |
|
); |
|
|
|
if (typeof generator === 'string') { |
|
try { |
|
const GeneratorImport = require(generator); // eslint-disable-line import/no-dynamic-require |
|
const Generator = |
|
typeof GeneratorImport.default === 'function' |
|
? GeneratorImport.default |
|
: GeneratorImport; |
|
|
|
instantiatedGenerator = instantiate(Generator, generator); |
|
} catch (err) { |
|
if (err.code === 'MODULE_NOT_FOUND') { |
|
instantiatedGenerator = this.env.create(generator, { |
|
options, |
|
arguments: options.arguments |
|
}); |
|
} else { |
|
throw err; |
|
} |
|
} |
|
} else { |
|
assert( |
|
generator.Generator, |
|
`${chalk.red('Missing Generator property')}\n` + |
|
`When passing an object to Generator${chalk.cyan( |
|
'#composeWith' |
|
)} include the generator class to run in the ${chalk.cyan( |
|
'Generator' |
|
)} property\n\n` + |
|
`this.composeWith({\n` + |
|
` ${chalk.yellow('Generator')}: MyGenerator,\n` + |
|
` ...\n` + |
|
`});` |
|
); |
|
assert( |
|
typeof generator.path === 'string', |
|
`${chalk.red('path property is not a string')}\n` + |
|
`When passing an object to Generator${chalk.cyan( |
|
'#composeWith' |
|
)} include the path to the generators files in the ${chalk.cyan( |
|
'path' |
|
)} property\n\n` + |
|
`this.composeWith({\n` + |
|
` ${chalk.yellow('path')}: '../my-generator',\n` + |
|
` ...\n` + |
|
`});` |
|
); |
|
instantiatedGenerator = instantiate(generator.Generator, generator.path); |
|
} |
|
|
|
if (!instantiatedGenerator) { |
|
return returnCompose(instantiatedGenerator); |
|
} |
|
|
|
if (this._running) { |
|
runGenerator(instantiatedGenerator); |
|
} else { |
|
this._composedWith.push(instantiatedGenerator); |
|
} |
|
|
|
return returnCompose(instantiatedGenerator); |
|
} |
|
|
|
/** |
|
* Determine the root generator name (the one who's extending Generator). |
|
* @return {String} The name of the root generator |
|
*/ |
|
rootGeneratorName() { |
|
const pkg = readPkgUp.sync({ cwd: this.resolved }).pkg; |
|
return pkg ? pkg.name : '*'; |
|
} |
|
|
|
/** |
|
* Determine the root generator version (the one who's extending Generator). |
|
* @return {String} The version of the root generator |
|
*/ |
|
rootGeneratorVersion() { |
|
const pkg = readPkgUp.sync({ cwd: this.resolved }).pkg; |
|
return pkg ? pkg.version : '0.0.0'; |
|
} |
|
|
|
/** |
|
* Return a storage instance. |
|
* @param {String} storePath The path of the json file |
|
* @param {String} [path] The name in which is stored inside the json |
|
* @param {String} [lodashPath] Treat path as an lodash path |
|
* @return {Storage} json storage |
|
*/ |
|
createStorage(storePath, path, lodashPath = false) { |
|
storePath = this.destinationPath(storePath); |
|
return new Storage(path, this.fs, storePath, lodashPath); |
|
} |
|
|
|
/** |
|
* Return a storage instance. |
|
* @param {String} [rootName] The rootName in which is stored inside .yo-rc.json |
|
* @return {Storage} Generator storage |
|
* @private |
|
*/ |
|
_getStorage(rootName = this.rootGeneratorName()) { |
|
const storePath = path.join(this.destinationRoot(), '.yo-rc.json'); |
|
return new Storage(rootName, this.fs, storePath); |
|
} |
|
|
|
/** |
|
* Setup a globalConfig storage instance. |
|
* @return {Storage} Global config storage |
|
* @private |
|
*/ |
|
_getGlobalStorage() { |
|
// When localConfigOnly === true simulate a globalConfig at local dir |
|
const globalStorageDir = this.options.localConfigOnly |
|
? this.destinationRoot() |
|
: os.homedir(); |
|
const storePath = path.join(globalStorageDir, '.yo-rc-global.json'); |
|
const storeName = `${this.rootGeneratorName()}:${this.rootGeneratorVersion()}`; |
|
return new Storage(storeName, this.fs, storePath); |
|
} |
|
|
|
/** |
|
* Change the generator destination root directory. |
|
* This path is used to find storage, when using a file system helper method (like |
|
* `this.write` and `this.copy`) |
|
* @param {String} rootPath new destination root path |
|
* @param {Boolean} skipEnvironment - don't update the environment cwd/chdir. |
|
* @return {String} destination root path |
|
*/ |
|
destinationRoot(rootPath, skipEnvironment = false) { |
|
if (typeof rootPath === 'string') { |
|
this._destinationRoot = path.resolve(rootPath); |
|
|
|
if (!fs.existsSync(this._destinationRoot)) { |
|
makeDir.sync(this._destinationRoot); |
|
} |
|
|
|
if (!skipEnvironment) { |
|
process.chdir(this._destinationRoot); |
|
this.env.cwd = this._destinationRoot; |
|
} |
|
|
|
// Reset the storage |
|
this.config = this._getStorage(); |
|
} |
|
|
|
return this._destinationRoot || this.env.cwd; |
|
} |
|
|
|
/** |
|
* Change the generator source root directory. |
|
* This path is used by multiples file system methods like (`this.read` and `this.copy`) |
|
* @param {String} rootPath new source root path |
|
* @return {String} source root path |
|
*/ |
|
sourceRoot(rootPath) { |
|
if (typeof rootPath === 'string') { |
|
this._sourceRoot = path.resolve(rootPath); |
|
} |
|
|
|
return this._sourceRoot; |
|
} |
|
|
|
/** |
|
* Join a path to the source root. |
|
* @param {...String} dest - path parts |
|
* @return {String} joined path |
|
*/ |
|
templatePath(...dest) { |
|
let filepath = path.join.apply(path, dest); |
|
|
|
if (!path.isAbsolute(filepath)) { |
|
filepath = path.join(this.sourceRoot(), filepath); |
|
} |
|
|
|
return filepath; |
|
} |
|
|
|
/** |
|
* Join a path to the destination root. |
|
* @param {...String} dest - path parts |
|
* @return {String} joined path |
|
*/ |
|
destinationPath(...dest) { |
|
let filepath = path.join.apply(path, dest); |
|
|
|
if (!path.isAbsolute(filepath)) { |
|
filepath = path.join(this.destinationRoot(), filepath); |
|
} |
|
|
|
return filepath; |
|
} |
|
|
|
/** |
|
* Determines the name of the application. |
|
* |
|
* First checks for name in bower.json. |
|
* Then checks for name in package.json. |
|
* Finally defaults to the name of the current directory. |
|
* @return {String} The name of the application |
|
*/ |
|
determineAppname() { |
|
let appname = this.fs.readJSON(this.destinationPath('bower.json'), {}).name; |
|
|
|
if (!appname) { |
|
appname = this.fs.readJSON(this.destinationPath('package.json'), {}).name; |
|
} |
|
|
|
if (!appname) { |
|
appname = path.basename(this.destinationRoot()); |
|
} |
|
|
|
return appname.replace(/[^\w\s]+?/g, ' '); |
|
} |
|
|
|
/** |
|
* Add a transform stream to the commit stream. |
|
* |
|
* Most usually, these transform stream will be Gulp plugins. |
|
* |
|
* @param {stream.Transform|stream.Transform[]} streams An array of Transform stream |
|
* or a single one. |
|
* @return {this} This generator |
|
*/ |
|
registerTransformStream(streams) { |
|
assert(streams, 'expected to receive a transform stream as parameter'); |
|
if (!Array.isArray(streams)) { |
|
streams = [streams]; |
|
} |
|
|
|
this._transformStreams = this._transformStreams.concat(streams); |
|
return this; |
|
} |
|
|
|
/** |
|
* Write memory fs file to disk and logging results |
|
* @param {Function} done - callback once files are written |
|
* @private |
|
*/ |
|
_writeFiles(done) { |
|
const self = this; |
|
|
|
const conflictChecker = through.obj(function(file, enc, cb) { |
|
const stream = this; |
|
|
|
// If the file has no state requiring action, move on |
|
if (file.state === null) { |
|
return cb(); |
|
} |
|
|
|
// Config file should not be processed by the conflicter. Just pass through |
|
const filename = path.basename(file.path); |
|
|
|
if (filename === '.yo-rc.json' || filename === '.yo-rc-global.json') { |
|
file.conflicter = 'force'; |
|
} |
|
|
|
self.conflicter.checkForCollision(file, (err, status) => { |
|
if (err) { |
|
cb(err); |
|
return; |
|
} |
|
|
|
if (status === 'skip') { |
|
delete file.state; |
|
} else { |
|
stream.push(file); |
|
} |
|
|
|
cb(); |
|
}); |
|
self.conflicter.resolve(); |
|
}); |
|
|
|
const transformStreams = this._transformStreams.concat([conflictChecker]); |
|
this.fs.commit(transformStreams, () => { |
|
done(); |
|
}); |
|
} |
|
} |
|
|
|
// Mixin the actions modules |
|
_.extend(Generator.prototype, require('./actions/install')); |
|
_.extend(Generator.prototype, require('./actions/help')); |
|
_.extend(Generator.prototype, require('./actions/spawn-command')); |
|
_.extend(Generator.prototype, require('./actions/fs')); |
|
Generator.prototype.user = require('./actions/user'); |
|
|
|
module.exports = Generator;
|
|
|