yeoman-generator 4.8.3 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,230 @@
1
+ /* eslint max-params: [1, 5] */
2
+ const assert = require('assert');
3
+
4
+ /**
5
+ * @mixin
6
+ * @alias actions/fs
7
+ */
8
+ const fs = module.exports;
9
+
10
+ /**
11
+ * Read file from templates folder.
12
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
13
+ * Shortcut for this.fs.read(this.templatePath(filepath))
14
+ *
15
+ * @param {String} filepath - absolute file path or relative to templates folder.
16
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
17
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
18
+ */
19
+ fs.readTemplate = function(filepath, ...args) {
20
+ return this.fs.read(this.templatePath(filepath), ...args);
21
+ };
22
+
23
+ /**
24
+ * Copy file from templates folder to destination folder.
25
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
26
+ * Shortcut for this.fs.copy(this.templatePath(from), this.destinationPath(to))
27
+ *
28
+ * @param {String} from - absolute file path or relative to templates folder.
29
+ * @param {String} to - absolute file path or relative to destination folder.
30
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
31
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
32
+ */
33
+ fs.copyTemplate = function(from, to, ...args) {
34
+ return this.fs.copy(this.templatePath(from), this.destinationPath(to), ...args);
35
+ };
36
+
37
+ /**
38
+ * Read file from destination folder
39
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
40
+ * Shortcut for this.fs.read(this.destinationPath(filepath)).
41
+ *
42
+ * @param {String} filepath - absolute file path or relative to destination folder.
43
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
44
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
45
+ */
46
+ fs.readDestination = function(filepath, ...args) {
47
+ return this.fs.read(this.destinationPath(filepath), ...args);
48
+ };
49
+
50
+ /**
51
+ * Read JSON file from destination folder
52
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
53
+ * Shortcut for this.fs.readJSON(this.destinationPath(filepath)).
54
+ *
55
+ * @param {String} filepath - absolute file path or relative to destination folder.
56
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
57
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
58
+ */
59
+ fs.readDestinationJSON = function(filepath, ...args) {
60
+ return this.fs.readJSON(this.destinationPath(filepath), ...args);
61
+ };
62
+
63
+ /**
64
+ * Write file to destination folder
65
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
66
+ * Shortcut for this.fs.write(this.destinationPath(filepath)).
67
+ *
68
+ * @param {String} filepath - absolute file path or relative to destination folder.
69
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
70
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
71
+ */
72
+ fs.writeDestination = function(filepath, ...args) {
73
+ return this.fs.write(this.destinationPath(filepath), ...args);
74
+ };
75
+
76
+ /**
77
+ * Write json file to destination folder
78
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
79
+ * Shortcut for this.fs.writeJSON(this.destinationPath(filepath)).
80
+ *
81
+ * @param {String} filepath - absolute file path or relative to destination folder.
82
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
83
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
84
+ */
85
+ fs.writeDestinationJSON = function(filepath, ...args) {
86
+ return this.fs.writeJSON(this.destinationPath(filepath), ...args);
87
+ };
88
+
89
+ /**
90
+ * Delete file from destination folder
91
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
92
+ * Shortcut for this.fs.delete(this.destinationPath(filepath)).
93
+ *
94
+ * @param {String} filepath - absolute file path or relative to destination folder.
95
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
96
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
97
+ */
98
+ fs.deleteDestination = function(filepath, ...args) {
99
+ return this.fs.delete(this.destinationPath(filepath), ...args);
100
+ };
101
+
102
+ /**
103
+ * Copy file from destination folder to another destination folder.
104
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
105
+ * Shortcut for this.fs.copy(this.destinationPath(from), this.destinationPath(to)).
106
+ *
107
+ * @param {String} from - absolute file path or relative to destination folder.
108
+ * @param {String} to - absolute file path or relative to destination folder.
109
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
110
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
111
+ */
112
+ fs.copyDestination = function(from, to, ...args) {
113
+ return this.fs.copy(this.destinationPath(from), this.destinationPath(to), ...args);
114
+ };
115
+
116
+ /**
117
+ * Move file from destination folder to another destination folder.
118
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
119
+ * Shortcut for this.fs.move(this.destinationPath(from), this.destinationPath(to)).
120
+ *
121
+ * @param {String} from - absolute file path or relative to destination folder.
122
+ * @param {String} to - absolute file path or relative to destination folder.
123
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
124
+ */
125
+ fs.moveDestination = function(from, to, ...args) {
126
+ return this.fs.move(this.destinationPath(from), this.destinationPath(to), ...args);
127
+ };
128
+
129
+ /**
130
+ * Exists file on destination folder.
131
+ * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}.
132
+ * Shortcut for this.fs.exists(this.destinationPath(filepath)).
133
+ *
134
+ * @param {String} filepath - absolute file path or relative to destination folder.
135
+ * @param {...*} args - for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
136
+ * @returns {*} for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}
137
+ */
138
+ fs.existsDestination = function(filepath, ...args) {
139
+ return this.fs.exists(this.destinationPath(filepath), ...args);
140
+ };
141
+
142
+ /**
143
+ * Copy a template from templates folder to the destination.
144
+ *
145
+ * @param {String|Array} source - template file, absolute or relative to templatePath().
146
+ * @param {String|Array} [destination] - destination, absolute or relative to destinationPath().
147
+ * @param {Object} [templateData] - ejs data
148
+ * @param {Object} [templateOptions] - ejs options
149
+ * @param {Object} [copyOptions] - mem-fs-editor copy options
150
+ */
151
+ fs.renderTemplate = function(
152
+ source = '',
153
+ destination = source,
154
+ templateData = this._templateData(),
155
+ templateOptions,
156
+ copyOptions
157
+ ) {
158
+ if (typeof templateData === 'string') {
159
+ templateData = this._templateData(templateData);
160
+ }
161
+
162
+ templateOptions = { context: this, ...templateOptions };
163
+
164
+ source = Array.isArray(source) ? source : [source];
165
+ const templatePath = this.templatePath(...source);
166
+ destination = Array.isArray(destination) ? destination : [destination];
167
+ const destinationPath = this.destinationPath(...destination);
168
+
169
+ this.fs.copyTpl(
170
+ templatePath,
171
+ destinationPath,
172
+ templateData,
173
+ templateOptions,
174
+ copyOptions
175
+ );
176
+ };
177
+
178
+ /**
179
+ * Copy templates from templates folder to the destination.
180
+ *
181
+ * @param {Array} templates - template file, absolute or relative to templatePath().
182
+ * @param {function} [templates.when] - conditional if the template should be written.
183
+ * First argument is the templateData, second is the generator.
184
+ * @param {String|Array} templates.source - template file, absolute or relative to templatePath().
185
+ * @param {String|Array} [templates.destination] - destination, absolute or relative to destinationPath().
186
+ * @param {Object} [templates.templateOptions] - ejs options
187
+ * @param {Object} [templates.copyOptions] - mem-fs-editor copy options
188
+ * @param {Object} [templateData] - ejs data
189
+ */
190
+ fs.renderTemplates = function(templates, templateData = this._templateData()) {
191
+ assert(Array.isArray(templates), 'Templates must an array');
192
+ if (typeof templateData === 'string') {
193
+ templateData = this._templateData(templateData);
194
+ }
195
+
196
+ const self = this;
197
+ const renderEachTemplate = template => {
198
+ if (template.when && !template.when(templateData, this)) {
199
+ return;
200
+ }
201
+
202
+ const { source, destination, templateOptions, copyOptions } = template;
203
+ self.renderTemplate(source, destination, templateData, templateOptions, copyOptions);
204
+ };
205
+
206
+ templates.forEach(template => renderEachTemplate(template));
207
+ };
208
+
209
+ /**
210
+ * Utility method to get a formatted data for templates.
211
+ *
212
+ * @param {String} path - path to the storage key.
213
+ * @return {Object} data to be passed to the templates.
214
+ */
215
+ fs._templateData = function(path) {
216
+ if (path) {
217
+ return this.config.getPath(path);
218
+ }
219
+
220
+ const allConfig = this.config.getAll();
221
+ if (this.generatorConfig) {
222
+ Object.assign(allConfig, this.generatorConfig.getAll());
223
+ }
224
+
225
+ if (this.instanceConfig) {
226
+ Object.assign(allConfig, this.instanceConfig.getAll());
227
+ }
228
+
229
+ return allConfig;
230
+ };
@@ -110,7 +110,7 @@ install.scheduleInstallTask = function(installer, paths, options, spawnOptions)
110
110
  * npm: false
