From 4594407ae214ce49985a5df315cae3ac8107147d Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 23 Nov 2023 12:38:17 +0000 Subject: [PATCH] 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 (cherry picked from commit 1725b91e357f613f7fb7547d13d6499973ee3849) (cherry picked from commit 4bcee31675eb7b552d0cfe21f705ed65a0cdaadb) --- .../src/tools/esbuild/watcher.ts | 90 ++++++++++++++++--- .../src/utils/environment-options.ts | 5 +- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts index 55f0f25314c2..a4c45feaf551 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/watcher.ts @@ -7,6 +7,7 @@ */ import { FSWatcher } from 'chokidar'; +import { normalize } from 'node:path'; export class ChangedFiles { readonly added = new Set(); @@ -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 | 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; @@ -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(); diff --git a/packages/angular_devkit/build_angular/src/utils/environment-options.ts b/packages/angular_devkit/build_angular/src/utils/environment-options.ts index 78e21154e578..9db966c6171f 100644 --- a/packages/angular_devkit/build_angular/src/utils/environment-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/environment-options.ts @@ -101,7 +101,4 @@ const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF']; export const debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable); 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);