Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): improve file watching on Windows …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
alan-agius4 committed Nov 23, 2023
1 parent 571722d commit 1725b91
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 17 deletions.
90 changes: 77 additions & 13 deletions packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { FSWatcher } from 'chokidar';
import { normalize } from 'node:path';

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

watcher.on('all', (event, path) => {
/**
* We group the current events in a map as on Windows with certain IDE a file contents change can trigger multiple events.
*
* Example:
* rename | 'C:/../src/app/app.component.css'
* rename | 'C:/../src/app/app.component.css'
* change | 'C:/../src/app/app.component.css'
*
*/
let currentEvents: Map</* Event name */ string, /* File path */ string> | undefined;

/**
* 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.
*
* 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.
*
* 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)
* ```
*/
watcher.on('raw', (event, path, { watchedPath }) => {
switch (event) {
case 'add':
currentChanges ??= new ChangedFiles();
currentChanges.added.add(path);
break;
case 'change':
currentChanges ??= new ChangedFiles();
currentChanges.modified.add(path);
break;
// When using Visual Studio the rename event is fired before a change event when the contents of the file changed
// or instead of `unlink` when the file has been renamed.
case 'unlink':
currentChanges ??= new ChangedFiles();
currentChanges.removed.add(path);
case 'rename':
// When polling is enabled `watchedPath` can be undefined.
// `path` is always normalized unlike `watchedPath`.
const changedPath = watchedPath ? normalize(watchedPath) : path;
currentEvents ??= new Map();
currentEvents.set(changedPath, event);
break;
default:
return;
Expand All @@ -74,10 +121,27 @@ export function createWatcher(options?: {
nextWaitTimeout = setTimeout(() => {
nextWaitTimeout = undefined;
const next = nextQueue.shift();
if (next) {
const value = currentChanges;
currentChanges = undefined;
next(value);
if (next && currentEvents) {
const events = currentEvents;
currentEvents = undefined;

const currentChanges = new ChangedFiles();
for (const [path, event] of events) {
switch (event) {
case 'add':
currentChanges.added.add(path);
break;
case 'change':
currentChanges.modified.add(path);
break;
case 'unlink':
case 'rename':
currentChanges.removed.add(path);
break;
}
}

next(currentChanges);
}
}, 250);
nextWaitTimeout?.unref();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,4 @@ export const debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugP

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

0 comments on commit 1725b91

Please sign in to comment.