diff --git a/lib/api.js b/lib/api.js index eaf223650..c9e42a34d 100644 --- a/lib/api.js +++ b/lib/api.js @@ -200,7 +200,6 @@ export default class Api extends Emittery { await this.emit('run', { bailWithoutReporting: debugWithoutSpecificFile, - clearLogOnNextRun: runtimeOptions.clearLogOnNextRun === true, debug: Boolean(this.options.debug), failFastEnabled: failFast, filePathPrefix: getFilePathPrefix(selectedFiles), @@ -208,7 +207,7 @@ export default class Api extends Emittery { matching: apiOptions.match.length > 0, previousFailures: runtimeOptions.previousFailures || 0, runOnlyExclusive: runtimeOptions.runOnlyExclusive === true, - runVector: runtimeOptions.runVector || 0, + firstRun: runtimeOptions.firstRun ?? true, status: runStatus, }); @@ -306,7 +305,8 @@ export default class Api extends Emittery { // Allow shared workers to clean up before the run ends. await Promise.all(deregisteredSharedWorkers); - scheduler.storeFailedTestFiles(runStatus, this.options.cacheEnabled === false ? null : this._createCacheDir()); + const files = scheduler.storeFailedTestFiles(runStatus, this.options.cacheEnabled === false ? null : this._createCacheDir()); + runStatus.emitStateChange({type: 'touched-files', files}); } catch (error) { if (error && error.name === 'AggregateError') { for (const error_ of error.errors) { diff --git a/lib/cli.js b/lib/cli.js index afe552801..9682356a1 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -25,7 +25,7 @@ import pkg from './pkg.cjs'; import providerManager from './provider-manager.js'; import DefaultReporter from './reporters/default.js'; import TapReporter from './reporters/tap.js'; -import Watcher from './watcher.js'; +import {start as startWatcher} from './watcher.js'; function exit(message) { console.error(`\n ${chalk.red(figures.cross)} ${message}`); @@ -469,15 +469,15 @@ export default async function loadCli() { // eslint-disable-line complexity }); if (combined.watch) { - const watcher = new Watcher({ + startWatcher({ api, filter, globs, projectDir, providers, reporter, + stdin: process.stdin, }); - watcher.observeStdin(process.stdin); } else { let debugWithoutSpecificFile = false; api.on('run', plan => { diff --git a/lib/glob-helpers.cjs b/lib/glob-helpers.cjs index 6e42c550f..c0d1bfef6 100644 --- a/lib/glob-helpers.cjs +++ b/lib/glob-helpers.cjs @@ -15,8 +15,6 @@ const defaultPicomatchIgnorePatterns = [ ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`), ]; -const defaultMatchNoIgnore = picomatch(defaultPicomatchIgnorePatterns); - const matchingCache = new WeakMap(); const processMatchingPatterns = input => { let result = matchingCache.get(input); @@ -45,15 +43,9 @@ const processMatchingPatterns = input => { exports.processMatchingPatterns = processMatchingPatterns; -const matchesIgnorePatterns = (file, patterns) => { - const {matchNoIgnore} = processMatchingPatterns(patterns); - return matchNoIgnore(file) || defaultMatchNoIgnore(file); -}; - -function classify(file, {cwd, extensions, filePatterns, ignoredByWatcherPatterns}) { +function classify(file, {cwd, extensions, filePatterns}) { file = normalizeFileForMatching(cwd, file); return { - isIgnoredByWatcher: matchesIgnorePatterns(file, ignoredByWatcherPatterns), isTest: hasExtension(extensions, file) && !isHelperish(file) && filePatterns.length > 0 && matches(file, filePatterns), }; } diff --git a/lib/globs.js b/lib/globs.js index c1a88cf7b..9e973cdf0 100644 --- a/lib/globs.js +++ b/lib/globs.js @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import {globby, globbySync} from 'globby'; +import picomatch from 'picomatch'; import { defaultIgnorePatterns, @@ -126,11 +127,13 @@ export async function findTests({cwd, extensions, filePatterns}) { return files.filter(file => !path.basename(file).startsWith('_')); } -export function getChokidarIgnorePatterns({ignoredByWatcherPatterns}) { - return [ +export function buildIgnoreMatcher({ignoredByWatcherPatterns}) { + const patterns = [ ...defaultIgnorePatterns.map(pattern => `${pattern}/**/*`), ...ignoredByWatcherPatterns.filter(pattern => !pattern.startsWith('!')), ]; + + return picomatch(patterns, {dot: true}); } export function applyTestFileFilter({ // eslint-disable-line complexity diff --git a/lib/reporters/default.js b/lib/reporters/default.js index 804e285cc..82d6cdd25 100644 --- a/lib/reporters/default.js +++ b/lib/reporters/default.js @@ -148,7 +148,7 @@ export default class Reporter { this.consumeStateChange(evt); }); - if (this.watching && plan.runVector > 1) { + if (this.watching && !plan.firstRun) { this.lineWriter.write(chalk.gray.dim('\u2500'.repeat(this.lineWriter.columns)) + os.EOL); } diff --git a/lib/scheduler.js b/lib/scheduler.js index b64c69225..c78dd3234 100644 --- a/lib/scheduler.js +++ b/lib/scheduler.js @@ -13,9 +13,22 @@ const scheduler = { return; } + const filename = path.join(cacheDir, FILENAME); + // Given that we're writing to a cache directory, consider this file + // temporary. + const temporaryFiles = [filename]; try { - writeFileAtomic.sync(path.join(cacheDir, FILENAME), JSON.stringify(runStatus.getFailedTestFiles())); + writeFileAtomic.sync(filename, JSON.stringify(runStatus.getFailedTestFiles()), { + tmpfileCreated(tmpfile) { + temporaryFiles.push(tmpfile); + }, + }); } catch {} + + return { + changedFiles: [], + temporaryFiles, + }; }, // Order test-files, so that files with failing tests come first diff --git a/lib/watcher.js b/lib/watcher.js index c1184f74f..cfd758b86 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -1,465 +1,417 @@ +import fs from 'node:fs'; import nodePath from 'node:path'; -import chokidar from 'chokidar'; +import {nodeFileTrace} from '@vercel/nft'; import createDebug from 'debug'; import {chalk} from './chalk.js'; -import {applyTestFileFilter, classify, getChokidarIgnorePatterns} from './globs.js'; +import {applyTestFileFilter, classify, buildIgnoreMatcher, findTests} from './globs.js'; const debug = createDebug('ava:watcher'); -function rethrowAsync(error) { - // Don't swallow exceptions. Note that any - // expected error should already have been logged - setImmediate(() => { - throw error; - }); -} - -const MIN_DEBOUNCE_DELAY = 10; -const INITIAL_DEBOUNCE_DELAY = 100; const END_MESSAGE = chalk.gray('Type `r` and press enter to rerun tests\nType `u` and press enter to update snapshots\n'); -class Debouncer { - constructor(watcher) { - this.watcher = watcher; - this.timer = null; - this.repeat = false; +export async function start({api, filter, globs, projectDir, providers, reporter, stdin}) { + for await (const {files, ...runtimeOptions} of plan({api, filter, globs, projectDir, providers, stdin})) { + await api.run({files, filter, runtimeOptions}); + reporter.endRun(); + reporter.lineWriter.writeLine(END_MESSAGE); } +} - debounce(delay) { - if (this.timer) { - this.again = true; - return; - } +async function * plan({api, filter, globs, projectDir, providers, stdin}) { + const fileTracer = new FileTracer({base: projectDir}); + fileTracer.update(findTests({cwd: projectDir, ...globs}).then(testFiles => testFiles.map(path => ({ + path: nodePath.relative(projectDir, path), + isTest: true, + exists: true, + })))); + + const isIgnored = buildIgnoreMatcher(globs); + const patternFilters = filter.map(({pattern}) => pattern); + + const filesWithExclusiveTests = new Set(); + const touchedFiles = new Set(); + const temporaryFiles = new Set(); + const failureCounts = new Map(); + + api.on('run', ({status}) => { + status.on('stateChange', evt => { + switch (evt.type) { + case 'accessed-snapshots': + fileTracer.addDependency(nodePath.relative(projectDir, evt.testFile), nodePath.relative(projectDir, evt.filename)); + break; + case 'touched-files': + for (const file of evt.files.changedFiles) { + touchedFiles.add(nodePath.relative(projectDir, file)); + } - delay = delay ? Math.max(delay, MIN_DEBOUNCE_DELAY) : INITIAL_DEBOUNCE_DELAY; + for (const file of evt.files.temporaryFiles) { + temporaryFiles.add(nodePath.relative(projectDir, file)); + } - const timer = setTimeout(async () => { - await this.watcher.busy; - // Do nothing if debouncing was canceled while waiting for the busy - // promise to fulfil - if (this.timer !== timer) { - return; - } + break; + case 'hook-failed': + case 'internal-error': + case 'process-exit': + case 'test-failed': + case 'uncaught-exception': + case 'unhandled-rejection': + case 'worker-failed': { + failureCounts.set(evt.testFile, 1 + (failureCounts.get(evt.testFile) ?? 0)); + break; + } - if (this.again) { - this.timer = null; - this.again = false; - this.debounce(delay / 2); - } else { - this.watcher.runAfterChanges(); - this.timer = null; - this.again = false; - } - }, delay); + case 'worker-finished': { + const fileStats = status.stats.byFile.get(evt.testFile); + if (fileStats.selectedTests > 0 && fileStats.declaredTests > fileStats.selectedTests) { + filesWithExclusiveTests.add(nodePath.relative(projectDir, evt.testFile)); + } else { + filesWithExclusiveTests.delete(nodePath.relative(projectDir, evt.testFile)); + } - this.timer = timer; - } + break; + } - cancel() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - this.again = false; - } - } -} + default: + break; + } + }); + }); -class TestDependency { - constructor(file, dependencies) { - this.file = file; - this.dependencies = dependencies; - } + let signalChanged; + let changed = Promise.resolve({}); + let firstRun = true; + let runAll = true; + let updateSnapshots = false; - contains(dependency) { - return this.dependencies.includes(dependency); - } -} + const reset = () => { + changed = new Promise(resolve => { + signalChanged = resolve; + }); + firstRun = false; + runAll = false; + updateSnapshots = false; + }; + + stdin.setEncoding('utf8'); + stdin.on('data', data => { + data = data.trim().toLowerCase(); + runAll = runAll || data === 'r'; + updateSnapshots = updateSnapshots || data === 'u'; + if (runAll || updateSnapshots) { + signalChanged({}); + } + }); -export default class Watcher { - constructor({api, filter = [], globs, projectDir, providers, reporter}) { - this.debouncer = new Debouncer(this); + let suspended = false; - this.clearLogOnNextRun = true; - this.runVector = 0; - this.previousFiles = []; - this.globs = {cwd: projectDir, ...globs}; + const dirtyPaths = new Set(); + const debounce = setTimeout(() => { + if (suspended || dirtyPaths.size === 0) { + return; + } - const patternFilters = filter.map(({pattern}) => pattern); + if (fileTracer.busy !== null) { + fileTracer.busy.then(() => debounce.refresh()); + return; + } - this.providers = providers; - this.run = (specificFiles = [], updateSnapshots = false) => { - const clearLogOnNextRun = this.clearLogOnNextRun && this.runVector > 0; - if (this.runVector > 0) { - this.clearLogOnNextRun = true; + const changes = [...dirtyPaths].filter(path => { + if (temporaryFiles.has(path)) { + debug('Ignoring known temporary file %s', path); + return false; } - this.runVector++; - - let runOnlyExclusive = false; - if (specificFiles.length > 0) { - const exclusiveFiles = specificFiles.filter(file => this.filesWithExclusiveTests.includes(file)); - runOnlyExclusive = exclusiveFiles.length !== this.filesWithExclusiveTests.length; - if (runOnlyExclusive) { - // The test files that previously contained exclusive tests are always - // run, together with the remaining specific files. - const remainingFiles = specificFiles.filter(file => !exclusiveFiles.includes(file)); - specificFiles = [...this.filesWithExclusiveTests, ...remainingFiles]; - } + if (touchedFiles.has(path)) { + debug('Ignoring known touched file %s', path); + return false; + } - if (filter.length > 0) { - specificFiles = applyTestFileFilter({ - cwd: projectDir, - expandDirectories: false, - filter: patternFilters, - testFiles: specificFiles, - treatFilterPatternsAsFiles: false, - }); + for (const {main} of providers) { + if (main.ignoreChange(path)) { + debug('Ignoring changed file %s', path); + return false; } + } - this.pruneFailures(specificFiles); + if (isIgnored(path)) { + debug('File %s is ignored by patterns', path); + return false; } - this.touchedFiles.clear(); - this.previousFiles = specificFiles; - this.busy = api.run({ - files: specificFiles, - filter, - runtimeOptions: { - clearLogOnNextRun, - previousFailures: this.sumPreviousFailures(this.runVector), - runOnlyExclusive, - runVector: this.runVector, - updateSnapshots: updateSnapshots === true, - }, - }) - .then(runStatus => { - reporter.endRun(); - reporter.lineWriter.writeLine(END_MESSAGE); - - if (this.clearLogOnNextRun && ( - runStatus.stats.failedHooks > 0 - || runStatus.stats.failedTests > 0 - || runStatus.stats.failedWorkers > 0 - || runStatus.stats.internalErrors > 0 - || runStatus.stats.timeouts > 0 - || runStatus.stats.uncaughtExceptions > 0 - || runStatus.stats.unhandledRejections > 0 - )) { - this.clearLogOnNextRun = false; - } - }) - .catch(rethrowAsync); - }; + return true; + }).map(path => { + const {isTest} = classify(path, globs); + const exists = fs.existsSync(nodePath.join(projectDir, path)); + return {path, isTest, exists}; + }); - this.testDependencies = []; - this.trackTestDependencies(api); + const uniqueTestFiles = new Set(); + const nonTestFiles = []; + for (const {path, isTest, exists} of changes) { + if (!exists) { + debug('File %s was deleted', path); + failureCounts.delete(path); + continue; + } - this.temporaryFiles = new Set(); - this.touchedFiles = new Set(); - this.trackTouchedFiles(api); + if (isTest) { + debug('File %s is a test file', path); + uniqueTestFiles.add(path); + } else { + debug('File %s is not a test file', path); - this.filesWithExclusiveTests = []; - this.trackExclusivity(api); + const dependingTestFiles = fileTracer.traceToTestFile(path); + if (dependingTestFiles.length > 0) { + debug('File %s is depended on by test files %o', path, dependingTestFiles); + for (const testFile of dependingTestFiles) { + uniqueTestFiles.add(testFile); + } + } else { + nonTestFiles.push(path); + } + } + } - this.filesWithFailures = []; - this.trackFailures(api); + dirtyPaths.clear(); + temporaryFiles.clear(); + touchedFiles.clear(); - this.dirtyStates = {}; - this.watchFiles(); - this.rerunAll(); - } + if (changes.length > 0) { + fileTracer.update(changes); + } - watchFiles() { - chokidar.watch(['**/*'], { - cwd: this.globs.cwd, - ignored: getChokidarIgnorePatterns(this.globs), - ignoreInitial: true, - }).on('all', (event, path) => { - if (event === 'add' || event === 'change' || event === 'unlink') { - debug('Detected %s of %s', event, path); - this.dirtyStates[nodePath.join(this.globs.cwd, path)] = event; - this.debouncer.debounce(); + let testFiles = [...uniqueTestFiles]; + + let runOnlyExclusive = false; + if (testFiles.length > 0) { + const exclusiveFiles = testFiles.filter(path => filesWithExclusiveTests.has(path)); + runOnlyExclusive = exclusiveFiles.length !== filesWithExclusiveTests.size; + if (runOnlyExclusive) { + // The test files that previously contained exclusive tests are always + // run, together with the test files. + debug('Running exclusive tests in %o', [...filesWithExclusiveTests]); + testFiles = [...new Set([...filesWithExclusiveTests, ...testFiles])]; } - }); - } - - trackTestDependencies(api) { - api.on('run', plan => { - plan.status.on('stateChange', evt => { - if (evt.type !== 'accessed-snapshots') { - return; - } + } - const dependencies = [evt.filename].filter(filePath => { - const {isIgnoredByWatcher} = classify(filePath, this.globs); - return !isIgnoredByWatcher; - }); - this.updateTestDependencies(evt.testFile, dependencies); + if (filter.length > 0) { + testFiles = applyTestFileFilter({ + cwd: projectDir, + expandDirectories: false, + filter: patternFilters, + testFiles, + treatFilterPatternsAsFiles: false, }); - }); - } - - updateTestDependencies(file, dependencies) { - // Ensure the rewritten test file path is included in the dependencies, - // since changes to non-rewritten paths are ignored. - for (const {main} of this.providers) { - const rewritten = main.resolveTestFile(file); - if (!dependencies.includes(rewritten)) { - dependencies = [rewritten, ...dependencies]; - } } - if (dependencies.length === 0) { - this.testDependencies = this.testDependencies.filter(dep => dep.file !== file); - return; + for (const path of testFiles) { + failureCounts.delete(path); } - const isUpdate = this.testDependencies.some(dep => { - if (dep.file !== file) { - return false; - } + if (nonTestFiles.length > 0) { + debug('Non-test files changed, running all tests'); + signalChanged({runOnlyExclusive}); + } else if (testFiles.length > 0) { + signalChanged({runOnlyExclusive, testFiles}); + } + }, 100); - dep.dependencies = dependencies; + // TODO: Support platforms without recursive mode. + fs.watch(projectDir, {recursive: true}, (_, filename) => { + dirtyPaths.add(filename); + debug('Detected change in %s', filename); + debounce.refresh(); + }); - return true; - }); + while (true) { + const {testFiles: files = [], runOnlyExclusive = false} = await changed; // eslint-disable-line no-await-in-loop - if (!isUpdate) { - this.testDependencies.push(new TestDependency(file, dependencies)); + let previousFailures = 0; + for (const count of failureCounts.values()) { + previousFailures += count; } + + const instructions = { + files, + firstRun, + previousFailures, + runOnlyExclusive, + updateSnapshots, + }; + reset(); + suspended = true; + yield instructions; + suspended = false; + debounce.refresh(); } +} - trackTouchedFiles(api) { - api.on('run', plan => { - plan.status.on('stateChange', evt => { - if (evt.type !== 'touched-files') { - return; - } +class Node { + children = new Set(); + parents = new Set(); + isTest = false; - for (const file of evt.files.changedFiles) { - this.touchedFiles.add(file); - } - - for (const file of evt.files.temporaryFiles) { - this.temporaryFiles.add(file); - } - }); - }); + constructor(path) { + this.path = path; } - trackExclusivity(api) { - api.on('run', plan => { - plan.status.on('stateChange', evt => { - if (evt.type !== 'worker-finished') { - return; - } - - const fileStats = plan.status.stats.byFile.get(evt.testFile); - const ranExclusiveTests = fileStats.selectedTests > 0 && fileStats.declaredTests > fileStats.selectedTests; - this.updateExclusivity(evt.testFile, ranExclusiveTests); - }); - }); + addChild(path) { + this.children.add(path); } - updateExclusivity(file, hasExclusiveTests) { - const index = this.filesWithExclusiveTests.indexOf(file); + removeChild(path) { + this.children.delete(path); + } - if (hasExclusiveTests && index === -1) { - this.filesWithExclusiveTests.push(file); - } else if (!hasExclusiveTests && index !== -1) { - this.filesWithExclusiveTests.splice(index, 1); - } + addParent(path) { + this.parents.add(path); } - trackFailures(api) { - api.on('run', plan => { - this.pruneFailures(plan.files); + removeParent(path) { + this.parents.delete(path); + } +} - const currentVector = this.runVector; - plan.status.on('stateChange', evt => { - if (!evt.testFile) { - return; - } +class FileTracer { + #base; + #cache = Object.create(null); + #pendingTrace = null; + #tree = new Map(); - switch (evt.type) { - case 'hook-failed': - case 'internal-error': - case 'process-exit': - case 'test-failed': - case 'uncaught-exception': - case 'unhandled-rejection': - case 'worker-failed': - this.countFailure(evt.testFile, currentVector); - break; - default: - break; - } - }); - }); + constructor({base}) { + this.#base = base; } - pruneFailures(files) { - const toPrune = new Set(files); - this.filesWithFailures = this.filesWithFailures.filter(state => !toPrune.has(state.file)); + get busy() { + return this.#pendingTrace; } - countFailure(file, vector) { - const isUpdate = this.filesWithFailures.some(state => { - if (state.file !== file) { - return false; + traceToTestFile(startingPath) { + const todo = [startingPath]; + const testFiles = new Set(); + const visited = new Set(); + for (const path of todo) { + if (visited.has(path)) { + continue; } - state.count++; - return true; - }); - - if (!isUpdate) { - this.filesWithFailures.push({ - file, - vector, - count: 1, - }); - } - } + visited.add(path); - sumPreviousFailures(beforeVector) { - let total = 0; + const node = this.#tree.get(path); + if (node === undefined) { + continue; + } - for (const state of this.filesWithFailures) { - if (state.vector < beforeVector) { - total += state.count; + if (node.isTest) { + testFiles.add(node.path); + } else { + todo.push(...node.parents); } } - return total; + return [...testFiles]; } - cleanUnlinkedTests(unlinkedTests) { - for (const testFile of unlinkedTests) { - this.updateTestDependencies(testFile, []); - this.updateExclusivity(testFile, false); - this.pruneFailures([testFile]); - } - } + addDependency(testFile, path) { + const testNode = this.#tree.get(testFile) ?? new Node(testFile); + testNode.isTest = true; + this.#tree.set(testFile, testNode); - observeStdin(stdin) { - stdin.resume(); - stdin.setEncoding('utf8'); + const node = this.#tree.get(path) ?? new Node(path); + this.#tree.set(path, node); - stdin.on('data', async data => { - data = data.trim().toLowerCase(); - if (data !== 'r' && data !== 'rs' && data !== 'u') { - return; - } + node.addParent(testNode.path); + testNode.addChild(node.path); + } - // Cancel the debouncer, it might rerun specific tests whereas *all* tests - // need to be rerun - this.debouncer.cancel(); - await this.busy; - // Cancel the debouncer again, it might have restarted while waiting for - // the busy promise to fulfil - this.debouncer.cancel(); - this.clearLogOnNextRun = false; - if (data === 'u') { - this.updatePreviousSnapshots(); - } else { - this.rerunAll(); + update(changes) { + const current = new Promise(resolve => { + resolve(this.#update(changes)); + }).finally(() => { + if (this.#pendingTrace === current) { + this.#pendingTrace = null; } }); - } - rerunAll() { - this.dirtyStates = {}; - this.run(); + this.#pendingTrace = current; } - updatePreviousSnapshots() { - this.dirtyStates = {}; - this.run(this.previousFiles, true); - } - - runAfterChanges() { - const {dirtyStates} = this; - this.dirtyStates = {}; + async #update(changes) { + await this.#pendingTrace; - let dirtyPaths = Object.keys(dirtyStates).filter(path => { - if (this.touchedFiles.has(path)) { - debug('Ignoring known touched file %s', path); - this.touchedFiles.delete(path); - return false; - } + const changedTestFiles = new Set(); + const deletedFiles = new Set(); + const filesToTrace = new Set(); + let reuseCache = true; + for (const {path, isTest, exists} of await changes) { + if (exists) { + if (isTest) { + changedTestFiles.add(path); + } - // Unlike touched files, temporary files are never cleared. We may see - // adds and unlinks detected separately, so we track the temporary files - // as long as AVA is running. - if (this.temporaryFiles.has(path)) { - debug('Ignoring known temporary file %s', path); - return false; + filesToTrace.add(path); + } else { + deletedFiles.add(path); } - return true; - }); - - for (const {main} of this.providers) { - dirtyPaths = dirtyPaths.filter(path => { - if (main.ignoreChange(path)) { - debug('Ignoring changed file %s', path); - return false; - } - - return true; - }); + reuseCache = reuseCache && !this.#tree.has(path); } - const dirtyHelpersAndSources = []; - const addedOrChangedTests = []; - const unlinkedTests = []; - for (const filePath of dirtyPaths) { - const {isIgnoredByWatcher, isTest} = classify(filePath, this.globs); - if (!isIgnoredByWatcher) { - if (isTest) { - if (dirtyStates[filePath] === 'unlink') { - unlinkedTests.push(filePath); - } else { - addedOrChangedTests.push(filePath); - } - } else { - dirtyHelpersAndSources.push(filePath); - } + for (const path of deletedFiles) { + const node = this.#tree.get(path); + if (node === undefined) { + continue; } - } - this.cleanUnlinkedTests(unlinkedTests); + this.#tree.delete(path); + for (const parent of node.parents) { + this.#tree.get(parent)?.removeChild(path); + } - // No need to rerun tests if the only change is that tests were deleted - if (unlinkedTests.length === dirtyPaths.length) { - return; + for (const child of node.children) { + this.#tree.get(child)?.removeParent(path); + } } - if (dirtyHelpersAndSources.length === 0) { - // Run any new or changed tests - this.run(addedOrChangedTests); - return; + if (!reuseCache) { + this.#cache = Object.create(null); } - // Try to find tests that depend on the changed source files - const testsByHelpersOrSource = dirtyHelpersAndSources.map(path => this.testDependencies.filter(dep => dep.contains(path)).map(dep => { - debug('%s is a dependency of %s', path, dep.file); - return dep.file; - })).filter(tests => tests.length > 0); - - // Rerun all tests if source files were changed that could not be traced to - // specific tests - if (testsByHelpersOrSource.length !== dirtyHelpersAndSources.length) { - debug('Files remain that cannot be traced to specific tests: %O', dirtyHelpersAndSources); - debug('Rerunning all tests'); - this.run(); + if (filesToTrace.size === 0) { return; } - // Run all affected tests - this.run([...new Set([addedOrChangedTests, testsByHelpersOrSource].flat(2))]); + const {fileList, reasons} = await nodeFileTrace([...filesToTrace], { + analysis: { + emitGlobs: false, + computeFileReferences: false, + evaluatePureExpressions: true, + }, + base: this.#base, + cache: this.#cache, + conditions: ['node'], + exportsOnly: true, + ignore: ['**/node_modules/**'], + }); + + for (const path of fileList) { + const {ignored, parents} = reasons.get(path); + if (ignored) { + continue; + } + + const node = this.#tree.get(path) ?? new Node(path); + this.#tree.set(path, node); + node.isTest = changedTestFiles.has(path); + for (const parent of parents) { + node.addParent(parent); + + const parentNode = this.#tree.get(parent) ?? new Node(parent); + parentNode.addChild(path); + this.#tree.set(parent, parentNode); + } + } } } diff --git a/package-lock.json b/package-lock.json index 7dce49a51..e9dc1a486 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "5.1.0", "license": "MIT", "dependencies": { + "@vercel/nft": "^0.22.1", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", @@ -17,7 +18,6 @@ "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.1.2", - "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.6.1", "ci-parallel-vars": "^1.0.1", @@ -1100,6 +1100,39 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1252,6 +1285,27 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@vercel/nft": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.22.1.tgz", + "integrity": "sha512-lYYZIoxRurqDOSoVIdBicGnpUIpfyaS5qVjdPq+EfI285WqtZK3NK/dyCkiyBul+X2U2OEhRyeMdXPCHGJbohw==", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "acorn": "^8.6.0", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.2", + "node-gyp-build": "^4.2.2", + "resolve-from": "^5.0.0", + "rollup-pluginutils": "^2.8.2" + }, + "bin": { + "nft": "out/cli.js" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -1430,8 +1484,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/acorn": { "version": "8.8.1", @@ -1471,6 +1524,17 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", @@ -1553,6 +1617,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1573,12 +1638,29 @@ "node": ">=8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1675,6 +1757,11 @@ "node": ">=10" } }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1684,6 +1771,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, "engines": { "node": ">=8" } @@ -1697,6 +1785,14 @@ "node": ">=10" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", @@ -1990,6 +2086,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "funding": [ { "type": "individual", @@ -2012,6 +2109,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2189,7 +2294,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, "bin": { "color-support": "bin.js" } @@ -2241,6 +2345,11 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2482,6 +2591,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -3566,6 +3688,11 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3686,6 +3813,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3814,6 +3946,17 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3823,6 +3966,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -3871,6 +4015,70 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4124,6 +4332,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -4179,6 +4392,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", @@ -4350,6 +4575,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -5307,7 +5533,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, "dependencies": { "semver": "^6.0.0" }, @@ -5322,7 +5547,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -5556,7 +5780,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -5564,11 +5787,22 @@ "node": ">=8" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -5625,6 +5859,35 @@ "type-detect": "4.0.8" } }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -5685,6 +5948,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5716,6 +5980,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nyc": { "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", @@ -6010,6 +6285,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -6902,10 +7185,24 @@ "node": ">=8" } }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -7070,6 +7367,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7096,7 +7406,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -7110,8 +7419,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "peer": true + ] }, "node_modules/safe-regex": { "version": "2.1.1", @@ -7207,8 +7515,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -7401,6 +7708,14 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -9566,6 +9881,22 @@ "node": ">=0.6" } }, + "node_modules/tar": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.12.tgz", + "integrity": "sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tcompare": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/tcompare/-/tcompare-5.0.7.tgz", @@ -9800,6 +10131,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -10044,6 +10380,11 @@ "url": "https://github.com/fisker/url-or-path?sponsor=1" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -10091,6 +10432,11 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "5.75.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", @@ -10212,6 +10558,15 @@ "node": ">=6" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10249,6 +10604,59 @@ "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", "dev": true }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index 545050b0f..39c11efb0 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "typescript" ], "dependencies": { + "@vercel/nft": "^0.22.1", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "ansi-styles": "^6.2.1", @@ -89,7 +90,6 @@ "callsites": "^4.0.0", "cbor": "^8.1.0", "chalk": "^5.1.2", - "chokidar": "^3.5.3", "chunkd": "^2.0.1", "ci-info": "^3.6.1", "ci-parallel-vars": "^1.0.1", diff --git a/test-tap/globs.js b/test-tap/globs.js index 73db32d47..e43123d35 100644 --- a/test-tap/globs.js +++ b/test-tap/globs.js @@ -201,101 +201,6 @@ test('isTest after provider modifications', t => { t.end(); }); -test('isIgnoredByWatcher with defaults', t => { - const options = { - ...globs.normalizeGlobs({extensions: ['js'], providers: []}), - cwd: fixture(), - }; - - function isIgnoredByWatcher(file) { - t.ok(globs.classify(fixture(file), options).isIgnoredByWatcher, `${file} should be ignored`); - } - - function notIgnored(file) { - t.notOk(globs.classify(fixture(file), options).isIgnoredByWatcher, `${file} should not be ignored`); - } - - notIgnored('foo-bar.js'); - notIgnored('foo.js'); - notIgnored('foo/blah.js'); - notIgnored('bar/foo.js'); - - notIgnored('_foo-bar.js'); - notIgnored('foo/_foo-bar.js'); - notIgnored('fixtures/foo.js'); - notIgnored('helpers/foo.js'); - - notIgnored('snapshots/foo.js.snap'); - isIgnoredByWatcher('snapshots/foo.js.snap.md'); - notIgnored('foo-bar.json'); - notIgnored('foo-bar.coffee'); - - notIgnored('bar.js'); - notIgnored('bar/bar.js'); - isIgnoredByWatcher('node_modules/foo.js'); - t.end(); -}); - -test('isIgnoredByWatcher with patterns', t => { - const options = { - ...globs.normalizeGlobs({ - files: ['**/foo*'], - ignoredByWatcher: ['**/bar*'], - extensions: ['js'], - providers: [], - }), - cwd: fixture(), - }; - - t.ok(globs.classify(fixture('node_modules/foo/foo.js'), options).isIgnoredByWatcher); - t.ok(globs.classify(fixture('bar.js'), options).isIgnoredByWatcher); - t.ok(globs.classify(fixture('foo/bar.js'), options).isIgnoredByWatcher); - t.end(); -}); - -test('isIgnoredByWatcher (pattern starts with directory)', t => { - const options = { - ...globs.normalizeGlobs({ - files: ['**/foo*'], - ignoredByWatcher: ['foo/**/*'], - extensions: ['js'], - providers: [], - }), - cwd: fixture(), - }; - - t.ok(globs.classify(fixture('node_modules/foo/foo.js'), options).isIgnoredByWatcher); - t.notOk(globs.classify(fixture('bar.js'), options).isIgnoredByWatcher); - t.ok(globs.classify(fixture('foo/bar.js'), options).isIgnoredByWatcher); - t.end(); -}); - -test('isIgnoredByWatcher after provider modifications', t => { - const options = { - ...globs.normalizeGlobs({ - extensions: ['js'], - providers: [{ - level: 2, - main: { - updateGlobs({filePatterns, ignoredByWatcherPatterns}) { - t.ok(filePatterns.length > 0); - t.ok(ignoredByWatcherPatterns.length > 0); - return { - filePatterns, - ignoredByWatcherPatterns: ['foo.js'], - }; - }, - }, - }], - }), - cwd: fixture(), - }; - - t.ok(globs.classify(fixture('foo.js'), options).isIgnoredByWatcher); - t.notOk(globs.classify(fixture('bar.js'), options).isIgnoredByWatcher); - t.end(); -}); - test('findFiles finds non-ignored files (just .cjs)', async t => { const fixtureDir = fixture('default-patterns'); process.chdir(fixtureDir); diff --git a/test-tap/helper/report.js b/test-tap/helper/report.js index 0dde8b04c..0ed0ba063 100644 --- a/test-tap/helper/report.js +++ b/test-tap/helper/report.js @@ -108,12 +108,12 @@ const run = async (type, reporter, {match = [], filter} = {}) => { } // Mimick watch mode - return api.run({files, filter, runtimeOptions: {clearLogOnNextRun: false, previousFailures: 0, runVector: 1}}).then(() => { + return api.run({files, filter, runtimeOptions: {previousFailures: 0, firstRun: true}}).then(() => { reporter.endRun(); - return api.run({files, filter, runtimeOptions: {clearLogOnNextRun: true, previousFailures: 2, runVector: 2}}); + return api.run({files, filter, runtimeOptions: {previousFailures: 2, firstRun: false}}); }).then(() => { reporter.endRun(); - return api.run({files, filter, runtimeOptions: {clearLogOnNextRun: false, previousFailures: 0, runVector: 3}}); + return api.run({files, filter, runtimeOptions: {previousFailures: 0, firstRun: false}}); }).then(() => { reporter.endRun(); });