ava 4.0.0-alpha.1 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/entrypoints/cli.mjs +4 -0
  2. package/{eslint-plugin-helper.js → entrypoints/eslint-plugin-helper.cjs} +20 -7
  3. package/entrypoints/main.cjs +2 -0
  4. package/entrypoints/main.mjs +1 -0
  5. package/entrypoints/plugin.cjs +2 -0
  6. package/entrypoints/plugin.mjs +4 -0
  7. package/index.d.ts +6 -709
  8. package/lib/api.js +95 -46
  9. package/lib/assert.js +122 -173
  10. package/lib/chalk.js +9 -14
  11. package/lib/cli.js +105 -97
  12. package/lib/code-excerpt.js +12 -17
  13. package/lib/concordance-options.js +30 -31
  14. package/lib/context-ref.js +3 -6
  15. package/lib/create-chain.js +32 -4
  16. package/lib/environment-variables.js +1 -4
  17. package/lib/eslint-plugin-helper-worker.js +16 -26
  18. package/lib/extensions.js +2 -2
  19. package/lib/fork.js +42 -83
  20. package/lib/glob-helpers.cjs +140 -0
  21. package/lib/globs.js +136 -163
  22. package/lib/{ipc-flow-control.js → ipc-flow-control.cjs} +1 -0
  23. package/lib/is-ci.js +4 -2
  24. package/lib/like-selector.js +7 -13
  25. package/lib/line-numbers.js +10 -17
  26. package/lib/load-config.js +62 -56
  27. package/lib/module-types.js +3 -3
  28. package/lib/node-arguments.js +4 -5
  29. package/lib/{now-and-timers.js → now-and-timers.cjs} +0 -0
  30. package/lib/parse-test-args.js +22 -11
  31. package/lib/pkg.cjs +2 -0
  32. package/lib/plugin-support/shared-worker-loader.js +45 -48
  33. package/lib/plugin-support/shared-workers.js +24 -43
  34. package/lib/provider-manager.js +20 -14
  35. package/lib/reporters/beautify-stack.js +6 -11
  36. package/lib/reporters/colors.js +40 -15
  37. package/lib/reporters/default.js +115 -350
  38. package/lib/reporters/format-serialized-error.js +7 -18
  39. package/lib/reporters/improper-usage-messages.js +8 -9
  40. package/lib/reporters/prefix-title.js +17 -15
  41. package/lib/reporters/tap.js +15 -16
  42. package/lib/run-status.js +25 -23
  43. package/lib/runner.js +138 -127
  44. package/lib/scheduler.js +42 -36
  45. package/lib/serialize-error.js +34 -34
  46. package/lib/snapshot-manager.js +83 -76
  47. package/lib/test.js +114 -195
  48. package/lib/watcher.js +65 -40
  49. package/lib/worker/base.js +48 -99
  50. package/lib/worker/channel.cjs +290 -0
  51. package/lib/worker/dependency-tracker.js +22 -22
  52. package/lib/worker/guard-environment.cjs +19 -0
  53. package/lib/worker/line-numbers.js +57 -19
  54. package/lib/worker/main.cjs +12 -0
  55. package/lib/worker/{options.js → options.cjs} +0 -0
  56. package/lib/worker/{plugin.js → plugin.cjs} +31 -16
  57. package/lib/worker/state.cjs +5 -0
  58. package/lib/worker/{utils.js → utils.cjs} +1 -1
  59. package/package.json +60 -68
  60. package/plugin.d.ts +51 -53
  61. package/readme.md +5 -12
  62. package/types/assertions.d.ts +327 -0
  63. package/types/subscribable.ts +6 -0
  64. package/types/test-fn.d.ts +231 -0
  65. package/types/try-fn.d.ts +58 -0
  66. package/cli.js +0 -11
  67. package/index.js +0 -8
  68. package/lib/worker/channel.js +0 -218
  69. package/lib/worker/main.js +0 -20
  70. package/plugin.js +0 -9
