index.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const os = require('os');
  5. const EventEmitter = require('events');
  6. const assert = require('assert');
  7. const _ = require('lodash');
  8. const findUp = require('find-up');
  9. const readPkgUp = require('read-pkg-up');
  10. const chalk = require('chalk');
  11. const makeDir = require('make-dir');
  12. const minimist = require('minimist');
  13. const runAsync = require('run-async');
  14. const through = require('through2');
  15. const FileEditor = require('mem-fs-editor');
  16. const debug = require('debug')('yeoman:generator');
  17. const Conflicter = require('./util/conflicter');
  18. const Storage = require('./util/storage');
  19. const promptSuggestion = require('./util/prompt-suggestion');
  20. const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@';
  21. /**
  22. * The `Generator` class provides the common API shared by all generators.
  23. * It define options, arguments, file, prompt, log, API, etc.
  24. *
  25. * It mixes into its prototype all the methods found in the `actions/` mixins.
  26. *
  27. * Every generator should extend this base class.
  28. *
  29. * @constructor
  30. * @mixes actions/help
  31. * @mixes actions/install
  32. * @mixes actions/spawn-command
  33. * @mixes actions/user
  34. * @mixes nodejs/EventEmitter
  35. *
  36. * @param {String|Array} args
  37. * @param {Object} options
  38. *
  39. * @property {Object} env - the current Environment being run
  40. * @property {Object} args - Provide arguments at initialization
  41. * @property {String} resolved - the path to the current generator
  42. * @property {String} description - Used in `--help` output
  43. * @property {String} appname - The application name
  44. * @property {Storage} config - `.yo-rc` config file manager
  45. * @property {Object} fs - An instance of {@link https://github.com/SBoudrias/mem-fs-editor Mem-fs-editor}
  46. * @property {Function} log - Output content through Interface Adapter
  47. *
  48. * @example
  49. * const Generator = require('yeoman-generator');
  50. * module.exports = class extends Generator {
  51. * writing() {
  52. * this.fs.write(this.destinationPath('index.js'), 'const foo = 1;');
  53. * }
  54. * };
  55. */
  56. class Generator extends EventEmitter {
  57. constructor(args, options) {
  58. super();
  59. if (!Array.isArray(args)) {
  60. options = args;
  61. args = [];
  62. }
  63. this.options = options || {};
  64. this._initOptions = _.clone(options);
  65. this._args = args || [];
  66. this._options = {};
  67. this._arguments = [];
  68. this._composedWith = [];
  69. this._transformStreams = [];
  70. this.option('help', {
  71. type: Boolean,
  72. alias: 'h',
  73. description: 'Print the generator\'s options and usage'
  74. });
  75. this.option('skip-cache', {
  76. type: Boolean,
  77. description: 'Do not remember prompt answers',
  78. default: false
  79. });
  80. this.option('skip-install', {
  81. type: Boolean,
  82. description: 'Do not automatically install dependencies',
  83. default: false
  84. });
  85. // Checks required parameters
  86. assert(this.options.env, 'You must provide the environment object. Use env#create() to create a new generator.');
  87. assert(this.options.resolved, 'You must provide the resolved path value. Use env#create() to create a new generator.');
  88. this.env = this.options.env;
  89. this.resolved = this.options.resolved;
  90. // Ensure the environment support features this yeoman-generator version require.
  91. require('yeoman-environment').enforceUpdate(this.env);
  92. this.description = this.description || '';
  93. this.async = () => () => {};
  94. this.fs = FileEditor.create(this.env.sharedFs);
  95. this.conflicter = new Conflicter(this.env.adapter, this.options.force);
  96. // Mirror the adapter log method on the generator.
  97. //
  98. // example:
  99. // this.log('foo');
  100. // this.log.error('bar');
  101. this.log = this.env.adapter.log;
  102. // Determine the app root
  103. this.contextRoot = this.env.cwd;
  104. let rootPath = findUp.sync('.yo-rc.json', {
  105. cwd: this.env.cwd
  106. });
  107. rootPath = rootPath ? path.dirname(rootPath) : this.env.cwd;
  108. if (rootPath !== this.env.cwd) {
  109. this.log([
  110. '',
  111. 'Just found a `.yo-rc.json` in a parent directory.',
  112. 'Setting the project root at: ' + rootPath
  113. ].join('\n'));
  114. this.destinationRoot(rootPath);
  115. }
  116. this.appname = this.determineAppname();
  117. this.config = this._getStorage();
  118. this._globalConfig = this._getGlobalStorage();
  119. // Ensure source/destination path, can be configured from subclasses
  120. this.sourceRoot(path.join(path.dirname(this.resolved), 'templates'));
  121. }
  122. /*
  123. * Prompt user to answer questions. The signature of this method is the same as {@link https://github.com/SBoudrias/Inquirer.js Inquirer.js}
  124. *
  125. * On top of the Inquirer.js API, you can provide a `{cache: true}` property for
  126. * every question descriptor. When set to true, Yeoman will store/fetch the
  127. * user's answers as defaults.
  128. *
  129. * @param {array} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation}
  130. * @return {Promise}
  131. */
  132. prompt(questions) {
  133. questions = promptSuggestion.prefillQuestions(this._globalConfig, questions);
  134. questions = promptSuggestion.prefillQuestions(this.config, questions);
  135. return this.env.adapter.prompt(questions).then(answers => {
  136. if (!this.options['skip-cache']) {
  137. promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false);
  138. promptSuggestion.storeAnswers(this.config, questions, answers, true);
  139. }
  140. return answers;
  141. });
  142. }
  143. /**
  144. * Adds an option to the set of generator expected options, only used to
  145. * generate generator usage. By default, generators get all the cli options
  146. * parsed by nopt as a `this.options` hash object.
  147. *
  148. * ### Options:
  149. *
  150. * - `description` Description for the option
  151. * - `type` Either Boolean, String or Number
  152. * - `alias` Option name alias (example `-h` and --help`)
  153. * - `default` Default value
  154. * - `hide` Boolean whether to hide from help
  155. *
  156. * @param {String} name
  157. * @param {Object} config
  158. */
  159. option(name, config) {
  160. config = config || {};
  161. // Alias default to defaults for backward compatibility.
  162. if ('defaults' in config) {
  163. config.default = config.defaults;
  164. }
  165. config.description = config.description || config.desc;
  166. _.defaults(config, {
  167. name,
  168. description: 'Description for ' + name,
  169. type: Boolean,
  170. hide: false
  171. });
  172. // Check whether boolean option is invalid (starts with no-)
  173. const boolOptionRegex = /^no-/;
  174. if (config.type === Boolean && name.match(boolOptionRegex)) {
  175. const simpleName = name.replace(boolOptionRegex, '');
  176. return this.emit('error', new Error([
  177. `Option name ${chalk.yellow(name)} cannot start with ${chalk.red('no-')}\n`,
  178. `Option name prefixed by ${chalk.yellow('--no')} are parsed as implicit`,
  179. ` boolean. To use ${chalk.yellow('--' + name)} as an option, use\n`,
  180. chalk.cyan(` this.option('${simpleName}', {type: Boolean})`)
  181. ].join('')));
  182. }
  183. if (this._options[name] === null || this._options[name] === undefined) {
  184. this._options[name] = config;
  185. }
  186. this.parseOptions();
  187. return this;
  188. }
  189. /**
  190. * Adds an argument to the class and creates an attribute getter for it.
  191. *
  192. * Arguments are different from options in several aspects. The first one
  193. * is how they are parsed from the command line, arguments are retrieved
  194. * based on their position.
  195. *
  196. * Besides, arguments are used inside your code as a property (`this.argument`),
  197. * while options are all kept in a hash (`this.options`).
  198. *
  199. * ### Options:
  200. *
  201. * - `description` Description for the argument
  202. * - `required` Boolean whether it is required
  203. * - `optional` Boolean whether it is optional
  204. * - `type` String, Number, Array, or Object
  205. * - `default` Default value for this argument
  206. *
  207. * @param {String} name
  208. * @param {Object} config
  209. */
  210. argument(name, config) {
  211. config = config || {};
  212. // Alias default to defaults for backward compatibility.
  213. if ('defaults' in config) {
  214. config.default = config.defaults;
  215. }
  216. config.description = config.description || config.desc;
  217. _.defaults(config, {
  218. name,
  219. required: config.default === null || config.default === undefined,
  220. type: String
  221. });
  222. this._arguments.push(config);
  223. this.parseOptions();
  224. return this;
  225. }
  226. parseOptions() {
  227. const minimistDef = {
  228. string: [],
  229. boolean: [],
  230. alias: {},
  231. default: {}
  232. };
  233. _.each(this._options, option => {
  234. if (option.type === Boolean) {
  235. minimistDef.boolean.push(option.name);
  236. if (!('default' in option) && !option.required) {
  237. minimistDef.default[option.name] = EMPTY;
  238. }
  239. } else {
  240. minimistDef.string.push(option.name);
  241. }
  242. if (option.alias) {
  243. minimistDef.alias[option.alias] = option.name;
  244. }
  245. // Only apply default values if we don't already have a value injected from
  246. // the runner
  247. if (option.name in this._initOptions) {
  248. minimistDef.default[option.name] = this._initOptions[option.name];
  249. } else if (option.alias && option.alias in this._initOptions) {
  250. minimistDef.default[option.name] = this._initOptions[option.alias];
  251. } else if ('default' in option) {
  252. minimistDef.default[option.name] = option.default;
  253. }
  254. });
  255. const parsedOpts = minimist(this._args, minimistDef);
  256. // Parse options to the desired type
  257. _.each(parsedOpts, (option, name) => {
  258. // Manually set value as undefined if it should be.
  259. if (option === EMPTY) {
  260. parsedOpts[name] = undefined;
  261. return;
  262. }
  263. if (this._options[name] && option !== undefined) {
  264. parsedOpts[name] = this._options[name].type(option);
  265. }
  266. });
  267. // Parse positional arguments to valid options
  268. this._arguments.forEach((config, index) => {
  269. let value;
  270. if (index >= parsedOpts._.length) {
  271. if (config.name in this._initOptions) {
  272. value = this._initOptions[config.name];
  273. } else if ('default' in config) {
  274. value = config.default;
  275. } else {
  276. return;
  277. }
  278. } else if (config.type === Array) {
  279. value = parsedOpts._.slice(index, parsedOpts._.length);
  280. } else {
  281. value = config.type(parsedOpts._[index]);
  282. }
  283. parsedOpts[config.name] = value;
  284. });
  285. // Make the parsed options available to the instance
  286. Object.assign(this.options, parsedOpts);
  287. this.args = parsedOpts._;
  288. this.arguments = parsedOpts._;
  289. // Make sure required args are all present
  290. this.checkRequiredArgs();
  291. }
  292. checkRequiredArgs() {
  293. // If the help option was provided, we don't want to check for required
  294. // arguments, since we're only going to print the help message anyway.
  295. if (this.options.help) {
  296. return;
  297. }
  298. // Bail early if it's not possible to have a missing required arg
  299. if (this.args.length > this._arguments.length) {
  300. return;
  301. }
  302. this._arguments.forEach((config, position) => {
  303. // If the help option was not provided, check whether the argument was
  304. // required, and whether a value was provided.
  305. if (config.required && position >= this.args.length) {
  306. return this.emit('error', new Error(`Did not provide required argument ${chalk.bold(config.name)}!`));
  307. }
  308. });
  309. }
  310. /**
  311. * Runs the generator, scheduling prototype methods on a run queue. Method names
  312. * will determine the order each method is run. Methods without special names
  313. * will run in the default queue.
  314. *
  315. * Any method named `constructor` and any methods prefixed by a `_` won't be scheduled.
  316. *
  317. * You can also supply the arguments for the method to be invoked. If none are
  318. * provided, the same values used to initialize the invoker are used to
  319. * initialize the invoked.
  320. *
  321. * @param {Function} [cb]
  322. */
  323. run(cb) {
  324. cb = cb || (() => {});
  325. const self = this;
  326. this._running = true;
  327. this.emit('run');
  328. const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
  329. const validMethods = methods.filter(methodIsValid);
  330. assert(validMethods.length, 'This Generator is empty. Add at least one method for it to run.');
  331. this.env.runLoop.once('end', () => {
  332. this.emit('end');
  333. cb();
  334. });
  335. // Ensure a prototype method is a candidate run by default
  336. function methodIsValid(name) {
  337. return name.charAt(0) !== '_' && name !== 'constructor';
  338. }
  339. function addMethod(method, methodName, queueName) {
  340. queueName = queueName || 'default';
  341. debug(`Queueing ${methodName} in ${queueName}`);
  342. self.env.runLoop.add(queueName, completed => {
  343. debug(`Running ${methodName}`);
  344. self.emit(`method:${methodName}`);
  345. runAsync(function () {
  346. self.async = () => this.async();
  347. return method.apply(self, self.args);
  348. })().then(completed).catch(err => {
  349. debug(`An error occured while running ${methodName}`, err);
  350. // Ensure we emit the error event outside the promise context so it won't be
  351. // swallowed when there's no listeners.
  352. setImmediate(() => {
  353. self.emit('error', err);
  354. cb(err);
  355. });
  356. });
  357. });
  358. }
  359. function addInQueue(name) {
  360. const item = Object.getPrototypeOf(self)[name];
  361. const queueName = self.env.runLoop.queueNames.indexOf(name) === -1 ? null : name;
  362. // Name points to a function; run it!
  363. if (typeof item === 'function') {
  364. return addMethod(item, name, queueName);
  365. }
  366. // Not a queue hash; stop
  367. if (!queueName) {
  368. return;
  369. }
  370. // Run each queue items
  371. _.each(item, (method, methodName) => {
  372. if (!_.isFunction(method) || !methodIsValid(methodName)) {
  373. return;
  374. }
  375. addMethod(method, methodName, queueName);
  376. });
  377. }
  378. validMethods.forEach(addInQueue);
  379. const writeFiles = () => {
  380. this.env.runLoop.add('conflicts', this._writeFiles.bind(this), {
  381. once: 'write memory fs to disk'
  382. });
  383. };
  384. this.env.sharedFs.on('change', writeFiles);
  385. writeFiles();
  386. // Add the default conflicts handling
  387. this.env.runLoop.add('conflicts', done => {
  388. this.conflicter.resolve(err => {
  389. if (err) {
  390. this.emit('error', err);
  391. }
  392. done();
  393. });
  394. });
  395. _.invokeMap(this._composedWith, 'run');
  396. return this;
  397. }
  398. /**
  399. * Compose this generator with another one.
  400. * @param {String} namespace The generator namespace to compose with
  401. * @param {Object} options The options passed to the Generator
  402. * @param {Object} [settings] Settings hash on the composition relation
  403. * @param {string} [settings.local] Path to a locally stored generator
  404. * @param {String} [settings.link="weak"] If "strong", the composition will occured
  405. * even when the composition is initialized by
  406. * the end user
  407. * @return {this}
  408. *
  409. * @example <caption>Using a peerDependency generator</caption>
  410. * this.composeWith('bootstrap', { sass: true });
  411. *
  412. * @example <caption>Using a direct dependency generator</caption>
  413. * this.composeWith(require.resolve('generator-bootstrap/app/main.js'), { sass: true });
  414. */
  415. composeWith(modulePath, options) {
  416. let generator;
  417. options = options || {};
  418. // Pass down the default options so they're correctly mirrored down the chain.
  419. options = _.extend({
  420. skipInstall: this.options.skipInstall,
  421. 'skip-install': this.options.skipInstall,
  422. skipCache: this.options.skipCache,
  423. 'skip-cache': this.options.skipCache
  424. }, options);
  425. try {
  426. const Generator = require(modulePath); // eslint-disable-line import/no-dynamic-require
  427. Generator.resolved = require.resolve(modulePath);
  428. Generator.namespace = this.env.namespace(modulePath);
  429. generator = this.env.instantiate(Generator, {
  430. options,
  431. arguments: options.arguments
  432. });
  433. } catch (err) {
  434. if (err.code === 'MODULE_NOT_FOUND') {
  435. generator = this.env.create(modulePath, {
  436. options,
  437. arguments: options.arguments
  438. });
  439. } else {
  440. throw err;
  441. }
  442. }
  443. if (this._running) {
  444. generator.run();
  445. } else {
  446. this._composedWith.push(generator);
  447. }
  448. return this;
  449. }
  450. /**
  451. * Determine the root generator name (the one who's extending Generator).
  452. * @return {String} The name of the root generator
  453. */
  454. rootGeneratorName() {
  455. const pkg = readPkgUp.sync({cwd: this.resolved}).pkg;
  456. return pkg ? pkg.name : '*';
  457. }
  458. /**
  459. * Determine the root generator version (the one who's extending Generator).
  460. * @return {String} The version of the root generator
  461. */
  462. rootGeneratorVersion() {
  463. const pkg = readPkgUp.sync({cwd: this.resolved}).pkg;
  464. return pkg ? pkg.version : '0.0.0';
  465. }
  466. /**
  467. * Return a storage instance.
  468. * @return {Storage} Generator storage
  469. * @private
  470. */
  471. _getStorage() {
  472. const storePath = path.join(this.destinationRoot(), '.yo-rc.json');
  473. return new Storage(this.rootGeneratorName(), this.fs, storePath);
  474. }
  475. /**
  476. * Setup a globalConfig storage instance.
  477. * @return {Storage} Global config storage
  478. * @private
  479. */
  480. _getGlobalStorage() {
  481. const storePath = path.join(os.homedir(), '.yo-rc-global.json');
  482. const storeName = `${this.rootGeneratorName()}:${this.rootGeneratorVersion()}`;
  483. return new Storage(storeName, this.fs, storePath);
  484. }
  485. /**
  486. * Change the generator destination root directory.
  487. * This path is used to find storage, when using a file system helper method (like
  488. * `this.write` and `this.copy`)
  489. * @param {String} rootPath new destination root path
  490. * @return {String} destination root path
  491. */
  492. destinationRoot(rootPath) {
  493. if (typeof rootPath === 'string') {
  494. this._destinationRoot = path.resolve(rootPath);
  495. if (!fs.existsSync(rootPath)) {
  496. makeDir.sync(rootPath);
  497. }
  498. process.chdir(rootPath);
  499. this.env.cwd = rootPath;
  500. // Reset the storage
  501. this.config = this._getStorage();
  502. }
  503. return this._destinationRoot || this.env.cwd;
  504. }
  505. /**
  506. * Change the generator source root directory.
  507. * This path is used by multiples file system methods like (`this.read` and `this.copy`)
  508. * @param {String} rootPath new source root path
  509. * @return {String} source root path
  510. */
  511. sourceRoot(rootPath) {
  512. if (typeof rootPath === 'string') {
  513. this._sourceRoot = path.resolve(rootPath);
  514. }
  515. return this._sourceRoot;
  516. }
  517. /**
  518. * Join a path to the source root.
  519. * @param {...String} path
  520. * @return {String} joined path
  521. */
  522. templatePath() {
  523. let filepath = path.join.apply(path, arguments);
  524. if (!path.isAbsolute(filepath)) {
  525. filepath = path.join(this.sourceRoot(), filepath);
  526. }
  527. return filepath;
  528. }
  529. /**
  530. * Join a path to the destination root.
  531. * @param {...String} path
  532. * @return {String} joined path
  533. */
  534. destinationPath() {
  535. let filepath = path.join.apply(path, arguments);
  536. if (!path.isAbsolute(filepath)) {
  537. filepath = path.join(this.destinationRoot(), filepath);
  538. }
  539. return filepath;
  540. }
  541. /**
  542. * Determines the name of the application.
  543. *
  544. * First checks for name in bower.json.
  545. * Then checks for name in package.json.
  546. * Finally defaults to the name of the current directory.
  547. * @return {String} The name of the application
  548. */
  549. determineAppname() {
  550. let appname = this.fs.readJSON(this.destinationPath('bower.json'), {}).name;
  551. if (!appname) {
  552. appname = this.fs.readJSON(this.destinationPath('package.json'), {}).name;
  553. }
  554. if (!appname) {
  555. appname = path.basename(this.destinationRoot());
  556. }
  557. return appname.replace(/[^\w\s]+?/g, ' ');
  558. }
  559. /**
  560. * Add a transform stream to the commit stream.
  561. *
  562. * Most usually, these transform stream will be Gulp plugins.
  563. *
  564. * @param {stream.Transform|stream.Transform[]} stream An array of Transform stream
  565. * or a single one.
  566. * @return {this}
  567. */
  568. registerTransformStream(streams) {
  569. assert(streams, 'expected to receive a transform stream as parameter');
  570. if (!Array.isArray(streams)) {
  571. streams = [streams];
  572. }
  573. this._transformStreams = this._transformStreams.concat(streams);
  574. return this;
  575. }
  576. /**
  577. * Write memory fs file to disk and logging results
  578. * @param {Function} done - callback once files are written
  579. * @private
  580. */
  581. _writeFiles(done) {
  582. const self = this;
  583. const conflictChecker = through.obj(function (file, enc, cb) {
  584. const stream = this;
  585. // If the file has no state requiring action, move on
  586. if (file.state === null) {
  587. return cb();
  588. }
  589. // Config file should not be processed by the conflicter. Just pass through
  590. const filename = path.basename(file.path);
  591. if (filename === '.yo-rc.json' || filename === '.yo-rc-global.json') {
  592. this.push(file);
  593. return cb();
  594. }
  595. self.conflicter.checkForCollision(file.path, file.contents, (err, status) => {
  596. if (err) {
  597. cb(err);
  598. return;
  599. }
  600. if (status === 'skip') {
  601. delete file.state;
  602. } else {
  603. stream.push(file);
  604. }
  605. cb();
  606. });
  607. self.conflicter.resolve();
  608. });
  609. const transformStreams = this._transformStreams.concat([conflictChecker]);
  610. this.fs.commit(transformStreams, () => {
  611. done();
  612. });
  613. }
  614. }
  615. // Mixin the actions modules
  616. _.extend(Generator.prototype, require('./actions/install'));
  617. _.extend(Generator.prototype, require('./actions/help'));
  618. _.extend(Generator.prototype, require('./actions/spawn-command'));
  619. Generator.prototype.user = require('./actions/user');
  620. module.exports = Generator;