111
111
  * });
112
112
  *
113
- * @param {Object} [options]
113
+ * @param {Object} [options] - options
114
114
  * @param {Boolean|Object} [options.npm=true] - whether to run `npm install` or can be options to pass to `dargs` as arguments
115
115
  * @param {Boolean|Object} [options.bower=true] - whether to run `bower install` or can be options to pass to `dargs` as arguments
116
116
  * @param {Boolean|Object} [options.yarn=false] - whether to run `yarn install` or can be options to pass to `dargs` as arguments
package/lib/index.js CHANGED
@@ -21,11 +21,11 @@ const promptSuggestion = require('./util/prompt-suggestion');
21
21
 
22
22
  const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@';
23
23
  const debug = createDebug('yeoman:generator');
24
- const ENV_VER_WITH_VER_API = '3.0.0';
24
+ const ENV_VER_WITH_VER_API = '2.9.0';
25
25
 
26
26
  // Ensure a prototype method is a candidate run by default
27
27
  const methodIsValid = function(name) {
28
- return name.charAt(0) !== '_' && name !== 'constructor';
28
+ return !['_', '#'].includes(name.charAt(0)) && name !== 'constructor';
29
29
  };
30
30
 
31
31
  // New runWithOptions should take precedence if exists.
@@ -98,6 +98,7 @@ class Generator extends EventEmitter {
98
98
  ];
