@@ -20,6 +20,34 @@ import type {
20
20
21
21
type WatchmanRoots = Map < string , Array < string > > ;
22
22
23
+ type WatchmanListCapabilitiesResponse = {
24
+ capabilities : Array < string > ;
25
+ } ;
26
+
27
+ type WatchmanWatchProjectResponse = {
28
+ watch : string ;
29
+ relative_path : string ;
30
+ } ;
31
+
32
+ type WatchmanQueryResponse = {
33
+ warning ?: string ;
34
+ is_fresh_instance : boolean ;
35
+ version : string ;
36
+ clock :
37
+ | string
38
+ | {
39
+ scm : { 'mergebase-with' : string ; mergebase : string } ;
40
+ clock : string ;
41
+ } ;
42
+ files : Array < {
43
+ name : string ;
44
+ exists : boolean ;
45
+ mtime_ms : number | { toNumber : ( ) => number } ;
46
+ size : number ;
47
+ 'content.sha1hex' ?: string ;
48
+ } > ;
49
+ } ;
50
+
23
51
const watchmanURL = 'https://facebook.github.io/watchman/docs/troubleshooting' ;
24
52
25
53
function WatchmanError ( error : Error ) : Error {
@@ -49,16 +77,17 @@ export = async function watchmanCrawl(
49
77
let clientError ;
50
78
client . on ( 'error' , error => ( clientError = WatchmanError ( error ) ) ) ;
51
79
52
- // TODO: type better than `any`
53
- const cmd = ( ...args : Array < any > ) : Promise < any > =>
80
+ const cmd = < T > ( ...args : Array < any > ) : Promise < T > =>
54
81
new Promise ( ( resolve , reject ) =>
55
82
client . command ( args , ( error , result ) =>
56
83
error ? reject ( WatchmanError ( error ) ) : resolve ( result ) ,
57
84
) ,
58
85
) ;
59
86
60
87
if ( options . computeSha1 ) {
61
- const { capabilities} = await cmd ( 'list-capabilities' ) ;
88
+ const { capabilities} = await cmd < WatchmanListCapabilitiesResponse > (
89
+ 'list-capabilities' ,
90
+ ) ;
62
91
63
92
if ( capabilities . indexOf ( 'field-content.sha1hex' ) !== - 1 ) {
64
93
fields . push ( 'content.sha1hex' ) ;
@@ -71,7 +100,10 @@ export = async function watchmanCrawl(
71
100
const watchmanRoots = new Map ( ) ;
72
101
await Promise . all (
73
102
roots . map ( async root => {
74
- const response = await cmd ( 'watch-project' , root ) ;
103
+ const response = await cmd < WatchmanWatchProjectResponse > (
104
+ 'watch-project' ,
105
+ root ,
106
+ ) ;
75
107
const existing = watchmanRoots . get ( response . watch ) ;
76
108
// A root can only be filtered if it was never seen with a
77
109
// relative_path before.
@@ -96,7 +128,7 @@ export = async function watchmanCrawl(
96
128
}
97
129
98
130
async function queryWatchmanForDirs ( rootProjectDirMappings : WatchmanRoots ) {
99
- const files = new Map ( ) ;
131
+ const results = new Map < string , WatchmanQueryResponse > ( ) ;
100
132
let isFresh = false ;
101
133
await Promise . all (
102
134
Array . from ( rootProjectDirMappings ) . map (
@@ -121,35 +153,58 @@ export = async function watchmanCrawl(
121
153
}
122
154
}
123
155
124
- const relativeRoot = fastPath . relative ( rootDir , root ) ;
125
- const query = clocks . has ( relativeRoot )
126
- ? // Use the `since` generator if we have a clock available
127
- { expression, fields, since : clocks . get ( relativeRoot ) }
128
- : // Otherwise use the `glob` filter
129
- { expression, fields, glob, glob_includedotfiles : true } ;
130
-
131
- const response = await cmd ( 'query' , root , query ) ;
156
+ // Jest is only going to store one type of clock; a string that
157
+ // represents a local clock. However, the Watchman crawler supports
158
+ // a second type of clock that can be written by automation outside of
159
+ // Jest, called an "scm query", which fetches changed files based on
160
+ // source control mergebases. The reason this is necessary is because
161
+ // local clocks are not portable across systems, but scm queries are.
162
+ // By using scm queries, we can create the haste map on a different
163
+ // system and import it, transforming the clock into a local clock.
164
+ const since = clocks . get ( fastPath . relative ( rootDir , root ) ) ;
165
+
166
+ const query =
167
+ since !== undefined
168
+ ? // Use the `since` generator if we have a clock available
169
+ { expression, fields, since}
170
+ : // Otherwise use the `glob` filter
171
+ { expression, fields, glob, glob_includedotfiles : true } ;
172
+
173
+ const response = await cmd < WatchmanQueryResponse > (
174
+ 'query' ,
175
+ root ,
176
+ query ,
177
+ ) ;
132
178
133
179
if ( 'warning' in response ) {
134
180
console . warn ( 'watchman warning: ' , response . warning ) ;
135
181
}
136
182
137
- isFresh = isFresh || response . is_fresh_instance ;
138
- files . set ( root , response ) ;
183
+ // When a source-control query is used, we ignore the "is fresh"
184
+ // response from Watchman because it will be true despite the query
185
+ // being incremental.
186
+ const isSourceControlQuery =
187
+ typeof since !== 'string' &&
188
+ since ?. scm ?. [ 'mergebase-with' ] !== undefined ;
189
+ if ( ! isSourceControlQuery ) {
190
+ isFresh = isFresh || response . is_fresh_instance ;
191
+ }
192
+
193
+ results . set ( root , response ) ;
139
194
} ,
140
195
) ,
141
196
) ;
142
197
143
198
return {
144
- files,
145
199
isFresh,
200
+ results,
146
201
} ;
147
202
}
148
203
149
204
let files = data . files ;
150
205
let removedFiles = new Map ( ) ;
151
206
const changedFiles = new Map ( ) ;
152
- let watchmanFiles : Map < string , any > ;
207
+ let results : Map < string , WatchmanQueryResponse > ;
153
208
let isFresh = false ;
154
209
try {
155
210
const watchmanRoots = await getWatchmanRoots ( roots ) ;
@@ -163,7 +218,7 @@ export = async function watchmanCrawl(
163
218
isFresh = true ;
164
219
}
165
220
166
- watchmanFiles = watchmanFileResults . files ;
221
+ results = watchmanFileResults . results ;
167
222
} finally {
168
223
client . end ( ) ;
169
224
}
@@ -172,11 +227,16 @@ export = async function watchmanCrawl(
172
227
throw clientError ;
173
228
}
174
229
175
- // TODO: remove non-null
176
- for ( const [ watchRoot , response ] of watchmanFiles ! ) {
230
+ for ( const [ watchRoot , response ] of results ) {
177
231
const fsRoot = normalizePathSep ( watchRoot ) ;
178
232
const relativeFsRoot = fastPath . relative ( rootDir , fsRoot ) ;
179
- clocks . set ( relativeFsRoot , response . clock ) ;
233
+ clocks . set (
234
+ relativeFsRoot ,
235
+ // Ensure we persist only the local clock.
236
+ typeof response . clock === 'string'
237
+ ? response . clock
238
+ : response . clock . clock ,
239
+ ) ;
180
240
181
241
for ( const fileData of response . files ) {
182
242
const filePath = fsRoot + path . sep + normalizePathSep ( fileData . name ) ;
@@ -209,7 +269,7 @@ export = async function watchmanCrawl(
209
269
210
270
let sha1hex = fileData [ 'content.sha1hex' ] ;
211
271
if ( typeof sha1hex !== 'string' || sha1hex . length !== 40 ) {
212
- sha1hex = null ;
272
+ sha1hex = undefined ;
213
273
}
214
274
215
275
let nextData : FileMetaData ;
@@ -231,7 +291,7 @@ export = async function watchmanCrawl(
231
291
] ;
232
292
} else {
233
293
// See ../constants.ts
234
- nextData = [ '' , mtime , size , 0 , '' , sha1hex ] ;
294
+ nextData = [ '' , mtime , size , 0 , '' , sha1hex ?? null ] ;
235
295
}
236
296
237
297
files . set ( relativeFilePath , nextData ) ;
0 commit comments