From 570c38039abc32b758f8d241896b21e28ca4ba83 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 15 Jun 2022 11:44:40 -0700 Subject: [PATCH] Add deferStates option for Watchman Summary: Adds a new config option which allows Metro, when using Watchman, to be configured to ignore file events (and avoid nondeterministic chunking that can trigger bugs) during specific external file operations such as large source control updates. This is achieved using Watchman's [`defer` behaviour](https://facebook.github.io/watchman/docs/cmd/subscribe.html#defer), which accepts a set of state names exposed by this option. A root config key, `watcher`, has been introduced in addition to the `watcher.watchman.deferStates` option which configures this feature. This config structure change anticipates exposing further options against file watchers and Watchman. A default value of `['hg.update']` is applied (at the `metro-config` level) in order to apply this behaviour for use cases within Meta. This should help mitigate consistency issues for developers caused by long-running `hg` operations. Reviewed By: motiz88 Differential Revision: D36959202 fbshipit-source-id: 61acdc70ad621b24ee5125cc10756892ec41a454 --- docs/Configuration.md | 25 +++++++++++++++++ .../__snapshots__/loadConfig-test.js.snap | 28 +++++++++++++++++++ packages/metro-config/src/configTypes.flow.js | 9 ++++++ packages/metro-config/src/defaults/index.js | 5 ++++ packages/metro-config/src/loadConfig.js | 8 ++++++ packages/metro-file-map/src/index.js | 4 +++ .../src/watchers/FSEventsWatcher.js | 1 + .../src/watchers/WatchmanWatcher.js | 1 + .../metro-file-map/src/watchers/common.js | 1 + .../DependencyGraph/createHasteMap.js | 1 + 10 files changed, 83 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index 07a15d5e83..90bdd3378a 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -37,6 +37,12 @@ module.exports = { }, server: { /* server options */ + }, + watcher: { + /* watcher options */ + watchman: { + /* Watchman-specific options */ + } } }; ``` @@ -373,6 +379,25 @@ Type: `boolean` (default: `true`) Run Inspector Proxy server inside Metro to be able to inspect React Native code. +--- + +### Watcher Options + +Options for the filesystem watcher. + +:::note + +Dot notation in this section indicates a nested structure, e.g. `watchman: { deferStates: ... }`. + +::: + +#### `watchman.deferStates` + +Type: `Array` + +Applies when using Watchman. Metro will [defer processing filesystem updates](https://facebook.github.io/watchman/docs/cmd/subscribe.html#defer) while these [states](https://facebook.github.io/watchman/docs/cmd/state-enter.html) are asserted in the watch. This is useful for debouncing builds while the filesystem hasn't settled, e.g. during large source control operations. + +The default value is `['hg.update']`. ## Merging Configurations diff --git a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap index a5773cfe52..db2e4175eb 100644 --- a/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap +++ b/packages/metro-config/src/__tests__/__snapshots__/loadConfig-test.js.snap @@ -142,6 +142,13 @@ Object { "watchFolders": Array [ "/", ], + "watcher": Object { + "watchman": Object { + "deferStates": Array [ + "hg.update", + ], + }, + }, } `; @@ -287,6 +294,13 @@ Object { "watchFolders": Array [ "/", ], + "watcher": Object { + "watchman": Object { + "deferStates": Array [ + "hg.update", + ], + }, + }, } `; @@ -432,6 +446,13 @@ Object { "watchFolders": Array [ "/", ], + "watcher": Object { + "watchman": Object { + "deferStates": Array [ + "hg.update", + ], + }, + }, } `; @@ -577,6 +598,13 @@ Object { "watchFolders": Array [ "/", ], + "watcher": Object { + "watchman": Object { + "deferStates": Array [ + "hg.update", + ], + }, + }, } `; diff --git a/packages/metro-config/src/configTypes.flow.js b/packages/metro-config/src/configTypes.flow.js index b9528ce22d..d537f2a377 100644 --- a/packages/metro-config/src/configTypes.flow.js +++ b/packages/metro-config/src/configTypes.flow.js @@ -166,6 +166,12 @@ type SymbolicatorConfigT = { }) => ?{+collapse?: boolean} | Promise, }; +type WatcherConfigT = { + watchman: { + deferStates?: $ReadOnlyArray, + }, +}; + export type InputConfigT = $Shape<{ ...MetalConfigT, ...$ReadOnly<{ @@ -177,6 +183,7 @@ export type InputConfigT = $Shape<{ serializer: $Shape, symbolicator: $Shape, transformer: $Shape, + watcher: $Shape, }>, }>; @@ -188,6 +195,7 @@ export type IntermediateConfigT = { serializer: SerializerConfigT, symbolicator: SymbolicatorConfigT, transformer: TransformerConfigT, + watcher: WatcherConfigT, }, }; @@ -199,6 +207,7 @@ export type ConfigT = $ReadOnly<{ serializer: $ReadOnly, symbolicator: $ReadOnly, transformer: $ReadOnly, + watcher: $ReadOnly, }>, }>; diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index 55a0b95376..99171a900a 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -126,6 +126,11 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({ unstable_disableNormalizePseudoGlobals: false, unstable_compactOutput: false, }, + watcher: { + watchman: { + deferStates: ['hg.update'], + }, + }, cacheStores: [ new FileStore({ root: path.join(os.tmpdir(), 'metro-cache'), diff --git a/packages/metro-config/src/loadConfig.js b/packages/metro-config/src/loadConfig.js index abe7482ca8..dd476f2e9e 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -162,6 +162,14 @@ function mergeConfig( ...totalConfig.symbolicator, ...(nextConfig.symbolicator || {}), }, + watcher: { + ...totalConfig.watcher, + ...nextConfig.watcher, + watchman: { + ...totalConfig.watcher?.watchman, + ...nextConfig.watcher?.watchman, + }, + }, }), defaultConfig, ); diff --git a/packages/metro-file-map/src/index.js b/packages/metro-file-map/src/index.js index c7c87a7144..a87a035a93 100644 --- a/packages/metro-file-map/src/index.js +++ b/packages/metro-file-map/src/index.js @@ -93,6 +93,7 @@ export type InputOptions = $ReadOnly<{ maxWorkers: number, throwOnModuleCollision?: ?boolean, useWatchman?: ?boolean, + watchmanDeferStates?: $ReadOnlyArray, watch?: ?boolean, console?: Console, cacheManagerFactory?: ?CacheManagerFactory, @@ -106,6 +107,7 @@ type InternalOptions = { throwOnModuleCollision: boolean, useWatchman: boolean, watch: boolean, + watchmanDeferStates: $ReadOnlyArray, }; interface Watcher { @@ -296,6 +298,7 @@ export default class HasteMap extends EventEmitter { throwOnModuleCollision: !!options.throwOnModuleCollision, useWatchman: options.useWatchman == null ? true : options.useWatchman, watch: !!options.watch, + watchmanDeferStates: options.watchmanDeferStates ?? [], }; this._console = options.console || global.console; @@ -861,6 +864,7 @@ export default class HasteMap extends EventEmitter { dot: true, glob: extensions.map(extension => '**/*.' + extension), ignored: ignorePattern, + watchmanDeferStates: this._options.watchmanDeferStates, }); return new Promise((resolve, reject) => { diff --git a/packages/metro-file-map/src/watchers/FSEventsWatcher.js b/packages/metro-file-map/src/watchers/FSEventsWatcher.js index 52bdd98269..f24d40f41e 100644 --- a/packages/metro-file-map/src/watchers/FSEventsWatcher.js +++ b/packages/metro-file-map/src/watchers/FSEventsWatcher.js @@ -94,6 +94,7 @@ export default class FSEventsWatcher extends EventEmitter { ignored?: Matcher, glob: string | $ReadOnlyArray, dot: boolean, + ... }>, ) { if (!fsevents) { diff --git a/packages/metro-file-map/src/watchers/WatchmanWatcher.js b/packages/metro-file-map/src/watchers/WatchmanWatcher.js index 9a207c567e..bc622c0fd0 100644 --- a/packages/metro-file-map/src/watchers/WatchmanWatcher.js +++ b/packages/metro-file-map/src/watchers/WatchmanWatcher.js @@ -119,6 +119,7 @@ WatchmanWatcher.prototype.init = function () { const options = { fields: ['name', 'exists', 'new'], since: resp.clock, + defer: self.watchmanDeferStates, }; // If the server has the wildmatch capability available it supports diff --git a/packages/metro-file-map/src/watchers/common.js b/packages/metro-file-map/src/watchers/common.js index 1d49fb6dd5..7a738a0cf4 100644 --- a/packages/metro-file-map/src/watchers/common.js +++ b/packages/metro-file-map/src/watchers/common.js @@ -35,6 +35,7 @@ exports.assignOptions = function (watcher, opts) { watcher.globs = opts.glob || []; watcher.dot = opts.dot || false; watcher.ignored = opts.ignored || false; + watcher.watchmanDeferStates = opts.watchmanDeferStates; if (!Array.isArray(watcher.globs)) { watcher.globs = [watcher.globs]; diff --git a/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js b/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js index 3b1fccdffb..69c7dceec1 100644 --- a/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js +++ b/packages/metro/src/node-haste/DependencyGraph/createHasteMap.js @@ -83,6 +83,7 @@ function createHasteMap( throwOnModuleCollision: options?.throwOnModuleCollision ?? true, useWatchman: config.resolver.useWatchman, watch: options?.watch == null ? !ci.isCI : options.watch, + watchmanDeferStates: config.watcher.watchman.deferStates, }); }