UNPKG

5.52 kBJavaScriptView Raw
1'use strict';
2const fs = require('fs');
3const path = require('path');
4const vm = require('vm');
5const isPlainObject = require('is-plain-object');
6const pkgConf = require('pkg-conf');
7
8const NO_SUCH_FILE = Symbol('no ava.config.js file');
9const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
10const EXPERIMENTS = new Set(['configurableModuleFormat', 'disableSnapshotsInHooks', 'reverseTeardowns']);
11
12// *Very* rudimentary support for loading ava.config.js files containing an `export default` statement.
13const evaluateJsConfig = configFile => {
14 const contents = fs.readFileSync(configFile, 'utf8');
15 const script = new vm.Script(`'use strict';(()=>{let __export__;\n${contents.replace(/export default/g, '__export__ =')};return __export__;})()`, {
16 filename: configFile,
17 lineOffset: -1
18 });
19 return {
20 default: script.runInThisContext()
21 };
22};
23
24const loadJsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.js')}) => {
25 if (!configFile.endsWith('.js')) {
26 return null;
27 }
28
29 const fileForErrorMessage = path.relative(projectDir, configFile);
30
31 let config;
32 try {
33 ({default: config = MISSING_DEFAULT_EXPORT} = evaluateJsConfig(configFile));
34 } catch (error) {
35 if (error.code === 'ENOENT') {
36 return null;
37 }
38
39 throw Object.assign(new Error(`Error loading ${fileForErrorMessage}: ${error.message}`), {parent: error});
40 }
41
42 if (config === MISSING_DEFAULT_EXPORT) {
43 throw new Error(`${fileForErrorMessage} must have a default export, using ES module syntax`);
44 }
45
46 return {config, fileForErrorMessage};
47};
48
49const loadCjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.cjs')}) => {
50 if (!configFile.endsWith('.cjs')) {
51 return null;
52 }
53
54 const fileForErrorMessage = path.relative(projectDir, configFile);
55 try {
56 return {config: require(configFile), fileForErrorMessage};
57 } catch (error) {
58 if (error.code === 'MODULE_NOT_FOUND') {
59 return null;
60 }
61
62 throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error});
63 }
64};
65
66const loadMjsConfig = ({projectDir, configFile = path.join(projectDir, 'ava.config.mjs')}) => {
67 if (!configFile.endsWith('.mjs')) {
68 return null;
69 }
70
71 const fileForErrorMessage = path.relative(projectDir, configFile);
72 try {
73 fs.readFileSync(configFile);
74 } catch (error) {
75 if (error.code === 'ENOENT') {
76 return null;
77 }
78
79 throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error});
80 }
81
82 throw new Error(`AVA cannot yet load ${fileForErrorMessage} files`);
83};
84
85function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) { // eslint-disable-line complexity
86 let packageConf = pkgConf.sync('ava', {cwd: resolveFrom});
87 const filepath = pkgConf.filepath(packageConf);
88 const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
89
90 if (configFile) {
91 configFile = path.resolve(configFile); // Relative to CWD
92 if (path.basename(configFile) !== path.relative(projectDir, configFile)) {
93 throw new Error('Config files must be located next to the package.json file');
94 }
95
96 if (!configFile.endsWith('.js') && !configFile.endsWith('.cjs') && !configFile.endsWith('.mjs')) {
97 throw new Error('Config files must have .js, .cjs or .mjs extensions');
98 }
99 }
100
101 const allowConflictWithPackageJson = Boolean(configFile);
102
103 let [{config: fileConf, fileForErrorMessage} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = [
104 loadJsConfig({projectDir, configFile}),
105 loadCjsConfig({projectDir, configFile}),
106 loadMjsConfig({projectDir, configFile})
107 ].filter(result => result !== null);
108
109 if (conflicting.length > 0) {
110 throw new Error(`Conflicting configuration in ${fileForErrorMessage} and ${conflicting.map(({fileForErrorMessage}) => fileForErrorMessage).join(' & ')}`);
111 }
112
113 if (fileConf !== NO_SUCH_FILE) {
114 if (allowConflictWithPackageJson) {
115 packageConf = {};
116 } else if (Object.keys(packageConf).length > 0) {
117 throw new Error(`Conflicting configuration in ${fileForErrorMessage} and package.json`);
118 }
119
120 if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
121 throw new TypeError(`${fileForErrorMessage} must not export a promise`);
122 }
123
124 if (!isPlainObject(fileConf) && typeof fileConf !== 'function') {
125 throw new TypeError(`${fileForErrorMessage} must export a plain object or factory function`);
126 }
127
128 if (typeof fileConf === 'function') {
129 fileConf = fileConf({projectDir});
130 if (fileConf && typeof fileConf.then === 'function') { // eslint-disable-line promise/prefer-await-to-then
131 throw new TypeError(`Factory method exported by ${fileForErrorMessage} must not return a promise`);
132 }
133
134 if (!isPlainObject(fileConf)) {
135 throw new TypeError(`Factory method exported by ${fileForErrorMessage} must return a plain object`);
136 }
137 }
138
139 if ('ava' in fileConf) {
140 throw new Error(`Encountered ’ava’ property in ${fileForErrorMessage}; avoid wrapping the configuration`);
141 }
142 }
143
144 const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir};
145
146 const {nonSemVerExperiments: experiments} = config;
147 if (!isPlainObject(experiments)) {
148 throw new Error(`nonSemVerExperiments from ${fileForErrorMessage} must be an object`);
149 }
150
151 for (const key of Object.keys(experiments)) {
152 if (!EXPERIMENTS.has(key)) {
153 throw new Error(`nonSemVerExperiments.${key} from ${fileForErrorMessage} is not a supported experiment`);
154 }
155 }
156
157 return config;
158}
159
160module.exports = loadConfig;