diff --git a/packages/angular/build/src/builders/application/chunk-optimizer.ts b/packages/angular/build/src/builders/application/chunk-optimizer.ts index cd5e4e1814ec..053f62f80868 100644 --- a/packages/angular/build/src/builders/application/chunk-optimizer.ts +++ b/packages/angular/build/src/builders/application/chunk-optimizer.ts @@ -296,19 +296,48 @@ export async function optimizeChunks( }, ]; + const chunkFileNames = (chunkInfo: { name: string }) => + `${chunkInfo.name.replace(/-[a-zA-Z0-9_-]{8}$/, '')}-[hash].js`; + if (useRolldownChunks) { const { rolldown } = await loadRolldown(); bundle = await rolldown({ input: mainFile, + // Allow Rolldown to extend the main entry chunk's exports when merging shared initial + // modules into `main` so that lazy chunks can import those symbols directly from `main`. + preserveEntrySignatures: 'allow-extension', plugins, }); - const result = await bundle.generate({ + let result = await bundle.generate({ minify: { mangle: false, compress: false }, sourcemap, - chunkFileNames: (chunkInfo) => - `${chunkInfo.name.replace(/-[a-zA-Z0-9_-]{8}$/, '')}-[hash].js`, + codeSplitting: { + // Consolidate all modules statically reachable from the initial entry point (`$initial`) + // back into the `main` chunk. This prevents multi-level initial chunk waterfalls caused + // by esbuild splitting out modules that are shared across `main` and subsets of lazy + // routes, while keeping shared chunks used exclusively by lazy routes separate. + groups: [{ name: 'main', tags: ['$initial'] }], + }, + chunkFileNames, }); + + // If any module in `$initial` uses top-level `await`, Rolldown cannot fold the manual + // `main` group into the entry chunk and emits an empty entry facade instead. Fall back to + // Rolldown's default TLA-aware chunking in that case. + if ( + result.output.some( + (chunk) => + chunk.type === 'chunk' && chunk.isEntry && Object.keys(chunk.modules).length === 0, + ) + ) { + result = await bundle.generate({ + minify: { mangle: false, compress: false }, + sourcemap, + chunkFileNames, + }); + } + optimizedOutput = result.output; } else { let rollup; @@ -328,8 +357,7 @@ export async function optimizeChunks( const result = await bundle.generate({ compact: true, sourcemap, - chunkFileNames: (chunkInfo) => - `${chunkInfo.name.replace(/-[a-zA-Z0-9_-]{8}$/, '')}-[hash].js`, + chunkFileNames, }); optimizedOutput = result.output; } diff --git a/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts index 18b92e7690a0..3bdef34cae06 100644 --- a/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts +++ b/packages/angular/build/src/builders/application/tests/behavior/chunk-optimization-server_spec.ts @@ -187,5 +187,149 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { .toBeTrue(); } }); + + it('consolidates multi-level initial shared modules into main while preserving lazy-only shared chunks', async () => { + await harness.writeFiles({ + 'src/shared-1.ts': ` + import { shared2 } from './shared-2'; + export function shared1(): string { + return 's1-' + shared2(); + } + `, + 'src/shared-2.ts': ` + import { shared3 } from './shared-3'; + export function shared2(): string { + return 's2-' + shared3(); + } + `, + 'src/shared-3.ts': ` + import { shared4 } from './shared-4'; + export function shared3(): string { + return 's3-' + shared4(); + } + `, + 'src/shared-4.ts': ` + export function shared4(): string { + return 's4-' + Date.now().toString(36); + } + `, + 'src/lazy-shared.ts': ` + export function lazyOnlyShared(): string { + return 'lazy-only-' + Date.now().toString(36); + } + `, + 'src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { RouterOutlet } from '@angular/router'; + import { shared1 } from '../shared-1'; + + @Component({ + selector: 'app-root', + imports: [RouterOutlet], + template: '
{{ val }}