UNPKG

27.8 kBJavaScriptView Raw
1/**
2 * @fileoverview Main CLI object.
3 * @author Nicholas C. Zakas
4 */
5
6"use strict";
7
8/*
9 * The CLI object should *not* call process.exit() directly. It should only return
10 * exit codes. This allows other programs to use the CLI object and still control
11 * when the program exits.
12 */
13
14//------------------------------------------------------------------------------
15// Requirements
16//------------------------------------------------------------------------------
17
18const fs = require("fs"),
19 path = require("path"),
20 defaultOptions = require("../conf/default-cli-options"),
21 Linter = require("./linter"),
22 IgnoredPaths = require("./ignored-paths"),
23 Config = require("./config"),
24 fileEntryCache = require("file-entry-cache"),
25 globUtil = require("./util/glob-util"),
26 SourceCodeFixer = require("./util/source-code-fixer"),
27 validator = require("./config/config-validator"),
28 stringify = require("json-stable-stringify"),
29 hash = require("./util/hash"),
30
31 pkg = require("../package.json");
32
33const debug = require("debug")("eslint:cli-engine");
34
35//------------------------------------------------------------------------------
36// Typedefs
37//------------------------------------------------------------------------------
38
39/**
40 * The options to configure a CLI engine with.
41 * @typedef {Object} CLIEngineOptions
42 * @property {boolean} allowInlineConfig Enable or disable inline configuration comments.
43 * @property {boolean|Object} baseConfig Base config object. True enables recommend rules and environments.
44 * @property {boolean} cache Enable result caching.
45 * @property {string} cacheLocation The cache file to use instead of .eslintcache.
46 * @property {string} configFile The configuration file to use.
47 * @property {string} cwd The value to use for the current working directory.
48 * @property {string[]} envs An array of environments to load.
49 * @property {string[]} extensions An array of file extensions to check.
50 * @property {boolean} fix Execute in autofix mode.
51 * @property {string[]} globals An array of global variables to declare.
52 * @property {boolean} ignore False disables use of .eslintignore.
53 * @property {string} ignorePath The ignore file to use instead of .eslintignore.
54 * @property {string} ignorePattern A glob pattern of files to ignore.
55 * @property {boolean} useEslintrc False disables looking for .eslintrc
56 * @property {string} parser The name of the parser to use.
57 * @property {Object} parserOptions An object of parserOption settings to use.
58 * @property {string[]} plugins An array of plugins to load.
59 * @property {Object<string,*>} rules An object of rules to use.
60 * @property {string[]} rulePaths An array of directories to load custom rules from.
61 */
62
63/**
64 * A linting warning or error.
65 * @typedef {Object} LintMessage
66 * @property {string} message The message to display to the user.
67 */
68
69/**
70 * A linting result.
71 * @typedef {Object} LintResult
72 * @property {string} filePath The path to the file that was linted.
73 * @property {LintMessage[]} messages All of the messages for the result.
74 * @property {number} errorCount Number of errors for the result.
75 * @property {number} warningCount Number of warnings for the result.
76 * @property {number} fixableErrorCount Number of fixable errors for the result.
77 * @property {number} fixableWarningCount Number of fixable warnings for the result.
78 * @property {string=} [source] The source code of the file that was linted.
79 * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible.
80 */
81
82//------------------------------------------------------------------------------
83// Helpers
84//------------------------------------------------------------------------------
85
86/**
87 * It will calculate the error and warning count for collection of messages per file
88 * @param {Object[]} messages - Collection of messages
89 * @returns {Object} Contains the stats
90 * @private
91 */
92function calculateStatsPerFile(messages) {
93 return messages.reduce((stat, message) => {
94 if (message.fatal || message.severity === 2) {
95 stat.errorCount++;
96 if (message.fix) {
97 stat.fixableErrorCount++;
98 }
99 } else {
100 stat.warningCount++;
101 if (message.fix) {
102 stat.fixableWarningCount++;
103 }
104 }
105 return stat;
106 }, {
107 errorCount: 0,
108 warningCount: 0,
109 fixableErrorCount: 0,
110 fixableWarningCount: 0
111 });
112}
113
114/**
115 * It will calculate the error and warning count for collection of results from all files
116 * @param {Object[]} results - Collection of messages from all the files
117 * @returns {Object} Contains the stats
118 * @private
119 */
120function calculateStatsPerRun(results) {
121 return results.reduce((stat, result) => {
122 stat.errorCount += result.errorCount;
123 stat.warningCount += result.warningCount;
124 stat.fixableErrorCount += result.fixableErrorCount;
125 stat.fixableWarningCount += result.fixableWarningCount;
126 return stat;
127 }, {
128 errorCount: 0,
129 warningCount: 0,
130 fixableErrorCount: 0,
131 fixableWarningCount: 0
132 });
133}
134
135/**
136 * Performs multiple autofix passes over the text until as many fixes as possible
137 * have been applied.
138 * @param {string} text The source text to apply fixes to.
139 * @param {Object} config The ESLint config object to use.
140 * @param {Object} options The ESLint options object to use.
141 * @param {string} options.filename The filename from which the text was read.
142 * @param {boolean} options.allowInlineConfig Flag indicating if inline comments
143 * should be allowed.
144 * @param {Linter} linter Linter context
145 * @returns {Object} The result of the fix operation as returned from the
146 * SourceCodeFixer.
147 * @private
148 */
149function multipassFix(text, config, options, linter) {
150 const MAX_PASSES = 10;
151 let messages = [],
152 fixedResult,
153 fixed = false,
154 passNumber = 0;
155
156 /**
157 * This loop continues until one of the following is true:
158 *
159 * 1. No more fixes have been applied.
160 * 2. Ten passes have been made.
161 *
162 * That means anytime a fix is successfully applied, there will be another pass.
163 * Essentially, guaranteeing a minimum of two passes.
164 */
165 do {
166 passNumber++;
167
168 debug(`Linting code for ${options.filename} (pass ${passNumber})`);
169 messages = linter.verify(text, config, options);
170
171 debug(`Generating fixed text for ${options.filename} (pass ${passNumber})`);
172 fixedResult = SourceCodeFixer.applyFixes(linter.getSourceCode(), messages);
173
174 // stop if there are any syntax errors.
175 // 'fixedResult.output' is a empty string.
176 if (messages.length === 1 && messages[0].fatal) {
177 break;
178 }
179
180 // keep track if any fixes were ever applied - important for return value
181 fixed = fixed || fixedResult.fixed;
182
183 // update to use the fixed output instead of the original text
184 text = fixedResult.output;
185
186 } while (
187 fixedResult.fixed &&
188 passNumber < MAX_PASSES
189 );
190
191
192 /*
193 * If the last result had fixes, we need to lint again to be sure we have
194 * the most up-to-date information.
195 */
196 if (fixedResult.fixed) {
197 fixedResult.messages = linter.verify(text, config, options);
198 }
199
200
201 // ensure the last result properly reflects if fixes were done
202 fixedResult.fixed = fixed;
203 fixedResult.output = text;
204
205 return fixedResult;
206
207}
208
209/**
210 * Processes an source code using ESLint.
211 * @param {string} text The source code to check.
212 * @param {Object} configHelper The configuration options for ESLint.
213 * @param {string} filename An optional string representing the texts filename.
214 * @param {boolean} fix Indicates if fixes should be processed.
215 * @param {boolean} allowInlineConfig Allow/ignore comments that change config.
216 * @param {Linter} linter Linter context
217 * @returns {LintResult} The results for linting on this text.
218 * @private
219 */
220function processText(text, configHelper, filename, fix, allowInlineConfig, linter) {
221
222 // clear all existing settings for a new file
223 linter.reset();
224
225 let filePath,
226 messages,
227 fileExtension,
228 processor,
229 fixedResult;
230
231 if (filename) {
232 filePath = path.resolve(filename);
233 fileExtension = path.extname(filename);
234 }
235
236 filename = filename || "<text>";
237 debug(`Linting ${filename}`);
238 const config = configHelper.getConfig(filePath);
239
240 if (config.plugins) {
241 configHelper.plugins.loadAll(config.plugins);
242 }
243
244 const loadedPlugins = configHelper.plugins.getAll();
245
246 for (const plugin in loadedPlugins) {
247 if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) {
248 processor = loadedPlugins[plugin].processors[fileExtension];
249 break;
250 }
251 }
252
253 if (processor) {
254 debug("Using processor");
255 const parsedBlocks = processor.preprocess(text, filename);
256 const unprocessedMessages = [];
257
258 parsedBlocks.forEach(block => {
259 unprocessedMessages.push(linter.verify(block, config, {
260 filename,
261 allowInlineConfig
262 }));
263 });
264
265 // TODO(nzakas): Figure out how fixes might work for processors
266
267 messages = processor.postprocess(unprocessedMessages, filename);
268
269 } else {
270
271 if (fix) {
272 fixedResult = multipassFix(text, config, {
273 filename,
274 allowInlineConfig
275 }, linter);
276 messages = fixedResult.messages;
277 } else {
278 messages = linter.verify(text, config, {
279 filename,
280 allowInlineConfig
281 });
282 }
283 }
284
285 const stats = calculateStatsPerFile(messages);
286
287 const result = {
288 filePath: filename,
289 messages,
290 errorCount: stats.errorCount,
291 warningCount: stats.warningCount,
292 fixableErrorCount: stats.fixableErrorCount,
293 fixableWarningCount: stats.fixableWarningCount
294 };
295
296 if (fixedResult && fixedResult.fixed) {
297 result.output = fixedResult.output;
298 }
299
300 if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") {
301 result.source = text;
302 }
303
304 return result;
305}
306
307/**
308 * Processes an individual file using ESLint. Files used here are known to
309 * exist, so no need to check that here.
310 * @param {string} filename The filename of the file being checked.
311 * @param {Object} configHelper The configuration options for ESLint.
312 * @param {Object} options The CLIEngine options object.
313 * @param {Linter} linter Linter context
314 * @returns {LintResult} The results for linting on this file.
315 * @private
316 */
317function processFile(filename, configHelper, options, linter) {
318
319 const text = fs.readFileSync(path.resolve(filename), "utf8"),
320 result = processText(text, configHelper, filename, options.fix, options.allowInlineConfig, linter);
321
322 return result;
323
324}
325
326/**
327 * Returns result with warning by ignore settings
328 * @param {string} filePath - File path of checked code
329 * @param {string} baseDir - Absolute path of base directory
330 * @returns {LintResult} Result with single warning
331 * @private
332 */
333function createIgnoreResult(filePath, baseDir) {
334 let message;
335 const isHidden = /^\./.test(path.basename(filePath));
336 const isInNodeModules = baseDir && /^node_modules/.test(path.relative(baseDir, filePath));
337 const isInBowerComponents = baseDir && /^bower_components/.test(path.relative(baseDir, filePath));
338
339 if (isHidden) {
340 message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!<relative/path/to/filename>'\") to override.";
341 } else if (isInNodeModules) {
342 message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.";
343 } else if (isInBowerComponents) {
344 message = "File ignored by default. Use \"--ignore-pattern '!bower_components/*'\" to override.";
345 } else {
346 message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override.";
347 }
348
349 return {
350 filePath: path.resolve(filePath),
351 messages: [
352 {
353 fatal: false,
354 severity: 1,
355 message
356 }
357 ],
358 errorCount: 0,
359 warningCount: 1,
360 fixableErrorCount: 0,
361 fixableWarningCount: 0
362 };
363}
364
365
366/**
367 * Checks if the given message is an error message.
368 * @param {Object} message The message to check.
369 * @returns {boolean} Whether or not the message is an error message.
370 * @private
371 */
372function isErrorMessage(message) {
373 return message.severity === 2;
374}
375
376
377/**
378 * return the cacheFile to be used by eslint, based on whether the provided parameter is
379 * a directory or looks like a directory (ends in `path.sep`), in which case the file
380 * name will be the `cacheFile/.cache_hashOfCWD`
381 *
382 * if cacheFile points to a file or looks like a file then in will just use that file
383 *
384 * @param {string} cacheFile The name of file to be used to store the cache
385 * @param {string} cwd Current working directory
386 * @returns {string} the resolved path to the cache file
387 */
388function getCacheFile(cacheFile, cwd) {
389
390 /*
391 * make sure the path separators are normalized for the environment/os
392 * keeping the trailing path separator if present
393 */
394 cacheFile = path.normalize(cacheFile);
395
396 const resolvedCacheFile = path.resolve(cwd, cacheFile);
397 const looksLikeADirectory = cacheFile[cacheFile.length - 1 ] === path.sep;
398
399 /**
400 * return the name for the cache file in case the provided parameter is a directory
401 * @returns {string} the resolved path to the cacheFile
402 */
403 function getCacheFileForDirectory() {
404 return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`);
405 }
406
407 let fileStats;
408
409 try {
410 fileStats = fs.lstatSync(resolvedCacheFile);
411 } catch (ex) {
412 fileStats = null;
413 }
414
415
416 /*
417 * in case the file exists we need to verify if the provided path
418 * is a directory or a file. If it is a directory we want to create a file
419 * inside that directory
420 */
421 if (fileStats) {
422
423 /*
424 * is a directory or is a file, but the original file the user provided
425 * looks like a directory but `path.resolve` removed the `last path.sep`
426 * so we need to still treat this like a directory
427 */
428 if (fileStats.isDirectory() || looksLikeADirectory) {
429 return getCacheFileForDirectory();
430 }
431
432 // is file so just use that file
433 return resolvedCacheFile;
434 }
435
436 /*
437 * here we known the file or directory doesn't exist,
438 * so we will try to infer if its a directory if it looks like a directory
439 * for the current operating system.
440 */
441
442 // if the last character passed is a path separator we assume is a directory
443 if (looksLikeADirectory) {
444 return getCacheFileForDirectory();
445 }
446
447 return resolvedCacheFile;
448}
449
450//------------------------------------------------------------------------------
451// Public Interface
452//------------------------------------------------------------------------------
453
454class CLIEngine {
455
456 /**
457 * Creates a new instance of the core CLI engine.
458 * @param {CLIEngineOptions} options The options for this instance.
459 * @constructor
460 */
461 constructor(options) {
462
463 options = Object.assign(
464 Object.create(null),
465 defaultOptions,
466 { cwd: process.cwd() },
467 options
468 );
469
470 /**
471 * Stored options for this instance
472 * @type {Object}
473 */
474 this.options = options;
475 this.linter = new Linter();
476
477 const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
478
479 /**
480 * Cache used to avoid operating on files that haven't changed since the
481 * last successful execution (e.g., file passed linting with no errors and
482 * no warnings).
483 * @type {Object}
484 */
485 this._fileCache = fileEntryCache.create(cacheFile);
486
487 // load in additional rules
488 if (this.options.rulePaths) {
489 const cwd = this.options.cwd;
490
491 this.options.rulePaths.forEach(rulesdir => {
492 debug(`Loading rules from ${rulesdir}`);
493 this.linter.rules.load(rulesdir, cwd);
494 });
495 }
496
497 Object.keys(this.options.rules || {}).forEach(name => {
498 validator.validateRuleOptions(name, this.options.rules[name], "CLI", this.linter.rules);
499 });
500
501 this.config = new Config(this.options, this.linter);
502 }
503
504 /**
505 * Returns results that only contains errors.
506 * @param {LintResult[]} results The results to filter.
507 * @returns {LintResult[]} The filtered results.
508 */
509 static getErrorResults(results) {
510 const filtered = [];
511
512 results.forEach(result => {
513 const filteredMessages = result.messages.filter(isErrorMessage);
514
515 if (filteredMessages.length > 0) {
516 filtered.push(
517 Object.assign(result, {
518 messages: filteredMessages,
519 errorCount: filteredMessages.length,
520 warningCount: 0,
521 fixableErrorCount: result.fixableErrorCount,
522 fixableWarningCount: 0
523 })
524 );
525 }
526 });
527
528 return filtered;
529 }
530
531 /**
532 * Outputs fixes from the given results to files.
533 * @param {Object} report The report object created by CLIEngine.
534 * @returns {void}
535 */
536 static outputFixes(report) {
537 report.results.filter(result => result.hasOwnProperty("output")).forEach(result => {
538 fs.writeFileSync(result.filePath, result.output);
539 });
540 }
541
542
543 /**
544 * Add a plugin by passing its configuration
545 * @param {string} name Name of the plugin.
546 * @param {Object} pluginobject Plugin configuration object.
547 * @returns {void}
548 */
549 addPlugin(name, pluginobject) {
550 this.config.plugins.define(name, pluginobject);
551 }
552
553 /**
554 * Resolves the patterns passed into executeOnFiles() into glob-based patterns
555 * for easier handling.
556 * @param {string[]} patterns The file patterns passed on the command line.
557 * @returns {string[]} The equivalent glob patterns.
558 */
559 resolveFileGlobPatterns(patterns) {
560 return globUtil.resolveFileGlobPatterns(patterns, this.options);
561 }
562
563 /**
564 * Executes the current configuration on an array of file and directory names.
565 * @param {string[]} patterns An array of file and directory names.
566 * @returns {Object} The results for all files that were linted.
567 */
568 executeOnFiles(patterns) {
569 const results = [],
570 options = this.options,
571 fileCache = this._fileCache,
572 configHelper = this.config;
573 let prevConfig; // the previous configuration used
574
575 /**
576 * Calculates the hash of the config file used to validate a given file
577 * @param {string} filename The path of the file to retrieve a config object for to calculate the hash
578 * @returns {string} the hash of the config
579 */
580 function hashOfConfigFor(filename) {
581 const config = configHelper.getConfig(filename);
582
583 if (!prevConfig) {
584 prevConfig = {};
585 }
586
587 // reuse the previously hashed config if the config hasn't changed
588 if (prevConfig.config !== config) {
589
590 /*
591 * config changed so we need to calculate the hash of the config
592 * and the hash of the plugins being used
593 */
594 prevConfig.config = config;
595
596 const eslintVersion = pkg.version;
597
598 prevConfig.hash = hash(`${eslintVersion}_${stringify(config)}`);
599 }
600
601 return prevConfig.hash;
602 }
603
604 /**
605 * Executes the linter on a file defined by the `filename`. Skips
606 * unsupported file extensions and any files that are already linted.
607 * @param {string} filename The resolved filename of the file to be linted
608 * @param {boolean} warnIgnored always warn when a file is ignored
609 * @param {Linter} linter Linter context
610 * @returns {void}
611 */
612 function executeOnFile(filename, warnIgnored, linter) {
613 let hashOfConfig,
614 descriptor;
615
616 if (warnIgnored) {
617 results.push(createIgnoreResult(filename, options.cwd));
618 return;
619 }
620
621 if (options.cache) {
622
623 /*
624 * get the descriptor for this file
625 * with the metadata and the flag that determines if
626 * the file has changed
627 */
628 descriptor = fileCache.getFileDescriptor(filename);
629 const meta = descriptor.meta || {};
630
631 hashOfConfig = hashOfConfigFor(filename);
632
633 const changed = descriptor.changed || meta.hashOfConfig !== hashOfConfig;
634
635 if (!changed) {
636 debug(`Skipping file since hasn't changed: ${filename}`);
637
638 /*
639 * Add the the cached results (always will be 0 error and
640 * 0 warnings). We should not cache results for files that
641 * failed, in order to guarantee that next execution will
642 * process those files as well.
643 */
644 results.push(descriptor.meta.results);
645
646 // move to the next file
647 return;
648 }
649 } else {
650 fileCache.destroy();
651 }
652
653 debug(`Processing ${filename}`);
654
655 const res = processFile(filename, configHelper, options, linter);
656
657 if (options.cache) {
658
659 /*
660 * if a file contains errors or warnings we don't want to
661 * store the file in the cache so we can guarantee that
662 * next execution will also operate on this file
663 */
664 if (res.errorCount > 0 || res.warningCount > 0) {
665 debug(`File has problems, skipping it: ${filename}`);
666
667 // remove the entry from the cache
668 fileCache.removeEntry(filename);
669 } else {
670
671 /*
672 * since the file passed we store the result here
673 * TODO: check this as we might not need to store the
674 * successful runs as it will always should be 0 errors and
675 * 0 warnings.
676 */
677 descriptor.meta.hashOfConfig = hashOfConfig;
678 descriptor.meta.results = res;
679 }
680 }
681
682 results.push(res);
683 }
684
685 const startTime = Date.now();
686
687
688
689 patterns = this.resolveFileGlobPatterns(patterns);
690 const fileList = globUtil.listFilesToProcess(patterns, options);
691
692 fileList.forEach(fileInfo => {
693 executeOnFile(fileInfo.filename, fileInfo.ignored, this.linter);
694 });
695
696 const stats = calculateStatsPerRun(results);
697
698 if (options.cache) {
699
700 // persist the cache to disk
701 fileCache.reconcile();
702 }
703
704 debug(`Linting complete in: ${Date.now() - startTime}ms`);
705
706 return {
707 results,
708 errorCount: stats.errorCount,
709 warningCount: stats.warningCount,
710 fixableErrorCount: stats.fixableErrorCount,
711 fixableWarningCount: stats.fixableWarningCount
712 };
713 }
714
715 /**
716 * Executes the current configuration on text.
717 * @param {string} text A string of JavaScript code to lint.
718 * @param {string} filename An optional string representing the texts filename.
719 * @param {boolean} warnIgnored Always warn when a file is ignored
720 * @returns {Object} The results for the linting.
721 */
722 executeOnText(text, filename, warnIgnored) {
723
724 const results = [],
725 options = this.options,
726 configHelper = this.config,
727 ignoredPaths = new IgnoredPaths(options);
728
729 // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves)
730 if (filename && !path.isAbsolute(filename)) {
731 filename = path.resolve(options.cwd, filename);
732 }
733
734 if (filename && ignoredPaths.contains(filename)) {
735 if (warnIgnored) {
736 results.push(createIgnoreResult(filename, options.cwd));
737 }
738 } else {
739 results.push(processText(text, configHelper, filename, options.fix, options.allowInlineConfig, this.linter));
740 }
741
742 const stats = calculateStatsPerRun(results);
743
744 return {
745 results,
746 errorCount: stats.errorCount,
747 warningCount: stats.warningCount,
748 fixableErrorCount: stats.fixableErrorCount,
749 fixableWarningCount: stats.fixableWarningCount
750 };
751 }
752
753 /**
754 * Returns a configuration object for the given file based on the CLI options.
755 * This is the same logic used by the ESLint CLI executable to determine
756 * configuration for each file it processes.
757 * @param {string} filePath The path of the file to retrieve a config object for.
758 * @returns {Object} A configuration object for the file.
759 */
760 getConfigForFile(filePath) {
761 const configHelper = this.config;
762
763 return configHelper.getConfig(filePath);
764 }
765
766 /**
767 * Checks if a given path is ignored by ESLint.
768 * @param {string} filePath The path of the file to check.
769 * @returns {boolean} Whether or not the given path is ignored.
770 */
771 isPathIgnored(filePath) {
772 const resolvedPath = path.resolve(this.options.cwd, filePath);
773 const ignoredPaths = new IgnoredPaths(this.options);
774
775 return ignoredPaths.contains(resolvedPath);
776 }
777
778 /**
779 * Returns the formatter representing the given format or null if no formatter
780 * with the given name can be found.
781 * @param {string} [format] The name of the format to load or the path to a
782 * custom formatter.
783 * @returns {Function} The formatter function or null if not found.
784 */
785 getFormatter(format) {
786
787 let formatterPath;
788
789 // default is stylish
790 format = format || "stylish";
791
792 // only strings are valid formatters
793 if (typeof format === "string") {
794
795 // replace \ with / for Windows compatibility
796 format = format.replace(/\\/g, "/");
797
798 // if there's a slash, then it's a file
799 if (format.indexOf("/") > -1) {
800 const cwd = this.options ? this.options.cwd : process.cwd();
801
802 formatterPath = path.resolve(cwd, format);
803 } else {
804 formatterPath = `./formatters/${format}`;
805 }
806
807 try {
808 return require(formatterPath);
809 } catch (ex) {
810 ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`;
811 throw ex;
812 }
813
814 } else {
815 return null;
816 }
817 }
818}
819
820CLIEngine.version = pkg.version;
821CLIEngine.getFormatter = CLIEngine.prototype.getFormatter;
822
823module.exports = CLIEngine;