diff --git a/packages/metro-file-map/package.json b/packages/metro-file-map/package.json index a97dea57d..726c2e374 100644 --- a/packages/metro-file-map/package.json +++ b/packages/metro-file-map/package.json @@ -13,7 +13,6 @@ }, "license": "MIT", "dependencies": { - "anymatch": "^3.0.3", "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", diff --git a/packages/metro-file-map/src/Watcher.js b/packages/metro-file-map/src/Watcher.js index 3029fe0ce..38f342914 100644 --- a/packages/metro-file-map/src/Watcher.js +++ b/packages/metro-file-map/src/Watcher.js @@ -9,12 +9,12 @@ */ import type { - ChangeEventMetadata, Console, CrawlerOptions, FileData, Path, PerfLogger, + WatcherBackendChangeEvent, WatchmanClocks, } from './flow-types'; import type {WatcherOptions as WatcherBackendOptions} from './watchers/common'; @@ -163,14 +163,7 @@ export class Watcher extends EventEmitter { } } - async watch( - onChange: ( - type: string, - filePath: string, - root: string, - metadata: ChangeEventMetadata, - ) => void, - ) { + async watch(onChange: (change: WatcherBackendChangeEvent) => void) { const {extensions, ignorePattern, useWatchman} = this._options; // WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher @@ -214,29 +207,21 @@ export class Watcher extends EventEmitter { watcher.once('ready', () => { clearTimeout(rejectTimeout); - watcher.on( - 'all', - ( - type: string, - filePath: string, - root: string, - metadata: ChangeEventMetadata, - ) => { - const basename = path.basename(filePath); - if (basename.startsWith(this._options.healthCheckFilePrefix)) { - if (type === ADD_EVENT || type === CHANGE_EVENT) { - debug( - 'Observed possible health check cookie: %s in %s', - filePath, - root, - ); - this._handleHealthCheckObservation(basename); - } - return; + watcher.on('all', (change: WatcherBackendChangeEvent) => { + const basename = path.basename(change.relativePath); + if (basename.startsWith(this._options.healthCheckFilePrefix)) { + if (change.event === ADD_EVENT || change.event === CHANGE_EVENT) { + debug( + 'Observed possible health check cookie: %s in %s', + change.relativePath, + root, + ); + this._handleHealthCheckObservation(basename); } - onChange(type, filePath, root, metadata); - }, - ); + return; + } + onChange(change); + }); resolve(watcher); }); }); diff --git a/packages/metro-file-map/src/flow-types.js b/packages/metro-file-map/src/flow-types.js index 7293c6a9f..d65aa6da2 100644 --- a/packages/metro-file-map/src/flow-types.js +++ b/packages/metro-file-map/src/flow-types.js @@ -316,6 +316,20 @@ export type RawMockMap = Map; export type ReadOnlyRawMockMap = $ReadOnlyMap; +export type WatcherBackendChangeEvent = + | $ReadOnly<{ + event: 'change' | 'add', + relativePath: string, + root: string, + metadata: ChangeEventMetadata, + }> + | $ReadOnly<{ + event: 'delete', + relativePath: string, + root: string, + metadata?: void, + }>; + export type WatchmanClockSpec = | string | $ReadOnly<{scm: $ReadOnly<{'mergebase-with': string}>}>; diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index 47e2e4c9f..b9a74e226 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -33,6 +33,7 @@ import type { PerfLoggerFactory, WatchmanClocks, WorkerMetadata, + WatcherBackendChangeEvent, } from './flow-types'; import type {IJestWorker} from 'jest-worker'; @@ -862,27 +863,23 @@ export default class FileMap extends EventEmitter { nextEmit = null; }; - const onChange = ( - type: string, - filePath: Path, - root: Path, - metadata: ?ChangeEventMetadata, - ) => { + const onChange = (change: WatcherBackendChangeEvent) => { if ( - metadata && + change.metadata && // Ignore all directory events - (metadata.type === 'd' || + (change.metadata.type === 'd' || // Ignore regular files with unwatched extensions - (metadata.type === 'f' && !hasWatchedExtension(filePath)) || + (change.metadata.type === 'f' && + !hasWatchedExtension(change.relativePath)) || // Don't emit events relating to symlinks if enableSymlinks: false - (!this._options.enableSymlinks && metadata?.type === 'l')) + (!this._options.enableSymlinks && change.metadata?.type === 'l')) ) { return; } const absoluteFilePath = path.join( - root, - normalizePathSeparatorsToSystem(filePath), + change.root, + normalizePathSeparatorsToSystem(change.relativePath), ); // Ignore files (including symlinks) whose path matches ignorePattern @@ -899,11 +896,10 @@ export default class FileMap extends EventEmitter { // null, then it is assumed that the watcher does not have capabilities // to detect modified time, and change processing proceeds. if ( - type === 'change' && + change.event === 'change' && linkStats != null && - metadata && - metadata.modifiedTime != null && - linkStats.modifiedTime === metadata.modifiedTime + change.metadata.modifiedTime != null && + linkStats.modifiedTime === change.metadata.modifiedTime ) { return; } @@ -917,14 +913,15 @@ export default class FileMap extends EventEmitter { nextEmit != null && nextEmit.eventsQueue.find( event => - event.type === type && + event.type === change.event && event.filePath === absoluteFilePath && - ((!event.metadata && !metadata) || + ((!event.metadata && !change.metadata) || (event.metadata && - metadata && + change.metadata && event.metadata.modifiedTime != null && - metadata.modifiedTime != null && - event.metadata.modifiedTime === metadata.modifiedTime)), + change.metadata.modifiedTime != null && + event.metadata.modifiedTime === + change.metadata.modifiedTime)), ) ) { return null; @@ -936,7 +933,7 @@ export default class FileMap extends EventEmitter { const event = { filePath: absoluteFilePath, metadata, - type, + type: change.event, }; if (nextEmit == null) { nextEmit = { @@ -963,19 +960,19 @@ export default class FileMap extends EventEmitter { // If the file was added or changed, // parse it and update the haste map. - if (type === 'add' || type === 'change') { + if (change.event === 'add' || change.event === 'change') { invariant( - metadata != null && metadata.size != null, - 'since the file exists or changed, it should have metadata', + change.metadata.size != null, + 'since the file exists or changed, it should have known size', ); const fileMetadata: FileMetaData = [ '', - metadata.modifiedTime, - metadata.size, + change.metadata.modifiedTime, + change.metadata.size, 0, '', null, - metadata.type === 'l' ? 1 : 0, + change.metadata.type === 'l' ? 1 : 0, ]; try { @@ -987,7 +984,7 @@ export default class FileMap extends EventEmitter { {forceInBand: true}, // No need to clean up workers ); fileSystem.addOrModify(relativeFilePath, fileMetadata); - enqueueEvent(metadata); + enqueueEvent(change.metadata); } catch (e) { if (!['ENOENT', 'EACCESS'].includes(e.code)) { throw e; @@ -999,7 +996,7 @@ export default class FileMap extends EventEmitter { // event for it, and we'll clean up in the usual way at that // point. } - } else if (type === 'delete') { + } else if (change.event === 'delete') { if (linkStats == null) { // Don't emit deletion events for files we weren't retaining. // This is expected for deletion of an ignored file. @@ -1012,7 +1009,7 @@ export default class FileMap extends EventEmitter { }); } else { throw new Error( - `metro-file-map: Unrecognized event type from watcher: ${type}`, + `metro-file-map: Unrecognized event type from watcher: ${change.event}`, ); } return null; diff --git a/packages/metro-file-map/src/watchers/FSEventsWatcher.js b/packages/metro-file-map/src/watchers/FSEventsWatcher.js index c646f77fb..0b11ddf07 100644 --- a/packages/metro-file-map/src/watchers/FSEventsWatcher.js +++ b/packages/metro-file-map/src/watchers/FSEventsWatcher.js @@ -9,24 +9,22 @@ */ import type {ChangeEventMetadata} from '../flow-types'; -import type {Stats} from 'fs'; // $FlowFixMe[cannot-resolve-module] - Optional, Darwin only // $FlowFixMe[untyped-type-import] import type {FSEvents} from 'fsevents'; -import {isIncluded, typeFromStat} from './common'; -// $FlowFixMe[untyped-import] - anymatch -import anymatch from 'anymatch'; +import { + isIncluded, + posixPathMatchesPattern, + recReaddir, + typeFromStat, +} from './common'; import EventEmitter from 'events'; import {promises as fsPromises} from 'fs'; import * as path from 'path'; -// $FlowFixMe[untyped-import] - walker -import walker from 'walker'; const debug = require('debug')('Metro:FSEventsWatcher'); -type Matcher = typeof anymatch.Matcher; - // $FlowFixMe[value-as-type] let fsevents: ?FSEvents = null; try { @@ -42,19 +40,13 @@ 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`. */ export default class FSEventsWatcher extends EventEmitter { +root: string; - +ignored: ?Matcher; + +ignored: ?RegExp; +glob: $ReadOnlyArray; +dot: boolean; +doIgnore: (path: string) => boolean; @@ -66,38 +58,14 @@ export default class FSEventsWatcher extends EventEmitter { return fsevents != null; } - static _normalizeProxy( - callback: (normalizedPath: string, stats: Stats) => void, - ): (filepath: string, stats: Stats) => void { - return (filepath: string, stats: Stats): void => - callback(path.normalize(filepath), stats); - } - - static _recReaddir( - dir: string, - dirCallback: (normalizedPath: string, stats: Stats) => void, - fileCallback: (normalizedPath: string, stats: Stats) => void, - symlinkCallback: (normalizedPath: string, stats: Stats) => void, - endCallback: () => void, - // $FlowFixMe[unclear-type] Add types for callback - errorCallback: Function, - ignored?: Matcher, - ) { - walker(dir) - .filterDir( - (currentDir: string) => !ignored || !anymatch(ignored, currentDir), - ) - .on('dir', FSEventsWatcher._normalizeProxy(dirCallback)) - .on('file', FSEventsWatcher._normalizeProxy(fileCallback)) - .on('symlink', FSEventsWatcher._normalizeProxy(symlinkCallback)) - .on('error', errorCallback) - .on('end', endCallback); - } - constructor( dir: string, - opts: $ReadOnly<{ - ignored?: Matcher, + { + ignored, + glob, + dot, + }: $ReadOnly<{ + ignored: ?RegExp, glob: string | $ReadOnlyArray, dot: boolean, ... @@ -111,10 +79,12 @@ export default class FSEventsWatcher extends EventEmitter { super(); - this.dot = opts.dot || false; - this.ignored = opts.ignored; - this.glob = Array.isArray(opts.glob) ? opts.glob : [opts.glob]; - this.doIgnore = opts.ignored ? anymatch(opts.ignored) : () => false; + this.dot = dot || false; + this.ignored = ignored; + this.glob = Array.isArray(glob) ? glob : [glob]; + this.doIgnore = ignored + ? (filePath: string) => posixPathMatchesPattern(ignored, filePath) + : () => false; this.root = path.resolve(dir); @@ -128,10 +98,10 @@ export default class FSEventsWatcher extends EventEmitter { this._tracked = new Set(); const trackPath = (filePath: string) => { - this._tracked.add(filePath); + this._tracked.add(path.normalize(filePath)); }; this.watcherInitialReaddirPromise = new Promise(resolve => { - FSEventsWatcher._recReaddir( + recReaddir( this.root, trackPath, trackPath, @@ -192,10 +162,10 @@ export default class FSEventsWatcher extends EventEmitter { }; if (this._tracked.has(filepath)) { - this._emit(CHANGE_EVENT, relativePath, metadata); + this._emit({event: CHANGE_EVENT, relativePath, metadata}); } else { this._tracked.add(filepath); - this._emit(ADD_EVENT, relativePath, metadata); + this._emit({event: ADD_EVENT, relativePath, metadata}); } } catch (error) { if (error?.code !== 'ENOENT') { @@ -208,7 +178,7 @@ export default class FSEventsWatcher extends EventEmitter { return; } - this._emit(DELETE_EVENT, relativePath); + this._emit({event: DELETE_EVENT, relativePath}); this._tracked.delete(filepath); } } @@ -216,13 +186,23 @@ export default class FSEventsWatcher extends EventEmitter { /** * Emit events. */ - _emit( - type: FsEventsWatcherEvent, - file: string, - metadata?: ChangeEventMetadata, - ) { - this.emit(type, file, this.root, metadata); - this.emit(ALL_EVENT, type, file, this.root, metadata); + _emit({ + event, + relativePath, + metadata, + }: + | { + event: 'change' | 'add', + relativePath: string, + metadata: ChangeEventMetadata, + } + | { + event: 'delete', + relativePath: string, + metadata?: void, + }) { + this.emit(event, relativePath, this.root, metadata); + this.emit(ALL_EVENT, event, relativePath, this.root, metadata); } getPauseReason(): ?string { diff --git a/packages/metro-file-map/src/watchers/NodeWatcher.js b/packages/metro-file-map/src/watchers/NodeWatcher.js index 4304f2cc8..75fa2829b 100644 --- a/packages/metro-file-map/src/watchers/NodeWatcher.js +++ b/packages/metro-file-map/src/watchers/NodeWatcher.js @@ -15,7 +15,10 @@ 'use strict'; -import type {ChangeEventMetadata} from '../flow-types'; +import type { + ChangeEventMetadata, + WatcherBackendChangeEvent, +} from '../flow-types'; import type {WatcherOptions} from './common'; import type {FSWatcher, Stats} from 'fs'; @@ -48,7 +51,7 @@ module.exports = class NodeWatcher extends EventEmitter { doIgnore: string => boolean; dot: boolean; globs: $ReadOnlyArray; - ignored: ?(boolean | RegExp); + ignored: ?RegExp; root: string; watched: {[key: string]: FSWatcher, __proto__: null}; watchmanDeferStates: $ReadOnlyArray; @@ -298,29 +301,42 @@ module.exports = class NodeWatcher extends EventEmitter { path.resolve(this.root, relativePath), (dir, stats) => { if (this._watchdir(dir)) { - this._emitEvent(ADD_EVENT, path.relative(this.root, dir), { - modifiedTime: stats.mtime.getTime(), - size: stats.size, - type: 'd', + this._emitEvent({ + event: ADD_EVENT, + relativePath: path.relative(this.root, dir), + metadata: { + modifiedTime: stats.mtime.getTime(), + size: stats.size, + type: 'd', + }, }); } }, (file, stats) => { if (this._register(file, 'f')) { - this._emitEvent(ADD_EVENT, path.relative(this.root, file), { - modifiedTime: stats.mtime.getTime(), - size: stats.size, - type: 'f', + this._emitEvent({ + event: ADD_EVENT, + relativePath: path.relative(this.root, file), + metadata: { + modifiedTime: stats.mtime.getTime(), + size: stats.size, + type: 'f', + }, }); } }, (symlink, stats) => { if (this._register(symlink, 'l')) { - this._rawEmitEvent(ADD_EVENT, path.relative(this.root, symlink), { - modifiedTime: stats.mtime.getTime(), - size: stats.size, - type: 'l', - }); + this.emit(ALL_EVENT, { + event: ADD_EVENT, + relativePath: path.relative(this.root, symlink), + root: this.root, + metadata: { + modifiedTime: stats.mtime.getTime(), + size: stats.size, + type: 'l', + }, + } as WatcherBackendChangeEvent); } }, function endCallback() {}, @@ -338,10 +354,10 @@ module.exports = class NodeWatcher extends EventEmitter { type, }; if (registered) { - this._emitEvent(CHANGE_EVENT, relativePath, metadata); + this._emitEvent({event: CHANGE_EVENT, relativePath, metadata}); } else { if (this._register(fullPath, type)) { - this._emitEvent(ADD_EVENT, relativePath, metadata); + this._emitEvent({event: ADD_EVENT, relativePath, metadata}); } } } @@ -353,7 +369,7 @@ module.exports = class NodeWatcher extends EventEmitter { this._unregister(fullPath); this._unregisterDir(fullPath); if (registered) { - this._emitEvent(DELETE_EVENT, relativePath); + this._emitEvent({event: DELETE_EVENT, relativePath}); } await this._stopWatching(fullPath); } @@ -366,10 +382,11 @@ module.exports = class NodeWatcher extends EventEmitter { * * See also note above for DEBOUNCE_MS. */ - _emitEvent(type: string, file: string, metadata?: ChangeEventMetadata) { - const key = type + '-' + file; - const addKey = ADD_EVENT + '-' + file; - if (type === CHANGE_EVENT && this._changeTimers.has(addKey)) { + _emitEvent(change: Omit) { + const {event, relativePath} = change; + const key = event + '-' + relativePath; + const addKey = ADD_EVENT + '-' + relativePath; + if (event === CHANGE_EVENT && this._changeTimers.has(addKey)) { // Ignore the change event that is immediately fired after an add event. // (This happens on Linux). return; @@ -382,23 +399,14 @@ module.exports = class NodeWatcher extends EventEmitter { key, setTimeout(() => { this._changeTimers.delete(key); - this._rawEmitEvent(type, file, metadata); + this.emit(ALL_EVENT, { + ...change, + root: this.root, + } as WatcherBackendChangeEvent); }, DEBOUNCE_MS), ); } - /** - * Actually emit the events - */ - _rawEmitEvent( - eventType: string, - file: string, - metadata: ?ChangeEventMetadata, - ) { - this.emit(eventType, file, this.root, metadata); - this.emit(ALL_EVENT, eventType, file, this.root, metadata); - } - getPauseReason(): ?string { return null; } diff --git a/packages/metro-file-map/src/watchers/WatchmanWatcher.js b/packages/metro-file-map/src/watchers/WatchmanWatcher.js index 3a0d57133..eb06a6328 100644 --- a/packages/metro-file-map/src/watchers/WatchmanWatcher.js +++ b/packages/metro-file-map/src/watchers/WatchmanWatcher.js @@ -9,7 +9,10 @@ * @oncall react_native */ -import type {ChangeEventMetadata} from '../flow-types'; +import type { + ChangeEventMetadata, + WatcherBackendChangeEvent, +} from '../flow-types'; import type {WatcherOptions} from './common'; import type { Client, @@ -272,7 +275,7 @@ export default class WatchmanWatcher extends EventEmitter { } if (!exists) { - self._emitEvent(DELETE_EVENT, relativePath, self.root); + self._emitEvent({event: DELETE_EVENT, relativePath}); } else { const eventType = isNew ? ADD_EVENT : CHANGE_EVENT; invariant( @@ -290,10 +293,14 @@ export default class WatchmanWatcher extends EventEmitter { !(type === 'd' && eventType === CHANGE_EVENT) ) { const mtime = Number(mtime_ms); - self._emitEvent(eventType, relativePath, self.root, { - modifiedTime: mtime !== 0 ? mtime : null, - size, - type, + self._emitEvent({ + event: eventType, + relativePath, + metadata: { + modifiedTime: mtime !== 0 ? mtime : null, + size, + type, + }, }); } } @@ -302,14 +309,11 @@ export default class WatchmanWatcher extends EventEmitter { /** * Dispatches the event. */ - _emitEvent( - eventType: string, - filepath: string, - root: string, - changeMetadata?: ChangeEventMetadata, - ) { - this.emit(eventType, filepath, root, changeMetadata); - this.emit(ALL_EVENT, eventType, filepath, root, changeMetadata); + _emitEvent(change: Omit) { + this.emit(ALL_EVENT, { + ...change, + root: this.root, + } as WatcherBackendChangeEvent); } /** diff --git a/packages/metro-file-map/src/watchers/__tests__/WatchmanWatcher-test.js b/packages/metro-file-map/src/watchers/__tests__/WatchmanWatcher-test.js index f4cccde53..876340d90 100644 --- a/packages/metro-file-map/src/watchers/__tests__/WatchmanWatcher-test.js +++ b/packages/metro-file-map/src/watchers/__tests__/WatchmanWatcher-test.js @@ -36,7 +36,7 @@ describe('WatchmanWatcher', () => { test('initializes with watch-project, clock, subscribe', () => { const watchmanWatcher = new WatchmanWatcher('/project/subdir/js', { dot: true, - ignored: false, + ignored: null, glob: ['**/*.js'], watchmanDeferStates: ['busy'], }); @@ -86,7 +86,7 @@ describe('WatchmanWatcher', () => { beforeEach(() => { watchmanWatcher = new WatchmanWatcher('/project/subdir/js', { dot: true, - ignored: false, + ignored: null, glob: ['**/*.js'], watchmanDeferStates: ['busy'], }); diff --git a/packages/metro-file-map/src/watchers/__tests__/helpers.js b/packages/metro-file-map/src/watchers/__tests__/helpers.js index e852e5d43..250e4874b 100644 --- a/packages/metro-file-map/src/watchers/__tests__/helpers.js +++ b/packages/metro-file-map/src/watchers/__tests__/helpers.js @@ -9,7 +9,10 @@ * @oncall react_native */ -import type {ChangeEventMetadata} from '../../flow-types'; +import type { + ChangeEventMetadata, + WatcherBackendChangeEvent, +} from '../../flow-types'; import type {WatcherOptions} from '../common'; import FSEventsWatcher from '../FSEventsWatcher'; @@ -117,23 +120,24 @@ export const startWatching = async ( metadata?: ChangeEventMetadata, path: string, }>((resolve, reject) => { - const listener = ( - eventType: string, - path: string, - root: string, - metadata?: ChangeEventMetadata, - ) => { - if (path === '') { + const listener = (change: WatcherBackendChangeEvent) => { + if (change.relativePath === '') { // FIXME: FSEventsWatcher sometimes reports 'change' events to // the watch root. return; } watcherInstance.removeListener('all', listener); - if (root !== watchRoot) { - reject(new Error(`Expected root ${watchRoot}, got ${root}`)); + if (change.root !== watchRoot) { + reject( + new Error(`Expected root ${watchRoot}, got ${change.root}`), + ); } - resolve({eventType, path, metadata}); + resolve({ + eventType: change.event, + path: change.relativePath, + metadata: change.metadata, + }); }; watcherInstance.on('all', listener); }), @@ -154,13 +158,13 @@ export const startWatching = async ( const allEventKeys = new Set( expectedEvents.map(tuple => tupleToKey(tuple)), ); - const listener = (eventType: string, path: string) => { - if (path === '') { + const listener = (change: WatcherBackendChangeEvent) => { + if (change.relativePath === '') { // FIXME: FSEventsWatcher sometimes reports 'change' events to // the watch root. return; } - const receivedKey = tupleToKey([path, eventType]); + const receivedKey = tupleToKey([change.relativePath, change.event]); if (allEventKeys.has(receivedKey)) { allEventKeys.delete(receivedKey); if (allEventKeys.size === 0) { @@ -169,7 +173,11 @@ export const startWatching = async ( } } else if (rejectUnexpected) { watcherInstance.removeListener('all', listener); - reject(new Error(`Unexpected event: ${eventType} ${path}.`)); + reject( + new Error( + `Unexpected event: ${change.event} ${change.relativePath}.`, + ), + ); } }; watcherInstance.on('all', listener); diff --git a/packages/metro-file-map/src/watchers/common.js b/packages/metro-file-map/src/watchers/common.js index 2dbdb6d99..5bfc97b81 100644 --- a/packages/metro-file-map/src/watchers/common.js +++ b/packages/metro-file-map/src/watchers/common.js @@ -19,8 +19,6 @@ import type {ChangeEventMetadata} from '../flow-types'; import type {Stats} from 'fs'; -// $FlowFixMe[untyped-import] - Write libdefs for `anymatch` -const anymatch = require('anymatch'); // $FlowFixMe[untyped-import] - Write libdefs for `micromatch` const micromatch = require('micromatch'); const platform = require('os').platform(); @@ -39,7 +37,7 @@ export const ALL_EVENT = 'all'; export type WatcherOptions = $ReadOnly<{ glob: $ReadOnlyArray, dot: boolean, - ignored: boolean | RegExp, + ignored: ?RegExp, watchmanDeferStates: $ReadOnlyArray, watchman?: mixed, watchmanPath?: string, @@ -49,7 +47,7 @@ interface Watcher { doIgnore: string => boolean; dot: boolean; globs: $ReadOnlyArray; - ignored?: ?(boolean | RegExp); + ignored?: ?RegExp; watchmanDeferStates: $ReadOnlyArray; watchmanPath?: ?string; } @@ -68,16 +66,16 @@ export const assignOptions = function ( ): WatcherOptions { watcher.globs = opts.glob ?? []; watcher.dot = opts.dot ?? false; - watcher.ignored = opts.ignored ?? false; + watcher.ignored = opts.ignored ?? null; watcher.watchmanDeferStates = opts.watchmanDeferStates; if (!Array.isArray(watcher.globs)) { watcher.globs = [watcher.globs]; } - watcher.doIgnore = - opts.ignored != null && opts.ignored !== false - ? anymatch(opts.ignored) - : () => false; + const ignored = watcher.ignored; + watcher.doIgnore = ignored + ? filePath => ignored.test(toPosixSeparators(filePath)) + : () => false; if (opts.watchman == true && opts.watchmanPath != null) { watcher.watchmanPath = opts.watchmanPath; @@ -107,6 +105,26 @@ export function isIncluded( return micromatch.some(relativePath, globs, {dot}); } +const toPosixSeparators: (filePath: string) => string = + path.sep === '/' + ? filePath => filePath + : filePath => filePath.replaceAll(path.sep, '/'); + +/** + * Whether the given filePath matches the given RegExp, after converting + * (on Windows only) system separators to posix separators. + * + * Conversion to posix is for backwards compatibility with the previous + * anymatch matcher, which normlises all inputs[1]. This may not be consistent + * with other parts of metro-file-map. + * + * [1]: https://github.com/micromatch/anymatch/blob/3.1.1/index.js#L50 + */ +export const posixPathMatchesPattern = ( + pattern: RegExp, + filePath: string, +): boolean => pattern.test(toPosixSeparators(filePath)); + /** * Traverse a directory recursively calling `callback` on every directory. */ @@ -117,10 +135,15 @@ export function recReaddir( symlinkCallback: (string, Stats) => void, endCallback: () => void, errorCallback: Error => void, - ignored: ?(boolean | RegExp), + ignored: ?RegExp, ) { - walker(dir) - .filterDir(currentDir => !anymatch(ignored, currentDir)) + const walk = walker(dir); + if (ignored) { + walk.filterDir( + (currentDir: string) => !posixPathMatchesPattern(ignored, currentDir), + ); + } + walk .on('dir', normalizeProxy(dirCallback)) .on('file', normalizeProxy(fileCallback)) .on('symlink', normalizeProxy(symlinkCallback))