1 | 'use strict';
|
2 | const fs = require('fs');
|
3 | const path = require('path');
|
4 | const vm = require('vm');
|
5 | const isPlainObject = require('is-plain-object');
|
6 | const pkgConf = require('pkg-conf');
|
7 |
|
8 | const NO_SUCH_FILE = Symbol('no ava.config.js file');
|
9 | const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
|
10 | const EXPERIMENTS = new Set(['configurableModuleFormat', 'disableSnapshotsInHooks', 'reverseTeardowns']);
|
11 |
|
12 |
|
13 | const 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 |
|
24 | const 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 |
|
49 | const 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 |
|
66 | const 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 |
|
85 | function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
|
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);
|
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') {
|
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') {
|
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 |
|
160 | module.exports = loadConfig;
|