Skip to content

Commit ee12a56

Browse files
committed
refactor(@angular/build): switch chunk optimizer to rollup by default
This commit transitions the chunk optimization logic in the application builder from the experimental Rolldown bundler to the stable Rollup bundler. Rollup is now used by default, while support for the NG_BUILD_CHUNKS_ROLLDOWN environment variable has been added to allow opting back into Rolldown for testing and debugging. To make Rolldown truly optional for end users, it has been moved from dependencies to devDependencies, and is now loaded via dynamic import only when requested.
1 parent 3601c75 commit ee12a56

File tree

7 files changed

+120
-54
lines changed

7 files changed

+120
-54
lines changed

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ ts_project(
102102
":node_modules/piscina",
103103
":node_modules/postcss",
104104
":node_modules/rolldown",
105+
":node_modules/rollup",
105106
":node_modules/sass",
106107
":node_modules/source-map-support",
107108
":node_modules/tinyglobby",

packages/angular/build/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"parse5-html-rewriting-stream": "8.0.0",
3737
"picomatch": "4.0.4",
3838
"piscina": "5.1.4",
39-
"rolldown": "1.0.0-rc.12",
39+
"rollup": "4.60.0",
4040
"sass": "1.98.0",
4141
"semver": "7.7.4",
4242
"source-map-support": "0.5.21",
@@ -55,6 +55,7 @@
5555
"less": "4.6.4",
5656
"ng-packagr": "22.0.0-next.1",
5757
"postcss": "8.5.8",
58+
"rolldown": "1.0.0-rc.12",
5859
"rxjs": "7.8.2",
5960
"vitest": "4.1.2"
6061
},

packages/angular/build/src/builders/application/chunk-optimizer.ts

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919

