index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const {promisify} = require('util');
  5. const camelcase = require('camelcase');
  6. const findUp = require('find-up');
  7. const resolveFrom = require('resolve-from');
  8. const getPackageType = require('get-package-type');
  9. const readFile = promisify(fs.readFile);
  10. let loadActive = false;
  11. function isLoading() {
  12. return loadActive;
  13. }
  14. const standardConfigFiles = [
  15. '.nycrc',
  16. '.nycrc.json',
  17. '.nycrc.yml',
  18. '.nycrc.yaml',
  19. 'nyc.config.js',
  20. 'nyc.config.cjs',
  21. 'nyc.config.mjs'
  22. ];
  23. function camelcasedConfig(config) {
  24. const results = {};
  25. for (const [field, value] of Object.entries(config)) {
  26. results[camelcase(field)] = value;
  27. }
  28. return results;
  29. }
  30. async function findPackage(options) {
  31. const cwd = options.cwd || process.env.NYC_CWD || process.cwd();
  32. const pkgPath = await findUp('package.json', {cwd});
  33. if (pkgPath) {
  34. const pkgConfig = JSON.parse(await readFile(pkgPath, 'utf8')).nyc || {};
  35. if ('cwd' in pkgConfig) {
  36. pkgConfig.cwd = path.resolve(path.dirname(pkgPath), pkgConfig.cwd);
  37. }
  38. return {
  39. cwd: path.dirname(pkgPath),
  40. pkgConfig
  41. };
  42. }
  43. return {
  44. cwd,
  45. pkgConfig: {}
  46. };
  47. }
  48. async function actualLoad(configFile) {
  49. if (!configFile) {
  50. return {};
  51. }
  52. const configExt = path.extname(configFile).toLowerCase();
  53. switch (configExt) {
  54. case '.js':
  55. /* istanbul ignore next: coverage for 13.2.0+ is shown in load-esm.js */
  56. if (await getPackageType(configFile) === 'module') {
  57. return require('./load-esm')(configFile);
  58. }
  59. /* fallthrough */
  60. case '.cjs':
  61. return require(configFile);
  62. /* istanbul ignore next: coverage for 13.2.0+ is shown in load-esm.js */
  63. case '.mjs':
  64. return require('./load-esm')(configFile);
  65. case '.yml':
  66. case '.yaml':
  67. return require('js-yaml').load(await readFile(configFile, 'utf8'));
  68. default:
  69. return JSON.parse(await readFile(configFile, 'utf8'));
  70. }
  71. }
  72. async function loadFile(configFile) {
  73. /* This lets @istanbuljs/esm-loader-hook avoid circular initialization when loading
  74. * configuration. This should generally only happen when the loader hook is active
  75. * on the main nyc process. */
  76. loadActive = true;
  77. try {
  78. return await actualLoad(configFile);
  79. } finally {
  80. loadActive = false;
  81. }
  82. }
  83. async function applyExtends(config, filename, loopCheck = new Set()) {
  84. config = camelcasedConfig(config);
  85. if ('extends' in config) {
  86. const extConfigs = [].concat(config.extends);
  87. if (extConfigs.some(e => typeof e !== 'string')) {
  88. throw new TypeError(`${filename} contains an invalid 'extends' option`);
  89. }
  90. delete config.extends;
  91. const filePath = path.dirname(filename);
  92. for (const extConfig of extConfigs) {
  93. const configFile = resolveFrom.silent(filePath, extConfig) ||
  94. resolveFrom.silent(filePath, './' + extConfig);
  95. if (!configFile) {
  96. throw new Error(`Could not resolve configuration file ${extConfig} from ${path.dirname(filename)}.`);
  97. }
  98. if (loopCheck.has(configFile)) {
  99. throw new Error(`Circular extended configurations: '${configFile}'.`);
  100. }
  101. loopCheck.add(configFile);
  102. // eslint-disable-next-line no-await-in-loop
  103. const configLoaded = await loadFile(configFile);
  104. if ('cwd' in configLoaded) {
  105. configLoaded.cwd = path.resolve(path.dirname(configFile), configLoaded.cwd);
  106. }
  107. Object.assign(
  108. config,
  109. // eslint-disable-next-line no-await-in-loop
  110. await applyExtends(configLoaded, configFile, loopCheck)
  111. );
  112. }
  113. }
  114. return config;
  115. }
  116. async function loadNycConfig(options = {}) {
  117. const {cwd, pkgConfig} = await findPackage(options);
  118. const configFiles = [].concat(options.nycrcPath || standardConfigFiles);
  119. const configFile = await findUp(configFiles, {cwd});
  120. if (options.nycrcPath && !configFile) {
  121. throw new Error(`Requested configuration file ${options.nycrcPath} not found`);
  122. }
  123. const config = {
  124. cwd,
  125. ...(await applyExtends(pkgConfig, path.join(cwd, 'package.json'))),
  126. ...(await applyExtends(await loadFile(configFile), configFile))
  127. };
  128. const arrayFields = ['require', 'extension', 'exclude', 'include'];
  129. for (const arrayField of arrayFields) {
  130. if (config[arrayField]) {
  131. config[arrayField] = [].concat(config[arrayField]);
  132. }
  133. }
  134. return config;
  135. }
  136. module.exports = {
  137. loadNycConfig,
  138. isLoading
  139. };