ava
Version:
180 lines (145 loc) • 5.72 kB
JavaScript
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};
}