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