package/lib/globs.js CHANGED
@@ -1,54 +1,36 @@
1
- 'use strict';
2
- const path = require('path');
3
- const globby = require('globby');
4
- const ignoreByDefault = require('ignore-by-default');
5
- const picomatch = require('picomatch');
6
- const slash = require('slash');
7
- const providerManager = require('./provider-manager');
8
-
9
- const defaultIgnorePatterns = [...ignoreByDefault.directories(), '**/node_modules'];
10
- const defaultPicomatchIgnorePatterns = [
11
- ...defaultIgnorePatterns,
12
- // Unlike globby(), picomatch needs a complete pattern when ignoring directories.
13
- ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`)
14
- ];
15
-
16
- const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns);
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {globby, globbySync} from 'globby';
5
+
6
+ import {
7
+ defaultIgnorePatterns,
8
+ hasExtension,
9
+ normalizeFileForMatching,
10
+ normalizePatterns,
11
+ processMatchingPatterns,
12
+ } from './glob-helpers.cjs';
13
+
14
+ export {
15
+ classify,
16
+ isHelperish,
17
+ matches,
18
+ normalizePattern,
19
+ defaultIgnorePatterns,
20
+ hasExtension,
21
+ normalizeFileForMatching,
22
+ normalizePatterns,
23
+ } from './glob-helpers.cjs';
17
24
 
18
25
  const defaultIgnoredByWatcherPatterns = [
19
26
  '**/*.snap.md', // No need to rerun tests when the Markdown files change.
20
27
  'ava.config.js', // Config is not reloaded so avoid rerunning tests when it changes.
21
- 'ava.config.cjs' // Config is not reloaded so avoid rerunning tests when it changes.
28
+ 'ava.config.cjs', // Config is not reloaded so avoid rerunning tests when it changes.
22
29
  ];
23
30
 
24
31
  const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
25
32
 
26
- function normalizePattern(pattern) {
27
- // Always use `/` in patterns, harmonizing matching across platforms
28
- if (process.platform === 'win32') {
29
- pattern = slash(pattern);
30
- }
31
-
32
- if (pattern.startsWith('./')) {
33
- return pattern.slice(2);
34
- }
35
-
36
- if (pattern.startsWith('!./')) {
37
- return `!${pattern.slice(3)}`;
38
- }
39
-
40
- return pattern;
41
- }
42
-
43
- exports.normalizePattern = normalizePattern;
44
-
45
- function normalizePatterns(patterns) {
46
- return patterns.map(pattern => normalizePattern(pattern));
47
- }
48
-
49
- exports.normalizePatterns = normalizePatterns;
50
-
51
- function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: ignoredByWatcherPatterns, providers}) {
33
+ export function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: ignoredByWatcherPatterns, providers}) {
52
34
  if (filePatterns !== undefined && (!Array.isArray(filePatterns) || filePatterns.length === 0)) {
53
35
  throw new Error('The ’files’ configuration must be an array containing glob patterns.');
54
36
  }
@@ -68,7 +50,7 @@ function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: igno
68
50
  `**/test/**/*.${extensionPattern}`,
69
51
  `**/tests/**/*.${extensionPattern}`,
70
52
  '!**/__tests__/**/__{helper,fixture}?(s)__/**/*',
71
- '!**/test?(s)/**/{helper,fixture}?(s)/**/*'
53
+ '!**/test?(s)/**/{helper,fixture}?(s)/**/*',
72
54
  ];
73
55
 
74
56
  if (filePatterns) {
@@ -84,40 +66,36 @@ function normalizeGlobs({extensions, files: filePatterns, ignoredByWatcher: igno
84
66
 
85
67
  ignoredByWatcherPatterns = ignoredByWatcherPatterns ? [...defaultIgnoredByWatcherPatterns, ...normalizePatterns(ignoredByWatcherPatterns)] : [...defaultIgnoredByWatcherPatterns];
86
68
 
87
- for (const {level, main} of providers) {
88
- if (level >= providerManager.levels.pathRewrites) {
89
- ({filePatterns, ignoredByWatcherPatterns} = main.updateGlobs({filePatterns, ignoredByWatcherPatterns}));
90
- }
69
+ for (const {main} of providers) {
70
+ ({filePatterns, ignoredByWatcherPatterns} = main.updateGlobs({filePatterns, ignoredByWatcherPatterns}));
91
71
  }
92
72
 
93
73
  return {extensions, filePatterns, ignoredByWatcherPatterns};
94
74
  }
95
75
 
96
- exports.normalizeGlobs = normalizeGlobs;
97
-
98
- const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
99
-
100
- exports.hasExtension = hasExtension;
76
+ const globOptions = {
77
+ // Globs should work relative to the cwd value only (this should be the
78
+ // project directory that AVA is run in).
79
+ absolute: false,
80
+ braceExpansion: true,
81
+ caseSensitiveMatch: false,
82
+ dot: false,
83
+ expandDirectories: false,
84
+ extglob: true,
85
+ followSymbolicLinks: true,
86
+ gitignore: false,
87
+ globstar: true,
88
+ ignore: defaultIgnorePatterns,
89
+ baseNameMatch: false,
90
+ stats: false,
91
+ unique: true,
92
+ };
101
93
 
102
94
  const globFiles = async (cwd, patterns) => {
103
95
  const files = await globby(patterns, {
104
- // Globs should work relative to the cwd value only (this should be the
105
- // project directory that AVA is run in).
106
- absolute: false,
107
- braceExpansion: true,
108
- caseSensitiveMatch: false,
96
+ ...globOptions,
109
97
  cwd,
110
- dot: false,
111
- expandDirectories: false,
112
- extglob: true,
113
- followSymbolicLinks: true,
114
- gitignore: false,
115
- globstar: true,
116
- ignore: defaultIgnorePatterns,
117
- baseNameMatch: false,
118
98
  onlyFiles: true,
119
- stats: false,
120
- unique: true
121
99
  });
122
100
 
123
101
  // Return absolute file paths. This has the side-effect of normalizing paths
@@ -125,119 +103,114 @@ const globFiles = async (cwd, patterns) => {
125
103
  return files.map(file => path.join(cwd, file));
126
104
  };
127
105
 
128
- async function findFiles({cwd, extensions, filePatterns}) {
129
- return (await globFiles(cwd, filePatterns)).filter(file => hasExtension(extensions, file));
130
- }
106
+ const globDirectoriesSync = (cwd, patterns) => {
107
+ const files = globbySync(patterns, {
108
+ ...globOptions,
109
+ cwd,
110
+ onlyDirectories: true,
111
+ });
131
112
 
132
- exports.findFiles = findFiles;
113
+ // Return absolute file paths. This has the side-effect of normalizing paths
114
+ // on Windows.
115
+ return files.map(file => path.join(cwd, file));
116
+ };
133
117
 
134
- async function findTests({cwd, extensions, filePatterns}) {
135
- return (await findFiles({cwd, extensions, filePatterns})).filter(file => !path.basename(file).startsWith('_'));
118
+ export async function findFiles({cwd, extensions, filePatterns}) {
119
+ const files = await globFiles(cwd, filePatterns);
120
+ return files.filter(file => hasExtension(extensions, file));
136
121
  }
137
122
 
138
- exports.findTests = findTests;
123
+ export async function findTests({cwd, extensions, filePatterns}) {
124
+ const files = await findFiles({cwd, extensions, filePatterns});
125
+ return files.filter(file => !path.basename(file).startsWith('_'));
126
+ }
139
127
 
140
- function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
128
+ export function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
141
129
  return [
142
130
  ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
143
- ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!'))
131
+ ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
144
132
  ];
145
133
  }
146
134
 
147
- exports.getChokidarIgnorePatterns = getChokidarIgnorePatterns;
148
-
149
- const matchingCache = new WeakMap();
150
- const processMatchingPatterns = input => {
151
- let result = matchingCache.get(input);
152
- if (!result) {
153
- const ignore = [...defaultPicomatchIgnorePatterns];
154
- const patterns = input.filter(pattern => {
155
- if (pattern.startsWith('!')) {
156
- // Unlike globby(), picomatch needs a complete pattern when ignoring directories.
157
- ignore.push(pattern.slice(1), `${pattern.slice(1)}/**/*`);
158
- return false;
135
+ export function applyTestFileFilter({ // eslint-disable-line complexity
136
+ cwd,
137
+ expandDirectories = true,
138
+ filter,
139
+ providers = [],
140
+ testFiles,
141
+ treatFilterPatternsAsFiles = true,
142
+ }) {
143
+ const {individualMatchers} = processMatchingPatterns(filter);
144
+ const normalizedFiles = testFiles.map(file => ({file, matcheable: normalizeFileForMatching(cwd, file)}));
145
+
146
+ const selected = new Set();
147
+ const unmatchedPatterns = new Set(individualMatchers.map(({pattern}) => pattern));
148
+
149
+ for (const {pattern, match} of individualMatchers) {
150
+ for (const {file, matcheable} of normalizedFiles) {
151
+ if (match(matcheable)) {
152
+ unmatchedPatterns.delete(pattern);
153
+ selected.add(file);
159
154
  }
160
-
161
- return true;
162
- });
163
-
164
- result = {
165
- match: picomatch(patterns, {ignore}),
166
- matchNoIgnore: picomatch(patterns)
167
- };
168
- matchingCache.set(input, result);
169
- }
170
-
171
- return result;
172
- };
173
-
174
- function matches(file, patterns) {
175
- const {match} = processMatchingPatterns(patterns);
176
- return match(file);
177
- }
178
-
179
- exports.matches = matches;
180
-
181
- const matchesIgnorePatterns = (file, patterns) => {
182
- const {matchNoIgnore} = processMatchingPatterns(patterns);
183
- return matchNoIgnore(file) || defaultMatchNoIgnore(file);
184
- };
185
-
186
- function normalizeFileForMatching(cwd, file) {
187
- if (process.platform === 'win32') {
188
- cwd = slash(cwd);
189
- file = slash(file);
155
+ }
190
156
  }
191
157
 
192
- if (!cwd) { // TODO: Ensure tests provide an actual value.
193
- return file;
194
- }
158
+ if (expandDirectories && unmatchedPatterns.size > 0) {
159
+ const expansion = [];
160
+ for (const pattern of unmatchedPatterns) {
161
+ const directories = globDirectoriesSync(cwd, pattern);
162
+ if (directories.length > 0) {
163
+ unmatchedPatterns.delete(pattern);
164
+ expansion.push(directories);
165
+ }
166
+ }
195
167
 
196
- // TODO: If `file` is outside `cwd` we can't normalize it. Need to figure
197
- // out if that's a real-world scenario, but we may have to ensure the file
198
- // isn't even selected.
199
- if (!file.startsWith(cwd)) {
200
- return file;
168
+ const directories = expansion.flat();
169
+ if (directories.length > 0) {
170
+ for (const file of testFiles) {
171
+ if (selected.has(file)) {
172
+ continue;
173
+ }
174
+
175
+ for (const dir of directories) {
176
+ if (file.startsWith(dir + path.sep)) { // eslint-disable-line max-depth
177
+ selected.add(file);
178
+ }
179
+ }
180
+ }
181
+ }
201
182
  }
202
183
 
203
- // Assume `cwd` does *not* end in a slash.
204
- return file.slice(cwd.length + 1);
205
- }
206
-
207
- exports.normalizeFileForMatching = normalizeFileForMatching;
184
+ const ignoredFilterPatternFiles = [];
185
+ if (treatFilterPatternsAsFiles && unmatchedPatterns.size > 0) {
186
+ const providerExtensions = new Set(providers.flatMap(({main}) => main.extensions));
187
+ for (const pattern of unmatchedPatterns) {
188
+ const file = path.join(cwd, pattern);
189
+ try {
190
+ const stats = fs.statSync(file);
191
+ if (!stats.isFile()) {
192
+ continue;
193
+ }
194
+ } catch (error) {
195
+ if (error.code === 'ENOENT') {
196
+ continue;
197
+ }
198
+
199
+ throw error;
200
+ }
208
201
 
209
- function isHelperish(file) { // Assume file has been normalized already.
210
- // File names starting with an underscore are deemed "helpers".
211
- if (path.basename(file).startsWith('_')) {
212
- return true;
213
- }
202
+ if (
203
+ path.basename(file).startsWith('_')
204
+ || providerExtensions.has(path.extname(file).slice(1))
205
+ || file.split(path.sep).includes('node_modules')
206
+ ) {
207
+ ignoredFilterPatternFiles.push(pattern);
208
+ continue;
209
+ }
214
210
 
215
- // This function assumes the file has been normalized. If it couldn't be,
216
- // don't check if it's got a parent directory that starts with an underscore.
217
- // Deem it not a "helper".
218
- if (path.isAbsolute(file)) {
219
- return false;
211
+ selected.add(file);
212
+ }
220
213
  }
221
214
 
222
- // If the file has a parent directory that starts with only a single
223
- // underscore, it's deemed a "helper".
224
- return path.dirname(file).split('/').some(dir => /^_(?:$|[^_])/.test(dir));
215
+ return Object.assign([...selected], {ignoredFilterPatternFiles});
225
216
  }
226
-
227
- exports.isHelperish = isHelperish;
228
-
229
- function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
230
- file = normalizeFileForMatching(cwd, file);
231
- return {
232
- isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
233
- isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns)
234
- };
235
- }
236
-
237
- exports.classify = classify;
238
-
239
- function applyTestFileFilter({cwd, filter, testFiles}) {
240
- return testFiles.filter(file => matches(normalizeFileForMatching(cwd, file), filter));
241
- }
242
-
243
- exports.applyTestFileFilter = applyTestFileFilter;
@@ -1,3 +1,4 @@
1
+ 'use strict';
1
2
  function controlFlow(channel) {
2
3
  let errored = false;
3
4
  let deliverImmediately = true;
package/lib/is-ci.js CHANGED
@@ -1,5 +1,7 @@
1
- const info = require('ci-info');
1
+ import process from 'node:process';
2
+
3
+ import info from 'ci-info';
2
4
 
3
5
  const {AVA_FORCE_CI} = process.env;
4
6
 
5
- module.exports = AVA_FORCE_CI === 'not-ci' ? false : AVA_FORCE_CI === 'ci' || info.isCI;
7
+ export default AVA_FORCE_CI === 'not-ci' ? false : AVA_FORCE_CI === 'ci' || info.isCI;
@@ -1,17 +1,13 @@
1
- 'use strict';
2
- function isLikeSelector(selector) {
3
- return selector !== null &&
4
- typeof selector === 'object' &&
5
- Reflect.getPrototypeOf(selector) === Object.prototype &&
6
- Reflect.ownKeys(selector).length > 0;
1
+ export function isLikeSelector(selector) {
2
+ return selector !== null
3
+ && typeof selector === 'object'
4
+ && Reflect.getPrototypeOf(selector) === Object.prototype
5
+ && Reflect.ownKeys(selector).length > 0;
7
6
  }
8
7
 
9
- exports.isLikeSelector = isLikeSelector;
8
+ export const CIRCULAR_SELECTOR = new Error('Encountered a circular selector');
10
9
 
11
- const CIRCULAR_SELECTOR = new Error('Encountered a circular selector');
12
- exports.CIRCULAR_SELECTOR = CIRCULAR_SELECTOR;
13
-
14
- function selectComparable(lhs, selector, circular = new Set()) {
10
+ export function selectComparable(lhs, selector, circular = new Set()) {
15
11
  if (circular.has(selector)) {
16
12
  throw CIRCULAR_SELECTOR;
17
13
  }
@@ -33,5 +29,3 @@ function selectComparable(lhs, selector, circular = new Set()) {
33
29
 
34
30
  return comparable;
35
31
  }
36
-
37
- exports.selectComparable = selectComparable;
@@ -1,7 +1,4 @@
1
- 'use strict';
2
-
3
- const picomatch = require('picomatch');
4
- const flatten = require('lodash/flatten');
1
+ import picomatch from 'picomatch';
5
2
 
6
3
  const NUMBER_REGEX = /^\d+$/;
7
4
  const RANGE_REGEX = /^(?<startGroup>\d+)-(?<endGroup>\d+)$/;
@@ -19,8 +16,8 @@ const parseNumber = string => Number.parseInt(string, 10);
19
16
  const removeAllWhitespace = string => string.replace(/\s/g, '');
20
17
  const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);
21
18
 
22
- const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(flatten(
23
- suffix.split(',').map(part => {
19
+ const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(
20
+ suffix.split(',').flatMap(part => {
24
21
  if (NUMBER_REGEX.test(part)) {
25
22
  return parseNumber(part);
26
23
  }
@@ -34,10 +31,10 @@ const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(flatten(
34
31
  }
35
32
 
36
33
  return range(start, end);
37
- })
38
- )));
34
+ }),
35
+ ));
39
36
 
40
- function splitPatternAndLineNumbers(pattern) {
37
+ export function splitPatternAndLineNumbers(pattern) {
41
38
  const parts = pattern.split(DELIMITER);
42
39
  if (parts.length === 1) {
43
40
  return {pattern, lineNumbers: null};
@@ -51,14 +48,10 @@ function splitPatternAndLineNumbers(pattern) {
51
48
  return {pattern: parts.join(DELIMITER), lineNumbers: parseLineNumbers(suffix)};
52
49
  }
53
50
 
54
- exports.splitPatternAndLineNumbers = splitPatternAndLineNumbers;
55
-
56
- function getApplicableLineNumbers(normalizedFilePath, filter) {
57
- return sortNumbersAscending(distinctArray(flatten(
51
+ export function getApplicableLineNumbers(normalizedFilePath, filter) {
52
+ return sortNumbersAscending(distinctArray(
58
53
  filter
59
54
  .filter(({pattern, lineNumbers}) => lineNumbers && picomatch.isMatch(normalizedFilePath, pattern))
60
- .map(({lineNumbers}) => lineNumbers)
61
- )));
55
+ .flatMap(({lineNumbers}) => lineNumbers),
56
+ ));
62
57
  }
63
-
64
- exports.getApplicableLineNumbers = getApplicableLineNumbers;
@@ -1,16 +1,14 @@
1
- 'use strict';
2
- const fs = require('fs');
3
- const path = require('path');
4
- const url = require('url');
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import url from 'node:url';
5
5
 
6
- const {isPlainObject} = require('is-plain-object');
7
- const pkgConf = require('pkg-conf');
6
+ import {isPlainObject} from 'is-plain-object';
7
+ import {packageConfig, packageJsonPath} from 'pkg-conf';
8
8
 
9
9
  const NO_SUCH_FILE = Symbol('no ava.config.js file');
10
10
  const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
11
- const EXPERIMENTS = new Set([
12
- 'sharedWorkers'
13
- ]);
11
+ const EXPERIMENTS = new Set();
14
12
 
15
13
  const importConfig = async ({configFile, fileForErrorMessage}) => {
16
14
  const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile)); // eslint-disable-line node/no-unsupported-features/es-syntax
@@ -21,51 +19,22 @@ const importConfig = async ({configFile, fileForErrorMessage}) => {
21
19
  return config;
22
20
  };
23
21
 
24
- const loadJsConfig = async ({projectDir, configFile = path.join(projectDir, 'ava.config.js')}) => {
25
- if (!configFile.endsWith('.js') || !fs.existsSync(configFile)) {
22
+ const loadConfigFile = async ({projectDir, configFile}) => {
23
+ if (!fs.existsSync(configFile)) {
26
24
  return null;
27
25
  }
28
26
 
29
27
  const fileForErrorMessage = path.relative(projectDir, configFile);
30
28
  try {
31
- return {config: await importConfig({configFile, fileForErrorMessage}), fileForErrorMessage};
29
+ return {config: await importConfig({configFile, fileForErrorMessage}), configFile, fileForErrorMessage};
32
30
  } catch (error) {
33
31
  throw Object.assign(new Error(`Error loading ${fileForErrorMessage}: ${error.message}`), {parent: error});
34
32
  }
35
33
  };
36
34
 
37
- const loadCjsConfig = async ({projectDir, configFile = path.join(projectDir, 'ava.config.cjs')}) => {
38
- if (!configFile.endsWith('.cjs') || !fs.existsSync(configFile)) {
39
- return null;
40
- }
41
-
42
- const fileForErrorMessage = path.relative(projectDir, configFile);
43
- try {
44
- return {config: await require(configFile), fileForErrorMessage};
45
- } catch (error) {
46
- throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error});
47
- }
48
- };
49
-
50
- const loadMjsConfig = async ({projectDir, configFile = path.join(projectDir, 'ava.config.mjs')}) => {
51
- if (!configFile.endsWith('.mjs') || !fs.existsSync(configFile)) {
52
- return null;
53
- }
54
-
55
- const fileForErrorMessage = path.relative(projectDir, configFile);
56
- try {
57
- return {config: await importConfig({configFile, fileForErrorMessage}), fileForErrorMessage};
58
- } catch (error) {
59
- throw Object.assign(new Error(`Error loading ${fileForErrorMessage}`), {parent: error});
60
- }
61
- };
62
-
63
- function resolveConfigFile(projectDir, configFile) {
35
+ function resolveConfigFile(configFile) {
64
36
  if (configFile) {
65
37
  configFile = path.resolve(configFile); // Relative to CWD
66
- if (path.basename(configFile) !== path.relative(projectDir, configFile)) {
67
- throw new Error('Config files must be located next to the package.json file');
68
- }
69
38
 
70
39
  if (!configFile.endsWith('.js') && !configFile.endsWith('.cjs') && !configFile.endsWith('.mjs')) {
71
40
  throw new Error('Config files must have .js, .cjs or .mjs extensions');
@@ -75,20 +44,59 @@ function resolveConfigFile(projectDir, configFile) {
75
44
  return configFile;
76
45
  }
77
46
 
78
- async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
79
- let packageConf = await pkgConf('ava', {cwd: resolveFrom});
80
- const filepath = pkgConf.filepath(packageConf);
81
- const projectDir = filepath === null ? resolveFrom : path.dirname(filepath);
47
+ const gitScmFile = process.env.AVA_FAKE_SCM_ROOT || '.git';
48
+
49
+ async function findRepoRoot(fromDir) {
50
+ const {root} = path.parse(fromDir);
51
+ let dir = fromDir;
52
+ while (root !== dir) {
53
+ try {
54
+ const stat = await fs.promises.stat(path.join(dir, gitScmFile)); // eslint-disable-line no-await-in-loop
55
+ if (stat.isFile() || stat.isDirectory()) {
56
+ return dir;
57
+ }
58
+ } catch {}
59
+
60
+ dir = path.dirname(dir);
61
+ }
82
62
 
83
- configFile = resolveConfigFile(projectDir, configFile);
63
+ return root;
64
+ }
65
+
66
+ export async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
67
+ let packageConf = await packageConfig('ava', {cwd: resolveFrom});
68
+ const filepath = packageJsonPath(packageConf);
69
+ const projectDir = filepath === undefined ? resolveFrom : path.dirname(filepath);
70
+
71
+ const repoRoot = await findRepoRoot(projectDir);
72
+
73
+ // Conflicts are only allowed when an explicit config file is provided.
84
74
  const allowConflictWithPackageJson = Boolean(configFile);
75
+ configFile = resolveConfigFile(configFile);
85
76
 
86
- // TODO: Refactor resolution logic to implement https://github.com/avajs/ava/issues/2285.
87
- let [{config: fileConf, fileForErrorMessage} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = (await Promise.all([
88
- loadJsConfig({projectDir, configFile}, true),
89
- loadCjsConfig({projectDir, configFile}),
90
- loadMjsConfig({projectDir, configFile}, true)
91
- ])).filter(result => result !== null);
77
+ let fileConf = NO_SUCH_FILE;
78
+ let fileForErrorMessage;
79
+ let conflicting = [];
80
+ if (configFile) {
81
+ const loaded = await loadConfigFile({projectDir, configFile});
82
+ if (loaded !== null) {
83
+ ({config: fileConf, fileForErrorMessage} = loaded);
84
+ }
85
+ } else {
86
+ let searchDir = projectDir;
87
+ const stopAt = path.dirname(repoRoot);
88
+ do {
89
+ const results = await Promise.all([ // eslint-disable-line no-await-in-loop
90
+ loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.js')}),
91
+ loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.cjs')}),
92
+ loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.mjs')}),
93
+ ]);
94
+
95
+ [{config: fileConf, fileForErrorMessage, configFile} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = results.filter(result => result !== null);
96
+
97
+ searchDir = path.dirname(searchDir);
98
+ } while (fileConf === NO_SUCH_FILE && searchDir !== stopAt);
99
+ }
92
100
 
93
101
  if (conflicting.length > 0) {
94
102
  throw new Error(`Conflicting configuration in ${fileForErrorMessage} and ${conflicting.map(({fileForErrorMessage}) => fileForErrorMessage).join(' & ')}`);
@@ -118,7 +126,7 @@ async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {
118
126
  }
119
127
  }
120
128
 
121
- const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir};
129
+ const config = {...defaults, nonSemVerExperiments: {}, ...fileConf, ...packageConf, projectDir, configFile};
122
130
 
123
131
  const {nonSemVerExperiments: experiments} = config;
124
132
  if (!isPlainObject(experiments)) {
@@ -133,5 +141,3 @@ async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {
133
141
 
134
142
  return config;
135
143
  }
136
-
137
- exports.loadConfig = loadConfig;
@@ -54,12 +54,12 @@ const deriveFromArray = (extensions, defaultModuleType) => {
54
54
  return moduleTypes;
55
55
  };
56
56
 
57
- module.exports = (configuredExtensions, defaultModuleType) => {
57
+ export default function moduleTypes(configuredExtensions, defaultModuleType) {
58
58
  if (configuredExtensions === undefined) {
59
59
  return {
60
60
  cjs: 'commonjs',
61
61
  mjs: 'module',
62
- js: defaultModuleType
62
+ js: defaultModuleType,
63
63
  };
64
64
  }
65
65
 
@@ -68,4 +68,4 @@ module.exports = (configuredExtensions, defaultModuleType) => {
68
68
  }
69
69
 
70
70
  return deriveFromObject(configuredExtensions, defaultModuleType);
71
- };
71
+ }
@@ -1,7 +1,8 @@
1
- 'use strict';
2
- const arrgv = require('arrgv');
1
+ import process from 'node:process';
3
2
 
4
- function normalizeNodeArguments(fromConf = [], fromArgv = '') {
3
+ import arrgv from 'arrgv';
4
+
5
+ export default function normalizeNodeArguments(fromConf = [], fromArgv = '') {
5
6
  let parsedArgv = [];
6
7
  if (fromArgv !== '') {
7
8
  try {
@@ -13,5 +14,3 @@ function normalizeNodeArguments(fromConf = [], fromArgv = '') {
13
14
 
14
15
  return [...process.execArgv, ...fromConf, ...parsedArgv];
15
16
  }
16
-
17
- module.exports = normalizeNodeArguments;
File without changes