From 768cf138e2c62a473ba4cf1493a69e32aeeaa5e1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:58:51 -0400 Subject: [PATCH] fix(@angular/build): prevent esbuild service hang on internal component stylesheet builds Switching to esbuild.context() + rebuild() for all builds in PR #33267 meant that all component stylesheet builds (which are separate BundlerContext instances) also created individual esbuild contexts. For builds with large amounts of stylesheets, this resulted in creating and concurrently disposing many esbuild contexts, leading to a timing deadlock/hang in the esbuild service child process. This change adds an alwaysUseContext parameter (defaulting to false) to BundlerContext to allow the main/global bundle contexts to continue using esbuild.context() (to ensure their plugins' onDispose callbacks run), while allowing component stylesheets to safely fall back to one-shot esbuild.build() during non-incremental/one-shot builds. --- .../src/builders/application/setup-bundling.ts | 16 +++++++++++++--- .../build/src/tools/esbuild/bundler-context.ts | 12 +++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index 1f3fbe56445d..bb53c0bae902 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -66,6 +66,7 @@ export function setupBundlerContexts( angularCompilationContext, templateUpdates, ), + true, ), ); @@ -82,6 +83,7 @@ export function setupBundlerContexts( workspaceRoot, watch, browserPolyfillBundleOptions, + true, ); if (typeof browserPolyfillBundleOptions === 'function') { otherContexts.push(browserPolyfillContext); @@ -95,7 +97,9 @@ export function setupBundlerContexts( for (const initial of [true, false]) { const bundleOptions = createGlobalStylesBundleOptions(options, target, initial); if (bundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial), + ); } } } @@ -105,7 +109,9 @@ export function setupBundlerContexts( for (const initial of [true, false]) { const bundleOptions = createGlobalScriptsBundleOptions(options, target, initial); if (bundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, bundleOptions, () => initial)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial), + ); } } } @@ -125,6 +131,7 @@ export function setupBundlerContexts( stylesheetBundler, angularCompilationContext.createSecondaryContext(), ), + true, ), ); @@ -141,6 +148,7 @@ export function setupBundlerContexts( stylesheetBundler, angularCompilationContext.createSecondaryContext(), ), + true, ), ); } @@ -153,7 +161,9 @@ export function setupBundlerContexts( ); if (serverPolyfillBundleOptions) { - otherContexts.push(new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions)); + otherContexts.push( + new BundlerContext(workspaceRoot, watch, serverPolyfillBundleOptions, true), + ); } } diff --git a/packages/angular/build/src/tools/esbuild/bundler-context.ts b/packages/angular/build/src/tools/esbuild/bundler-context.ts index 7fe28e5a51d4..d3f3ca567a0f 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-context.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-context.ts @@ -70,6 +70,7 @@ export class BundlerContext { private workspaceRoot: string, private incremental: boolean, options: BuildOptions | BundlerOptionsFactory, + private alwaysUseContext = false, private initialFilter?: (initial: Readonly) => boolean, ) { // To cache the results an option factory is needed to capture the full set of dependencies @@ -217,7 +218,7 @@ export class BundlerContext { if (this.#esbuildContext) { // Rebuild using the existing incremental build context result = await this.#esbuildContext.rebuild(); - } else { + } else if (this.incremental || this.alwaysUseContext) { // Create a build context and perform the build. // Context creation does not perform a build. const esbuildContext = await context(this.#esbuildOptions); @@ -227,6 +228,15 @@ export class BundlerContext { } this.#esbuildContext = esbuildContext; result = await this.#esbuildContext.rebuild(); + } else { + // For non-incremental builds, perform a single build + if (this.#disposed) { + throw new Error('BundlerContext was disposed during build.'); + } + result = await build(this.#esbuildOptions); + if (this.#disposed) { + throw new Error('BundlerContext was disposed during build.'); + } } } catch (failure) { // Build failures will throw an exception which contains errors/warnings