Skip to content

Commit 2d9b166

Browse files
committed
fix(@angular/build): prevent esbuild service child process leakage
Use scoped esbuild.context() for single builds to guarantee accurate tracking and clean disposal of all internal esbuild service child processes. Additionally, wrap runEsBuildBuildAction in a try/finally block leveraging a state tracking flag to deterministically trigger context disposal for all single builds and watch initialization failure paths. This leakage primarily impacts programmatic usage and custom Architect decorators, where the hosting Node.js process remains alive after build completion. Fixes #33201
1 parent 9eccdef commit 2d9b166

2 files changed

Lines changed: 59 additions & 53 deletions

File tree

packages/angular/build/src/builders/application/build-action.ts

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -95,62 +95,71 @@ export async function* runEsBuildBuildAction(
9595
}
9696
}
9797

98-
// Setup watcher if watch mode enabled
9998
let watcher: import('../../tools/esbuild/watcher').BuildWatcher | undefined;
100-
if (watch) {
101-
if (progress) {
102-
logger.info('Watch mode enabled. Watching for file changes...');
103-
}
99+
let watchLoopStarted = false;
100+
try {
101+
// Setup watcher if watch mode enabled
102+
if (watch) {
103+
if (progress) {
104+
logger.info('Watch mode enabled. Watching for file changes...');
105+
}
106+
107+
const ignored: string[] = [
108+
// Ignore the output and cache paths to avoid infinite rebuild cycles
109+
outputOptions.base,
110+
cacheOptions.basePath,
111+
`${toPosixPath(workspaceRoot)}/**/.*/**`,
112+
];
113+
114+
// Setup a watcher
115+
const { createWatcher } = await import('../../tools/esbuild/watcher');
116+
watcher = createWatcher({
117+
polling: typeof poll === 'number',
118+
interval: poll,
119+
followSymlinks: preserveSymlinks,
120+
ignored,
121+
});
122+
123+
// Setup abort support
124+
options.signal?.addEventListener('abort', () => void watcher?.close());
125+
126+
// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
127+
if (shouldWatchRoot) {
128+
if (!preserveSymlinks) {
129+
// Ignore all node modules directories to avoid excessive file watchers.
130+
// Package changes are handled below by watching manifest and lock files.
131+
// NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages.
132+
ignored.push('**/node_modules/**');
133+
134+
watcher.add(
135+
packageWatchFiles
136+
.map((file) => path.join(workspaceRoot, file))
137+
.filter((file) => existsSync(file)),
138+
);
139+
}
104140

105-
const ignored: string[] = [
106-
// Ignore the output and cache paths to avoid infinite rebuild cycles
107-
outputOptions.base,
108-
cacheOptions.basePath,
109-
`${toPosixPath(workspaceRoot)}/**/.*/**`,
110-
];
111-
112-
// Setup a watcher
113-
const { createWatcher } = await import('../../tools/esbuild/watcher');
114-
watcher = createWatcher({
115-
polling: typeof poll === 'number',
116-
interval: poll,
117-
followSymlinks: preserveSymlinks,
118-
ignored,
119-
});
120-
121-
// Setup abort support
122-
options.signal?.addEventListener('abort', () => void watcher?.close());
123-
124-
// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
125-
if (shouldWatchRoot) {
126-
if (!preserveSymlinks) {
127-
// Ignore all node modules directories to avoid excessive file watchers.
128-
// Package changes are handled below by watching manifest and lock files.
129-
// NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages.
130-
ignored.push('**/node_modules/**');
131-
132-
watcher.add(
133-
packageWatchFiles
134-
.map((file) => path.join(workspaceRoot, file))
135-
.filter((file) => existsSync(file)),
136-
);
141+
watcher.add(projectRoot);
137142
}
138143

139-
watcher.add(projectRoot);
144+
// Watch locations provided by the initial build result
145+
watcher.add(result.watchFiles);
140146
}
141147

142-
// Watch locations provided by the initial build result
143-
watcher.add(result.watchFiles);
144-
}
148+
// Output the first build results after setting up the watcher to ensure that any code executed
149+
// higher in the iterator call stack will trigger the watcher. This is particularly relevant for
150+
// unit tests which execute the builder and modify the file system programmatically.
151+
yield* emitOutputResults(result, outputOptions);
145152

146-
// Output the first build results after setting up the watcher to ensure that any code executed
147-
// higher in the iterator call stack will trigger the watcher. This is particularly relevant for
148-
// unit tests which execute the builder and modify the file system programmatically.
149-
yield* emitOutputResults(result, outputOptions);
153+
// Finish if watch mode is not enabled
154+
if (!watcher) {
155+
return;
156+
}
150157

151-
// Finish if watch mode is not enabled
152-
if (!watcher) {
153-
return;
158+
watchLoopStarted = true;
159+
} finally {
160+
if (!watchLoopStarted && result) {
161+
await result.dispose();
162+
}
154163
}
155164

156165
// Used to force a full result on next rebuild if there were initial errors.

packages/angular/build/src/tools/esbuild/bundler-context.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,11 @@ export class BundlerContext {
204204
if (this.#esbuildContext) {
205205
// Rebuild using the existing incremental build context
206206
result = await this.#esbuildContext.rebuild();
207-
} else if (this.incremental) {
208-
// Create an incremental build context and perform the first build.
207+
} else {
208+
// Create a build context and perform the build.
209209
// Context creation does not perform a build.
210210
this.#esbuildContext = await context(this.#esbuildOptions);
211211
result = await this.#esbuildContext.rebuild();
212-
} else {
213-
// For non-incremental builds, perform a single build
214-
result = await build(this.#esbuildOptions);
215212
}
216213
} catch (failure) {
217214
// Build failures will throw an exception which contains errors/warnings

0 commit comments

Comments
 (0)