6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
- import { FSWatcher } from 'chokidar' ;
10
- import { extname , normalize } from 'node:path' ;
9
+ import WatchPack from 'watchpack' ;
11
10
12
11
export class ChangedFiles {
13
12
readonly added = new Set < string > ( ) ;
@@ -41,93 +40,44 @@ export function createWatcher(options?: {
41
40
ignored ?: string [ ] ;
42
41
followSymlinks ?: boolean ;
43
42
} ) : BuildWatcher {
44
- const watcher = new FSWatcher ( {
45
- usePolling : options ?. polling ,
46
- interval : options ?. interval ,
43
+ const watcher = new WatchPack ( {
44
+ poll : options ?. polling ? options ?. interval ?? true : false ,
47
45
ignored : options ?. ignored ,
48
46
followSymlinks : options ?. followSymlinks ,
49
- disableGlobbing : true ,
50
- ignoreInitial : true ,
47
+ aggregateTimeout : 250 ,
51
48
} ) ;
49
+ const watchedFiles = new Set < string > ( ) ;
52
50
53
51
const nextQueue : ( ( value ?: ChangedFiles ) => void ) [ ] = [ ] ;
54
- let currentChanges : ChangedFiles | undefined ;
55
- let nextWaitTimeout : NodeJS . Timeout | undefined ;
56
-
57
- /**
58
- * We group the current events in a map as on Windows with certain IDE a file contents change can trigger multiple events.
59
- *
60
- * Example:
61
- * rename | 'C:/../src/app/app.component.css'
62
- * rename | 'C:/../src/app/app.component.css'
63
- * change | 'C:/../src/app/app.component.css'
64
- *
65
- */
66
- let currentEvents : Map < /* Event name */ string , /* File path */ string > | undefined ;
67
-
68
- /**
69
- * Using `watcher.on('all')` does not capture some of events fired when using Visual studio and this does not happen all the time,
70
- * but only after a file has been changed 3 or more times.
71
- *
72
- * Also, some IDEs such as Visual Studio (not VS Code) will fire a rename event instead of unlink when a file is renamed or changed.
73
- *
74
- * Example:
75
- * ```
76
- * watcher.on('raw')
77
- * Change 1
78
- * rename | 'C:/../src/app/app.component.css'
79
- * rename | 'C:/../src/app/app.component.css'
80
- * change | 'C:/../src/app/app.component.css'
81
- *
82
- * Change 2
83
- * rename | 'C:/../src/app/app.component.css'
84
- * rename | 'C:/../src/app/app.component.css'
85
- * change | 'C:/../src/app/app.component.css'
86
- *
87
- * Change 3
88
- * rename | 'C:/../src/app/app.component.css'
89
- * rename | 'C:/../src/app/app.component.css'
90
- * change | 'C:/../src/app/app.component.css'
91
- *
92
- * watcher.on('all')
93
- * Change 1
94
- * change | 'C:\\..\\src\\app\\app.component.css'
95
- *
96
- * Change 2
97
- * unlink | 'C:\\..\\src\\app\\app.component.css'
98
- *
99
- * Change 3
100
- * ... (Nothing)
101
- * ```
102
- */
103
- watcher
104
- . on ( 'raw' , ( event , path , { watchedPath } ) => {
105
- if ( watchedPath && ! extname ( watchedPath ) ) {
106
- // Ignore directories, file changes in directories will be fired seperatly.
107
- return ;
108
- }
52
+ let currentChangedFiles : ChangedFiles | undefined ;
109
53
110
- switch ( event ) {
111
- case 'rename' :
112
- case 'change' :
113
- // When polling is enabled `watchedPath` can be undefined.
114
- // `path` is always normalized unlike `watchedPath`.
115
- const changedPath = watchedPath ? normalize ( watchedPath ) : path ;
116
- handleFileChange ( event , changedPath ) ;
117
- break ;
118
- }
119
- } )
120
- . on ( 'all' , handleFileChange ) ;
54
+ watcher . on ( 'aggregated' , ( changes , removals ) => {
55
+ const changedFiles = currentChangedFiles ?? new ChangedFiles ( ) ;
56
+ for ( const file of changes ) {
57
+ changedFiles . modified . add ( file ) ;
58
+ }
59
+ for ( const file of removals ) {
60
+ changedFiles . removed . add ( file ) ;
61
+ }
62
+
63
+ const next = nextQueue . shift ( ) ;
64
+ if ( next ) {
65
+ currentChangedFiles = undefined ;
66
+ next ( changedFiles ) ;
67
+ } else {
68
+ currentChangedFiles = changedFiles ;
69
+ }
70
+ } ) ;
121
71
122
72
return {
123
73
[ Symbol . asyncIterator ] ( ) {
124
74
return this ;
125
75
} ,
126
76
127
77
async next ( ) {
128
- if ( currentChanges && nextQueue . length === 0 ) {
129
- const result = { value : currentChanges } ;
130
- currentChanges = undefined ;
78
+ if ( currentChangedFiles && nextQueue . length === 0 ) {
79
+ const result = { value : currentChangedFiles } ;
80
+ currentChangedFiles = undefined ;
131
81
132
82
return result ;
133
83
}
@@ -138,19 +88,42 @@ export function createWatcher(options?: {
138
88
} ,
139
89
140
90
add ( paths ) {
141
- watcher . add ( paths ) ;
91
+ const previousSize = watchedFiles . size ;
92
+ if ( typeof paths === 'string' ) {
93
+ watchedFiles . add ( paths ) ;
94
+ } else {
95
+ for ( const file of paths ) {
96
+ watchedFiles . add ( file ) ;
97
+ }
98
+ }
99
+
100
+ if ( previousSize !== watchedFiles . size ) {
101
+ watcher . watch ( {
102
+ files : watchedFiles ,
103
+ } ) ;
104
+ }
142
105
} ,
143
106
144
107
remove ( paths ) {
145
- watcher . unwatch ( paths ) ;
108
+ const previousSize = watchedFiles . size ;
109
+ if ( typeof paths === 'string' ) {
110
+ watchedFiles . delete ( paths ) ;
111
+ } else {
112
+ for ( const file of paths ) {
113
+ watchedFiles . delete ( file ) ;
114
+ }
115
+ }
116
+
117
+ if ( previousSize !== watchedFiles . size ) {
118
+ watcher . watch ( {
119
+ files : watchedFiles ,
120
+ } ) ;
121
+ }
146
122
} ,
147
123
148
124
async close ( ) {
149
125
try {
150
- await watcher . close ( ) ;
151
- if ( nextWaitTimeout ) {
152
- clearTimeout ( nextWaitTimeout ) ;
153
- }
126
+ watcher . close ( ) ;
154
127
} finally {
155
128
let next ;
156
129
while ( ( next = nextQueue . shift ( ) ) !== undefined ) {
@@ -159,51 +132,4 @@ export function createWatcher(options?: {
159
132
}
160
133
} ,
161
134
} ;
162
-
163
- function handleFileChange ( event : string , path : string ) : void {
164
- switch ( event ) {
165
- case 'add' :
166
- case 'change' :
167
- // When using Visual Studio the rename event is fired before a change event when the contents of the file changed
168
- // or instead of `unlink` when the file has been renamed.
169
- case 'unlink' :
170
- case 'rename' :
171
- currentEvents ??= new Map ( ) ;
172
- currentEvents . set ( path , event ) ;
173
- break ;
174
- default :
175
- return ;
176
- }
177
-
178
- // Wait 250ms from next change to better capture groups of file save operations.
179
- if ( ! nextWaitTimeout ) {
180
- nextWaitTimeout = setTimeout ( ( ) => {
181
- nextWaitTimeout = undefined ;
182
- const next = nextQueue . shift ( ) ;
183
- if ( next && currentEvents ) {
184
- const events = currentEvents ;
185
- currentEvents = undefined ;
186
-
187
- const currentChanges = new ChangedFiles ( ) ;
188
- for ( const [ path , event ] of events ) {
189
- switch ( event ) {
190
- case 'add' :
191
- currentChanges . added . add ( path ) ;
192
- break ;
193
- case 'change' :
194
- currentChanges . modified . add ( path ) ;
195
- break ;
196
- case 'unlink' :
197
- case 'rename' :
198
- currentChanges . removed . add ( path ) ;
199
- break ;
200
- }
201
- }
202
-
203
- next ( currentChanges ) ;
204
- }
205
- } , 250 ) ;
206
- nextWaitTimeout ?. unref ( ) ;
207
- }
208
- }
209
135
}
0 commit comments