Skip to content

Commit 11bbe7c

Browse files
clydinfilipesilva
authored andcommitted
refactor(@angular-devkit/build-angular): add internal custom file watcher support
This change adds internal support for providing a custom file watching mechanism to the browser (and associated) builders. The support integrates and overrides the Webpack watch system when enabled. This is currently intended to support builder unit testing use cases.
1 parent 1194b9b commit 11bbe7c

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '../utils';
2727
import { WebpackConfigOptions } from '../utils/build-options';
2828
import { readTsconfig } from '../utils/read-tsconfig';
29+
import { BuilderWatchPlugin, BuilderWatcherFactory } from '../webpack/plugins/builder-watch-plugin';
2930
import { getEsVersionForFileName } from '../webpack/utils/helpers';
3031
import { profilingEnabled } from './environment-options';
3132
import { I18nOptions, configureI18nBuild } from './i18n-options';
@@ -229,6 +230,18 @@ export async function generateBrowserWebpackConfigFromContext(
229230
extraBuildOptions,
230231
);
231232

233+
// If builder watch support is present in the context, add watch plugin
234+
// This is internal only and currently only used for testing
235+
const watcherFactory = (context as {
236+
watcherFactory?: BuilderWatcherFactory;
237+
}).watcherFactory;
238+
if (watcherFactory) {
239+
if (!config.plugins) {
240+
config.plugins = [];
241+
}
242+
config.plugins.push(new BuilderWatchPlugin(watcherFactory));
243+
}
244+
232245
return {
233246
config,
234247
projectRoot: getSystemPath(projectRoot),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { Compiler } from 'webpack';
9+
import { isWebpackFiveOrHigher } from '../../utils/webpack-version';
10+
11+
export type BuilderWatcherCallback = (
12+
events: Array<{ path: string; type: 'created' | 'modified' | 'deleted'; time?: number }>,
13+
) => void;
14+
15+
export interface BuilderWatcherFactory {
16+
watch(
17+
files: Iterable<string>,
18+
directories: Iterable<string>,
19+
callback: BuilderWatcherCallback,
20+
): { close(): void };
21+
}
22+
23+
export interface WebpackWatcher {
24+
close(): void;
25+
pause(): void;
26+
// Webpack 4
27+
getFileTimestamps(): Map<string, number>;
28+
getContextTimestamps(): Map<string, number>;
29+
// Webpack 5
30+
getFileTimeInfoEntries(): Map<string, { safeTime: number; timestamp: number }>;
31+
getContextTimeInfoEntries(): Map<string, { safeTime: number; timestamp: number }>;
32+
}
33+
34+
class TimeInfoMap extends Map<string, { safeTime: number; timestamp: number }> {
35+
update(path: string, timestamp: number): void {
36+
this.set(path, Object.freeze({ safeTime: timestamp, timestamp }));
37+
}
38+
39+
toTimestamps(): Map<string, number> {
40+
const timestamps = new Map<string, number>();
41+
for (const [file, entry] of this) {
42+
timestamps.set(file, entry.timestamp);
43+
}
44+
45+
return timestamps;
46+
}
47+
}
48+
49+
type WatchCallback4 = (
50+
error: Error | undefined,
51+
fileChanges: Set<string>,
52+
directoryChanges: Set<string>,
53+
missingChanges: Set<string>,
54+
files: Map<string, number>,
55+
contexts: Map<string, number>,
56+
removals: Set<string>,
57+
) => void;
58+
type WatchCallback5 = (
59+
error: Error | undefined,
60+
files: Map<string, { safeTime: number; timestamp: number }>,
61+
contexts: Map<string, { safeTime: number; timestamp: number }>,
62+
changes: Set<string>,
63+
removals: Set<string>,
64+
) => void;
65+
66+
export interface WebpackWatchFileSystem {
67+
watch(
68+
files: Iterable<string>,
69+
directories: Iterable<string>,
70+
missing: Iterable<string>,
71+
startTime: number,
72+
options: {},
73+
callback: WatchCallback4 | WatchCallback5,
74+
callbackUndelayed: (file: string, time: number) => void,
75+
): WebpackWatcher;
76+
}
77+
78+
class BuilderWatchFileSystem implements WebpackWatchFileSystem {
79+
constructor(
80+
private readonly watcherFactory: BuilderWatcherFactory,
81+
private readonly inputFileSystem: { purge?(path?: string): void },
82+
) {}
83+
84+
watch(
85+
files: Iterable<string>,
86+
directories: Iterable<string>,
87+
missing: Iterable<string>,
88+
startTime: number,
89+
_options: {},
90+
callback: WatchCallback4 | WatchCallback5,
91+
callbackUndelayed?: (file: string, time: number) => void,
92+
): WebpackWatcher {
93+
const watchedFiles = new Set(files);
94+
const watchedDirectories = new Set(directories);
95+
const watchedMissing = new Set(missing);
96+
97+
const timeInfo = new TimeInfoMap();
98+
for (const file of files) {
99+
timeInfo.update(file, startTime);
100+
}
101+
for (const directory of directories) {
102+
timeInfo.update(directory, startTime);
103+
}
104+
105+
const watcher = this.watcherFactory.watch(files, directories, (events) => {
106+
if (events.length === 0) {
107+
return;
108+
}
109+
110+
if (callbackUndelayed) {
111+
process.nextTick(() => callbackUndelayed(events[0].path, events[0].time ?? Date.now()));
112+
}
113+
114+
process.nextTick(() => {
115+
const removals = new Set<string>();
116+
const fileChanges = new Set<string>();
117+
const directoryChanges = new Set<string>();
118+
const missingChanges = new Set<string>();
119+
120+
for (const event of events) {
121+
this.inputFileSystem.purge?.(event.path);
122+
123+
if (event.type === 'deleted') {
124+
timeInfo.delete(event.path);
125+
removals.add(event.path);
126+
} else {
127+
timeInfo.update(event.path, event.time ?? Date.now());
128+
if (watchedFiles.has(event.path)) {
129+
fileChanges.add(event.path);
130+
} else if (watchedDirectories.has(event.path)) {
131+
directoryChanges.add(event.path);
132+
} else if (watchedMissing.has(event.path)) {
133+
missingChanges.add(event.path);
134+
}
135+
}
136+
}
137+
138+
if (isWebpackFiveOrHigher()) {
139+
(callback as WatchCallback5)(
140+
undefined,
141+
new Map(timeInfo),
142+
new Map(timeInfo),
143+
new Set([...fileChanges, ...directoryChanges, ...missingChanges]),
144+
removals,
145+
);
146+
} else {
147+
(callback as WatchCallback4)(
148+
undefined,
149+
fileChanges,
150+
directoryChanges,
151+
missingChanges,
152+
timeInfo.toTimestamps(),
153+
timeInfo.toTimestamps(),
154+
removals,
155+
);
156+
}
157+
});
158+
});
159+
160+
return {
161+
close() {
162+
watcher.close();
163+
},
164+
pause() {},
165+
getFileTimestamps() {
166+
return timeInfo.toTimestamps();
167+
},
168+
getContextTimestamps() {
169+
return timeInfo.toTimestamps();
170+
},
171+
getFileTimeInfoEntries() {
172+
return new Map(timeInfo);
173+
},
174+
getContextTimeInfoEntries() {
175+
return new Map(timeInfo);
176+
},
177+
};
178+
}
179+
}
180+
181+
export class BuilderWatchPlugin {
182+
constructor(private readonly watcherFactory: BuilderWatcherFactory) {}
183+
184+
apply(compiler: Compiler & { watchFileSystem: WebpackWatchFileSystem }): void {
185+
compiler.hooks.environment.tap('BuilderWatchPlugin', () => {
186+
compiler.watchFileSystem = new BuilderWatchFileSystem(
187+
this.watcherFactory,
188+
compiler.inputFileSystem,
189+
);
190+
});
191+
}
192+
}

0 commit comments

Comments
 (0)