Skip to content

Commit b4b89fd

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`)` The former does not fire for all events example ``` 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 angular#26437
1 parent d97a902 commit b4b89fd

File tree

2 files changed

+76
-17
lines changed

2 files changed

+76
-17
lines changed

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

+75-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,63 @@ 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+
65+
let currentEvents: Map</* Event name */ string, /* File path */ string> | undefined;
66+
67+
/**
68+
* Using `watcher.on('all')` does not capture some of events fired when using Visual studio and this does not happen all the time,
69+
* but only after a file has been changed 3 or more times.
70+
*
71+
* 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.
72+
*
73+
* Example:
74+
* ```
75+
* watcher.on('raw')
76+
* Change 1
77+
* rename | 'C:/../src/app/app.component.css'
78+
* rename | 'C:/../src/app/app.component.css'
79+
* change | 'C:/../src/app/app.component.css'
80+
*
81+
* Change 2
82+
* rename | 'C:/../src/app/app.component.css'
83+
* rename | 'C:/../src/app/app.component.css'
84+
* change | 'C:/../src/app/app.component.css'
85+
*
86+
* Change 3
87+
* rename | 'C:/../src/app/app.component.css'
88+
* rename | 'C:/../src/app/app.component.css'
89+
* change | 'C:/../src/app/app.component.css'
90+
*
91+
* watcher.on('all')
92+
* Change 1
93+
* change | 'C:\\..\\src\\app\\app.component.css'
94+
*
95+
* Change 2
96+
* unlink | 'C:\\..\\src\\app\\app.component.css'
97+
*
98+
* Change 3
99+
* ... (Nothing)
100+
* ```
101+
*/
102+
watcher.on('raw', (event, _basename, { watchedPath }) => {
55103
switch (event) {
56104
case 'add':
57-
currentChanges ??= new ChangedFiles();
58-
currentChanges.added.add(path);
59-
break;
60105
case 'change':
61-
currentChanges ??= new ChangedFiles();
62-
currentChanges.modified.add(path);
63-
break;
106+
// When using Visual Studio the rename event is fired before a change event when the contents of the file changed
107+
// or instead of `unlink` when the file name has changed.
64108
case 'unlink':
65-
currentChanges ??= new ChangedFiles();
66-
currentChanges.removed.add(path);
109+
case 'rename':
110+
currentEvents ??= new Map();
111+
currentEvents.set(normalize(watchedPath), event);
67112
break;
68113
default:
69114
return;
@@ -74,10 +119,27 @@ export function createWatcher(options?: {
74119
nextWaitTimeout = setTimeout(() => {
75120
nextWaitTimeout = undefined;
76121
const next = nextQueue.shift();
77-
if (next) {
78-
const value = currentChanges;
79-
currentChanges = undefined;
80-
next(value);
122+
if (next && currentEvents) {
123+
const events = currentEvents;
124+
currentEvents = undefined;
125+
126+
const currentChanges = new ChangedFiles();
127+
for (const [path, event] of events) {
128+
switch (event) {
129+
case 'add':
130+
currentChanges.added.add(path);
131+
break;
132+
case 'change':
133+
currentChanges.modified.add(path);
134+
break;
135+
case 'unlink':
136+
case 'rename':
137+
currentChanges.removed.add(path);
138+
break;
139+
}
140+
}
141+
142+
next(currentChanges);
81143
}
82144
}, 250);
83145
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)