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]);
}
}
});