99
99
  }
100
100
 
101
+ /* eslint-disable complexity */
101
102
  /**
102
103
  * @classdesc The `Generator` class provides the common API shared by all generators.
103
104
  * It define options, arguments, file, prompt, log, API, etc.
@@ -111,6 +112,7 @@ class Generator extends EventEmitter {
111
112
  * @mixes actions/install
112
113
  * @mixes actions/spawn-command
113
114
  * @mixes actions/user
115
+ * @mixes actions/fs
114
116
  * @mixes nodejs/EventEmitter
115
117
  *
116
118
  * @param {string[]} args - Provide arguments at initialization
@@ -147,8 +149,11 @@ class Generator extends EventEmitter {
147
149
  this._args = args || [];
148
150
  this._options = {};
149
151
  this._arguments = [];
152
+ this._prompts = [];
150
153
  this._composedWith = [];
151
154
  this._transformStreams = [];
155
+ this._namespace = this.options.namespace;
156
+ this._namespaceId = this.options.namespaceId;
152
157
 
153
158
  this.option('help', {
154
159
  type: Boolean,
@@ -174,6 +179,12 @@ class Generator extends EventEmitter {
174
179
  default: false
175
180
  });
176
181
 
182
+ this.option('ask-answered', {
183
+ type: Boolean,
184
+ description: 'Show prompts for already configured options',
185
+ default: false
186
+ });
187
+
177
188
  this.resolved = this.options.resolved || __dirname;
178
189
  this.env = this.options.env;
179
190
 
@@ -210,13 +221,7 @@ class Generator extends EventEmitter {
210
221
  }
211
222
  }
212
223
 
213
- try {
214
- this.fs = this.env.fs || require('mem-fs-editor').create(this.env.sharedFs);
215
- } catch (_) {
216
- throw new Error(
217
- "Current environment don't provides some necessary feature this generator needs"
218
- );
219
- }
224
+ this.fs = require('mem-fs-editor').create(this.env.sharedFs);
220
225
 
221
226
  this.description = this.description || '';
222
227
 
@@ -266,6 +271,15 @@ class Generator extends EventEmitter {
266
271
 
267
272
  this.appname = this.determineAppname();
268
273
  this.config = this._getStorage();
274
+ if (this._namespaceId && this._namespaceId.generator) {
275
+ this.generatorConfig = this.config.createStorage(`:${this._namespaceId.generator}`);
276
+ if (this._namespaceId.instanceId) {
277
+ this.instanceConfig = this.generatorConfig.createStorage(
278
+ `#${this._namespaceId.instanceId}`
279
+ );
280
+ }
281
+ }
282
+
269
283
  this._globalConfig = this._getGlobalStorage();
270
284
 
271
285
  // Ensure source/destination path, can be configured from subclasses
@@ -368,6 +382,11 @@ class Generator extends EventEmitter {
368
382
  this.env.runLoop.addSubQueue(customQueue.queueName, beforeQueue);
369
383
  });
370
384
  }
385
+
386
+ this.compose = this.options.compose;
387
+
388
+ // Expose utilities for dependency-less generators.
389
+ this._ = _;
371
390
  }
372
391
 
373
392
  checkEnvironmentVersion(packageDependency, version) {
@@ -423,6 +442,48 @@ class Generator extends EventEmitter {
423
442
  this._debug(...args);
424
443
  }
425
444
 
445
+ /**
446
+ * Register stored config prompts and optional option alternative.
447
+ *
448
+ * @param {Inquirer|Inquirer[]} questions - Inquirer question or questions.
449
+ * @param {Object|Boolean} [questions.exportOption] - Additional data to export this question as an option.
450
+ * @param {Storage|String} [question.storage=this.config] - Storage to store the answers.
451
+ */
452
+ registerConfigPrompts(questions) {
453
+ questions = Array.isArray(questions) ? questions : [questions];
454
+ const getOptionTypeFromInquirerType = type => {
455
+ if (type === 'number') {
456
+ return Number;
457
+ }
458
+
459
+ if (type === 'confirm') {
460
+ return Boolean;
461
+ }
462
+
463
+ if (type === 'checkbox') {
464
+ return Array;
465
+ }
466
+
467
+ return String;
468
+ };
469
+
470
+ questions.forEach(q => {
471
+ const question = { ...q };
472
+ if (q.exportOption) {
473
+ let option = typeof q.exportOption === 'boolean' ? {} : q.exportOption;
474
+ this.option({
475
+ name: q.name,
476
+ type: getOptionTypeFromInquirerType(q.type),
477
+ description: q.message,
478
+ ...option,
479
+ storage: q.storage || this.config
480
+ });
481
+ }
482
+
483
+ this._prompts.push(question);
484
+ });
485
+ }
486
+
426
487
  /**
427
488
  * Prompt user to answer questions. The signature of this method is the same as {@link https://github.com/SBoudrias/Inquirer.js Inquirer.js}
428
489
  *
@@ -430,17 +491,79 @@ class Generator extends EventEmitter {
430
491
  * every question descriptor. When set to true, Yeoman will store/fetch the
431
492
  * user's answers as defaults.
432
493
  *
433
- * @param {array} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation}
494
+ * @param {object|object[]} questions Array of question descriptor objects. See {@link https://github.com/SBoudrias/Inquirer.js/blob/master/README.md Documentation}
495
+ * @param {Storage} [questions.storage] Store/fetch the question on the storage.
496
+ * @param {Storage} [storage] Storage object
434
497
  * @return {Promise} prompt promise
435
498
  */
