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 |
|
21 | const EMPTY = '@@_YEOMAN_EMPTY_MARKER_@@';
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | class Generator extends EventEmitter {
|
59 | constructor(args, options) {
|
60 | super();
|
61 |
|
62 | if (!Array.isArray(args)) {
|
63 | options = args;
|
64 | args = [];
|
65 | }
|
66 |
|
67 | this.options = options || {};
|
68 | this._initOptions = _.clone(options);
|
69 | this._args = args || [];
|
70 | this._options = {};
|
71 | this._arguments = [];
|
72 | this._composedWith = [];
|
73 | this._transformStreams = [];
|
74 |
|
75 | this.option('help', {
|
76 | type: Boolean,
|
77 | alias: 'h',
|
78 | description: "Print the generator's options and usage"
|
79 | });
|
80 |
|
81 | this.option('skip-cache', {
|
82 | type: Boolean,
|
83 | description: 'Do not remember prompt answers',
|
84 | default: false
|
85 | });
|
86 |
|
87 | this.option('skip-install', {
|
88 | type: Boolean,
|
89 | description: 'Do not automatically install dependencies',
|
90 | default: false
|
91 | });
|
92 |
|
93 |
|
94 | assert(
|
95 | this.options.env,
|
96 | 'You must provide the environment object. Use env#create() to create a new generator.'
|
97 | );
|
98 | assert(
|
99 | this.options.resolved,
|
100 | 'You must provide the resolved path value. Use env#create() to create a new generator.'
|
101 | );
|
102 | this.env = this.options.env;
|
103 | this.resolved = this.options.resolved;
|
104 |
|
105 |
|
106 | require('yeoman-environment').enforceUpdate(this.env);
|
107 |
|
108 | this.description = this.description || '';
|
109 |
|
110 | this.async = () => () => {};
|
111 |
|
112 | this.fs = FileEditor.create(this.env.sharedFs);
|
113 | this.conflicter = new Conflicter(this.env.adapter, this.options.force);
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 | this.log = this.env.adapter.log;
|
121 |
|
122 |
|
123 | this.contextRoot = this.env.cwd;
|
124 |
|
125 | let rootPath = findUp.sync('.yo-rc.json', {
|
126 | cwd: this.env.cwd
|
127 | });
|
128 | rootPath = rootPath ? path.dirname(rootPath) : this.env.cwd;
|
129 |
|
130 | if (rootPath !== this.env.cwd) {
|
131 | this.log(
|
132 | [
|
133 | '',
|
134 | 'Just found a `.yo-rc.json` in a parent directory.',
|
135 | 'Setting the project root at: ' + rootPath
|
136 | ].join('\n')
|
137 | );
|
138 | this.destinationRoot(rootPath);
|
139 | }
|
140 |
|
141 | this.appname = this.determineAppname();
|
142 | this.config = this._getStorage();
|
143 | this._globalConfig = this._getGlobalStorage();
|
144 |
|
145 |
|
146 | this.sourceRoot(path.join(path.dirname(this.resolved), 'templates'));
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | prompt(questions) {
|
160 | questions = promptSuggestion.prefillQuestions(this._globalConfig, questions);
|
161 | questions = promptSuggestion.prefillQuestions(this.config, questions);
|
162 |
|
163 | return this.env.adapter.prompt(questions).then(answers => {
|
164 | if (!this.options['skip-cache']) {
|
165 | promptSuggestion.storeAnswers(this._globalConfig, questions, answers, false);
|
166 | promptSuggestion.storeAnswers(this.config, questions, answers, true);
|
167 | }
|
168 |
|
169 | return answers;
|
170 | });
|
171 | }
|
172 |
|
173 | |
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | option(name, config) {
|
190 | config = config || {};
|
191 |
|
192 |
|
193 | if ('defaults' in config) {
|
194 | config.default = config.defaults;
|
195 | }
|
196 | config.description = config.description || config.desc;
|
197 |
|
198 | _.defaults(config, {
|
199 | name,
|
200 | description: 'Description for ' + name,
|
201 | type: Boolean,
|
202 | hide: false
|
203 | });
|
204 |
|
205 |
|
206 | const boolOptionRegex = /^no-/;
|
207 | if (config.type === Boolean && name.match(boolOptionRegex)) {
|
208 | const simpleName = name.replace(boolOptionRegex, '');
|
209 | return this.emit(
|
210 | 'error',
|
211 | new Error(
|
212 | [
|
213 | `Option name ${chalk.yellow(name)} cannot start with ${chalk.red('no-')}\n`,
|
214 | `Option name prefixed by ${chalk.yellow('--no')} are parsed as implicit`,
|
215 | ` boolean. To use ${chalk.yellow('--' + name)} as an option, use\n`,
|
216 | chalk.cyan(` this.option('${simpleName}', {type: Boolean})`)
|
217 | ].join('')
|
218 | )
|
219 | );
|
220 | }
|
221 |
|
222 | if (this._options[name] === null || this._options[name] === undefined) {
|
223 | this._options[name] = config;
|
224 | }
|
225 |
|
226 | this.parseOptions();
|
227 | return this;
|
228 | }
|
229 |
|
230 | |
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | argument(name, config) {
|
252 | config = config || {};
|
253 |
|
254 |
|
255 | if ('defaults' in config) {
|
256 | config.default = config.defaults;
|
257 | }
|
258 | config.description = config.description || config.desc;
|
259 |
|
260 | _.defaults(config, {
|
261 | name,
|
262 | required: config.default === null || config.default === undefined,
|
263 | type: String
|
264 | });
|
265 |
|
266 | this._arguments.push(config);
|
267 |
|
268 | this.parseOptions();
|
269 | return this;
|
270 | }
|
271 |
|
272 | parseOptions() {
|
273 | const minimistDef = {
|
274 | string: [],
|
275 | boolean: [],
|
276 | alias: {},
|
277 | default: {}
|
278 | };
|
279 |
|
280 | _.each(this._options, option => {
|
281 | if (option.type === Boolean) {
|
282 | minimistDef.boolean.push(option.name);
|
283 | if (!('default' in option) && !option.required) {
|
284 | minimistDef.default[option.name] = EMPTY;
|
285 | }
|
286 | } else {
|
287 | minimistDef.string.push(option.name);
|
288 | }
|
289 |
|
290 | if (option.alias) {
|
291 | minimistDef.alias[option.alias] = option.name;
|
292 | }
|
293 |
|
294 |
|
295 |
|
296 | if (option.name in this._initOptions) {
|
297 | minimistDef.default[option.name] = this._initOptions[option.name];
|
298 | } else if (option.alias && option.alias in this._initOptions) {
|
299 | minimistDef.default[option.name] = this._initOptions[option.alias];
|
300 | } else if ('default' in option) {
|
301 | minimistDef.default[option.name] = option.default;
|
302 | }
|
303 | });
|
304 |
|
305 | const parsedOpts = minimist(this._args, minimistDef);
|
306 |
|
307 |
|
308 | _.each(parsedOpts, (option, name) => {
|
309 |
|
310 | if (option === EMPTY) {
|
311 | parsedOpts[name] = undefined;
|
312 | return;
|
313 | }
|
314 | if (this._options[name] && option !== undefined) {
|
315 | parsedOpts[name] = this._options[name].type(option);
|
316 | }
|
317 | });
|
318 |
|
319 |
|
320 | this._arguments.forEach((config, index) => {
|
321 | let value;
|
322 | if (index >= parsedOpts._.length) {
|
323 | if (config.name in this._initOptions) {
|
324 | value = this._initOptions[config.name];
|
325 | } else if ('default' in config) {
|
326 | value = config.default;
|
327 | } else {
|
328 | return;
|
329 | }
|
330 | } else if (config.type === Array) {
|
331 | value = parsedOpts._.slice(index, parsedOpts._.length);
|
332 | } else {
|
333 | value = config.type(parsedOpts._[index]);
|
334 | }
|
335 |
|
336 | parsedOpts[config.name] = value;
|
337 | });
|
338 |
|
339 |
|
340 | Object.assign(this.options, parsedOpts);
|
341 | this.args = parsedOpts._;
|
342 | this.arguments = parsedOpts._;
|
343 |
|
344 |
|
345 | this.checkRequiredArgs();
|
346 | }
|
347 |
|
348 | checkRequiredArgs() {
|
349 |
|
350 |
|
351 | if (this.options.help) {
|
352 | return;
|
353 | }
|
354 |
|
355 |
|
356 | if (this.args.length > this._arguments.length) {
|
357 | return;
|
358 | }
|
359 |
|
360 | this._arguments.forEach((config, position) => {
|
361 |
|
362 |
|
363 | if (config.required && position >= this.args.length) {
|
364 | return this.emit(
|
365 | 'error',
|
366 | new Error(`Did not provide required argument ${chalk.bold(config.name)}!`)
|
367 | );
|
368 | }
|
369 | });
|
370 | }
|
371 |
|
372 | |
373 |
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 | run(cb) {
|
387 | const promise = new Promise((resolve, reject) => {
|
388 | const self = this;
|
389 | this._running = true;
|
390 | this.emit('run');
|
391 |
|
392 | const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
393 | const validMethods = methods.filter(methodIsValid);
|
394 | assert(
|
395 | validMethods.length,
|
396 | 'This Generator is empty. Add at least one method for it to run.'
|
397 | );
|
398 |
|
399 | this.env.runLoop.once('end', () => {
|
400 | this.emit('end');
|
401 | resolve();
|
402 | });
|
403 |
|
404 |
|
405 | function methodIsValid(name) {
|
406 | return name.charAt(0) !== '_' && name !== 'constructor';
|
407 | }
|
408 |
|
409 | function addMethod(method, methodName, queueName) {
|
410 | queueName = queueName || 'default';
|
411 | debug(`Queueing ${methodName} in ${queueName}`);
|
412 | self.env.runLoop.add(queueName, completed => {
|
413 | debug(`Running ${methodName}`);
|
414 | self.emit(`method:${methodName}`);
|
415 |
|
416 | runAsync(function() {
|
417 | self.async = () => this.async();
|
418 | return method.apply(self, self.args);
|
419 | })()
|
420 | .then(completed)
|
421 | .catch(err => {
|
422 | debug(`An error occured while running ${methodName}`, err);
|
423 |
|
424 |
|
425 |
|
426 | setImmediate(() => {
|
427 | self.emit('error', err);
|
428 | reject(err);
|
429 | });
|
430 | });
|
431 | });
|
432 | }
|
433 |
|
434 | function addInQueue(name) {
|
435 | const item = Object.getPrototypeOf(self)[name];
|
436 | const queueName = self.env.runLoop.queueNames.indexOf(name) === -1 ? null : name;
|
437 |
|
438 |
|
439 | if (typeof item === 'function') {
|
440 | return addMethod(item, name, queueName);
|
441 | }
|
442 |
|
443 |
|
444 | if (!queueName) {
|
445 | return;
|
446 | }
|
447 |
|
448 |
|
449 | _.each(item, (method, methodName) => {
|
450 | if (!_.isFunction(method) || !methodIsValid(methodName)) {
|
451 | return;
|
452 | }
|
453 |
|
454 | addMethod(method, methodName, queueName);
|
455 | });
|
456 | }
|
457 |
|
458 | validMethods.forEach(addInQueue);
|
459 |
|
460 | const writeFiles = () => {
|
461 | this.env.runLoop.add('conflicts', this._writeFiles.bind(this), {
|
462 | once: 'write memory fs to disk'
|
463 | });
|
464 | };
|
465 |
|
466 | this.env.sharedFs.on('change', writeFiles);
|
467 | writeFiles();
|
468 |
|
469 |
|
470 | this.env.runLoop.add('conflicts', done => {
|
471 | this.conflicter.resolve(err => {
|
472 | if (err) {
|
473 | this.emit('error', err);
|
474 | }
|
475 |
|
476 | done();
|
477 | });
|
478 | });
|
479 |
|
480 | _.invokeMap(this._composedWith, 'run');
|
481 | });
|
482 |
|
483 | promise.then(cb, cb);
|
484 | return promise;
|
485 | }
|
486 |
|
487 | |
488 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 | composeWith(modulePath, options) {
|
505 | let generator;
|
506 | options = options || {};
|
507 |
|
508 |
|
509 | options = _.extend(
|
510 | {
|
511 | skipInstall: this.options.skipInstall,
|
512 | 'skip-install': this.options.skipInstall,
|
513 | skipCache: this.options.skipCache,
|
514 | 'skip-cache': this.options.skipCache
|
515 | },
|
516 | options
|
517 | );
|
518 |
|
519 | try {
|
520 | const Generator = require(modulePath);
|
521 | Generator.resolved = require.resolve(modulePath);
|
522 | Generator.namespace = this.env.namespace(modulePath);
|
523 | generator = this.env.instantiate(Generator, {
|
524 | options,
|
525 | arguments: options.arguments
|
526 | });
|
527 | } catch (err) {
|
528 | if (err.code === 'MODULE_NOT_FOUND') {
|
529 | generator = this.env.create(modulePath, {
|
530 | options,
|
531 | arguments: options.arguments
|
532 | });
|
533 | } else {
|
534 | throw err;
|
535 | }
|
536 | }
|
537 |
|
538 | if (this._running) {
|
539 | generator.run();
|
540 | } else {
|
541 | this._composedWith.push(generator);
|
542 | }
|
543 |
|
544 | return this;
|
545 | }
|
546 |
|
547 | |
548 |
|
549 |
|
550 |
|
551 | rootGeneratorName() {
|
552 | const pkg = readPkgUp.sync({ cwd: this.resolved }).pkg;
|
553 | return pkg ? pkg.name : '*';
|
554 | }
|
555 |
|
556 | |
557 |
|
558 |
|
559 |
|
560 | rootGeneratorVersion() {
|
561 | const pkg = readPkgUp.sync({ cwd: this.resolved }).pkg;
|
562 | return pkg ? pkg.version : '0.0.0';
|
563 | }
|
564 |
|
565 | |
566 |
|
567 |
|
568 |
|
569 |
|
570 | _getStorage() {
|
571 | const storePath = path.join(this.destinationRoot(), '.yo-rc.json');
|
572 | return new Storage(this.rootGeneratorName(), this.fs, storePath);
|
573 | }
|
574 |
|
575 | |
576 |
|
577 |
|
578 |
|
579 |
|
580 | _getGlobalStorage() {
|
581 | const storePath = path.join(os.homedir(), '.yo-rc-global.json');
|
582 | const storeName = `${this.rootGeneratorName()}:${this.rootGeneratorVersion()}`;
|
583 | return new Storage(storeName, this.fs, storePath);
|
584 | }
|
585 |
|
586 | |
587 |
|
588 |
|
589 |
|
590 |
|
591 |
|
592 |
|
593 | destinationRoot(rootPath) {
|
594 | if (typeof rootPath === 'string') {
|
595 | this._destinationRoot = path.resolve(rootPath);
|
596 |
|
597 | if (!fs.existsSync(rootPath)) {
|
598 | makeDir.sync(rootPath);
|
599 | }
|
600 |
|
601 | process.chdir(rootPath);
|
602 | this.env.cwd = rootPath;
|
603 |
|
604 |
|
605 | this.config = this._getStorage();
|
606 | }
|
607 |
|
608 | return this._destinationRoot || this.env.cwd;
|
609 | }
|
610 |
|
611 | |
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 | sourceRoot(rootPath) {
|
618 | if (typeof rootPath === 'string') {
|
619 | this._sourceRoot = path.resolve(rootPath);
|
620 | }
|
621 |
|
622 | return this._sourceRoot;
|
623 | }
|
624 |
|
625 | |
626 |
|
627 |
|
628 |
|
629 |
|
630 | templatePath() {
|
631 | let filepath = path.join.apply(path, arguments);
|
632 |
|
633 | if (!path.isAbsolute(filepath)) {
|
634 | filepath = path.join(this.sourceRoot(), filepath);
|
635 | }
|
636 |
|
637 | return filepath;
|
638 | }
|
639 |
|
640 | |
641 |
|
642 |
|
643 |
|
644 |
|
645 | destinationPath() {
|
646 | let filepath = path.join.apply(path, arguments);
|
647 |
|
648 | if (!path.isAbsolute(filepath)) {
|
649 | filepath = path.join(this.destinationRoot(), filepath);
|
650 | }
|
651 |
|
652 | return filepath;
|
653 | }
|
654 |
|
655 | |
656 |
|
657 |
|
658 |
|
659 |
|
660 |
|
661 |
|
662 |
|
663 | determineAppname() {
|
664 | let appname = this.fs.readJSON(this.destinationPath('bower.json'), {}).name;
|
665 |
|
666 | if (!appname) {
|
667 | appname = this.fs.readJSON(this.destinationPath('package.json'), {}).name;
|
668 | }
|
669 |
|
670 | if (!appname) {
|
671 | appname = path.basename(this.destinationRoot());
|
672 | }
|
673 |
|
674 | return appname.replace(/[^\w\s]+?/g, ' ');
|
675 | }
|
676 |
|
677 | |
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 | registerTransformStream(streams) {
|
687 | assert(streams, 'expected to receive a transform stream as parameter');
|
688 | if (!Array.isArray(streams)) {
|
689 | streams = [streams];
|
690 | }
|
691 | this._transformStreams = this._transformStreams.concat(streams);
|
692 | return this;
|
693 | }
|
694 |
|
695 | |
696 |
|
697 |
|
698 |
|
699 |
|
700 | _writeFiles(done) {
|
701 | const self = this;
|
702 |
|
703 | const conflictChecker = through.obj(function(file, enc, cb) {
|
704 | const stream = this;
|
705 |
|
706 |
|
707 | if (file.state === null) {
|
708 | return cb();
|
709 | }
|
710 |
|
711 |
|
712 | const filename = path.basename(file.path);
|
713 |
|
714 | if (filename === '.yo-rc.json' || filename === '.yo-rc-global.json') {
|
715 | this.push(file);
|
716 | return cb();
|
717 | }
|
718 |
|
719 | self.conflicter.checkForCollision(file.path, file.contents, (err, status) => {
|
720 | if (err) {
|
721 | cb(err);
|
722 | return;
|
723 | }
|
724 |
|
725 | if (status === 'skip') {
|
726 | delete file.state;
|
727 | } else {
|
728 | stream.push(file);
|
729 | }
|
730 |
|
731 | cb();
|
732 | });
|
733 | self.conflicter.resolve();
|
734 | });
|
735 |
|
736 | const transformStreams = this._transformStreams.concat([conflictChecker]);
|
737 | this.fs.commit(transformStreams, () => {
|
738 | done();
|
739 | });
|
740 | }
|
741 | }
|
742 |
|
743 |
|
744 | _.extend(Generator.prototype, require('./actions/install'));
|
745 | _.extend(Generator.prototype, require('./actions/help'));
|
746 | _.extend(Generator.prototype, require('./actions/spawn-command'));
|
747 | Generator.prototype.user = require('./actions/user');
|
748 |
|
749 | module.exports = Generator;
|