Skip to content

Commit 1725b91

Browse files
committed
fix(@angular-devkit/build-angular): improve file watching on Windows when using certain IDEs
This commit, fixes a file watching issue where file changes events are not triggered when using IDEs like Visual Studio (not VS Code). The main changes to solve the issue are; ## Replace `watcher.on('all')` with `watcher.on('raw')` Using `watcher.on('all')` does not capture some of events fired when using Visual studio and this does not happen all the time, but only after a file has been changed 3 or more times. ``` watcher.on('raw') Change 1 rename | 'C:/../src/app/app.component.css' rename | 'C:/../src/app/app.component.css' change | 'C:/../src/app/app.component.css' Change 2 rename | 'C:/../src/app/app.component.css' rename | 'C:/../src/app/app.component.css' change | 'C:/../src/app/app.component.css' Change 3 rename | 'C:/../src/app/app.component.css' rename | 'C:/../src/app/app.component.css' change | 'C:/../src/app/app.component.css' watcher.on('all') Change 1 change | 'C:\\..\\src\\app\\app.component.css' Change 2 unlink | 'C:\\..\\src\\app\\app.component.css' Change 3 ... (Nothing) ``` ## Handle `rename` events Some IDEs will fire a rename event instead of unlink/changed when a file is modified} Closes #26437
1 parent 571722d commit 1725b91

File tree

2 files changed

+78
-17
lines changed

2 files changed

+78
-17
lines changed

packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts

+77-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { FSWatcher } from 'chokidar';
10+
import { normalize } from 'node:path';
1011

1112
export class ChangedFiles {
1213
readonly added = new Set<string>();
@@ -51,19 +52,65 @@ export function createWatcher(options?: {
5152
let currentChanges: ChangedFiles | undefined;
5253
let nextWaitTimeout: NodeJS.Timeout | undefined;
5354

54-
watcher.on('all', (event, path) => {
55+
/**
56+
* We group the current events in a map as on Windows with certain IDE a file contents change can trigger multiple events.
57+
*
58+
* Example:
59+
* rename | 'C:/../src/app/app.component.css'
60+
* rename | 'C:/../src/app/app.component.css'
61+
* change | 'C:/../src/app/app.component.css'
62+
*
63+
*/
64+
let currentEvents: Map</* Event name */ string, /* File path */ string> | undefined;
65+
66+
/**
67+
* Using `watcher.on('all')` does not capture some of events fired when using Visual studio and this does not happen all the time,
68+
* but only after a file has been changed 3 or more times.
69+
*
70+
* 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.
71+
*
72+
* Example:
73+
* ```
74+
* watcher.on('raw')
75+
* Change 1
76+
* rename | 'C:/../src/app/app.component.css'
77+
* rename | 'C:/../src/app/app.component.css'
78+
* change | 'C:/../src/app/app.component.css'
79+
*
80+
* Change 2
81+
* rename | 'C:/../src/app/app.component.css'
82+
* rename | 'C:/../src/app/app.component.css'
83+
* change | 'C:/../src/app/app.component.css'
84+
*
85+
* Change 3
86+
* rename | 'C:/../src/app/app.component.css'
87+
* rename | 'C:/../src/app/app.component.css'
88+
* change | 'C:/../src/app/app.component.css'
89+
*
90+
* watcher.on('all')
91+
* Change 1
92+
* change | 'C:\\..\\src\\app\\app.component.css'
93+
*
94+
* Change 2
95+
* unlink | 'C:\\..\\src\\app\\app.component.css'
96+
*
97+
* Change 3
98+
* ... (Nothing)
99+
* ```
100+
*/
101+
watcher.on('raw', (event, path, { watchedPath }) => {
55102
switch (event) {
56103
case 'add':
57-
currentChanges ??= new ChangedFiles();
58-
currentChanges.added.add(path);
59-
break;
60104
case 'change':
61-
currentChanges ??= new ChangedFiles();
62-
currentChanges.modified.add(path);
63-
break;
105+
// When using Visual Studio the rename event is fired before a change event when the contents of the file changed
106+
// or instead of `unlink` when the file has been renamed.
64107
case 'unlink':
65-
currentChanges ??= new ChangedFiles();
66-
currentChanges.removed.add(path);
108+
case 'rename':
109+
// When polling is enabled `watchedPath` can be undefined.
110+
// `path` is always normalized unlike `watchedPath`.
111+
const changedPath = watchedPath ? normalize(watchedPath) : path;
112+
currentEvents ??= new Map();
113+
currentEvents.set(changedPath, event);
67114
break;
68115
default:
69116
return;
@@ -74,10 +121,27 @@ export function createWatcher(options?: {
74121
nextWaitTimeout = setTimeout(() => {
75122
nextWaitTimeout = undefined;
76123
const next = nextQueue.shift();
77-
if (next) {
78-
const value = currentChanges;
79-
currentChanges = undefined;
80-
next(value);
124+
if (next && currentEvents) {
125+
const events = currentEvents;
126+
currentEvents = undefined;
127+
128+
const currentChanges = new ChangedFiles();
129+
for (const [path, event] of events) {
130+
switch (event) {
131+
case 'add':
132+
currentChanges.added.add(path);
133+
break;
134+
case 'change':
135+
currentChanges.modified.add(path);
136+
break;
137+
case 'unlink':
138+
case 'rename':
139+
currentChanges.removed.add(path);
140+
break;
141+
}
142+
}
143+
144+
next(currentChanges);
81145
}
82146
}, 250);
83147
nextWaitTimeout?.unref();

packages/angular_devkit/build_angular/src/utils/environment-options.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,4 @@ export const debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugP
102102

103103
// Default to true on Windows to workaround Visual Studio atomic file saving watch issues
104104
const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT'];
105-
export const shouldWatchRoot =
106-
process.platform === 'win32'
107-
? !isPresent(watchRootVariable) || !isDisabled(watchRootVariable)
108-
: isPresent(watchRootVariable) && isEnabled(watchRootVariable);
105+
export const shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable);

0 commit comments

Comments
 (0)