config.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /*
  2. Copyright 2012-2015, Yahoo Inc.
  3. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
  4. */
  5. const path = require('path');
  6. const fs = require('fs');
  7. const existsSync = fs.existsSync;
  8. const CAMEL_PATTERN = /([a-z])([A-Z])/g;
  9. const YML_PATTERN = /\.ya?ml$/;
  10. const yaml = require('js-yaml');
  11. const libReport = require('istanbul-lib-report');
  12. const inputError = require('./input-error');
  13. function defaultConfig() {
  14. const ret = {
  15. verbose: false,
  16. instrumentation: {
  17. root: '.',
  18. extensions: ['.js'],
  19. 'default-excludes': true,
  20. excludes: [],
  21. variable: '__coverage__',
  22. compact: true,
  23. 'preserve-comments': false,
  24. 'complete-copy': false,
  25. 'save-baseline': false,
  26. 'baseline-file': './coverage/coverage-baseline.raw.json',
  27. 'include-all-sources': false,
  28. 'include-pid': false,
  29. 'es-modules': false,
  30. 'auto-wrap': false,
  31. 'ignore-class-methods': []
  32. },
  33. reporting: {
  34. print: 'summary',
  35. reports: ['lcov'],
  36. dir: './coverage',
  37. summarizer: 'pkg',
  38. 'report-config': {}
  39. },
  40. hooks: {
  41. 'hook-run-in-context': false,
  42. 'hook-run-in-this-context': false,
  43. 'post-require-hook': null,
  44. 'handle-sigint': false
  45. },
  46. check: {
  47. global: {
  48. statements: 0,
  49. lines: 0,
  50. branches: 0,
  51. functions: 0,
  52. excludes: [] // Currently list of files (root + path). For future, extend to patterns.
  53. },
  54. each: {
  55. statements: 0,
  56. lines: 0,
  57. branches: 0,
  58. functions: 0,
  59. excludes: []
  60. }
  61. }
  62. };
  63. ret.reporting.watermarks = libReport.getDefaultWatermarks();
  64. ret.reporting['report-config'] = {};
  65. return ret;
  66. }
  67. function dasherize(word) {
  68. return word.replace(
  69. CAMEL_PATTERN,
  70. (match, lch, uch) => lch + '-' + uch.toLowerCase()
  71. );
  72. }
  73. function isScalar(v) {
  74. if (v === null) {
  75. return true;
  76. }
  77. return v !== undefined && !Array.isArray(v) && typeof v !== 'object';
  78. }
  79. function isObject(v) {
  80. return typeof v === 'object' && v !== null && !Array.isArray(v);
  81. }
  82. function mergeObjects(explicit, template, bothWays) {
  83. const ret = {};
  84. const keys = Object.keys(template);
  85. if (bothWays) {
  86. keys.push(...Object.keys(explicit));
  87. }
  88. keys.forEach(k => {
  89. const v1 = template[k];
  90. let v2 = explicit[k];
  91. if (Array.isArray(v1)) {
  92. ret[k] = Array.isArray(v2) && v2.length > 0 ? v2 : v1;
  93. } else if (isObject(v1)) {
  94. v2 = isObject(v2) ? v2 : {};
  95. ret[k] = mergeObjects(v2, v1, bothWays);
  96. } else if (!v1 && v2) {
  97. ret[k] = v2;
  98. } else {
  99. ret[k] = isScalar(v2) ? v2 : v1;
  100. }
  101. });
  102. return ret;
  103. }
  104. function mergeDefaults(explicit, implicit) {
  105. explicit = explicit || {};
  106. const initialMerge = mergeObjects(explicit || {}, implicit);
  107. const explicitReportConfig =
  108. (explicit.reporting || {})['report-config'] || {};
  109. const implicitReportConfig = initialMerge.reporting['report-config'] || {};
  110. initialMerge.reporting['report-config'] = mergeObjects(
  111. explicitReportConfig,
  112. implicitReportConfig,
  113. true
  114. );
  115. return initialMerge;
  116. }
  117. function addMethods(cons, ...args) {
  118. args.forEach(arg => {
  119. const property = dasherize(arg);
  120. cons.prototype[arg] = function() {
  121. return this.config[property];
  122. };
  123. });
  124. }
  125. /**
  126. * Object that returns instrumentation options
  127. * @class InstrumentOptions
  128. * @module config
  129. * @constructor
  130. * @param config the instrumentation part of the config object
  131. */
  132. function InstrumentOptions(config) {
  133. this.config = config;
  134. }
  135. /**
  136. * returns if default excludes should be turned on. Used by the `cover` command.
  137. * @method defaultExcludes
  138. * @return {Boolean} true if default excludes should be turned on
  139. */
  140. /**
  141. * returns if non-JS files should be copied during instrumentation. Used by the
  142. * `instrument` command.
  143. * @method completeCopy
  144. * @return {Boolean} true if non-JS files should be copied
  145. */
  146. /**
  147. * the coverage variable name to use. Used by the `instrument` command.
  148. * @method variable
  149. * @return {String} the coverage variable name to use
  150. */
  151. /**
  152. * returns if the output should be compact JS. Used by the `instrument` command.
  153. * @method compact
  154. * @return {Boolean} true if the output should be compact
  155. */
  156. /**
  157. * returns if comments should be preserved in the generated JS. Used by the
  158. * `cover` and `instrument` commands.
  159. * @method preserveComments
  160. * @return {Boolean} true if comments should be preserved in the generated JS
  161. */
  162. /**
  163. * returns if a zero-coverage baseline file should be written as part of
  164. * instrumentation. This allows reporting to display numbers for files that have
  165. * no tests. Used by the `instrument` command.
  166. * @method saveBaseline
  167. * @return {Boolean} true if a baseline coverage file should be written.
  168. */
  169. /**
  170. * Sets the baseline coverage filename. Used by the `instrument` command.
  171. * @method baselineFile
  172. * @return {String} the name of the baseline coverage file.
  173. */
  174. /**
  175. * returns if comments the JS to instrument contains es6 Module syntax.
  176. * @method esModules
  177. * @return {Boolean} true if code contains es6 import/export statements.
  178. */
  179. /**
  180. * returns if the coverage filename should include the PID. Used by the `instrument` command.
  181. * @method includePid
  182. * @return {Boolean} true to include pid in coverage filename.
  183. */
  184. addMethods(
  185. InstrumentOptions,
  186. 'extensions',
  187. 'defaultExcludes',
  188. 'completeCopy',
  189. 'variable',
  190. 'compact',
  191. 'preserveComments',
  192. 'saveBaseline',
  193. 'baselineFile',
  194. 'esModules',
  195. 'includeAllSources',
  196. 'includePid',
  197. 'autoWrap',
  198. 'ignoreClassMethods'
  199. );
  200. /**
  201. * returns the root directory used by istanbul which is typically the root of the
  202. * source tree. Used by the `cover` and `report` commands.
  203. * @method root
  204. * @return {String} the root directory used by istanbul.
  205. */
  206. InstrumentOptions.prototype.root = function() {
  207. return path.resolve(this.config.root);
  208. };
  209. /**
  210. * returns an array of fileset patterns that should be excluded for instrumentation.
  211. * Used by the `instrument` and `cover` commands.
  212. * @method excludes
  213. * @return {Array} an array of fileset patterns that should be excluded for
  214. * instrumentation.
  215. */
  216. InstrumentOptions.prototype.excludes = function(excludeTests) {
  217. let defs;
  218. if (this.defaultExcludes()) {
  219. defs = ['**/node_modules/**'];
  220. if (excludeTests) {
  221. defs = defs.concat(['**/test/**', '**/tests/**']);
  222. }
  223. return defs.concat(this.config.excludes);
  224. }
  225. return this.config.excludes;
  226. };
  227. InstrumentOptions.prototype.getInstrumenterOpts = function() {
  228. return {
  229. coverageVariable: this.variable(),
  230. compact: this.compact(),
  231. preserveComments: this.preserveComments(),
  232. esModules: this.esModules(),
  233. autoWrap: this.autoWrap(),
  234. ignoreClassMethods: this.ignoreClassMethods()
  235. };
  236. };
  237. /**
  238. * Object that returns reporting options
  239. * @class ReportingOptions
  240. * @module config
  241. * @constructor
  242. * @param config the reporting part of the config object
  243. */
  244. function ReportingOptions(config) {
  245. this.config = config;
  246. }
  247. /**
  248. * returns the kind of information to be printed on the console. May be one
  249. * of `summary`, `detail`, `both` or `none`. Used by the
  250. * `cover` command.
  251. * @method print
  252. * @return {String} the kind of information to print to the console at the end
  253. * of the `cover` command execution.
  254. */
  255. /**
  256. * returns a list of reports that should be generated at the end of a run. Used
  257. * by the `cover` and `report` commands.
  258. * @method reports
  259. * @return {Array} an array of reports that should be produced
  260. */
  261. /**
  262. * returns the directory under which reports should be generated. Used by the
  263. * `cover` and `report` commands.
  264. *
  265. * @method dir
  266. * @return {String} the directory under which reports should be generated.
  267. */
  268. /**
  269. * returns an object that has keys that are report format names and values that are objects
  270. * containing detailed configuration for each format. Running `istanbul help config`
  271. * will give you all the keys per report format that can be overridden.
  272. * Used by the `cover` and `report` commands.
  273. * @method reportConfig
  274. * @return {Object} detailed report configuration per report format.
  275. */
  276. addMethods(
  277. ReportingOptions,
  278. 'print',
  279. 'reports',
  280. 'dir',
  281. 'reportConfig',
  282. 'summarizer'
  283. );
  284. function isInvalidMark(v, key) {
  285. const prefix = 'Watermark for [' + key + '] :';
  286. if (v.length !== 2) {
  287. return prefix + 'must be an array of length 2';
  288. }
  289. v[0] = Number(v[0]);
  290. v[1] = Number(v[1]);
  291. if (isNaN(v[0]) || isNaN(v[1])) {
  292. return prefix + 'must have valid numbers';
  293. }
  294. if (v[0] < 0 || v[1] < 0) {
  295. return prefix + 'must be positive numbers';
  296. }
  297. if (v[1] > 100) {
  298. return prefix + 'cannot exceed 100';
  299. }
  300. if (v[1] <= v[0]) {
  301. return prefix + 'low must be less than high';
  302. }
  303. return null;
  304. }
  305. /**
  306. * returns the low and high watermarks to be used to designate whether coverage
  307. * is `low`, `medium` or `high`. Statements, functions, branches and lines can
  308. * have independent watermarks. These are respected by all reports
  309. * that color for low, medium and high coverage. See the default configuration for exact syntax
  310. * using `istanbul help config`. Used by the `cover` and `report` commands.
  311. *
  312. * @method watermarks
  313. * @return {Object} an object containing low and high watermarks for statements,
  314. * branches, functions and lines.
  315. */
  316. ReportingOptions.prototype.watermarks = function() {
  317. const v = this.config.watermarks;
  318. const defs = libReport.getDefaultWatermarks();
  319. const ret = {};
  320. Object.keys(defs).forEach(k => {
  321. const mark = v[k];
  322. //it will already be a non-zero length array because of the way the merge works
  323. const message = isInvalidMark(mark, k);
  324. if (message) {
  325. console.error(message);
  326. ret[k] = defs[k];
  327. } else {
  328. ret[k] = mark;
  329. }
  330. });
  331. return ret;
  332. };
  333. /**
  334. * Object that returns hook options. Note that istanbul does not provide an
  335. * option to hook `require`. This is always done by the `cover` command.
  336. * @class HookOptions
  337. * @module config
  338. * @constructor
  339. * @param config the hooks part of the config object
  340. */
  341. function HookOptions(config) {
  342. this.config = config;
  343. }
  344. /**
  345. * returns if `vm.runInContext` needs to be hooked. Used by the `cover` command.
  346. * @method hookRunInContext
  347. * @return {Boolean} true if `vm.runInContext` needs to be hooked for coverage
  348. */
  349. /**
  350. * returns if `vm.runInThisContext` needs to be hooked, in addition to the standard
  351. * `require` hooks added by istanbul. This should be true for code that uses
  352. * RequireJS for example. Used by the `cover` command.
  353. * @method hookRunInThisContext
  354. * @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage
  355. */
  356. /**
  357. * returns a path to JS file or a dependent module that should be used for
  358. * post-processing files after they have been required. See the `yui-istanbul` module for
  359. * an example of a post-require hook. This particular hook modifies the yui loader when
  360. * that file is required to add istanbul interceptors. Use by the `cover` command
  361. *
  362. * @method postRequireHook
  363. * @return {String} a path to a JS file or the name of a node module that needs
  364. * to be used as a `require` post-processor
  365. */
  366. /**
  367. * returns if istanbul needs to add a SIGINT (control-c, usually) handler to
  368. * save coverage information. Useful for getting code coverage out of processes
  369. * that run forever and need a SIGINT to terminate.
  370. * @method handleSigint
  371. * @return {Boolean} true if SIGINT needs to be hooked to write coverage information
  372. */
  373. addMethods(
  374. HookOptions,
  375. 'hookRunInContext',
  376. 'hookRunInThisContext',
  377. 'postRequireHook',
  378. 'handleSigint'
  379. );
  380. /**
  381. * represents the istanbul configuration and provides sub-objects that can
  382. * return instrumentation, reporting and hook options respectively.
  383. * Usage
  384. * -----
  385. *
  386. * var configObj = require('istanbul').config.loadFile();
  387. *
  388. * console.log(configObj.reporting.reports());
  389. *
  390. * @class Configuration
  391. * @module config
  392. * @param {Object} obj the base object to use as the configuration
  393. * @param {Object} overrides optional - override attributes that are merged into
  394. * the base config
  395. * @constructor
  396. */
  397. function Configuration(obj, overrides) {
  398. let config = mergeDefaults(obj, defaultConfig(true));
  399. if (isObject(overrides)) {
  400. config = mergeDefaults(overrides, config);
  401. }
  402. if (config.verbose) {
  403. console.error('Using configuration');
  404. console.error('-------------------');
  405. console.error(yaml.safeDump(config, { indent: 4, flowLevel: 3 }));
  406. console.error('-------------------\n');
  407. }
  408. this.verbose = config.verbose;
  409. this.instrumentation = new InstrumentOptions(config.instrumentation);
  410. this.reporting = new ReportingOptions(config.reporting);
  411. this.hooks = new HookOptions(config.hooks);
  412. this.check = config.check; // Pass raw config sub-object.
  413. }
  414. /**
  415. * true if verbose logging is required
  416. * @property verbose
  417. * @type Boolean
  418. */
  419. /**
  420. * instrumentation options
  421. * @property instrumentation
  422. * @type InstrumentOptions
  423. */
  424. /**
  425. * reporting options
  426. * @property reporting
  427. * @type ReportingOptions
  428. */
  429. /**
  430. * hook options
  431. * @property hooks
  432. * @type HookOptions
  433. */
  434. function loadFile(file, overrides) {
  435. const defaultConfigFile = path.resolve('.istanbul.yml');
  436. let configObject;
  437. if (file) {
  438. if (!existsSync(file)) {
  439. throw inputError.create(
  440. 'Invalid configuration file specified:' + file
  441. );
  442. }
  443. } else {
  444. if (existsSync(defaultConfigFile)) {
  445. file = defaultConfigFile;
  446. }
  447. }
  448. if (file) {
  449. if (overrides && overrides.verbose === true) {
  450. console.error('Loading config: ' + file);
  451. }
  452. configObject = file.match(YML_PATTERN)
  453. ? yaml.safeLoad(fs.readFileSync(file, 'utf8'), { filename: file })
  454. : require(path.resolve(file));
  455. }
  456. return new Configuration(configObject, overrides);
  457. }
  458. function loadObject(obj, overrides) {
  459. return new Configuration(obj, overrides);
  460. }
  461. /**
  462. * methods to load the configuration object.
  463. * Usage
  464. * -----
  465. *
  466. * var config = require('istanbul').config,
  467. * configObj = config.loadFile();
  468. *
  469. * console.log(configObj.reporting.reports());
  470. *
  471. * @class Config
  472. * @module main
  473. * @static
  474. */
  475. module.exports = {
  476. /**
  477. * loads the specified configuration file with optional overrides. Throws
  478. * when a file is specified and it is not found.
  479. * @method loadFile
  480. * @static
  481. * @param {String} file the file to load. If falsy, the default config file, if present, is loaded.
  482. * If not a default config is used.
  483. * @param {Object} overrides - an object with override keys that are merged into the
  484. * config object loaded
  485. * @return {Configuration} the config object with overrides applied
  486. */
  487. loadFile,
  488. /**
  489. * loads the specified configuration object with optional overrides.
  490. * @method loadObject
  491. * @static
  492. * @param {Object} obj the object to use as the base configuration.
  493. * @param {Object} overrides - an object with override keys that are merged into the
  494. * config object
  495. * @return {Configuration} the config object with overrides applied
  496. */
  497. loadObject,
  498. /**
  499. * returns the default configuration object. Note that this is a plain object
  500. * and not a `Configuration` instance.
  501. * @method defaultConfig
  502. * @static
  503. * @return {Object} an object that represents the default config
  504. */
  505. defaultConfig
  506. };