From 3d133b3a7badcf34e9119675176aaf04a9bd6c8c Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:42:09 -0400 Subject: [PATCH 1/2] fix(@angular/build): use distinct chunk naming for web worker bundles Previously, `bundleWebWorker` inherited `chunkNames` from the main esbuild configuration while bundling web workers in a separate synchronous esbuild pass. When both the main application bundle and a web worker dynamically imported modules that shared a common dependency, esbuild generated a shared chunk with identical content and the same `chunk-[hash].js` filename in both builds. During post-build chunk optimization, Rolldown traced only the main application entrypoint, merged the shared chunk into new optimized chunks, and removed the original `chunk-[hash].js` file from the build outputs while the unoptimized web worker chunks still imported it. Setting `chunkNames` to `'worker-chunk-[hash]'` in `bundleWebWorker` ensures that chunks emitted for web workers have distinct filenames and do not collide with main bundle chunks during metafile merging or chunk optimization. Closes #34292 --- .../behavior/web-workers-application_spec.ts | 76 +++++++++++++++++++ .../tools/esbuild/angular/compiler-plugin.ts | 1 + 2 files changed, 77 insertions(+) diff --git a/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts index 135d5ff68165..68bcc20db311 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts @@ -57,5 +57,81 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { // Ensure built worker is referenced in the application code harness.expectFile('dist/browser/main.js').content.toMatch(REFERENCED_WORKER_REGEXP); }); + + it('should emit all chunks referenced by a web worker when chunk optimization is enabled', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + optimization: true, + }); + + await harness.writeFile( + 'src/app/shared.ts', + `export function sharedHelper(value: string) { return [value, value.toUpperCase(), value.toLowerCase(), value.repeat(3)].join('-'); }`, + ); + await harness.writeFile( + 'src/app/lazy-a.ts', + `import { sharedHelper } from './shared'; export const a = () => sharedHelper('a');`, + ); + await harness.writeFile( + 'src/app/lazy-b.ts', + `import { sharedHelper } from './shared'; export const b = () => sharedHelper('b');`, + ); + await harness.writeFile('src/app/lazy-c.ts', `export const c = () => 'c';`); + + await harness.writeFile( + 'src/app/worker.ts', + ` + addEventListener('message', async () => { + const [{ a }, { b }] = await Promise.all([import('./lazy-a'), import('./lazy-b')]); + postMessage(a() + b()); + }); + `, + ); + + await harness.writeFile( + 'src/app/app.component.ts', + ` + import { Component } from '@angular/core'; + @Component({ + selector: 'app-root', + standalone: false, + template: '

Worker Test

', + }) + export class AppComponent { + worker = new Worker(new URL('./worker', import.meta.url), { type: 'module' }); + constructor() { + Promise.all([import('./lazy-a'), import('./lazy-b'), import('./lazy-c')]).then(console.log); + } + } + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + expect(harness.hasFileMatch('dist/browser', /^worker-chunk-/)).toBeTrue(); + + const mainContent = harness.readFile('dist/browser/main.js'); + const workerMatch = mainContent.match(/(worker-[A-Z0-9]{8}\.js)/); + expect(workerMatch).not.toBeNull(); + + const visited = new Set(); + const queue = ['main.js', workerMatch![1]]; + while (queue.length > 0) { + const file = queue.pop()!; + if (visited.has(file)) { + continue; + } + visited.add(file); + + expect(harness.hasFile(`dist/browser/${file}`)) + .withContext(`Expected dist/browser/${file} to exist`) + .toBeTrue(); + + const content = harness.readFile(`dist/browser/${file}`); + for (const match of content.matchAll(/(?:from|import)\s*\(?["']\.\/([^"']+\.js)["']\)?/g)) { + queue.push(match[1]); + } + } + }); }); }); diff --git a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts index 9a36eea56d61..928c9dac7797 100644 --- a/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/angular/compiler-plugin.ts @@ -727,6 +727,7 @@ function bundleWebWorker( metafile: true, format: 'esm', entryNames: 'worker-[hash]', + chunkNames: 'worker-chunk-[hash]', entryPoints: [workerFile], sourcemap: pluginOptions.sourcemap, // Zone.js is not used in Web workers so no need to disable From 1700a33c154c3c241a2d8e8ec6a10db782fd07d1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:22:36 -0400 Subject: [PATCH 2/2] fixup! fix(@angular/build): use distinct chunk naming for web worker bundles --- .../behavior/web-workers-application_spec.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts index 68bcc20db311..10112afd3b6b 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/web-workers-application_spec.ts @@ -114,22 +114,19 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const workerMatch = mainContent.match(/(worker-[A-Z0-9]{8}\.js)/); expect(workerMatch).not.toBeNull(); - const visited = new Set(); - const queue = ['main.js', workerMatch![1]]; - while (queue.length > 0) { - const file = queue.pop()!; - if (visited.has(file)) { - continue; - } - visited.add(file); + const filesToCheck = new Set(['main.js']); + if (workerMatch) { + filesToCheck.add(workerMatch[1]); + } + for (const file of filesToCheck) { expect(harness.hasFile(`dist/browser/${file}`)) .withContext(`Expected dist/browser/${file} to exist`) .toBeTrue(); const content = harness.readFile(`dist/browser/${file}`); for (const match of content.matchAll(/(?:from|import)\s*\(?["']\.\/([^"']+\.js)["']\)?/g)) { - queue.push(match[1]); + filesToCheck.add(match[1]); } } });