repo_description: Node.js test runner that lets you develop with confidence 🚀
repo_topics: assert async async-functions ava cli cli-app concurrency javascript node nodejs performance tap tdd test-framework test-runner testing unicorns unit-testing
code: import fs from 'node:fs';
  import path from 'node:path';
  import process from 'node:process';
  import url from 'node:url';
  
  import {isPlainObject} from 'is-plain-object';
  import {packageConfig, packageJsonPath} from 'package-config';
  
  const NO_SUCH_FILE = Symbol('no ava.config.js file');
  const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
  const EXPERIMENTS = new Set(['observeRunsFromConfig']);
  
  const importConfig = async ({configFile, fileForErrorMessage}) => {
  	const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile));
  	if (config === MISSING_DEFAULT_EXPORT) {
  		throw new Error(`${fileForErrorMessage} must have a default export`);
  	}
  
  	return config;
  };
  
  const loadConfigFile = async ({projectDir, configFile}) => {
  	const fileForErrorMessage = path.relative(projectDir, configFile);
  	try {
  		await fs.promises.access(configFile);
  		return {config: await importConfig({configFile, fileForErrorMessage}), configFile, fileForErrorMessage};
  	} catch (error) {
  		if (error.code === 'ENOENT') {
  			return null;
  		}
  
  		throw Object.assign(new Error(`Error loading ${fileForErrorMessage}: ${error.message}`), {cause: error});
  	}
  };
  
  function resolveConfigFile(configFile) {
  	if (configFile) {
  		return path.resolve(configFile); // Relative to CWD
  	}
  
  	return configFile;
  }
  
  const gitScmFile = process.env.AVA_FAKE_SCM_ROOT ?? '.git';
  
  async function findRepoRoot(fromDir) {
  	const {root} = path.parse(fromDir);
  	let dir = fromDir;
  	while (root !== dir) {
  		try {
  			const stat = await fs.promises.stat(path.join(dir, gitScmFile)); // eslint-disable-line no-await-in-loop
  			if (stat.isFile() || stat.isDirectory()) {
  				return dir;
  			}
  		} catch {}
  
  		dir = path.dirname(dir);
  	}
  
  	return root;
  }
  
  async function checkJsonFile(searchDir) {
  	const file = path.join(searchDir, 'ava.config.json');
  	try {
  		await fs.promises.access(file);
  		return file;
  	} catch (error) {
  		if (error.code === 'ENOENT') {
  			return null;
  		}
  
  		throw error;
  	}
  }
  
  export async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
  	let packageConf = await packageConfig('ava', {cwd: resolveFrom});
  	const filepath = packageJsonPath(packageConf);
  	const projectDir = filepath === undefined ? resolveFrom : path.dirname(filepath);
  
  	const repoRoot = await findRepoRoot(projectDir);
  
  	// Conflicts are only allowed when an explicit config file is provided.
  	const allowConflictWithPackageJson = Boolean(configFile);
  	configFile = resolveConfigFile(configFile);
  
  	const unsupportedFiles = [];
  	let fileConf = NO_SUCH_FILE;
  	let fileForErrorMessage;
  	let conflicting = [];
  	if (configFile) {
  		let loaded;
  		try {
  			loaded = await loadConfigFile({projectDir, configFile});
  		} catch (error) {
  			if (!configFile.endsWith('.js') && !configFile.endsWith('.cjs') && !configFile.endsWith('.mjs')) {
  				throw Object.assign(new Error('Could not load config file; it should have .js, .cjs or .mjs extension'), {cause: error});
  			}
  
  			throw error;
  		}
  
  		if (loaded !== null) {
  			({config: fileConf, fileForErrorMessage} = loaded);
  		}
  	} else {
  		let searchDir = projectDir;
  		const stopAt = path.dirname(repoRoot);
  		do {
  			const [jsonFile, ...results] = await Promise.all([ // eslint-disable-line no-await-in-loop
  				checkJsonFile(searchDir),
  				loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.js')}),
  				loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.cjs')}),
  				loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.mjs')}),
  			]);
  
  			if (jsonFile !== null) {
  				unsupportedFiles.push(jsonFile);
  			}
  
  			[{config: fileConf, fileForErrorMessage, configFile} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = results.filter(result => result !== null);
  
  			searchDir = path.dirname(searchDir);
  		} while (fileConf === NO_SUCH_FILE && searchDir !== stopAt);
  	}
  
  	if (conflicting.length > 0) {
  		throw new Error(`Conflicting configuration in ${fileForErrorMessage} and ${conflicting.map(({fileForErrorMessage}) => fileForErrorMessage).join(' & ')}`);
  	}
  
  	if (fileConf !== NO_SUCH_FILE) {
  		if (allowConflictWithPackageJson) {
  			packageConf = {};
  		} else if (Object.keys(packageConf).length > 0) {
  			throw new Error(`Conflicting configuration in ${fileForErrorMessage} and package.json`);
  		}
  
  		if (!isPlainObject(fileConf) && typeof fileConf !== 'function') {
  			throw new TypeError(`${fileForErrorMessage} must export a plain object or factory function`);
  		}
  
  		if (typeof fileConf === 'function') {
  			fileConf = await fileConf({projectDir});
  
  			if (!isPlainObject(fileConf)) {
  				throw new TypeError(`Factory method exported by ${fileForErrorMessage} must return a plain object`);
  			}
  		}
  
  		if ('ava' in fileConf) {
  			throw new Error(`Encountered ’ava’ property in ${fileForErrorMessage}; avoid wrapping the configuration`);
  		}
  	}
  
  	const config = {
  		...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir, configFile,
  	};
  
  	if (
  		'filterNodeArgumentsForWorkerThreads' in config
  		&& typeof config.filterNodeArgumentsForWorkerThreads !== 'function'
  	) {
  		throw new Error(`filterNodeArgumentsForWorkerThreads from ${fileForErrorMessage} must be a function`);
  	}
  
  	const {nonSemVerExperiments: experiments} = config;
  	if (!isPlainObject(experiments)) {
  		throw new Error(`nonSemVerExperiments from ${fileForErrorMessage} must be an object`);
  	}
  
  	for (const key of Object.keys(experiments)) {
  		if (!EXPERIMENTS.has(key)) {
  			throw new Error(`nonSemVerExperiments.${key} from ${fileForErrorMessage} is not a supported experiment`);
  		}
  	}
  
  	return {config, unsupportedFiles};
  }
  