UNPKG

27.8 kBJavaScriptView Raw
1'use strict';
2
3/* eslint-disable global-require */
4/* eslint-disable prefer-rest-params */
5
6let Mustache;
7let downloadRepo;
8
9// constants
10
11const cp = require('child_process');
12const lp = require('log-pose');
13const fs = require('fs-extra');
14const path = require('path');
15const glob = require('glob');
16const rimraf = require('rimraf');
17const _ = require('./constants');
18
19// convert handlebars-like helpers into mustache fn-blocks
20const reTransformHelper = /\{\{\s*([a-z]\w+)\s+([.\w]+)\s*\}\}/g;
21
22function _merge(obj) {
23 const args = Array.prototype.slice.call(arguments, 1);
24
25 args.forEach(data => {
26 Object.keys(data).forEach(key => {
27 obj[key] = data[key];
28 });
29 });
30
31 return obj;
32}
33
34function _render(str) {
35 const args = Array.prototype.slice.call(arguments, 1);
36 const obj = args.reduce((prev, x) => _merge(prev, x), {});
37
38 Mustache = Mustache || require('mustache');
39
40 return Mustache.render(str.replace(reTransformHelper, '{{#$1}}$2{{/$1}}'), obj);
41}
42
43function _prompt($, input) {
44 const enquirer = $.haki.getEnquirer();
45
46 // pause earlier
47 lp.pause();
48
49 return enquirer.prompt(input.map(p => {
50 p.message = p.message || p.name;
51 p.type = p.type || 'input';
52
53 /* istanbul ignore else */
54 if (p.options) {
55 p.choices = p.options.map(c => {
56 c = typeof c === 'string' ? { name: c } : c;
57 c.message = c.message || c.name;
58 return c;
59 });
60
61 delete p.options;
62 }
63
64 return p;
65 }))
66 .then(response => {
67 lp.resume();
68
69 /* istanbul ignore else */
70 if (!Object.keys(response).length) {
71 throw new Error('Missing input');
72 }
73
74 const out = input.reduce((prev, cur) => {
75 /* istanbul ignore else */
76 if (typeof response[cur.name] === 'undefined') {
77 throw new Error(`Invalid ${cur.name} input`);
78 }
79
80 const found = cur.choices && cur.choices
81 .find(x => x.name === response[cur.name]);
82
83 prev[cur.name] = found ? (found.value || found.name) : response[cur.name];
84
85 return prev;
86 }, {});
87
88 return out;
89 })
90 .catch(error => {
91 lp.resume();
92 throw error;
93 });
94}
95
96function _exec(cmd, currentPath) {
97 return new Promise((resolve, reject) => {
98 const env = {};
99
100 // TODO: enhance this
101 env.PATH = process.env.PATH;
102
103 const ps = Array.isArray(cmd)
104 ? cp.spawn(cmd[0], cmd.slice(1), { env, cwd: currentPath })
105 : cp.exec(cmd, { env, cwd: currentPath });
106
107 let stderr = '';
108 let stdout = '';
109
110 ps.stdout.on('data', data => {
111 stdout += data;
112 });
113
114 ps.stderr.on('data', data => {
115 stderr += data;
116 });
117
118 ps.on('close', code => {
119 if (code === 0) {
120 resolve(stdout);
121 } else {
122 reject(new Error(stderr));
123 }
124 });
125 });
126}
127
128function _askIf($, err, label, options, skipFlag) {
129 return Promise.resolve()
130 .then(() => {
131 /* istanbul ignore else */
132 if (!err) {
133 return;
134 }
135
136 /* istanbul ignore else */
137 if (skipFlag) {
138 return 'skip';
139 }
140
141 return _prompt($, [{
142 options,
143 name: 'action',
144 type: 'select',
145 message: label,
146 default: options[0].name,
147 }]).then(({ action }) => action);
148 });
149}
150
151function _install($, task, logger, quietly, destPath) {
152 const result = { type: 'install' };
153
154 const tasks = [];
155
156 _.DEPS.forEach(key => {
157 const args = [];
158
159 /* istanbul ignore else */
160 if (key === 'optionalDependencies' && $.options.installOpts === false) {
161 return;
162 }
163
164 /* istanbul ignore else */
165 if (key === 'devDependencies' && $.options.installDev === false) {
166 return;
167 }
168
169 /* istanbul ignore else */
170 if (key === 'dependencies' && $.options.install === false) {
171 return;
172 }
173
174 /* istanbul ignore else */
175 if (typeof task[key] === 'undefined') {
176 return;
177 }
178
179 // reduce nested deps
180 (task[key] || []).slice().forEach(dep => {
181 if (Array.isArray(dep)) {
182 Array.prototype.push.apply(args, dep.filter(x => x));
183 } else if (dep) {
184 args.push(dep);
185 }
186 });
187
188 // backup
189 const _deps = args.slice();
190
191 tasks.push(() => {
192 /* istanbul ignore else */
193 if (args.length && $.options.yarn === true) {
194 args.unshift('add');
195 }
196
197 /* istanbul ignore else */
198 if ($.options.yarn !== true) {
199 args.unshift('install');
200 }
201
202 args.unshift($.options.yarn !== true ? 'npm' : 'yarn');
203
204 /* istanbul ignore else */
205 if (args.length > 2) {
206 if (key === 'devDependencies') {
207 args.push(`--${$.options.yarn !== true ? 'save-dev' : 'dev'}`);
208 } else if (key === 'optionalDependencies') {
209 args.push(`--${$.options.yarn !== true ? 'save-optional' : 'optional'}`);
210 } else if ($.options.yarn !== true) {
211 args.push('--save');
212 }
213 }
214
215 args.push('--silent');
216 args.push('--no-progress');
217
218 if (key !== 'optionalDependencies') {
219 if ($.options.yarn !== true) {
220 args.push('--no-optional');
221 } else {
222 args.push('--ignore-optional');
223 }
224 }
225
226 return logger('install', _deps.join(' '), end => _exec(args, destPath)
227 .then(data => {
228 /* istanbul ignore else */
229 if (task[key]) {
230 result[key] = task[key];
231 }
232
233 if (!end) {
234 logger.printf(`\r\r${data.replace(/\n+$/, '\n')}`);
235 } else if (!quietly) {
236 end(() => logger.printf(`\r\r${data.replace(/\n+$/, '\n')}`));
237 } else {
238 end();
239 }
240 }));
241 });
242 });
243
244 return tasks
245 .reduce((prev, cur) => prev.then(() => cur()),
246 Promise.resolve()).then(() => result);
247}
248
249function _runTask($, task, logger, _helpers) {
250 const _values = {};
251
252 const _changes = [];
253 const _failures = [];
254
255 // normalize input
256 const options = $.options || {};
257 const defaults = $.defaults || {};
258
259 // merge initial values
260 Object.keys(defaults).forEach(key => {
261 /* istanbul ignore else */
262 if (typeof task.validate === 'object' && typeof task.validate[key] === 'function') {
263 const test = task.validate[key](defaults[key]);
264
265 /* istanbul ignore else */
266 if (test !== true) {
267 throw new Error(test || `Invalid input for '${key}'`);
268 }
269 }
270
271 _values[key] = defaults[key];
272 });
273
274 /* istanbul ignore else */
275 if (typeof task === 'function') {
276 task = task(_values, $.haki) || {};
277 }
278
279 /* istanbul ignore else */
280 if (task.arguments) {
281 /* istanbul ignore else */
282 if (!$.options.data) {
283 throw new Error('Missing data for arguments');
284 }
285
286 task.arguments.forEach(key => {
287 _values[key] = $.options.data.shift();
288 });
289 }
290
291 // normalize task actions and params
292 let _actions = task.actions || [];
293
294 // main
295 const run = () => Promise.resolve()
296 .then(() => {
297 let _prompts = task.prompts || [];
298
299 /* istanbul ignore else */
300 if (typeof _prompts === 'function') {
301 _prompts = _prompts(_values, $.haki) || [];
302 }
303
304 /* istanbul ignore else */
305 if (typeof _prompts.then === 'function') {
306 return _prompts;
307 }
308
309 // filter out pending input
310 _prompts = _prompts
311 .filter(p => {
312 /* istanbul ignore else */
313 if (typeof _values[p.name] === 'undefined') {
314 /* istanbul ignore else */
315 if (!p.validate
316 && typeof task.validate === 'object'
317 && typeof task.validate[p.name] === 'function') {
318 p.validate = task.validate[p.name];
319 }
320
321 return true;
322 }
323
324 return false;
325 });
326
327 return (_prompts.length && _prompt($, _prompts)) || undefined;
328 })
329 .then(response => {
330 // merge user input
331 Object.assign(_values, response);
332
333 /* istanbul ignore else */
334 if (typeof _actions === 'function') {
335 _actions = _actions.call($.haki, _values, options) || [];
336 }
337
338 /* istanbul ignore else */
339 if (typeof _actions.then === 'function') {
340 return _actions;
341 }
342
343 logger.printf('\r{% wait Loading %s task%s ... %}\r\r', _actions.length, _actions.length === 1 ? '' : 's');
344
345 return _actions.reduce((prev, a) => {
346 /* istanbul ignore else */
347 if (!a) {
348 return prev;
349 }
350
351 let _tpl;
352 let _src;
353 let _dest;
354
355 Object.keys(_.SHORTHANDS).forEach(key => {
356 /* istanbul ignore else */
357 if (a[key]) {
358 a.type = key;
359 a[_.SHORTHANDS[key]] = a[key];
360 }
361 });
362
363 const _srcPath = () => {
364 /* istanbul ignore else */
365 if (!(a.src && typeof a.src === 'string')) {
366 throw new Error(`Invalid src, given '${a.src}'`);
367 }
368
369 /* istanbul ignore else */
370 if (a.src.indexOf('*') !== -1 || (a.src.indexOf('{') && a.src.indexOf('}'))) {
371 return glob.sync(a.src, { cwd: task.basePath || '' }).map(x => path.join(task.basePath || '', x));
372 }
373
374 /* istanbul ignore else */
375 if (!fs.existsSync(path.join(task.basePath || '', a.src))) {
376 throw new Error(`Source '${a.src}' does not exists`);
377 }
378
379 return [path.join(task.basePath || '', a.src)];
380 };
381
382 const _destPath = () => {
383 /* istanbul ignore else */
384 if (!(a.dest && typeof a.dest === 'string')) {
385 throw new Error(`Invalid dest, given '${a.dest}'`);
386 }
387
388 return path.join($.cwd, _render(a.dest, _values, _helpers, _.HELPERS));
389 };
390
391 const _getTemplate = () => {
392 /* istanbul ignore else */
393 if (!(typeof a.template === 'undefined' && typeof a.templateFile === 'undefined')) {
394 const tpl = a.templateFile
395 ? fs.readFileSync(path.join(task.basePath || '', a.templateFile)).toString()
396 : a.template;
397
398 return _render(tpl, _values, _helpers, _.HELPERS);
399 }
400
401 return a.content;
402 };
403
404 const _sourceFiles = () => {
405 if (typeof _src === 'string' && fs.statSync(_src).isDirectory()) {
406 return glob.sync(`${_src}/**/*`, { dot: true, nodir: true });
407 }
408
409 return !Array.isArray(_src)
410 ? [_src]
411 : _src;
412 };
413
414 const _repository = () => {
415 const _url = a.gitUrl ? _render(a.gitUrl, _values, _helpers, _.HELPERS) : '';
416
417 /* istanbul ignore else */
418 if (!(_url && _url.indexOf('/') > 0)) {
419 throw new Error(`Invalid gitUrl, given '${_url}'`);
420 }
421
422 return _url;
423 };
424
425 return prev.then(() => {
426 /* istanbul ignore else */
427 if (typeof a === 'function') {
428 return Promise.resolve(a.call($.haki, _values, options));
429 }
430
431 const skipMe = a.skipIfExists || task.skipIfExists;
432
433 switch (a.type) {
434 case 'copy': {
435 _src = _srcPath();
436
437 let _skipAll = false;
438 let _replaceAll = false;
439
440 return options.copy !== false && _sourceFiles().reduce((_prev, cur, i) => _prev.then(() => {
441 _dest = path.join($.cwd, _render(a.dest || '', _values, _helpers, _.HELPERS), path.relative(path.dirname(_src[i]), cur));
442
443 return logger('write', path.relative($.cwd, _dest), end => _askIf($,
444 _skipAll || _replaceAll ? false : fs.existsSync(_dest) && options.force !== true,
445 `Replace '${path.relative($.cwd, _dest)}'`,
446 _.MULTIPLE_REPLACE_CHOICES, skipMe)
447 .then(result => {
448 /* istanbul ignore else */
449 if (result === 'abort') {
450 throw new Error(`Source '${path.relative($.cwd, _dest)}' won't be copied!`);
451 }
452
453 /* istanbul ignore else */
454 if (result === 'replaceAll') {
455 _replaceAll = true;
456 }
457
458 /* istanbul ignore else */
459 if (result === 'skipAll') {
460 _skipAll = true;
461 }
462
463 /* istanbul ignore else */
464 if (!result || _replaceAll || result === 'replace') {
465 fs.outputFileSync(_dest, _render(fs.readFileSync(cur).toString(), _values, _helpers, _.HELPERS));
466 }
467
468 /* istanbul ignore else */
469 if (end) {
470 if (_skipAll || result === 'skip') {
471 end(path.relative($.cwd, _dest), 'skip', 'end');
472 } else {
473 end();
474 }
475 }
476 }));
477 }), Promise.resolve());
478 }
479
480 case 'modify':
481 _dest = _destPath();
482
483 return logger('change', path.relative($.cwd, _dest), end => {
484 const isAfter = !!a.after;
485 const pattern = a.after || a.before || a.pattern;
486
487 /* istanbul ignore else */
488 if (!(pattern && (typeof pattern === 'string' || pattern instanceof RegExp))) {
489 throw new Error(`Invalid pattern, given '${pattern}'`);
490 }
491
492 _tpl = _getTemplate() || '';
493 _dest = _destPath();
494
495 /* istanbul ignore else */
496 if (!fs.existsSync(_dest)) {
497 /* istanbul ignore else */
498 if (!a.defaultContent) {
499 throw new Error(`Missing ${path.relative($.cwd, _dest)} file`);
500 }
501
502 fs.outputFileSync(_dest, _render(a.defaultContent, _values, _helpers, _.HELPERS));
503 }
504
505 _changes.push({
506 type: a.type,
507 dest: path.relative($.cwd, _dest),
508 });
509
510 const unless = typeof a.unless === 'string'
511 ? new RegExp(_render(a.unless, _values, _helpers, _.HELPERS))
512 : a.unless;
513
514 const content = fs.readFileSync(_dest).toString();
515
516 /* istanbul ignore else */
517 if (a.unless && (unless instanceof RegExp && unless.test(content))) {
518 /* istanbul ignore else */
519 if (end) {
520 end(path.relative($.cwd, _dest), 'skip', 'end');
521 }
522 return;
523 }
524
525 const regexp = !(pattern instanceof RegExp)
526 ? new RegExp(_render(pattern, _values, _helpers, _.HELPERS))
527 : pattern;
528
529 /* istanbul ignore else */
530 if (a.deleteContent && !regexp.test(content)) {
531 /* istanbul ignore else */
532 if (end) {
533 end(path.relative($.cwd, _dest), 'skip', 'end');
534 }
535 return;
536 }
537
538 const output = a.deleteContent
539 ? content.replace(regexp, '')
540 : content.replace(regexp, isAfter ? `$&${_tpl}` : `${_tpl}$&`);
541
542 fs.outputFileSync(_dest, output);
543
544 /* istanbul ignore else */
545 if (end) {
546 end();
547 }
548 });
549
550 case 'extend':
551 _dest = _destPath();
552
553 return logger('extend', path.relative($.cwd, _dest), () => {
554 /* istanbul ignore else */
555 if (typeof a.callback !== 'function') {
556 throw new Error(`Invalid callback, given '${a.callback}'`);
557 }
558
559 const data = fs.existsSync(_dest)
560 ? fs.readJsonSync(_dest)
561 : {};
562
563 _changes.push({
564 type: a.type,
565 dest: path.relative($.cwd, _dest),
566 });
567
568 const _utils = _merge({}, _helpers, _.HELPERS);
569
570 Object.keys(_utils).forEach(k => {
571 if (typeof _values[k] === 'undefined') {
572 _values[k] = v => _utils[k]()(v, y => y.substr(3, y.length - 6));
573 }
574 });
575
576 a.callback(data, _values);
577 fs.outputJsonSync(_dest, data, {
578 spaces: 2,
579 });
580 });
581
582 case 'clone':
583 downloadRepo = downloadRepo || require('download-github-repo');
584
585 _src = _repository();
586 _dest = _destPath();
587
588 return options.clone !== false && logger('clone', _src, end => _askIf($, (fs.existsSync(_dest)
589 ? fs.readdirSync(_dest).length !== 0 : false) && options.force !== true,
590 `Overwrite '${path.relative($.cwd, _dest) || '.'}' with '${_src}'`,
591 _.SINGLE_REPLACE_CHOICES, skipMe)
592 .then(result => new Promise((resolve, reject) => {
593 /* istanbul ignore else */
594 if (result === 'abort') {
595 reject(new Error(`Repository '${_src}' won't be cloned!`));
596 return;
597 }
598
599 /* istanbul ignore else */
600 if (result === 'skip') {
601 resolve();
602 return;
603 }
604
605 downloadRepo(_src, _dest, err => {
606 if (err) {
607 reject(new Error(`Not found https://github.com/${_src}`));
608 } else {
609 _changes.push({
610 type: a.type,
611 repository: _src,
612 });
613 resolve();
614 }
615 });
616 }).then(() => end && end(`${path.relative($.cwd, _dest) || '.'} (${_src})`))));
617
618 case 'add':
619 _tpl = _getTemplate() || '';
620 _dest = _destPath();
621
622 // eslint-disable-next-line
623 return options.add !== false && logger('write', path.relative($.cwd, _dest), end => _askIf($, fs.existsSync(_dest) && options.force !== true,
624 `Replace '${path.relative($.cwd, _dest)}'`,
625 _.SINGLE_REPLACE_CHOICES, skipMe)
626 .then(result => {
627 /* istanbul ignore else */
628 if (result === 'abort') {
629 throw new Error(`Source '${path.relative($.cwd, _dest)}' won't be added!`);
630 }
631
632 /* istanbul ignore else */
633 if (!result || result === 'replace') {
634 _changes.push({
635 type: a.type,
636 dest: path.relative($.cwd, _dest),
637 });
638
639 fs.outputFileSync(_dest, _tpl);
640 }
641
642 /* istanbul ignore else */
643 if (end && result === 'skip') {
644 return end(path.relative($.cwd, _dest), 'skip', 'end');
645 }
646
647 if (end) {
648 end();
649 }
650 }));
651
652 case 'exec':
653 /* istanbul ignore else */
654 if (!(a.command && (typeof a.command === 'string'))) {
655 throw new Error(`Invalid command, given '${a.command}'`);
656 }
657
658 a.command = _render(a.command || '', _values, _helpers, _.HELPERS);
659
660 return options.exec !== false && logger('exec', a.command, end => _exec(a.command)
661 .then(result => {
662 _changes.push({
663 type: a.type,
664 stdOut: result,
665 });
666
667 if (!end) {
668 logger.printf(`\r\r${result.replace(/\n+$/, '\n')}`);
669 } else if (!(options.quiet || a.quiet)) {
670 end(() => logger.printf(`\r\r${result.replace(/\n+$/, '\n')}`));
671 } else {
672 end();
673 }
674 }));
675
676 case 'clean':
677 _dest = _destPath();
678
679 return logger('clean', path.relative($.cwd, _dest), end => _askIf($, options.force !== true,
680 `Delete '${path.relative($.cwd, _dest) || '.'}'`,
681 _.SINGLE_DELETE_CHOICES).then(result => {
682 /* istanbul ignore else */
683 if (result === 'abort') {
684 throw new Error(`Output '${path.relative($.cwd, _dest) || '.'}' won't be destroyed!`);
685 }
686
687 /* istanbul ignore else */
688 if (result === 'skip') {
689 /* istanbul ignore else */
690 if (end) {
691 end(path.relative($.cwd, _dest), 'skip', 'end');
692 }
693 return;
694 }
695
696 rimraf.sync(_dest);
697
698 if (end) {
699 end();
700 }
701 }));
702
703 // FIXME: validate dest-input for render/install
704
705 case 'render':
706 return (Array.isArray(a.dest) ? a.dest : [a.dest]).forEach(dest => {
707 _dest = path.join($.cwd, _render(dest, _values, _helpers, _.HELPERS));
708 _tpl = _render(fs.readFileSync(_dest).toString(), _values, _helpers, _.HELPERS);
709
710 fs.outputFileSync(_dest, _tpl);
711 });
712
713 case 'install':
714 return _install($, a, logger, a.quiet || options.quiet,
715 path.join($.cwd, _render(a.dest || '', _values, _helpers, _.HELPERS)))
716 .then(result => {
717 _changes.push(result);
718 });
719
720 default:
721 throw new Error(`Unsupported '${a.type || JSON.stringify(a)}' action`);
722 }
723 })
724 .catch(err => {
725 _failures.push(err);
726
727 /* istanbul ignore else */
728 if (a.abortOnFail || task.abortOnFail) {
729 throw err;
730 }
731
732 logger.printf('\r%s\r\n', (options.debug && err.stack) || err.message);
733 });
734 }, Promise.resolve());
735 })
736 .then(() => ({ values: _values, changes: _changes, failures: _failures }))
737 .catch(error => {
738 /* istanbul ignore else */
739 if (task.abortOnFail) {
740 throw error;
741 }
742
743 return {
744 error,
745 values: _values,
746 changes: _changes,
747 failures: _failures,
748 };
749 });
750
751 /* istanbul ignore else */
752 if (task.quiet || options.quiet) {
753 // logs
754 const _logger = logger;
755
756 // bypass everything but any given callback
757 logger = function $logger() {
758 return Promise.resolve().then(() => Promise.all(Array.prototype.slice.call(arguments)
759 .filter(cb => typeof cb === 'function')
760 .map(cb => cb())));
761 };
762
763 // do nothing
764 logger.write = () => {};
765 logger.printf = () => {};
766
767 // call original logger
768 return _logger('Running tasks...', end => run().then(() => end && end('Tasks completed')));
769 }
770
771 return run();
772}
773
774module.exports = function Haki(cwd, options) {
775 options = options || {};
776
777 /* istanbul ignore else */
778 if (typeof cwd === 'object') {
779 options = _merge(options, cwd);
780 cwd = options.cwd;
781 }
782
783 const _helpers = {};
784 const _tasks = {};
785
786 /* eslint-disable no-nested-ternary */
787 const _logger = (lp.setLogger(options.stdout, options.stderr)
788 .setLevel(options.verbose ? 3 : options.debug ? 2 : options.info ? 1 : options.log)
789 .getLogger(options.depth));
790
791 // fallback to write() if missing
792 _logger.printf = _logger.printf || _logger.write;
793
794 // normalize defaults
795 cwd = cwd || options.cwd || process.cwd();
796
797 delete options.cwd;
798
799 let _enquirer;
800
801 return {
802 load(file) {
803 /* istanbul ignore else */
804 if (!(file && typeof file === 'string')) {
805 throw new Error(`File must be a string, given '${file}'`);
806 }
807
808 try {
809 file = require.resolve(file);
810 } catch (e) {
811 file = path.resolve(cwd, file);
812 }
813
814 require(file)(this);
815
816 return this;
817 },
818
819 prompt(opts) {
820 /* istanbul ignore else */
821 if (!(opts && typeof opts === 'object')) {
822 throw new Error(`Prompt options are invalid, given '${opts}'`);
823 }
824
825 return _prompt({ haki: this }, !Array.isArray(opts) ? [opts] : opts);
826 },
827
828 getEnquirer() {
829 if (!_enquirer) {
830 const Enquirer = require('enquirer');
831
832 _enquirer = new Enquirer({
833 show: options.log !== false,
834 stdout: options.stdout,
835 stderr: options.stderr,
836 });
837 }
838
839 return _enquirer;
840 },
841
842 getLogger() {
843 return _logger;
844 },
845
846 getPath(dest) {
847 /* istanbul ignore else */
848 if (dest && typeof dest !== 'string') {
849 throw new Error(`Path must be a string, given '${dest}'`);
850 }
851
852 return path.join(cwd, dest || '');
853 },
854
855 addHelper(name, fn) {
856 /* istanbul ignore else */
857 if (!(name && typeof name === 'string')) {
858 throw new Error(`Helper name must be a string, given '${name}'`);
859 }
860
861 /* istanbul ignore else */
862 if (typeof fn !== 'function') {
863 throw new Error(`Helper for '${name}' must be a function, given '${fn}'`);
864 }
865
866 // pass raw-value and rendered-value
867 _helpers[name] = () => (text, render) => fn(text, _expr => {
868 /* istanbul ignore else */
869 if (!_expr) {
870 throw new Error(`Missing expression for '${name}' helper`);
871 }
872
873 return render(!(_expr.charAt() === '{' && _expr.substr(_expr.length - 1, 1) === '}')
874 ? `{{{${_expr}}}}`
875 : _expr);
876 });
877
878 return this;
879 },
880
881 getHelperList() {
882 return Object.keys(_helpers).concat(Object.keys(_.HELPERS));
883 },
884
885 renderString(value, data) {
886 /* istanbul ignore else */
887 if (!(value && typeof value === 'string')) {
888 throw new Error(`Template must be a string, given '${value}'`);
889 }
890
891 return _render(value, data || {}, _helpers, _.HELPERS);
892 },
893
894 setGenerator(name, opts) {
895 /* istanbul ignore else */
896 if (!(name && typeof name === 'string')) {
897 throw new Error(`Generator name must be a string, given '${name}'`);
898 }
899
900 _tasks[name] = opts || {};
901 _tasks[name].run = defaults => this.runGenerator(_tasks[name], defaults);
902
903 return this;
904 },
905
906 getGenerator(name) {
907 /* istanbul ignore else */
908 if (!_tasks[name]) {
909 throw new Error(`The '${name}' generator does not exists`);
910 }
911
912 return _tasks[name];
913 },
914
915 runGenerator(name, defaults) {
916 /* istanbul ignore else */
917 if (typeof name === 'object') {
918 return Promise.resolve()
919 .then(() => _runTask({
920 cwd,
921 options,
922 defaults,
923 haki: this,
924 }, name, _logger, _helpers));
925 }
926
927 return this.getGenerator(name).run(defaults);
928 },
929
930 hasGenerator(name) {
931 return typeof _tasks[name] !== 'undefined';
932 },
933
934 getGeneratorList(hints) {
935 return Object.keys(_tasks).map(t => ({
936 name: t,
937 message: (_tasks[t].description
938 && (hints && `${t} - ${_tasks[t].description}`))
939 || _tasks[t].description
940 || t,
941 }));
942 },
943
944 chooseGeneratorList(defaults) {
945 /* istanbul ignore else */
946 if (!Object.keys(_tasks).length) {
947 throw new Error('There are no registered generators');
948 }
949
950 return _prompt({ haki: this }, [{
951 name: 'task',
952 type: 'autocomplete',
953 message: 'Choose a generator:',
954 options: this.getGeneratorList(true),
955 }]).then(({ task }) => this.runGenerator(task, defaults));
956 },
957 };
958};