2020
import type { Message, Metafile } from 'esbuild';
2121
import assert from 'node:assert';
22-
import { type OutputAsset, type OutputChunk, rolldown } from 'rolldown';
22+
import { rollup } from 'rollup';
23+
import { useRolldownChunks } from '../../utils/environment-options';
2324
import {
2425
BuildOutputFile,
2526
BuildOutputFileType,
@@ -30,13 +31,45 @@ import { createOutputFile } from '../../tools/esbuild/utils';
3031
import { assertIsError } from '../../utils/error';
3132

3233
/**
33-
* Converts the output of a rolldown build into an esbuild-compatible metafile.
34-
* @param rolldownOutput The output of a rolldown build.
34+
* Represents a minimal subset of a Rollup/Rolldown output asset.
35+
* This is manually defined to avoid hard dependencies on both bundlers' types
36+
* and to ensure compatibility since Rolldown and Rollup types have slight differences
37+
* but share these core properties.
38+
*/
39+
interface OutputAsset {
40+
type: 'asset';
41+
fileName: string;
42+
source: string | Uint8Array;
43+
}
44+
45+
/**
46+
* Represents a minimal subset of a Rollup/Rolldown output chunk.
47+
* This is manually defined to avoid hard dependencies on both bundlers' types
48+
* and to ensure compatibility since Rolldown and Rollup types have slight differences
49+
* but share these core properties.
50+
*/
51+
interface OutputChunk {
52+
type: 'chunk';
53+
fileName: string;
54+
code: string;
55+
modules: Record<string, { renderedLength: number }>;
56+
imports: string[];
57+
dynamicImports?: string[];
58+
exports: string[];
59+
isEntry: boolean;
60+
facadeModuleId: string | null | undefined;
61+
map?: { toString(): string } | null;
62+
sourcemapFileName?: string | null;
63+
}
64+
65+
/**
66+
* Converts the output of a bundle build into an esbuild-compatible metafile.
67+
* @param bundleOutput The output of a bundle build.
3568
* @param originalMetafile The original esbuild metafile from the build.
3669
* @returns An esbuild-compatible metafile.
3770
*/
38-
function rolldownToEsbuildMetafile(
39-
rolldownOutput: (OutputChunk | OutputAsset)[],
71+
function bundleOutputToEsbuildMetafile(
72+
bundleOutput: (OutputChunk | OutputAsset)[],
4073
originalMetafile: Metafile,
4174
): Metafile {
4275
const newMetafile: Metafile = {
@@ -52,7 +85,7 @@ function rolldownToEsbuildMetafile(
5285
);
5386
}
5487

55-
for (const chunk of rolldownOutput) {
88+
for (const chunk of bundleOutput) {
5689
if (chunk.type === 'asset') {
5790
newMetafile.outputs[chunk.fileName] = {
5891
bytes:
@@ -214,49 +247,65 @@ export async function optimizeChunks(
214247
const usedChunks = new Set<string>();
215248

216249
let bundle;
217-
let optimizedOutput;
250+
let optimizedOutput: (OutputChunk | OutputAsset)[];
218251
try {
219-
bundle = await rolldown({
220-
input: mainFile,
221-
plugins: [
222-
{
223-
name: 'angular-bundle',
224-
resolveId(source) {
225-
// Remove leading `./` if present
226-
const file = source[0] === '.' && source[1] === '/' ? source.slice(2) : source;
227-
228-
if (chunks[file]) {
229-
return file;
230-
}
231-
232-
// All other identifiers are considered external to maintain behavior
233-
return { id: source, external: true };
234-
},
235-
load(id) {
236-
assert(
237-
chunks[id],
238-
`Angular chunk content should always be present in chunk optimizer [${id}].`,
239-
);
240-
241-
usedChunks.add(id);
242-
243-
const result = {
244-
code: chunks[id].text,
245-
map: maps[id]?.text,
246-
};
247-
248-
return result;
249-
},
252+
const plugins = [
253+
{
254+
name: 'angular-bundle',
255+
resolveId(source: string) {
256+
// Remove leading `./` if present
257+
const file = source[0] === '.' && source[1] === '/' ? source.slice(2) : source;
258+
259+
if (chunks[file]) {
260+
return file;
261+
}
262+
263+
// All other identifiers are considered external to maintain behavior
264+
return { id: source, external: true };
265+
},
266+
load(id: string) {
267+
assert(
268+
chunks[id],
269+
`Angular chunk content should always be present in chunk optimizer [${id}].`,
270+
);
271+
272+
usedChunks.add(id);
273+
274+
const result = {
275+
code: chunks[id].text,
276+
map: maps[id]?.text,
277+
};
278+
279+
return result;
250280
},
251-
],
252-
});
253-
254-
const result = await bundle.generate({
255-
minify: { mangle: false, compress: false },
256-
sourcemap,
257-
chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`,
258-
});
259-
optimizedOutput = result.output;
281+
},
282+
];
283+
284+
if (useRolldownChunks) {
285+
const { rolldown } = await import('rolldown');
286+
bundle = await rolldown({
287+
input: mainFile,
288+
plugins,
289+
});
290+
291+
const result = await bundle.generate({
292+
minify: { mangle: false, compress: false },
293+
sourcemap,
294+
chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`,
295+
});
296+
optimizedOutput = result.output;
297+
} else {
298+
bundle = await rollup({
299+
input: mainFile,
300+
plugins: plugins as any,
301+
});
302+
303+
const result = await bundle.generate({
304+
sourcemap,
305+
chunkFileNames: (chunkInfo) => `${chunkInfo.name.replace(/-[a-zA-Z0-9]{8}$/, '')}-[hash].js`,
306+
});
307+
optimizedOutput = result.output;
308+
}
260309
} catch (e) {
261310
assertIsError(e);
262311

@@ -269,7 +318,7 @@ export async function optimizeChunks(
269318
}
270319

271320
// Update metafile
272-
const newMetafile = rolldownToEsbuildMetafile(optimizedOutput, original.metafile);
321+
const newMetafile = bundleOutputToEsbuildMetafile(optimizedOutput, original.metafile);
273322
// Add back the outputs that were not part of the optimization
274323
for (const [path, output] of Object.entries(original.metafile.outputs)) {
275324
if (usedChunks.has(path)) {

packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { readFile } from 'node:fs/promises';
1111
import { createRequire } from 'node:module';
1212
import { platform } from 'node:os';
1313
import path from 'node:path';
14-
import type { ExistingRawSourceMap } from 'rolldown';
14+
15+
interface ExistingRawSourceMap {
16+
sources?: string[];
17+
sourcesContent?: string[];
18+
mappings?: string;
19+
}
20+
1521
import type {
1622
BrowserConfigOptions,
1723
InlineConfig,

packages/angular/build/src/utils/environment-options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ export const shouldBeautify = debugOptimize.beautify;
102102
*/
103103
export const allowMinify = debugOptimize.minify;
104104

105+
/**
106+
* Allows using Rolldown for chunk optimization instead of Rollup.
107+
* This is useful for debugging and testing scenarios.
108+
*/
109+
export const useRolldownChunks = parseTristate(process.env['NG_BUILD_CHUNKS_ROLLDOWN']) ?? false;
110+
105111
/**
106112
* Some environments, like CircleCI which use Docker report a number of CPUs by the host and not the count of available.
107113
* This cause `Error: Call retries were exceeded` errors when trying to use them.

pnpm-lock.yaml

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/e2e/tests/build/chunk-optimizer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ export default async function () {
1515
});
1616

1717
const content = await readFile('dist/test-project/browser/main.js', 'utf-8');
18-
assert.match(content, /ɵɵdefineComponent/u);
18+
assert.match(content, /(ɵɵ|\\u0275\\u0275)defineComponent/u);
1919
}

0 commit comments

Comments
 (0)