config.js 16 KB

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