From 80cb8e1c9256f37fb734afd65f49a9dc366c7620 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Fri, 15 May 2020 15:43:10 +0200 Subject: [PATCH] feat: replace `sane` with `chokidar` --- .eslintrc.js | 1 - CHANGELOG.md | 1 + packages/jest-haste-map/package.json | 11 +- .../src/__tests__/index.test.js | 7 +- packages/jest-haste-map/src/index.ts | 61 +++--- .../jest-haste-map/src/lib/FSEventsWatcher.ts | 192 ------------------ .../jest-haste-map/src/lib/WatchmanWatcher.js | 6 +- yarn.lock | 50 ++--- 8 files changed, 52 insertions(+), 277 deletions(-) delete mode 100644 packages/jest-haste-map/src/lib/FSEventsWatcher.ts diff --git a/.eslintrc.js b/.eslintrc.js index a8f9d91bab87..b4f13466adbb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -95,7 +95,6 @@ module.exports = { 'packages/jest-core/src/plugins/UpdateSnapshotsInteractive.ts', 'packages/jest-fake-timers/src/legacyFakeTimers.ts', 'packages/jest-haste-map/src/index.ts', - 'packages/jest-haste-map/src/lib/FSEventsWatcher.ts', 'packages/jest-jasmine2/src/jasmine/SpyStrategy.ts', 'packages/jest-jasmine2/src/jasmine/Suite.ts', 'packages/jest-leak-detector/src/index.ts', diff --git a/CHANGELOG.md b/CHANGELOG.md index e8493698859b..cdc558195401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixes +- `[jest-haste-map]` Replace `sane` with `chokidar` ([#10048](https://github.com/facebook/jest/pull/10048)) - `[jest-runtime]` [**BREAKING**] Do not inject `global` variable into module wrapper ([#10644](https://github.com/facebook/jest/pull/10644)) - `[jest-transform]` Show enhanced `SyntaxError` message for all `SyntaxError`s ([#10749](https://github.com/facebook/jest/pull/10749)) - `[jest-transform]` [**BREAKING**] Refactor API to pass an options bag around rather than multiple boolean options ([#10753](https://github.com/facebook/jest/pull/10753)) diff --git a/packages/jest-haste-map/package.json b/packages/jest-haste-map/package.json index b22e18f50ef8..8ad5b0fb3143 100644 --- a/packages/jest-haste-map/package.json +++ b/packages/jest-haste-map/package.json @@ -17,29 +17,22 @@ "@jest/types": "^26.6.2", "@types/graceful-fs": "^4.1.2", "@types/node": "*", - "anymatch": "^3.0.3", + "chokidar": "^3.4.2", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.4", "jest-regex-util": "^26.0.0", "jest-serializer": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7" + "micromatch": "^4.0.2" }, "devDependencies": { "@jest/test-utils": "^26.6.2", - "@types/anymatch": "^1.3.1", "@types/fb-watchman": "^2.0.0", "@types/micromatch": "^4.0.0", - "@types/sane": "^2.0.0", "jest-snapshot-serializer-raw": "^1.1.0", "slash": "^3.0.0" }, - "optionalDependencies": { - "fsevents": "^2.1.2" - }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" }, diff --git a/packages/jest-haste-map/src/__tests__/index.test.js b/packages/jest-haste-map/src/__tests__/index.test.js index 6dba04a9a81e..7917fb7ed779 100644 --- a/packages/jest-haste-map/src/__tests__/index.test.js +++ b/packages/jest-haste-map/src/__tests__/index.test.js @@ -72,14 +72,13 @@ jest.mock('../crawlers/watchman', () => const mockWatcherConstructor = jest.fn(root => { const EventEmitter = require('events').EventEmitter; mockEmitters[root] = new EventEmitter(); - mockEmitters[root].close = jest.fn(callback => callback()); + mockEmitters[root].close = jest.fn(); setTimeout(() => mockEmitters[root].emit('ready'), 0); return mockEmitters[root]; }); -jest.mock('sane', () => ({ - NodeWatcher: mockWatcherConstructor, - WatchmanWatcher: mockWatcherConstructor, +jest.mock('chokidar', () => ({ + watch: jest.fn((patten, opts) => mockWatcherConstructor(opts.cwd)), })); jest.mock('../lib/WatchmanWatcher', () => mockWatcherConstructor); diff --git a/packages/jest-haste-map/src/index.ts b/packages/jest-haste-map/src/index.ts index c7d82bf36c0a..6d286822506e 100644 --- a/packages/jest-haste-map/src/index.ts +++ b/packages/jest-haste-map/src/index.ts @@ -12,8 +12,8 @@ import {createHash} from 'crypto'; import {EventEmitter} from 'events'; import {tmpdir} from 'os'; import * as path from 'path'; +import {FSWatcher as ChokidarFsWatcher, watch as chokidarWatch} from 'chokidar'; import type {Stats} from 'graceful-fs'; -import {NodeWatcher, Watcher as SaneWatcher} from 'sane'; import type {Config} from '@jest/types'; import {escapePathForRegex} from 'jest-regex-util'; import serializer from 'jest-serializer'; @@ -101,7 +101,7 @@ type InternalOptions = { }; type Watcher = { - close(callback: () => void): void; + close(): Promise; }; type WorkerInterface = {worker: typeof worker; getSha1: typeof getSha1}; @@ -788,14 +788,6 @@ class HasteMap extends EventEmitter { this._options.throwOnModuleCollision = false; this._options.retainAllFiles = true; - // WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher - const Watcher: SaneWatcher = - canUseWatchman && this._options.useWatchman - ? WatchmanWatcher - : FSEventsWatcher.isSupported() - ? FSEventsWatcher - : NodeWatcher; - const extensions = this._options.extensions; const ignorePattern = this._options.ignorePattern; const rootDir = this._options.rootDir; @@ -806,12 +798,21 @@ class HasteMap extends EventEmitter { let mustCopy = true; const createWatcher = (root: Config.Path): Promise => { - // @ts-expect-error: TODO how? "Cannot use 'new' with an expression whose type lacks a call or construct signature." - const watcher = new Watcher(root, { - dot: true, - glob: extensions.map(extension => '**/*.' + extension), - ignored: ignorePattern, - }); + const useWatchman = canUseWatchman && this._options.useWatchman; + const patterns = extensions.map(extension => '**/*.' + extension); + // Prefer Watchman over Chokidar + const watcher = useWatchman + ? new WatchmanWatcher(root, { + dot: true, + glob: patterns, + ignored: ignorePattern, + }) + : chokidarWatch(patterns, { + alwaysStat: true, + cwd: root, + ignoreInitial: true, + ignored: ignorePattern, + }); return new Promise((resolve, reject) => { const rejectTimeout = setTimeout( @@ -821,7 +822,14 @@ class HasteMap extends EventEmitter { watcher.once('ready', () => { clearTimeout(rejectTimeout); - watcher.on('all', onChange); + + if (useWatchman) { + watcher.on('all', onChange); + } else { + (watcher as ChokidarFsWatcher).on('all', (type, filePath, stat) => { + onChange(type, filePath, root, stat); + }); + } resolve(watcher); }); }); @@ -832,10 +840,7 @@ class HasteMap extends EventEmitter { mustCopy = true; const changeEvent: ChangeEvent = { eventsQueue, - hasteFS: new HasteFS({ - files: hasteMap.files, - rootDir, - }), + hasteFS: new HasteFS({files: hasteMap.files, rootDir}), moduleMap: new HasteModuleMap({ duplicates: hasteMap.duplicates, map: hasteMap.map, @@ -1051,20 +1056,16 @@ class HasteMap extends EventEmitter { } } - end(): Promise { + async end(): Promise { // @ts-expect-error: TODO TS cannot decide if `setInterval` and `clearInterval` comes from NodeJS or the DOM clearInterval(this._changeInterval); if (!this._watchers.length) { - return Promise.resolve(); + return; } - return Promise.all( - this._watchers.map( - watcher => new Promise(resolve => watcher.close(resolve)), - ), - ).then(() => { - this._watchers = []; - }); + await Promise.all(this._watchers.map(watcher => watcher.close())); + + this._watchers = []; } /** diff --git a/packages/jest-haste-map/src/lib/FSEventsWatcher.ts b/packages/jest-haste-map/src/lib/FSEventsWatcher.ts deleted file mode 100644 index faf15f7b6617..000000000000 --- a/packages/jest-haste-map/src/lib/FSEventsWatcher.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -/* eslint-disable local/ban-types-eventually */ - -import {EventEmitter} from 'events'; -import * as path from 'path'; -import anymatch, {Matcher} from 'anymatch'; -import * as fs from 'graceful-fs'; -import micromatch = require('micromatch'); -// @ts-expect-error no types -import walker from 'walker'; - -// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error -// @ts-ignore: this is for CI which runs linux and might not have this -let fsevents: typeof import('fsevents') | null = null; -try { - fsevents = require('fsevents'); -} catch { - // Optional dependency, only supported on Darwin. -} - -const CHANGE_EVENT = 'change'; -const DELETE_EVENT = 'delete'; -const ADD_EVENT = 'add'; -const ALL_EVENT = 'all'; - -type FsEventsWatcherEvent = - | typeof CHANGE_EVENT - | typeof DELETE_EVENT - | typeof ADD_EVENT - | typeof ALL_EVENT; - -/** - * Export `FSEventsWatcher` class. - * Watches `dir`. - */ -class FSEventsWatcher extends EventEmitter { - public readonly root: string; - public readonly ignored?: Matcher; - public readonly glob: Array; - public readonly dot: boolean; - public readonly hasIgnore: boolean; - public readonly doIgnore: (path: string) => boolean; - public readonly fsEventsWatchStopper: () => Promise; - private _tracked: Set; - - static isSupported(): boolean { - return fsevents !== null; - } - - private static normalizeProxy( - callback: (normalizedPath: string, stats: fs.Stats) => void, - ) { - return (filepath: string, stats: fs.Stats): void => - callback(path.normalize(filepath), stats); - } - - private static recReaddir( - dir: string, - dirCallback: (normalizedPath: string, stats: fs.Stats) => void, - fileCallback: (normalizedPath: string, stats: fs.Stats) => void, - endCallback: Function, - errorCallback: Function, - ignored?: Matcher, - ) { - walker(dir) - .filterDir( - (currentDir: string) => !ignored || !anymatch(ignored, currentDir), - ) - .on('dir', FSEventsWatcher.normalizeProxy(dirCallback)) - .on('file', FSEventsWatcher.normalizeProxy(fileCallback)) - .on('error', errorCallback) - .on('end', () => { - endCallback(); - }); - } - - constructor( - dir: string, - opts: { - root: string; - ignored?: Matcher; - glob: string | Array; - dot: boolean; - }, - ) { - if (!fsevents) { - throw new Error( - '`fsevents` unavailable (this watcher can only be used on Darwin)', - ); - } - - super(); - - this.dot = opts.dot || false; - this.ignored = opts.ignored; - this.glob = Array.isArray(opts.glob) ? opts.glob : [opts.glob]; - - this.hasIgnore = - Boolean(opts.ignored) && !(Array.isArray(opts) && opts.length > 0); - this.doIgnore = opts.ignored ? anymatch(opts.ignored) : () => false; - - this.root = path.resolve(dir); - this.fsEventsWatchStopper = fsevents.watch( - this.root, - this.handleEvent.bind(this), - ); - - this._tracked = new Set(); - FSEventsWatcher.recReaddir( - this.root, - (filepath: string) => { - this._tracked.add(filepath); - }, - (filepath: string) => { - this._tracked.add(filepath); - }, - this.emit.bind(this, 'ready'), - this.emit.bind(this, 'error'), - this.ignored, - ); - } - - /** - * End watching. - */ - close(callback?: () => void): void { - this.fsEventsWatchStopper().then(() => { - this.removeAllListeners(); - if (typeof callback === 'function') { - process.nextTick(callback.bind(null, null, true)); - } - }); - } - - private isFileIncluded(relativePath: string) { - if (this.doIgnore(relativePath)) { - return false; - } - return this.glob.length - ? micromatch([relativePath], this.glob, {dot: this.dot}).length > 0 - : this.dot || micromatch([relativePath], '**/*').length > 0; - } - - private handleEvent(filepath: string) { - const relativePath = path.relative(this.root, filepath); - if (!this.isFileIncluded(relativePath)) { - return; - } - - fs.lstat(filepath, (error, stat) => { - if (error && error.code !== 'ENOENT') { - this.emit('error', error); - return; - } - - if (error) { - // Ignore files that aren't tracked and don't exist. - if (!this._tracked.has(filepath)) { - return; - } - - this._emit(DELETE_EVENT, relativePath); - this._tracked.delete(filepath); - return; - } - - if (this._tracked.has(filepath)) { - this._emit(CHANGE_EVENT, relativePath, stat); - } else { - this._tracked.add(filepath); - this._emit(ADD_EVENT, relativePath, stat); - } - }); - } - - /** - * Emit events. - */ - private _emit(type: FsEventsWatcherEvent, file: string, stat?: fs.Stats) { - this.emit(type, file, this.root, stat); - this.emit(ALL_EVENT, type, file, this.root, stat); - } -} - -export = FSEventsWatcher; diff --git a/packages/jest-haste-map/src/lib/WatchmanWatcher.js b/packages/jest-haste-map/src/lib/WatchmanWatcher.js index 4471c0ec34ae..548843543d9f 100644 --- a/packages/jest-haste-map/src/lib/WatchmanWatcher.js +++ b/packages/jest-haste-map/src/lib/WatchmanWatcher.js @@ -278,14 +278,12 @@ WatchmanWatcher.prototype.emitEvent = function ( /** * Closes the watcher. * - * @param {function} callback - * @private */ -WatchmanWatcher.prototype.close = function (callback) { +WatchmanWatcher.prototype.close = function () { this.client.removeAllListeners(); this.client.end(); - callback && callback(null, true); + return Promise.resolve(); }; /** diff --git a/yarn.lock b/yarn.lock index 1b26c9903522..427e379f024f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3245,13 +3245,6 @@ __metadata: languageName: node linkType: hard -"@types/anymatch@npm:^1.3.1": - version: 1.3.1 - resolution: "@types/anymatch@npm:1.3.1" - checksum: 1647865e528a168f66f57a077e9651c10a4c172b656cc3686fddf176555d42ca0a1647bfc626ea2fceb68fc7701426ab708224be1762b4a5216fe8368ffdba3c - languageName: node - linkType: hard - "@types/aria-query@npm:^4.2.0": version: 4.2.0 resolution: "@types/aria-query@npm:4.2.0" @@ -3743,15 +3736,6 @@ __metadata: languageName: node linkType: hard -"@types/sane@npm:^2.0.0": - version: 2.0.0 - resolution: "@types/sane@npm:2.0.0" - dependencies: - "@types/node": "*" - checksum: 8e50935136662acd7d71fc5ccdff70c1821ceb175ad8fbab0fb43b44b1cc2a4d57c0d5916408c46444361b310d70e6b22a75dd55fc06241fc123a2fd23e29e72 - languageName: node - linkType: hard - "@types/semver@npm:^7.1.0": version: 7.3.4 resolution: "@types/semver@npm:7.3.4" @@ -4347,7 +4331,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.1": +"anymatch@npm:~3.1.1": version: 3.1.1 resolution: "anymatch@npm:3.1.1" dependencies: @@ -5782,7 +5766,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.3.0": +"chokidar@npm:^3.3.0, chokidar@npm:^3.4.2": version: 3.4.3 resolution: "chokidar@npm:3.4.3" dependencies: @@ -9349,15 +9333,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"fsevents@^2.1.2, fsevents@~2.1.2": - version: 2.1.3 - resolution: "fsevents@npm:2.1.3" - dependencies: - node-gyp: latest - checksum: 8977781884d06c5bcb97b5f909efdce9683c925f2a0ce7e098d2cdffe2e0a0a50b1868547bb94dca75428c06535a4a70517a7bb3bb5a974d93bf9ffc067291eb - languageName: node - linkType: hard - "fsevents@patch:fsevents@^1.2.7#builtin": version: 1.2.13 resolution: "fsevents@patch:fsevents@npm%3A1.2.13#builtin::version=1.2.13&hash=127e8e" @@ -9368,7 +9343,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.1.2#builtin, fsevents@patch:fsevents@~2.1.2#builtin": +"fsevents@patch:fsevents@~2.1.2#builtin": version: 2.1.3 resolution: "fsevents@patch:fsevents@npm%3A2.1.3#builtin::version=2.1.3&hash=127e8e" dependencies: @@ -9377,6 +9352,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +fsevents@~2.1.2: + version: 2.1.3 + resolution: "fsevents@npm:2.1.3" + dependencies: + node-gyp: latest + checksum: 8977781884d06c5bcb97b5f909efdce9683c925f2a0ce7e098d2cdffe2e0a0a50b1868547bb94dca75428c06535a4a70517a7bb3bb5a974d93bf9ffc067291eb + languageName: node + linkType: hard + "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -11689,15 +11673,12 @@ fsevents@^1.2.7: dependencies: "@jest/test-utils": ^26.6.2 "@jest/types": ^26.6.2 - "@types/anymatch": ^1.3.1 "@types/fb-watchman": ^2.0.0 "@types/graceful-fs": ^4.1.2 "@types/micromatch": ^4.0.0 "@types/node": "*" - "@types/sane": ^2.0.0 - anymatch: ^3.0.3 + chokidar: ^3.4.2 fb-watchman: ^2.0.0 - fsevents: ^2.1.2 graceful-fs: ^4.2.4 jest-regex-util: ^26.0.0 jest-serializer: ^26.6.2 @@ -11705,12 +11686,7 @@ fsevents@^1.2.7: jest-util: ^26.6.2 jest-worker: ^26.6.2 micromatch: ^4.0.2 - sane: ^4.0.3 slash: ^3.0.0 - walker: ^1.0.7 - dependenciesMeta: - fsevents: - optional: true languageName: unknown linkType: soft