index.js 4.16 KB
'use strict';

const fs = require('fs');
const path = require('path');
const {promisify} = require('util');
const camelcase = require('camelcase');
const findUp = require('find-up');
const resolveFrom = require('resolve-from');
const getPackageType = require('get-package-type');

const readFile = promisify(fs.readFile);

let loadActive = false;

function isLoading() {
	return loadActive;
}

const standardConfigFiles = [
	'.nycrc',
	'.nycrc.json',
	'.nycrc.yml',
	'.nycrc.yaml',
	'nyc.config.js',
	'nyc.config.cjs',
	'nyc.config.mjs'
];

function camelcasedConfig(config) {
	const results = {};
	for (const [field, value] of Object.entries(config)) {
		results[camelcase(field)] = value;
	}

	return results;
}

async function findPackage(options) {
	const cwd = options.cwd || process.env.NYC_CWD || process.cwd();
	const pkgPath = await findUp('package.json', {cwd});
	if (pkgPath) {
		const pkgConfig = JSON.parse(await readFile(pkgPath, 'utf8')).nyc || {};
		if ('cwd' in pkgConfig) {
			pkgConfig.cwd = path.resolve(path.dirname(pkgPath), pkgConfig.cwd);
		}

		return {
			cwd: path.dirname(pkgPath),
			pkgConfig
		};
	}

	return {
		cwd,
		pkgConfig: {}
	};
}

async function actualLoad(configFile) {
	if (!configFile) {
		return {};
	}

	const configExt = path.extname(configFile).toLowerCase();
	switch (configExt) {
		case '.js':
			/* istanbul ignore next: coverage for 13.2.0+ is shown in load-esm.js */
			if (await getPackageType(configFile) === 'module') {
				return require('./load-esm')(configFile);
			}

			/* fallthrough */
		case '.cjs':
			return require(configFile);
		/* istanbul ignore next: coverage for 13.2.0+ is shown in load-esm.js */
		case '.mjs':
			return require('./load-esm')(configFile);
		case '.yml':
		case '.yaml':
			return require('js-yaml').load(await readFile(configFile, 'utf8'));
		default:
			return JSON.parse(await readFile(configFile, 'utf8'));
	}
}

async function loadFile(configFile) {
	/* This lets @istanbuljs/esm-loader-hook avoid circular initialization when loading
	 * configuration.  This should generally only happen when the loader hook is active
	 * on the main nyc process. */
	loadActive = true;

	try {
		return await actualLoad(configFile);
	} finally {
		loadActive = false;
	}
}

async function applyExtends(config, filename, loopCheck = new Set()) {
	config = camelcasedConfig(config);
	if ('extends' in config) {
		const extConfigs = [].concat(config.extends);
		if (extConfigs.some(e => typeof e !== 'string')) {
			throw new TypeError(`${filename} contains an invalid 'extends' option`);
		}

		delete config.extends;
		const filePath = path.dirname(filename);
		for (const extConfig of extConfigs) {
			const configFile = resolveFrom.silent(filePath, extConfig) ||
				resolveFrom.silent(filePath, './' + extConfig);
			if (!configFile) {
				throw new Error(`Could not resolve configuration file ${extConfig} from ${path.dirname(filename)}.`);
			}

			if (loopCheck.has(configFile)) {
				throw new Error(`Circular extended configurations: '${configFile}'.`);
			}

			loopCheck.add(configFile);

			// eslint-disable-next-line no-await-in-loop
			const configLoaded = await loadFile(configFile);
			if ('cwd' in configLoaded) {
				configLoaded.cwd = path.resolve(path.dirname(configFile), configLoaded.cwd);
			}

			Object.assign(
				config,
				// eslint-disable-next-line no-await-in-loop
				await applyExtends(configLoaded, configFile, loopCheck)
			);
		}
	}

	return config;
}

async function loadNycConfig(options = {}) {
	const {cwd, pkgConfig} = await findPackage(options);
	const configFiles = [].concat(options.nycrcPath || standardConfigFiles);
	const configFile = await findUp(configFiles, {cwd});
	if (options.nycrcPath && !configFile) {
		throw new Error(`Requested configuration file ${options.nycrcPath} not found`);
	}

	const config = {
		cwd,
		...(await applyExtends(pkgConfig, path.join(cwd, 'package.json'))),
		...(await applyExtends(await loadFile(configFile), configFile))
	};

	const arrayFields = ['require', 'extension', 'exclude', 'include'];
	for (const arrayField of arrayFields) {
		if (config[arrayField]) {
			config[arrayField] = [].concat(config[arrayField]);
		}
	}

	return config;
}

module.exports = {
	loadNycConfig,
	isLoading
};