ava 5.3.0 → 6.0.0

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 (41) hide show
  1. package/entrypoints/internal.d.mts +7 -0
  2. package/lib/api-event-iterator.js +12 -0
  3. package/lib/api.js +14 -23
  4. package/lib/assert.js +289 -444
  5. package/lib/cli.js +95 -61
  6. package/lib/code-excerpt.js +2 -2
  7. package/lib/eslint-plugin-helper-worker.js +3 -3
  8. package/lib/fork.js +3 -13
  9. package/lib/glob-helpers.cjs +1 -9
  10. package/lib/globs.js +7 -3
  11. package/lib/like-selector.js +26 -17
  12. package/lib/line-numbers.js +1 -1
  13. package/lib/load-config.js +3 -3
  14. package/lib/parse-test-args.js +3 -3
  15. package/lib/plugin-support/shared-workers.js +4 -4
  16. package/lib/provider-manager.js +11 -13
  17. package/lib/reporters/beautify-stack.js +0 -1
  18. package/lib/reporters/default.js +92 -45
  19. package/lib/reporters/format-serialized-error.js +6 -6
  20. package/lib/reporters/improper-usage-messages.js +5 -5
  21. package/lib/reporters/tap.js +30 -30
  22. package/lib/run-status.js +9 -0
  23. package/lib/runner.js +7 -7
  24. package/lib/scheduler.js +14 -1
  25. package/lib/serialize-error.js +44 -116
  26. package/lib/slash.cjs +1 -1
  27. package/lib/snapshot-manager.js +14 -8
  28. package/lib/test.js +90 -81
  29. package/lib/watcher.js +494 -365
  30. package/lib/worker/base.js +90 -51
  31. package/lib/worker/channel.cjs +9 -53
  32. package/license +1 -1
  33. package/package.json +36 -42
  34. package/readme.md +6 -12
  35. package/types/assertions.d.cts +107 -49
  36. package/types/shared-worker.d.cts +0 -2
  37. package/types/state-change-events.d.cts +143 -0
  38. package/types/test-fn.d.cts +10 -5
  39. package/lib/worker/dependency-tracker.js +0 -48
  40. /package/entrypoints/{main.d.ts → main.d.mts} +0 -0
  41. /package/entrypoints/{plugin.d.ts → plugin.d.mts} +0 -0
package/lib/cli.js CHANGED
@@ -1,30 +1,25 @@
1
-
2
1
  import fs from 'node:fs';
3
2
  import path from 'node:path';
4
3
  import process from 'node:process';
4
+ import v8 from 'node:v8';
5
5
 
6
6
  import arrify from 'arrify';
7
- import ciParallelVars from 'ci-parallel-vars';
8
7
  import figures from 'figures';
9
8
  import yargs from 'yargs';
10
- import {hideBin} from 'yargs/helpers'; // eslint-disable-line n/file-extension-in-import
9
+ import {hideBin} from 'yargs/helpers';
11
10
 
11
+ import {asyncEventIteratorFromApi} from './api-event-iterator.js';
12
12
  import Api from './api.js';
13
13
  import {chalk} from './chalk.js';
14
14
  import validateEnvironmentVariables from './environment-variables.js';
15
15
  import normalizeExtensions from './extensions.js';
16
16
  import {normalizeGlobs, normalizePattern} from './globs.js';
17
- import {controlFlow} from './ipc-flow-control.cjs';
18
17
  import isCi from './is-ci.js';
19
18
  import {splitPatternAndLineNumbers} from './line-numbers.js';
20
19
  import {loadConfig} from './load-config.js';
21
20
  import normalizeModuleTypes from './module-types.js';
22
21
  import normalizeNodeArguments from './node-arguments.js';
23
22
  import pkg from './pkg.cjs';
24
- import providerManager from './provider-manager.js';
25
- import DefaultReporter from './reporters/default.js';
26
- import TapReporter from './reporters/tap.js';
27
- import Watcher from './watcher.js';
28
23
 