436
- prompt(questions) {
499
+ prompt(questions, storage) {
500
+ const checkInquirer = () => {
501
+ if (this.inquireSupportsPrefilled === undefined) {
502
+ this.checkEnvironmentVersion();
503
+ this.inquireSupportsPrefilled = this.checkEnvironmentVersion('inquirer', '7.1.0');
504
+ }
505
+ };
506
+
507
+ if (storage !== undefined) {
508
+ checkInquirer();
509
+ }
510
+
511
+ const storageForQuestion = {};
512
+
513
+ const getAnswerFromStorage = function(question) {
514
+ let questionStorage = question.storage || storage;
515
+ questionStorage =
516
+ typeof questionStorage === 'string' ? this[questionStorage] : questionStorage;
517
+ if (questionStorage) {
518
+ checkInquirer();
519
+
520
+ const name = question.name;
521
+ storageForQuestion[name] = questionStorage;
522
+ const value = questionStorage.getPath(name);
523
+ if (value !== undefined) {
524
+ question.default = value;
525
+ }
526
+
527
+ return [name, value];
528
+ }
529
+
530
+ return undefined;
531
+ };
532
+
533
+ if (!Array.isArray(questions)) {
534
+ questions = [questions];
535
+ }
536
+
537
+ // Shows the prompt even if the answer already exists.
538
+ questions.forEach(question => {
539
+ if (question.askAnswered === undefined) {
540
+ question.askAnswered = this.options.askAnswered === true;
541
+ }
542
+ });
543
+
544
+ // Pre-fill answers with storage values.
545
+ const answers = {};
546
+ questions
547
+ .map(getAnswerFromStorage)
548
+ .filter(a => a)
549
+ .forEach(([key, value]) => {
550
+ answers[key] = value;
551
+ });
552
+
437
553
  questions = promptSuggestion.prefillQuestions(this._globalConfig, questions);
438
554
  questions = promptSuggestion.prefillQuestions(this.config, questions);
439
555
 
440
- return this.env.adapter.prompt(questions).then(answers => {
556
+ return this.env.adapter.prompt(questions, answers).then(answers => {
557
+ Object.entries(storageForQuestion).forEach(([name, questionStorage]) => {
558
+ const answer = answers[name] === undefined ? null : answers[name];
559
+ questionStorage.setPath(name, answer);
560
+ });
561
+
441
562
  if (!this.options['skip-cache'] && !this.options.skipCache) {
442
563
  promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false);
443
- promptSuggestion.storeAnswers(this.config, questions, answers, true);
564
+ if (!this.options.skipLocalCache) {
565
+ promptSuggestion.storeAnswers(this.config, questions, answers, true);
566
+ }
444
567
  }
445
568
 
446
569
  return answers;
@@ -452,16 +575,29 @@ class Generator extends EventEmitter {
452
575
  * generate generator usage. By default, generators get all the cli options
453
576
  * parsed by nopt as a `this.options` hash object.
454
577
  *
455
- * @param {String} name - Option name
578
+ * @param {String} [name] - Option name
456
579
  * @param {Object} config - Option options
457
580
  * @param {any} config.type - Either Boolean, String or Number
458
581
  * @param {string} [config.description] - Description for the option
459
582
  * @param {any} [config.default] - Default value
460
583
  * @param {any} [config.alias] - Option name alias (example `-h` and --help`)
461
584
  * @param {any} [config.hide] - Boolean whether to hide from help
585
+ * @param {Storage} [config.storage] - Storage to persist the option
462
586
  * @return {this} This generator
463
587
  */
464
588
  option(name, config) {
589
+ if (Array.isArray(name)) {
590
+ name.forEach(option => {
591
+ this.option(option);
592
+ });
593
+ return;
594
+ }
595
+
596
+ if (typeof name === 'object') {
597
+ config = name;
598
+ name = config.name;
599
+ }
600
+
465
601
  config = config || {};
466
602
 
467
603
  // Alias default to defaults for backward compatibility.
@@ -500,6 +636,12 @@ class Generator extends EventEmitter {
500
636
  }
501
637
 
502
638
  this.parseOptions();
639
+ if (config.storage && this.options[name] !== undefined) {
640
+ const storage =
641
+ typeof config.storage === 'string' ? this[config.storage] : config.storage;
642
+ storage.set(name, this.options[name]);
643
+ }
644
+
503
645
  return this;
504
646
  }
505
647
 
@@ -703,6 +845,84 @@ class Generator extends EventEmitter {
703
845
  });
704
846
  }
705
847
 
848
+ /**
849
+ * @private
850
+ * Schedule a generator's method on a run queue.
851
+ *
852
+ * @param {String} name: The method name to schedule.
853
+ * @param {TaskOptions} [taskOptions]: options.
854
+ */
855
+ queueOwnTask(name, taskOptions = {}) {
856
+ const property = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), name);
857
+ const item = property.value ? property.value : property.get.call(this);
858
+
859
+ const priority = this._queues[name];
860
+ taskOptions = {
861
+ ...priority,
862
+ cancellable: true,
863
+ run: false,
864
+ ...taskOptions
865
+ };
866
+
867
+ // Name points to a function; run it!
868
+ if (typeof item === 'function') {
869
+ taskOptions.taskName = name;
870
+ taskOptions.method = item;
871
+ this.queueTask(taskOptions);
872
+ return;
873
+ }
874
+
875
+ // Not a queue hash; stop
876
+ if (!priority) {
877
+ return;
878
+ }
879
+
880
+ this.queueTaskGroup(item, taskOptions);
881
+ }
882
+
883
+ /**
884
+ * @private
885
+ * Schedule every generator's methods on a run queue.
886
+ *
887
+ * @param {TaskOptions} [taskOptions]: options.
888
+ */
889
+ queueOwnTasks(taskOptions) {
890
+ this._running = true;
891
+ this._taskStatus = { cancelled: false, timestamp: new Date() };
892
+
893
+ const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
894
+ const validMethods = methods.filter(methodIsValid);
895
+ if (validMethods.length === 0 && this._prompts.length === 0) {
896
+ const error = new Error(
897
+ 'This Generator is empty. Add at least one method for it to run.'
898
+ );
899
+ this.emit('error', error);
900
+ throw error;
901
+ }
902
+
903
+ if (this._prompts.length > 0) {
904
+ this.queueTask({
905
+ method: () => this.prompt(this._prompts, this.config),
906
+ taskName: 'Prompt registered questions',
907
+ queueName: 'prompting',
908
+ cancellable: true
909
+ });
910
+
911
+ if (validMethods.length === 0) {
912
+ this.queueTask({
913
+ method: () => {
914
+ this.renderTemplate();
915
+ },
916
+ taskName: 'Empty generator: copy templates',
917
+ queueName: 'writing',
918
+ cancellable: true
919
+ });
920
+ }
921
+ }
922
+
923
+ validMethods.forEach(methodName => this.queueOwnTask(methodName, taskOptions));
924
+ }
925
+
706
926
  /**
707
927
  * Schedule tasks on a run queue.
708
928
  *
@@ -730,6 +950,10 @@ class Generator extends EventEmitter {
730
950
  namespace = self.options.namespace;
731
951
  }
732
952
 
953
+ // Task status allows to ignore (cancel) current queued tasks.
954
+ // Each queueOwnTasks (complete run) create a new taskStatus.
955
+ const taskStatus = this._taskStatus || {};
956
+
733
957
  debug(
734
958
  `Queueing ${namespace}#${methodName} with options %o`,
735
959
  _.omit(task, ['method'])
@@ -740,23 +964,18 @@ class Generator extends EventEmitter {
740
964
  continueQueue => {
741
965
  debug(`Running ${namespace}#${methodName}`);
742
966
  self.emit(`method:${methodName}`);
743
- const taskCancelled = task.cancellable && !self._running;
967
+ const taskCancelled = task.cancellable && taskStatus.cancelled;
968
+ if (taskCancelled) {
969
+ continueQueue();
970
+ return;
971
+ }
744
972
 
745
973
  runAsync(function() {
746
- if (taskCancelled) {
747
- return Promise.resolve();
748
- }
749
-
750
974
  self.async = () => this.async();
751
975
  self.runningState = { namespace, queueName, methodName };
752
976
  return method.apply(self, self.args);
753
977
  })()
754
978
  .then(function() {
755
- if (taskCancelled) {
756
- continueQueue();
757
- return;
758
- }
759
-
760
979
  delete self.runningState;
761
980
  const eventName = `done$${namespace || 'unknownnamespace'}#${methodName}`;
762
981
  debug(`Emiting event ${eventName}`);
@@ -798,6 +1017,21 @@ class Generator extends EventEmitter {
798
1017
  */
799
1018
  cancelCancellableTasks() {
800
1019
  this._running = false;
1020
+ // Task status references is registered at each running task
1021
+ this._taskStatus.cancelled = true;
1022
+ // Create a new task status.
1023
+ delete this._taskStatus;
1024
+ }
1025
+
1026
+ /**
1027
+ * Start the generator again.
1028
+ *
1029
+ * @param {Object} [options]: options.
1030
+ */
1031
+ startOver(options = {}) {
1032
+ this.cancelCancellableTasks();
1033
+ Object.assign(this.options, options);
1034
+ this.queueOwnTasks();
801
1035
  }
802
1036
 
803
1037
  /**
@@ -853,8 +1087,6 @@ class Generator extends EventEmitter {
853
1087
  }
854
1088
 
855
1089
  const promise = new Promise((resolve, reject) => {
856
- const self = this;
857
- this._running = true;
858
1090
  this.debug('Generator is starting');
859
1091
  this.emit('run');
860
1092
 
@@ -868,74 +1100,20 @@ class Generator extends EventEmitter {
868
1100
  this.on('error', reject);
869
1101
  }
870
1102
 
871
- const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
872
- const validMethods = methods.filter(methodIsValid);
873
- if (!validMethods.length) {
874
- return this.emit(
875
- 'error',
876
- new Error('This Generator is empty. Add at least one method for it to run.')
877
- );
878
- }
879
-
880
1103
  this.env.runLoop.once('end', () => {
881
1104
  this.debug('Generator has ended');
882
1105
  this.emit('end');
883
1106
  resolve();
884
1107
  });
885
1108
 
886
- function addInQueue(name) {
887
- const property = Object.getOwnPropertyDescriptor(
888
- Object.getPrototypeOf(self),
889
- name
890
- );
891
- const item = property.value ? property.value : property.get.call(self);
892
-
893
- const priority = self._queues[name];
894
- let taskOptions = {
895
- ...priority,
896
- cancellable: true,
897
- run: false,
898
- generatorReject: usePromise ? undefined : reject
899
- };
900
-
901
- // Name points to a function; run it!
902
- if (typeof item === 'function') {
903
- taskOptions.taskName = name;
904
- taskOptions.method = item;
905
- return self.queueTask(taskOptions);
906
- }
907
-
908
- // Not a queue hash; stop
909
- if (!priority) {
910
- return;
911
- }
912
-
913
- self.queueTaskGroup(item, taskOptions);
914
- }
915
-
916
- validMethods.forEach(addInQueue);
917
-
918
- const writeFiles = () => {
919
- this.env.runLoop.add('conflicts', this._writeFiles.bind(this), {
920
- once: 'write memory fs to disk'
921
- });
922
- };
923
-
924
- this.env.sharedFs.on('change', writeFiles);
925
- writeFiles();
926
-
927
- // Add the default conflicts handling
928
- this.env.runLoop.add('conflicts', done => {
929
- this.conflicter.resolve(err => {
930
- if (err) {
931
- return this.emit('error', err);
932
- }
933
-
934
- done();
935
- });
1109
+ this.queueOwnTasks({
1110
+ generatorReject: usePromise ? undefined : reject
936
1111
  });
937
1112
 
1113
+ this.queueBasicTasks();
1114
+
938
1115
  this._composedWith.forEach(runGenerator);
1116
+ this._composedWith = [];
939
1117
  });
940
1118
 
941
1119
  // For composed generators, otherwise error will not be catched.
@@ -948,6 +1126,31 @@ class Generator extends EventEmitter {
948
1126
  return promise;
949
1127
  }
950
1128
 
1129
+ /**
1130
+ * Queue generator's basic tasks, only once execution is required for each environment.
1131
+ */
1132
+ queueBasicTasks() {
1133
+ const writeFiles = () => {
1134
+ this.env.runLoop.add('conflicts', this._writeFiles.bind(this), {
1135
+ once: 'write memory fs to disk'
1136
+ });
1137
+ };
1138
+
1139
+ this.env.sharedFs.on('change', writeFiles);
1140
+ writeFiles();
1141
+
1142
+ // Add the default conflicts handling
1143
+ this.env.runLoop.add('conflicts', done => {
1144
+ this.conflicter.resolve(err => {
1145
+ if (err) {
1146
+ return this.emit('error', err);
1147
+ }
1148
+
1149
+ done();
1150
+ });
1151
+ });
1152
+ }
1153
+
951
1154
  /**
952
1155
  * Compose this generator with another one.
953
1156
  * @param {String|Object|Array} generator The path to the generator module or an object (see examples)
@@ -975,7 +1178,9 @@ class Generator extends EventEmitter {
975
1178
  let instantiatedGenerator;
976
1179
 
977
1180
  if (Array.isArray(generator)) {
978
- const generators = generator.map(gen => this.composeWith(gen, options));
1181
+ const generators = generator.map(gen =>
1182
+ this.composeWith(gen, options, returnNewGenerator)
1183
+ );
979
1184
  return returnCompose(generators);
980
1185
  }
981
1186
 
@@ -1005,6 +1210,7 @@ class Generator extends EventEmitter {
1005
1210
  'skip-cache': this.options.skipCache || this.options['skip-cache'],
1006
1211
  forceInstall: this.options.forceInstall || this.options['force-install'],
1007
1212
  'force-install': this.options.forceInstall || this.options['force-install'],
1213
+ skipLocalCache: this.options.skipLocalCache,
1008
1214
  destinationRoot: this._destinationRoot
1009
1215
  },
1010
1216
  options
@@ -1292,6 +1498,7 @@ class Generator extends EventEmitter {
1292
1498
  _.extend(Generator.prototype, require('./actions/install'));
1293
1499
  _.extend(Generator.prototype, require('./actions/help'));
1294
1500
  _.extend(Generator.prototype, require('./actions/spawn-command'));
1501
+ _.extend(Generator.prototype, require('./actions/fs'));
1295
1502
  Generator.prototype.user = require('./actions/user');
1296
1503
 
1297
1504
  module.exports = Generator;
@@ -2,6 +2,32 @@
2
2
  const assert = require('assert');
3
3
  const _ = require('lodash');
4
4
 
5
+ /**
6
+ * Proxy handler for Storage
7
+ */
8
+ const proxyHandler = {
9
+ get(storage, property) {
10
+ return storage.get(property);
11
+ },
12
+ set(storage, property, value) {
13
+ storage.set(property, value);
14
+ return true;
15
+ },
16
+ ownKeys(storage) {
17
+ return Reflect.ownKeys(storage._store);
18
+ },
19
+ has(target, prop) {
20
+ return target.get(prop) !== undefined;
21
+ },
22
+ getOwnPropertyDescriptor(target, key) {
23
+ return {
24
+ get: () => this.get(target, key),
25
+ enumerable: true,
26
+ configurable: true
27
+ };
28
+ }
29
+ };
30
+
5
31
  /**
6
32
  * Storage instances handle a json file where Generator authors can store data.
7
33
  *
@@ -11,7 +37,10 @@ const _ = require('lodash');
11
37
  * @param {String} [name] The name of the new storage (this is a namespace)
12
38
  * @param {mem-fs-editor} fs A mem-fs editor instance
13
39
  * @param {String} configPath The filepath used as a storage.
14
- * @param {Boolean} lodashPath Set true to treat name as a lodash path.
40
+ * @param {Object} [options] Storage options.
41
+ * @param {Boolean} [options.lodashPath=false] Set true to treat name as a lodash path.
42
+ * @param {Boolean} [options.disableCache=false] Set true to disable json object cache.
43
+ * @param {Boolean} [options.disableCacheByFile=false] Set true to cleanup cache for every fs change.
15
44
  *
16
45
  * @example
17
46
  * class extend Generator {
@@ -21,21 +50,43 @@ const _ = require('lodash');
21
50
  * }
22
51
  */
23
52
  class Storage {
24
- constructor(name, fs, configPath, lodashPath = false) {
53
+ constructor(name, fs, configPath, options = {}) {
25
54
  if (name !== undefined && typeof name !== 'string') {
26
55
  configPath = fs;
27
56
  fs = name;
28
57
  name = undefined;
29
58
  }
30
59
 
60
+ if (typeof options === 'boolean') {
61
+ options = { lodashPath: options };
62
+ }
63
+
64
+ _.defaults(options, {
65
+ lodash: false,
66
+ disableCache: false,
67
+ disableCacheByFile: false
68
+ });
69
+
31
70
  assert(configPath, 'A config filepath is required to create a storage');
32
71
 
33
72
  this.path = configPath;
34
73
  this.name = name;
35
74
  this.fs = fs;
36
- this.existed = Object.keys(this._store).length > 0;
37
75
  this.indent = 2;
38
- this.lodashPath = lodashPath;
76
+ this.lodashPath = options.lodashPath;
77
+ this.disableCache = options.disableCache;
78
+ this.disableCacheByFile = options.disableCacheByFile;
79
+
80
+ this.existed = Object.keys(this._store).length > 0;
81
+
82
+ this.fs.store.on('change', filename => {
83
+ // At mem-fs 1.1.3 filename is not passed to the event.
84
+ if (this.disableCacheByFile || (filename && filename !== this.path)) {
85
+ return;
86
+ }
87
+
88
+ delete this._cachedStore;
89
+ });
39
90
  }
40
91
 
41
92
  /**
@@ -44,7 +95,11 @@ class Storage {
44
95
  * @private
45
96
  */
46
97
  get _store() {
47
- const store = this.fs.readJSON(this.path, {});
98
+ const store = this._cachedStore || this.fs.readJSON(this.path, {});
99
+ if (!this.disableCache) {
100
+ this._cachedStore = store;
101
+ }
102
+
48
103
  if (!this.name) {
49
104
  return store || {};
50
105
  }
@@ -164,6 +219,24 @@ class Storage {
164
219
  this.set(val);
165
220
  return val;
166
221
  }
222
+
223
+ /**
224
+ * Create a child storage.
225
+ * @param {String} path - relative path of the key to create a new storage.
226
+ * @return {Storage} Returns a new Storage.
227
+ */
228
+ createStorage(path) {
229
+ const childName = this.name ? `${this.name}.${path}` : path;
230
+ return new Storage(childName, this.fs, this.path, true);
231
+ }
232
+
233
+ /**
234
+ * Creates a proxy object.
235
+ * @return {Object} proxy.
236
+ */
237
+ createProxy() {
238
+ return new Proxy(this, proxyHandler);
239
+ }
167
240
  }
168
241
 
169
242
  module.exports = Storage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yeoman-generator",
3
- "version": "4.8.3",
3
+ "version": "4.11.0",
4
4
  "description": "Rails-inspired generator system that provides scaffolding for your apps",
5
5
  "homepage": "http://yeoman.io",
6
6
  "author": "Yeoman",
@@ -31,6 +31,7 @@
31
31
  "inquirer": "^6.3.1",
32
32
  "jsdoc": "^3.6.3",
33
33
  "lint-staged": "^8.1.7",
34
+ "mem-fs": "^1.2.0",
34
35
  "mocha": "^7.1.1",
35
36
  "mockery": "^2.1.0",
36
37
  "nock": "^12.0.3",
@@ -40,7 +41,7 @@
40
41
  "sinon": "^7.3.2",
41
42
  "tui-jsdoc-template": "^1.2.2",
42
43
  "yeoman-assert": "^3.1.1",
43
- "yeoman-test": "^2.3.0"
44
+ "yeoman-test": "^2.6.0"
44
45
  },
45
46
  "license": "BSD-2-Clause",
46
47
  "repository": "yeoman/generator",
@@ -68,6 +69,7 @@
68
69
  "istextorbinary": "^2.5.1",
69
70
  "lodash": "^4.17.11",
70
71
  "make-dir": "^3.0.0",
72
+ "mem-fs-editor": "^7.0.1",
71
73
  "minimist": "^1.2.5",
72
74
  "pretty-bytes": "^5.2.0",
73
75
  "read-chunk": "^3.2.0",
@@ -81,8 +83,7 @@
81
83
  },
82
84
  "optionalDependencies": {
83
85
  "grouped-queue": "^1.1.0",
84
- "mem-fs-editor": "^6.0.0",
85
- "yeoman-environment": "^2.3.4"
86
+ "yeoman-environment": "^2.9.5"
86
87
  },
87
88
  "lint-staged": {
88
89
  "*.js": [