Skip to content

Commit fe51926

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): convert watching to use watchpack package
The underlying file watching functionality now uses the `watchpack` library for all builders. This includes the Webpack-based `browser` and the esbuild-based `application`/ `browser-esbuild`. This not only has the advantage of a single dependency for both but also provides more consistent behavior between the two build system in regards to file watching. Since the implementation of the file watching is fully encapsulated, there is no change to the options or configurations of consuming applications.
1 parent 16d5486 commit fe51926

File tree

6 files changed

+82
-133
lines changed

6 files changed

+82
-133
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"@types/shelljs": "^0.8.11",
115115
"@types/tar": "^6.1.2",
116116
"@types/text-table": "^0.2.1",
117+
"@types/watchpack": "^2.4.4",
117118
"@types/yargs": "^17.0.20",
118119
"@types/yargs-parser": "^21.0.0",
119120
"@types/yarnpkg__lockfile": "^1.1.5",
@@ -211,6 +212,7 @@
211212
"verdaccio": "5.29.0",
212213
"verdaccio-auth-memory": "^10.0.0",
213214
"vite": "5.0.7",
215+
"watchpack": "2.4.0",
214216
"webpack": "5.89.0",
215217
"webpack-dev-middleware": "6.1.1",
216218
"webpack-dev-server": "4.15.1",

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,14 @@ ts_library(
154154
"@npm//@types/picomatch",
155155
"@npm//@types/semver",
156156
"@npm//@types/text-table",
157+
"@npm//@types/watchpack",
157158
"@npm//@vitejs/plugin-basic-ssl",
158159
"@npm//ajv",
159160
"@npm//ansi-colors",
160161
"@npm//autoprefixer",
161162
"@npm//babel-loader",
162163
"@npm//babel-plugin-istanbul",
163164
"@npm//browserslist",
164-
"@npm//chokidar",
165165
"@npm//copy-webpack-plugin",
166166
"@npm//critters",
167167
"@npm//css-loader",
@@ -202,6 +202,7 @@ ts_library(
202202
"@npm//typescript",
203203
"@npm//undici",
204204
"@npm//vite",
205+
"@npm//watchpack",
205206
"@npm//webpack",
206207
"@npm//webpack-dev-middleware",
207208
"@npm//webpack-dev-server",

packages/angular_devkit/build_angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"babel-loader": "9.1.3",
2828
"babel-plugin-istanbul": "6.1.1",
2929
"browserslist": "^4.21.5",
30-
"chokidar": "3.5.3",
3130
"copy-webpack-plugin": "11.0.0",
3231
"critters": "0.0.20",
3332
"css-loader": "6.8.1",
@@ -65,6 +64,7 @@
6564
"tslib": "2.6.2",
6665
"undici": "6.0.1",
6766
"vite": "5.0.7",
67+
"watchpack": "2.4.0",
6868
"webpack": "5.89.0",
6969
"webpack-dev-middleware": "6.1.1",
7070
"webpack-dev-server": "4.15.1",

packages/angular_devkit/build_angular/src/builders/application/build-action.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { BuilderOutput } from '@angular-devkit/architect';
1010
import type { logging } from '@angular-devkit/core';
11+
import { existsSync } from 'node:fs';
1112
import path from 'node:path';
1213
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
1314
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
@@ -115,7 +116,11 @@ export async function* runEsBuildBuildAction(
115116
'.pnp.data.json',
116117
];
117118

118-
watcher.add(packageWatchFiles.map((file) => path.join(workspaceRoot, file)));
119+
watcher.add(
120+
packageWatchFiles
121+
.map((file) => path.join(workspaceRoot, file))
122+
.filter((file) => existsSync(file)),
123+
);
119124

120125
// Watch locations provided by the initial build result
121126
watcher.add(result.watchFiles);

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

Lines changed: 55 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { FSWatcher } from 'chokidar';
10-
import { extname, normalize } from 'node:path';
9+
import WatchPack from 'watchpack';
1110

1211
export class ChangedFiles {
1312
readonly added = new Set<string>();
@@ -41,93 +40,44 @@ export function createWatcher(options?: {
4140
ignored?: string[];
4241
followSymlinks?: boolean;
4342
}): 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,
4745
ignored: options?.ignored,
4846
followSymlinks: options?.followSymlinks,
49-
disableGlobbing: true,
50-
ignoreInitial: true,
47+
aggregateTimeout: 250,
5148
});
49+
const watchedFiles = new Set<string>();
5250

5351
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;
10953

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+
});
12171

12272
return {
12373
[Symbol.asyncIterator]() {
12474
return this;
12575
},
12676

12777
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;
13181

13282
return result;
13383
}
@@ -138,19 +88,42 @@ export function createWatcher(options?: {
13888
},
13989

14090
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+
}
142105
},
143106

144107
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+
}
146122
},
147123

148124
async close() {
149125
try {
150-
await watcher.close();
151-
if (nextWaitTimeout) {
152-
clearTimeout(nextWaitTimeout);
153-
}
126+
watcher.close();
154127
} finally {
155128
let next;
156129
while ((next = nextQueue.shift()) !== undefined) {
@@ -159,51 +132,4 @@ export function createWatcher(options?: {
159132
}
160133
},
161134
};
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-
}
209135
}

yarn.lock

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3707,6 +3707,13 @@
37073707
"@types/minimatch" "*"
37083708
"@types/node" "*"
37093709

3710+
"@types/graceful-fs@*":
3711+
version "4.1.9"
3712+
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4"
3713+
integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==
3714+
dependencies:
3715+
"@types/node" "*"
3716+
37103717
"@types/http-errors@*":
37113718
version "2.0.4"
37123719
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
@@ -4051,6 +4058,14 @@
40514058
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8"
40524059
integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==
40534060

4061+
"@types/watchpack@^2.4.4":
4062+
version "2.4.4"
4063+
resolved "https://registry.yarnpkg.com/@types/watchpack/-/watchpack-2.4.4.tgz#d492bca62ba73cf041eda26e494fe7a540852836"
4064+
integrity sha512-SbuSavsPxfOPZwVHBgQUVuzYBe6+8KL7dwiJLXaj5rmv3DxktOMwX5WP1J6UontwUbewjVoc7pCgZvqy6rPn+A==
4065+
dependencies:
4066+
"@types/graceful-fs" "*"
4067+
"@types/node" "*"
4068+
40544069
"@types/webpack-sources@*":
40554070
version "3.2.3"
40564071
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.3.tgz#b667bd13e9fa15a9c26603dce502c7985418c3d8"
@@ -12820,7 +12835,7 @@ walk-up-path@^1.0.0:
1282012835
resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e"
1282112836
integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg==
1282212837

12823-
watchpack@^2.4.0:
12838+
watchpack@2.4.0, watchpack@^2.4.0:
1282412839
version "2.4.0"
1282512840
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
1282612841
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==

0 commit comments

Comments
 (0)