29
24
  function exit(message) {
30
25
  console.error(`\n ${chalk.red(figures.cross)} ${message}`);
@@ -174,7 +169,7 @@ export default async function loadCli() { // eslint-disable-line complexity
174
169
  type: 'string',
175
170
  }), argv => {
176
171
  if (activeInspector) {
177
- debug.files = argv.pattern || [];
172
+ debug.files = argv.pattern ?? [];
178
173
  }
179
174
  })
180
175
  .command(
@@ -224,7 +219,7 @@ export default async function loadCli() { // eslint-disable-line complexity
224
219
  const combined = {...conf};
225
220
 
226
221
  for (const flag of Object.keys(FLAGS)) {
227
- if (flag === 'no-worker-threads' && Reflect.has(argv, 'worker-threads')) {
222
+ if (flag === 'no-worker-threads' && Object.hasOwn(argv, 'worker-threads')) {
228
223
  combined.workerThreads = argv['worker-threads'];
229
224
  continue;
230
225
  }
@@ -284,7 +279,7 @@ export default async function loadCli() { // eslint-disable-line complexity
284
279
 
285
280
  process.exit(0); // eslint-disable-line unicorn/no-process-exit
286
281
  } catch (error) {
287
- exit(`Error removing AVA cache files in ${cacheDir}\n\n${chalk.gray((error && error.stack) || error)}`);
282
+ exit(`Error removing AVA cache files in ${cacheDir}\n\n${chalk.gray(error?.stack ?? error)}`);
288
283
  }
289
284
  }
290
285
 
@@ -297,7 +292,7 @@ export default async function loadCli() { // eslint-disable-line complexity
297
292
  exit('Watch mode is not available in CI, as it prevents AVA from terminating.');
298
293
  }
299
294
 
300
- if (debug !== null) {
295
+ if (debug !== null && !process.env.TEST_AVA) {
301
296
  exit('Watch mode is not available when debugging.');
302
297
  }
303
298
  }
@@ -316,32 +311,24 @@ export default async function loadCli() { // eslint-disable-line complexity
316
311
  }
317
312
  }
318
313
 
319
- if (Reflect.has(combined, 'concurrency') && (!Number.isInteger(combined.concurrency) || combined.concurrency < 0)) {
320
- exit('The --concurrency or -c flag must be provided with a nonnegative integer.');
321
- }
322
-
323
- if (!combined.tap && Object.keys(experiments).length > 0) {
324
- console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`));
314
+ if (Object.hasOwn(combined, 'concurrency') && (!Number.isInteger(combined.concurrency) || combined.concurrency < 0)) {
315
+ exit('The --concurrency or -c flag must be provided with a non-negative integer.');
325
316
  }
326
317
 
327
- if (Reflect.has(conf, 'babel')) {
328
- exit('Built-in Babel support has been removed.');
329
- }
330
-
331
- if (Reflect.has(conf, 'compileEnhancements')) {
332
- exit('Enhancement compilation must be configured in AVA’s Babel options.');
318
+ if (Object.hasOwn(conf, 'sortTestFiles') && typeof conf.sortTestFiles !== 'function') {
319
+ exit('’sortTestFiles’ must be a comparator function.');
333
320
  }
334
321
 
335
- if (Reflect.has(conf, 'helpers')) {
336
- exit('AVA no longer compiles helpers. Add exclusion patterns to the filesconfiguration and specify ’compileAsTests’ in the Babel options instead.');
322
+ if (Object.hasOwn(conf, 'watch')) {
323
+ exit('’watchmust not be configured, use the --watch CLI flag instead.');
337
324
  }
338
325
 
339
- if (Reflect.has(conf, 'sources')) {
340
- exit('’sources’ has been removed. Use ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.');
326
+ if (Object.hasOwn(conf, 'ignoredByWatcher')) {
327
+ exit('’ignoredByWatcher’ has moved towatchMode.ignoreChanges’.');
341
328
  }
342
329
 
343
- if (Reflect.has(conf, 'sortTestFiles') && typeof conf.sortTestFiles !== 'function') {
344
- exit('’sortTestFiles’ must be a comparator function.');
330
+ if (!combined.tap && Object.keys(experiments).length > 0) {
331
+ console.log(chalk.magenta(` ${figures.warning} Experiments are enabled. These are unsupported and may change or be removed at any time.`));
345
332
  }
346
333
 
347
334
  let projectPackageObject;
@@ -353,14 +340,16 @@ export default async function loadCli() { // eslint-disable-line complexity
353
340
  }
354
341
  }
355
342
 
356
- const {type: defaultModuleType = 'commonjs'} = projectPackageObject || {};
343
+ const {type: defaultModuleType = 'commonjs'} = projectPackageObject ?? {};
357
344
 
358
345
  const providers = [];
359
- if (Reflect.has(conf, 'typescript')) {
346
+ if (Object.hasOwn(conf, 'typescript')) {
347
+ const {default: providerManager} = await import('./provider-manager.js');
360
348
  try {
361
- const {level, main} = await providerManager.typescript(projectDir);
349
+ const {identifier: protocol, level, main} = await providerManager.typescript(projectDir);
362
350
  providers.push({
363
351
  level,
352
+ protocol,
364
353
  main: main({config: conf.typescript}),
365
354
  type: 'typescript',
366
355
  });
@@ -392,7 +381,7 @@ export default async function loadCli() { // eslint-disable-line complexity
392
381
 
393
382
  let globs;
394
383
  try {
395
- globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.ignoredByWatcher, extensions, providers});
384
+ globs = normalizeGlobs({files: conf.files, ignoredByWatcher: conf.watchMode?.ignoreChanges, extensions, providers});
396
385
  } catch (error) {
397
386
  exit(error.message);
398
387
  }
@@ -405,14 +394,17 @@ export default async function loadCli() { // eslint-disable-line complexity
405
394
  }
406
395
 
407
396
  let parallelRuns = null;
408
- if (isCi && ciParallelVars && combined.utilizeParallelBuilds !== false) {
409
- const {index: currentIndex, total: totalRuns} = ciParallelVars;
410
- parallelRuns = {currentIndex, totalRuns};
397
+ if (isCi && combined.utilizeParallelBuilds !== false) {
398
+ const {default: ciParallelVars} = await import('ci-parallel-vars');
399
+ if (ciParallelVars) {
400
+ const {index: currentIndex, total: totalRuns} = ciParallelVars;
401
+ parallelRuns = {currentIndex, totalRuns};
402
+ }
411
403
  }
412
404
 
413
405
  const match = combined.match === '' ? [] : arrify(combined.match);
414
406
 
415
- const input = debug ? debug.files : (argv.pattern || []);
407
+ const input = debug ? debug.files : (argv.pattern ?? []);
416
408
  const filter = input
417
409
  .map(pattern => splitPatternAndLineNumbers(pattern))
418
410
  .map(({pattern, ...rest}) => ({
@@ -423,7 +415,7 @@ export default async function loadCli() { // eslint-disable-line complexity
423
415
  const api = new Api({
424
416
  cacheEnabled: combined.cache !== false,
425
417
  chalkOptions,
426
- concurrency: combined.concurrency || 0,
418
+ concurrency: combined.concurrency ?? 0,
427
419
  workerThreads: combined.workerThreads !== false,
428
420
  debug,
429
421
  environmentVariables,
@@ -443,36 +435,58 @@ export default async function loadCli() { // eslint-disable-line complexity
443
435
  require: arrify(combined.require),
444
436
  serial: combined.serial,
445
437
  snapshotDir: combined.snapshotDir ? path.resolve(projectDir, combined.snapshotDir) : null,
446
- timeout: combined.timeout || '10s',
438
+ timeout: combined.timeout ?? '10s',
447
439
  updateSnapshots: combined.updateSnapshots,
448
440
  workerArgv: argv['--'],
449
441
  });
450
442
 
451
- const reporter = combined.tap && !combined.watch && debug === null ? new TapReporter({
452
- extensions: globs.extensions,
453
- projectDir,
454
- reportStream: process.stdout,
455
- stdStream: process.stderr,
456
- }) : new DefaultReporter({
457
- extensions: globs.extensions,
458
- projectDir,
459
- reportStream: process.stdout,
460
- stdStream: process.stderr,
461
- watching: combined.watch,
462
- });
463
-
464
- api.on('run', plan => {
465
- reporter.startRun(plan);
443
+ let reporter;
444
+ if (combined.tap && !argv.watch && debug === null) {
445
+ const {default: TapReporter} = await import('./reporters/tap.js');
446
+ reporter = new TapReporter({
447
+ extensions: globs.extensions,
448
+ projectDir,
449
+ reportStream: process.stdout,
450
+ stdStream: process.stderr,
451
+ });
452
+ } else {
453
+ const {default: Reporter} = await import('./reporters/default.js');
454
+ reporter = new Reporter({
455
+ extensions: globs.extensions,
456
+ projectDir,
457
+ reportStream: process.stdout,
458
+ stdStream: process.stderr,
459
+ watching: argv.watch,
460
+ });
461
+ }
466
462
 
467
- if (process.env.AVA_EMIT_RUN_STATUS_OVER_IPC === 'I\'ll find a payphone baby / Take some time to talk to you') {
468
- const bufferedSend = controlFlow(process);
463
+ if (process.env.TEST_AVA) {
464
+ const {controlFlow} = await import('./ipc-flow-control.cjs');
465
+ const bufferedSend = controlFlow(process);
469
466
 
467
+ api.on('run', plan => {
470
468
  plan.status.on('stateChange', evt => {
471
469
  bufferedSend(evt);
472
470
  });
473
- }
471
+ });
472
+ }
473
+
474
+ if (combined.observeRun && experiments.observeRunsFromConfig) {
475
+ combined.observeRun({
476
+ events: asyncEventIteratorFromApi(api),
477
+ });
478
+ }
479
+
480
+ api.on('run', plan => {
481
+ reporter.startRun(plan);
474
482
 
475
483
  plan.status.on('stateChange', evt => {
484
+ if (evt.type === 'end' || evt.type === 'interrupt') {
485
+ // Write out code coverage data when the run ends, lest a process
486
+ // interrupt causes it to be lost.
487
+ v8.takeCoverage();
488
+ }
489
+
476
490
  if (evt.type === 'interrupt') {
477
491
  reporter.endRun();
478
492
  process.exit(1); // eslint-disable-line unicorn/no-process-exit
@@ -480,16 +494,36 @@ export default async function loadCli() { // eslint-disable-line complexity
480
494
  });
481
495
  });
482
496
 
483
- if (combined.watch) {
484
- const watcher = new Watcher({
497
+ if (argv.watch) {
498
+ const {available, start} = await import('./watcher.js');
499
+ if (!available(projectDir)) {
500
+ exit('Watch mode requires support for recursive fs.watch()');
501
+ return;
502
+ }
503
+
504
+ let abortController;
505
+ if (process.env.TEST_AVA) {
506
+ const {takeCoverage} = await import('node:v8');
507
+ abortController = new AbortController();
508
+ process.on('message', message => {
509
+ if (message === 'abort-watcher') {
510
+ abortController.abort();
511
+ takeCoverage();
512
+ }
513
+ });
514
+ process.channel?.unref();
515
+ }
516
+
517
+ start({
485
518
  api,
486
519
  filter,
487
520
  globs,
488
521
  projectDir,
489
522
  providers,
490
523
  reporter,
524
+ stdin: process.stdin,
525
+ signal: abortController.signal,
491
526
  });
492
- watcher.observeStdin(process.stdin);
493
527
  } else {
494
528
  let debugWithoutSpecificFile = false;
495
529
  api.on('run', plan => {
@@ -8,13 +8,13 @@ import {chalk} from './chalk.js';
8
8
  const formatLineNumber = (lineNumber, maxLineNumber) =>
9
9
  ' '.repeat(Math.max(0, String(maxLineNumber).length - String(lineNumber).length)) + lineNumber;
10
10
 
11
- export default function exceptCode(source, options = {}) {
11
+ export default function excerptCode(source, options = {}) {
12
12
  if (!source.isWithinProject || source.isDependency) {
13
13
  return null;
14
14
  }
15
15
 
16
16
  const {file, line} = source;
17
- const maxWidth = options.maxWidth || 80;
17
+ const maxWidth = (options.maxWidth ?? 0) || 80;
18
18
 
19
19
  let contents;
20
20
  try {
@@ -12,8 +12,8 @@ const configCache = new Map();
12
12
 
13
13
  const collectProviders = async ({conf, projectDir}) => {
14
14
  const providers = [];
15
- if (Reflect.has(conf, 'typescript')) {
16
- const {level, main} = await providerManager.typescript(projectDir);
15
+ if (Object.hasOwn(conf, 'typescript')) {
16
+ const {level, main} = await providerManager.typescript(projectDir, {fullConfig: conf});
17
17
  providers.push({
18
18
  level,
19
19
  main: main({config: conf.typescript}),
@@ -33,7 +33,7 @@ const buildGlobs = ({conf, providers, projectDir, overrideExtensions, overrideFi
33
33
  cwd: projectDir,
34
34
  ...normalizeGlobs({
35
35
  extensions,
36
- files: overrideFiles || conf.files,
36
+ files: overrideFiles ?? conf.files,
37
37
  providers,
38
38
  }),
39
39
  };
package/lib/fork.js CHANGED
@@ -4,10 +4,9 @@ import {fileURLToPath} from 'node:url';
4
4
  import {Worker} from 'node:worker_threads';
5
5
 
6
6
  import Emittery from 'emittery';
7
- import {pEvent} from 'p-event';
8
7
 
9
8
  import {controlFlow} from './ipc-flow-control.cjs';
10
- import serializeError from './serialize-error.js';
9
+ import serializeError, {tagWorkerError} from './serialize-error.js';
11
10
 
12
11
  let workerPath = new URL('worker/base.js', import.meta.url);
13
12
  export function _testOnlyReplaceWorkerPath(replacement) {
@@ -35,13 +34,8 @@ const createWorker = (options, execArgv) => {
35
34
  });
36
35
  postMessage = worker.postMessage.bind(worker);
37
36
 
38
- // Ensure we've seen this event before we terminate the worker thread, as a
39
- // workaround for https://github.com/nodejs/node/issues/38418.
40
- const starting = pEvent(worker, 'message', ({ava}) => ava && ava.type === 'starting');
41
-
42
37
  close = async () => {
43
38
  try {
44
- await starting;
45
39
  await worker.terminate();
46
40
  } finally {
47
41
  // No-op
@@ -53,6 +47,7 @@ const createWorker = (options, execArgv) => {
53
47
  silent: true,
54
48
  env: {NODE_ENV: 'test', ...process.env, ...options.environmentVariables},
55
49
  execArgv: [...execArgv, ...additionalExecArgv],
50
+ serialization: 'advanced',
56
51
  });
57
52
  postMessage = controlFlow(worker);
58
53
  close = async () => worker.kill();
@@ -127,11 +122,6 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
127
122
  break;
128
123
  }
129
124
 
130
- case 'ping': {
131
- send({type: 'pong'});
132
- break;
133
- }
134
-
135
125
  default: {
136
126
  emitStateChange(message.ava);
137
127
  }
@@ -139,7 +129,7 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
139
129
  });
140
130
 
141
131
  worker.on('error', error => {
142
- emitStateChange({type: 'worker-failed', err: serializeError('Worker error', false, error, file)});
132
+ emitStateChange({type: 'worker-failed', err: serializeError(tagWorkerError(error))});
143
133
  finish();
144
134
  });
145
135
 
@@ -16,8 +16,6 @@ const defaultPicomatchIgnorePatterns = [
16
16
  ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
17
17
  ];
18
18
 
19
- const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns);
20
-
21
19
  const matchingCache = new WeakMap();
22
20
  const processMatchingPatterns = input => {
23
21
  let result = matchingCache.get(input);
@@ -46,15 +44,9 @@ const processMatchingPatterns = input => {
46
44
 
47
45
  exports.processMatchingPatterns = processMatchingPatterns;
48
46
 
49
- const matchesIgnorePatterns = (file, patterns) => {
50
- const {matchNoIgnore} = processMatchingPatterns(patterns);
51
- return matchNoIgnore(file) || defaultMatchNoIgnore(file);
52
- };
53
-
54
- function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) {
47
+ function classify(file, {cwd, extensions, filePatterns}) {
55
48
  file = normalizeFileForMatching(cwd, file);
56
49
  return {
57
- isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns),
58
50
  isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns),
59
51
  };
60
52
  }
package/lib/globs.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  import {globby, globbySync} from 'globby';
5
+ import picomatch from 'picomatch';
5
6
 
6
7
  import {
7
8
  defaultIgnorePatterns,
@@ -26,6 +27,7 @@ const defaultIgnoredByWatcherPatterns = [
26
27
  '**/*.snap.md', // No need to rerun tests when the Markdown files change.
27
28
  'ava.config.js', // Config is not reloaded so avoid rerunning tests when it changes.
28
29
  'ava.config.cjs', // Config is not reloaded so avoid rerunning tests when it changes.
30
+ 'ava.config.mjs', // Config is not reloaded so avoid rerunning tests when it changes.
29
31
  ];
30
32
 
31
33
  const buildExtensionPattern = extensions => extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`;
@@ -36,7 +38,7 @@ export function normalizeGlobs({extensions, files: filePatterns, ignoredByWatche
36
38
  }
37
39
 
38
40
  if (ignoredByWatcherPatterns !== undefined && (!Array.isArray(ignoredByWatcherPatterns) || ignoredByWatcherPatterns.length === 0)) {
39
- throw new Error('The ’ignoredByWatcher’ configuration must be an array containing glob patterns.');
41
+ throw new Error('The ’watchMode.ignoreChanges’ configuration must be an array containing glob patterns.');
40
42
  }
41
43
 
42
44
  const extensionPattern = buildExtensionPattern(extensions);
@@ -125,11 +127,13 @@ export async function findTests({cwd, extensions, filePatterns}) {
125
127
  return files.filter(file => !path.basename(file).startsWith('_'));
126
128
  }
127
129
 
128
- export function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) {
129
- return [
130
+ export function buildIgnoreMatcher({ignoredByWatcherPatterns}) {
131
+ const patterns = [
130
132
  ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`),
131
133
  ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')),
132
134
  ];
135
+
136
+ return picomatch(patterns, {dot: true});
133
137
  }
134
138
 
135
139
  export function applyTestFileFilter({ // eslint-disable-line complexity
@@ -1,32 +1,41 @@
1
- const isObject = selector => Reflect.getPrototypeOf(selector) === Object.prototype;
1
+ const isPrimitive = value => value === null || typeof value !== 'object';
2
2
 
3
3
  export function isLikeSelector(selector) {
4
- if (selector === null || typeof selector !== 'object') {
4
+ // Require selector to be an array or plain object.
5
+ if (
6
+ isPrimitive(selector)
7
+ || (!Array.isArray(selector) && Reflect.getPrototypeOf(selector) !== Object.prototype)
8
+ ) {
5
9
  return false;
6
10
  }
7
11
 
8
- const keyCount = Reflect.ownKeys(selector).length;
9
- return (Array.isArray(selector) && keyCount > 1) || (isObject(selector) && keyCount > 0);
12
+ // Also require at least one enumerable property.
13
+ const descriptors = Object.getOwnPropertyDescriptors(selector);
14
+ return Reflect.ownKeys(descriptors).some(key => descriptors[key].enumerable === true);
10
15
  }
11
16
 
12
17
  export const CIRCULAR_SELECTOR = new Error('Encountered a circular selector');
13
18
 
14
- export function selectComparable(lhs, selector, circular = new Set()) {
15
- if (circular.has(selector)) {
16
- throw CIRCULAR_SELECTOR;
17
- }
18
-
19
- circular.add(selector);
20
-
21
- if (lhs === null || typeof lhs !== 'object') {
22
- return lhs;
19
+ export function selectComparable(actual, selector, circular = [selector]) {
20
+ if (isPrimitive(actual)) {
21
+ return actual;
23
22
  }
24
23
 
25
24
  const comparable = Array.isArray(selector) ? [] : {};
26
- for (const [key, rhs] of Object.entries(selector)) {
27
- comparable[key] = isLikeSelector(rhs)
28
- ? selectComparable(Reflect.get(lhs, key), rhs, circular)
29
- : Reflect.get(lhs, key);
25
+ const enumerableKeys = Reflect.ownKeys(selector).filter(key => Reflect.getOwnPropertyDescriptor(selector, key).enumerable);
26
+ for (const key of enumerableKeys) {
27
+ const subselector = Reflect.get(selector, key);
28
+ if (isLikeSelector(subselector)) {
29
+ if (circular.includes(subselector)) {
30
+ throw CIRCULAR_SELECTOR;
31
+ }
32
+
33
+ circular.push(subselector);
34
+ comparable[key] = selectComparable(Reflect.get(actual, key), subselector, circular);
35
+ circular.pop();
36
+ } else {
37
+ comparable[key] = Reflect.get(actual, key);
38
+ }
30
39
  }
31
40
 
32
41
  return comparable;
@@ -13,7 +13,7 @@ const sortNumbersAscending = array => {
13
13
  };
14
14
 
15
15
  const parseNumber = string => Number.parseInt(string, 10);
16
- const removeAllWhitespace = string => string.replace(/\s/g, '');
16
+ const removeAllWhitespace = string => string.replaceAll(/\s/g, '');
17
17
  const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);
18
18
 
19
19
  const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(
@@ -4,11 +4,11 @@ import process from 'node:process';
4
4
  import url from 'node:url';
5
5
 
6
6
  import {isPlainObject} from 'is-plain-object';
7
- import {packageConfig, packageJsonPath} from 'pkg-conf';
7
+ import {packageConfig, packageJsonPath} from 'package-config';
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();
11
+ const EXPERIMENTS = new Set(['observeRunsFromConfig']);
12
12
 
13
13
  const importConfig = async ({configFile, fileForErrorMessage}) => {
14
14
  const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile));
@@ -41,7 +41,7 @@ function resolveConfigFile(configFile) {
41
41
  return configFile;
42
42
  }
43
43
 
44
- const gitScmFile = process.env.AVA_FAKE_SCM_ROOT || '.git';
44
+ const gitScmFile = process.env.AVA_FAKE_SCM_ROOT ?? '.git';
45
45
 
46
46
  async function findRepoRoot(fromDir) {
47
47
  const {root} = path.parse(fromDir);
@@ -1,8 +1,8 @@
1
1
  const buildTitle = (raw, implementation, args) => {
2
- let value = implementation && implementation.title ? implementation.title(raw, ...args) : raw;
2
+ let value = implementation?.title?.(raw, ...args) ?? raw;
3
3
  const isValid = typeof value === 'string';
4
4
  if (isValid) {
5
- value = value.trim().replace(/\s+/g, ' ');
5
+ value = value.trim().replaceAll(/\s+/g, ' ');
6
6
  }
7
7
 
8
8
  return {
@@ -20,7 +20,7 @@ export default function parseTestArgs(args) {
20
20
 
21
21
  return {
22
22
  args,
23
- implementation: implementation && implementation.exec ? implementation.exec : implementation,
23
+ implementation: implementation?.exec ?? implementation,
24
24
  title: buildTitle(rawTitle, implementation, args),
25
25
  };
26
26
  }
@@ -2,7 +2,7 @@ import events from 'node:events';
2
2
  import {pathToFileURL} from 'node:url';
3
3
  import {Worker} from 'node:worker_threads';
4
4
 
5
- import serializeError from '../serialize-error.js';
5
+ import serializeError, {tagWorkerError} from '../serialize-error.js';
6
6
 
7
7
  const LOADER = new URL('shared-worker-loader.js', import.meta.url);
8
8
 
@@ -34,7 +34,7 @@ function launchWorker(filename, initialData) {
34
34
  const launched = {
35
35
  statePromises: {
36
36
  available: waitForAvailable(worker),
37
- error: events.once(worker, 'error').then(([error]) => error),
37
+ error: events.once(worker, 'error').then(([error]) => tagWorkerError(error)),
38
38
  },
39
39
  exited: false,
40
40
  worker,
@@ -97,14 +97,14 @@ export async function observeWorkerProcess(fork, runStatus) {
97
97
  launched.statePromises.error.then(error => {
98
98
  launched.worker.off('message', handleWorkerMessage);
99
99
  removeAllInstances();
100
- runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError('Shared worker error', true, error)});
100
+ runStatus.emitStateChange({type: 'shared-worker-error', err: serializeError(error)});
101
101
  signalError();
102
102
  });
103
103
 
104
104
  try {
105
105
  await launched.statePromises.available;
106
106
 
107
- port.postMessage({type: 'ready'});
107
+ port.postMessage({ava: {type: 'ready'}});
108
108
 
109
109
  launched.worker.postMessage({
110
110
  type: 'register-test-worker',
@@ -1,19 +1,17 @@
1
1
  import * as globs from './globs.js';
2
2
  import pkg from './pkg.cjs';
3
3
 
4
- const levels = {
4
+ export const levels = {
5
5
  // As the protocol changes, comparing levels by integer allows AVA to be
6
- // compatible with different versions. Currently there is only one supported
7
- // version, so this is effectively unused. The infrastructure is retained for
8
- // future use.
9
- levelIntegersAreCurrentlyUnused: 0,
6
+ // compatible with different versions.
7
+ ava6: 1,
10
8
  };
11
9
 
12
- const levelsByProtocol = {
13
- 'ava-3.2': levels.levelIntegersAreCurrentlyUnused,
14
- };
10
+ const levelsByProtocol = Object.assign(Object.create(null), {
11
+ 'ava-6': levels.ava6,
12
+ });
15
13
 
16
- async function load(providerModule, projectDir) {
14
+ async function load(providerModule, projectDir, selectProtocol = () => true) {
17
15
  const ava = {version: pkg.version};
18
16
  const {default: makeProvider} = await import(providerModule);
19
17
 
@@ -21,7 +19,8 @@ async function load(providerModule, projectDir) {
21
19
  let level;
22
20
  const provider = makeProvider({
23
21
  negotiateProtocol(identifiers, {version}) {
24
- const identifier = identifiers.find(identifier => Reflect.has(levelsByProtocol, identifier));
22
+ const identifier = identifiers
23
+ .find(identifier => selectProtocol(identifier) && Object.hasOwn(levelsByProtocol, identifier));
25
24
 
26
25
  if (identifier === undefined) {
27
26
  fatal = new Error(`This version of AVA (${ava.version}) is not compatible with ${providerModule}@${version}`);
@@ -50,9 +49,8 @@ async function load(providerModule, projectDir) {
50
49
  }
51
50
 
52
51
  const providerManager = {
53
- levels,
54
- async typescript(projectDir) {
55
- return load('@ava/typescript', projectDir);
52
+ async typescript(projectDir, {protocol} = {}) {
53
+ return load('@ava/typescript', projectDir, identifier => protocol === undefined || identifier === protocol);
56
54
  },
57
55
  };
58
56
 
@@ -4,7 +4,6 @@ const stackUtils = new StackUtils({
4
4
  ignoredPackages: [
5
5
  '@ava/typescript',
6
6
  'ava',
7
- 'nyc',
8
7
  ],
9
8
  internals: [
10
9
  // AVA internals, which ignoredPackages don't ignore when we run our own unit tests.