@@ -76,6 +76,19 @@ const getCanonicalWatchOptions = (options) => {
7676 return snapshot ;
7777} ;
7878
79+ /** @typedef {Iterable<string> & { has(item: string): boolean } } Lookup */
80+
81+ /**
82+ * Returns the items as they are when they have `has`, else as a set, which
83+ * also keeps a later change by the caller or a one-shot iterable from mattering.
84+ * @param {Iterable<string> } items items
85+ * @returns {Lookup } the items, as something with `has`
86+ */
87+ const toLookup = ( items ) =>
88+ typeof ( /** @type {Set<string> } */ ( items ) . has ) === "function"
89+ ? /** @type {Set<string> } */ ( items )
90+ : new Set ( items ) ;
91+
7992class NodeWatchFileSystem {
8093 /**
8194 * Creates an instance of NodeWatchFileSystem.
@@ -91,6 +104,39 @@ class NodeWatchFileSystem {
91104 // `watch` replaces this with a watcher built from its own options
92105 /** @type {Watchpack | null } */
93106 this . watcher = null ;
107+ /** @type {{ files: Lookup, directories: Lookup, missing: Lookup } | undefined } */
108+ this . _watched = undefined ;
109+ }
110+
111+ /**
112+ * Purges what the previous call watched and this one does not: an edit to a
113+ * path nobody watches is never reported, so its cached stat and content would
114+ * be read stale once the path is watched again.
115+ * @param {Lookup } files files to watch
116+ * @param {Lookup } directories directories to watch
117+ * @param {Lookup } missing missing paths to watch
118+ * @returns {void }
119+ */
120+ _purgeUnwatched ( files , directories , missing ) {
121+ const previous = this . _watched ;
122+ this . _watched = { files, directories, missing } ;
123+ const fs = this . inputFileSystem ;
124+ if ( previous === undefined || ! fs || ! fs . purge ) return ;
125+ /** @type {string[] } */
126+ const dropped = [ ] ;
127+ for ( const list of [
128+ previous . files ,
129+ previous . directories ,
130+ previous . missing
131+ ] ) {
132+ for ( const item of list ) {
133+ if ( ! files . has ( item ) && ! directories . has ( item ) && ! missing . has ( item ) ) {
134+ dropped . push ( item ) ;
135+ }
136+ }
137+ }
138+ // a prefix purge scans the whole cache once per path
139+ if ( dropped . length > 0 ) fs . purge ( dropped , { exact : true } ) ;
94140 }
95141
96142 /** @type {WatchMethod } */
@@ -124,6 +170,10 @@ class NodeWatchFileSystem {
124170 if ( typeof callbackUndelayed !== "function" && callbackUndelayed ) {
125171 throw new Error ( "Invalid arguments: 'callbackUndelayed'" ) ;
126172 }
173+ const filesLookup = toLookup ( files ) ;
174+ const directoriesLookup = toLookup ( directories ) ;
175+ const missingLookup = toLookup ( missing ) ;
176+ this . _purgeUnwatched ( filesLookup , directoriesLookup , missingLookup ) ;
127177 const oldWatcher = this . watcher ;
128178 this . watcher = new ( getWatchpack ( ) ) ( getCanonicalWatchOptions ( options ) ) ;
129179
@@ -144,9 +194,6 @@ class NodeWatchFileSystem {
144194 }
145195 return { fileTimeInfoEntries, contextTimeInfoEntries } ;
146196 } ;
147- const directoriesSet =
148- directories instanceof Set ? directories : new Set ( directories ) ;
149-
150197 // Watchpack reports a watched directory whenever its contents change, and
151198 // `fs.purge(dir)` matches by prefix, wiping every file's stat cache with it.
152199 // Only its own entry is purged; the batch's file events carry the rest.
@@ -159,7 +206,7 @@ class NodeWatchFileSystem {
159206 if ( ! fs || ! fs . purge ) return ;
160207 if ( changes ) {
161208 for ( const item of changes ) {
162- if ( directoriesSet . has ( item ) ) {
209+ if ( directoriesLookup . has ( item ) ) {
163210 fs . purge ( item , { exact : true } ) ;
164211 } else {
165212 fs . purge ( item ) ;
@@ -197,7 +244,12 @@ class NodeWatchFileSystem {
197244 }
198245 ) ;
199246
200- this . watcher . watch ( { files, directories, missing, startTime } ) ;
247+ this . watcher . watch ( {
248+ files : filesLookup ,
249+ directories : directoriesLookup ,
250+ missing : missingLookup ,
251+ startTime
252+ } ) ;
201253
202254 if ( oldWatcher ) {
203255 oldWatcher . close ( ) ;
0 commit comments