From 2120fb1979bb251c2454f0eea579225952ca7e3b Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:16:23 +0000 Subject: [PATCH 1/5] perf(@angular/build): optimize library dts bundling and enable dts tree-shaking - Enable tree-shaking (`{ moduleSideEffects: false }`) for `.d.ts` bundles in `executeMultiBundle` so unexported internal types and unused namespace declarations are stripped, and skip emitting empty or virtual-only `rolldown-runtime` non-entry `.d.ts` chunks in `processRolldownOutput`. - Cache the `rolldown-plugin-dts` module import promise and invoke `createFakeJsPlugin` directly in `bundleAllDts` instead of calling `dts({ dtsInput: true, ... })`, eliminating redundant per-build `oxc-resolver` (`ResolverFactory`) instantiation. - Disable Rolldown `tsconfig` auto-discovery (`tsconfig: false`) and plugin timing instrumentation (`checks: { circularDependency: false, pluginTimings: false }`) in `executeMultiBundle`. --- .../src/builders/library/pipeline/bundler.ts | 93 +++++++---- .../builders/library/pipeline/bundler_spec.ts | 151 ++++++++++++++++++ .../library/tests/behavior/build_spec.ts | 62 ++++++- 3 files changed, 275 insertions(+), 31 deletions(-) create mode 100644 packages/angular/build/src/builders/library/pipeline/bundler_spec.ts diff --git a/packages/angular/build/src/builders/library/pipeline/bundler.ts b/packages/angular/build/src/builders/library/pipeline/bundler.ts index eccd4cd04e12..31079416bb22 100644 --- a/packages/angular/build/src/builders/library/pipeline/bundler.ts +++ b/packages/angular/build/src/builders/library/pipeline/bundler.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import assert from 'node:assert'; import path from 'node:path'; import type { OutputChunk, Plugin, RolldownOutput, RolldownPluginOption } from 'rolldown'; import { loadRolldown } from '../../../utils/load-rolldown'; @@ -19,6 +18,24 @@ import { createMemoryOutputFile, } from './utils'; +type RolldownPluginDtsModule = typeof import('rolldown-plugin-dts', { + with: { 'resolution-mode': 'import' }, +}); + +let rolldownPluginDtsPromise: Promise | undefined; + +/** + * Dynamically loads the `rolldown-plugin-dts` module after ensuring that + * Rolldown's native binding has been initialized via `loadRolldown()`. + * + * @returns A promise resolving to the `rolldown-plugin-dts` module namespace. + */ +function loadRolldownPluginDts(): Promise { + return (rolldownPluginDtsPromise ??= loadRolldown().then( + () => import('rolldown-plugin-dts' as string) as Promise, + )); +} + /** * Output of the entry point bundling process. */ @@ -228,6 +245,22 @@ function createMemoryFileLoaderPlugin( }; } +/** + * Checks whether a non-entry declaration chunk is empty or only contains + * virtual Rolldown runtime helpers (e.g., `rolldown-runtime-[hash].d.ts`). + */ +function isEmptyDtsChunk(chunk: OutputChunk): boolean { + if (chunk.isEntry) { + return false; + } + + if (chunk.moduleIds.every((id) => id[0] === '\0')) { + return true; + } + + return chunk.code.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '').trim() === ''; +} + /** * Processes Rolldown output assets and chunks, extracting emitted files and computing * the transitive sets of module IDs, output file paths, and tslib imports belonging to @@ -235,19 +268,35 @@ function createMemoryFileLoaderPlugin( * * @param output The array of output chunks and assets from Rolldown. * @param dir The destination POSIX output directory prefix. + * @param isDts Whether the output is from a declaration (.d.ts) bundle. * @returns The processed multi-bundle output. */ function processRolldownOutput( output: RolldownOutput['output'], dir: string, + isDts: boolean, ): Omit { const filesToEmit: MemoryOutputFile[] = []; const bundles = new Map(); const chunksByFileName = new Map(); const outputFileNames = new Set(); const entryChunks: OutputChunk[] = []; + const skippedFiles = new Set(); + + if (isDts) { + for (const item of output) { + if (item.type === 'chunk' && isEmptyDtsChunk(item)) { + skippedFiles.add(item.fileName); + skippedFiles.add(`${item.fileName}.map`); + } + } + } for (const item of output) { + if (skippedFiles.has(item.fileName)) { + continue; + } + outputFileNames.add(item.fileName); filesToEmit.push( createMemoryOutputFile( @@ -337,10 +386,11 @@ async function executeMultiBundle( context: 'this', input, cwd: workspaceRoot, + tsconfig: false, plugins, - treeshake: false, + treeshake: isDts ? { moduleSideEffects: false } : false, resolve: { symlinks: !preserveSymlinks }, - checks: { circularDependency: false }, + checks: { circularDependency: false, pluginTimings: false }, onLog(level, log) { // Ignore specified warning codes if (level === 'warn') { @@ -368,7 +418,7 @@ async function executeMultiBundle( }); return { - ...processRolldownOutput(output, dir), + ...processRolldownOutput(output, dir, isDts), warnings, }; } finally { @@ -426,39 +476,22 @@ async function bundleAllDts( }; } - // Ensure Rolldown's native binding is initialized via `loadRolldown()` before evaluating - // `rolldown-plugin-dts`, which has top-level imports of `rolldown/experimental`. - await loadRolldown(); - const { dts } = (await import('rolldown-plugin-dts' as string)) as typeof import( - 'rolldown-plugin-dts', - { with: { 'resolution-mode': 'import' } } - ); + const { createFakeJsPlugin } = await loadRolldownPluginDts(); const dtsSourcemap = !!options.declarationMap; - // Filter out `rolldown-plugin-dts:resolver` because all `.d.ts` files are already emitted - // in-memory by the Angular/TypeScript compilation and resolved via `createMemoryFileLoaderPlugin`. - // The default `rolldown-plugin-dts:resolver` plugin performs filesystem resolution (`oxc-resolver`) - // and calls `this.load()` on on-disk `.ts` source files, which is unnecessary and causes a - // significant performance regression across multi-entry builds. - const rawDtsPlugins = dts({ - dtsInput: true, - tsconfig: false, + // Directly instantiate `createFakeJsPlugin` instead of calling `dts({ dtsInput: true, ... })` + // to avoid `createDtsResolvePlugin` unconditionally instantiating a native `oxc-resolver` + // (`ResolverFactory`) on every build. All `.d.ts` files are already emitted in-memory by the + // Angular/TypeScript compilation and resolved via `createMemoryFileLoaderPlugin`. + const fakeJsPlugin = createFakeJsPlugin({ sourcemap: dtsSourcemap, - generator: 'oxc', + cjsDefault: false, + sideEffects: false, }); - const dtsPlugins = rawDtsPlugins.filter( - (plugin) => plugin.name !== 'rolldown-plugin-dts:resolver', - ); - - assert( - dtsPlugins.length < rawDtsPlugins.length, - 'Expected "rolldown-plugin-dts:resolver" plugin to be present in rolldown-plugin-dts.', - ); - return executeMultiBundle( resolveEntryInputMap(entryPoints, true), - [createMemoryFileLoaderPlugin(dtsFiles, DTS_EXTENSIONS, dtsSourcemap), ...dtsPlugins], + [createMemoryFileLoaderPlugin(dtsFiles, DTS_EXTENSIONS, dtsSourcemap), fakeJsPlugin], 'd.ts', options, ); diff --git a/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts new file mode 100644 index 000000000000..5096e4a49d7e --- /dev/null +++ b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts @@ -0,0 +1,151 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { NormalizedEntryPoint, NormalizedLibraryOptions } from '../types'; +import { bundleEntryPoints } from './bundler'; + +describe('bundleEntryPoints', () => { + const workspaceRoot = '/workspace/my-lib'; + + function createOptions(entryPoints: Map): NormalizedLibraryOptions { + return { + workspaceRoot, + projectRoot: workspaceRoot, + packageName: 'my-lib', + packageJson: { name: 'my-lib' }, + outputPath: `${workspaceRoot}/dist`, + deleteOutputPath: true, + packageJsonPath: `${workspaceRoot}/package.json`, + tsConfigPath: `${workspaceRoot}/tsconfig.lib.json`, + entryPoints, + inlineStyleLanguage: 'css', + styleIncludePaths: [], + assets: [], + compilationMode: 'partial', + declarationMap: false, + allowedNonPeerDependencies: [], + keepLifecycleScripts: false, + watch: false, + preserveSymlinks: false, + progress: false, + colors: false, + cacheOptions: { + enabled: false, + basePath: '', + path: '', + cacheId: '', + } as unknown as NormalizedLibraryOptions['cacheOptions'], + }; + } + + it('should tree-shake unexported internal types in .d.ts and skip rolldown-runtime chunks when using export *', async () => { + const primaryEntry: NormalizedEntryPoint = { + subpath: '.', + name: 'my-lib', + displayName: 'my-lib', + bundleName: 'my-lib', + entryFilePath: `${workspaceRoot}/src/public-api.ts`, + isPrimary: true, + }; + const secondaryEntry: NormalizedEntryPoint = { + subpath: './secondary', + name: 'my-lib/secondary', + displayName: 'my-lib/secondary', + bundleName: 'my-lib-secondary', + entryFilePath: `${workspaceRoot}/secondary/src/public-api.ts`, + isPrimary: false, + }; + + const entryPoints = new Map([ + ['my-lib', primaryEntry], + ['my-lib/secondary', secondaryEntry], + ]); + + const esmFiles = new Map([ + [`${workspaceRoot}/src/public-api.js`, 'export const a = 1;'], + [`${workspaceRoot}/secondary/src/public-api.js`, 'export const b = 2;'], + ]); + + const dtsFiles = new Map([ + [ + `${workspaceRoot}/src/internal.d.ts`, + 'export interface PublicA { id: string; }\ninterface UnexportedInternalA { secret: number; }', + ], + [ + `${workspaceRoot}/src/public-api.d.ts`, + 'export * from "./internal.js";\nexport * as nsA from "./internal.js";\nexport declare const a = 1;', + ], + [ + `${workspaceRoot}/secondary/src/internal.d.ts`, + 'export interface PublicB { flag: boolean; }\ninterface UnexportedInternalB { hidden: string; }', + ], + [ + `${workspaceRoot}/secondary/src/public-api.d.ts`, + 'export * from "./internal.js";\nexport * as nsB from "./internal.js";\nexport declare const b = 2;', + ], + ]); + + const output = await bundleEntryPoints( + [ + { entryPoint: primaryEntry, hasEsmChanges: true, hasDtsChanges: true }, + { entryPoint: secondaryEntry, hasEsmChanges: true, hasDtsChanges: true }, + ], + esmFiles, + dtsFiles, + createOptions(entryPoints), + ); + + const emittedPaths = output.filesToEmit.map((f) => f.path); + expect(emittedPaths.some((p) => p.includes('rolldown-runtime'))).toBeFalse(); + + const primaryDtsFile = output.filesToEmit.find((f) => f.path === 'types/my-lib.d.ts'); + expect(primaryDtsFile).toBeDefined(); + expect(String(primaryDtsFile?.contents)).toContain('PublicA'); + expect(String(primaryDtsFile?.contents)).not.toContain('UnexportedInternalA'); + + const secondaryDtsFile = output.filesToEmit.find( + (f) => f.path === 'types/my-lib-secondary.d.ts', + ); + expect(secondaryDtsFile).toBeDefined(); + expect(String(secondaryDtsFile?.contents)).toContain('PublicB'); + expect(String(secondaryDtsFile?.contents)).not.toContain('UnexportedInternalB'); + }); + + it('should preserve legal comments in emitted FESM2022 .mjs bundles', async () => { + const primaryEntry: NormalizedEntryPoint = { + subpath: '.', + name: 'my-lib', + displayName: 'my-lib', + bundleName: 'my-lib', + entryFilePath: `${workspaceRoot}/src/public-api.ts`, + isPrimary: true, + }; + const entryPoints = new Map([['my-lib', primaryEntry]]); + + const esmFiles = new Map([ + [ + `${workspaceRoot}/src/public-api.js`, + '/*! @license MIT Custom Header */\nexport const VALUE = 42;', + ], + ]); + const dtsFiles = new Map([ + [`${workspaceRoot}/src/public-api.d.ts`, 'export declare const VALUE = 42;'], + ]); + + const output = await bundleEntryPoints( + [{ entryPoint: primaryEntry, hasEsmChanges: true, hasDtsChanges: true }], + esmFiles, + dtsFiles, + createOptions(entryPoints), + ); + + const mjsFile = output.filesToEmit.find((f) => f.path === 'fesm2022/my-lib.mjs'); + expect(mjsFile).toBeDefined(); + expect(String(mjsFile?.contents)).toContain('/*! @license MIT Custom Header */'); + }); +}); diff --git a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts index 0b6de2a26c7f..968f38b3b302 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts @@ -89,10 +89,11 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(logs.some((l) => l.level === 'warn')).toBeFalse(); }); - it('should preserve JSDoc comments in .d.ts and annotation comments in .mjs bundles', async () => { + it('should preserve JSDoc comments in .d.ts and legal and annotation comments in .mjs bundles', async () => { await harness.writeFile( 'projects/lib/src/public-api.ts', ` + /*! @license Custom license header */ function identity(x: T): T { return x; } /** Public API JSDoc comment */ export const VALUE = /* @__PURE__ */ identity('test'); @@ -107,6 +108,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(result?.success).toBeTrue(); const fesmContent = harness.readFile('dist/lib/fesm2022/lib.mjs'); + expect(fesmContent).toContain('/*! @license Custom license header */'); expect(fesmContent).toContain('@__PURE__'); expect(fesmContent).not.toContain('Public API JSDoc comment'); @@ -115,6 +117,64 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(dtsContent).not.toContain('@__PURE__'); }); + it('should tree-shake unexported internal types from .d.ts and not emit rolldown-runtime chunks when using export *', async () => { + await harness.writeFiles({ + 'projects/lib/src/lib/internal.ts': ` + export interface PublicExportedType { + id: string; + } + interface UnexportedInternalType { + secret: number; + } + `, + 'projects/lib/src/public-api.ts': ` + export * from './lib/internal'; + export * as internalNs from './lib/internal'; + export * from './lib/lib.service'; + export * from './lib/lib.component'; + `, + 'projects/lib/secondary/src/lib/sec-internal.ts': ` + export interface SecondaryPublicType { + flag: boolean; + } + interface SecondaryInternalType { + hidden: string; + } + `, + 'projects/lib/secondary/src/public-api.ts': ` + export * from './lib/sec-internal'; + export * as secNs from './lib/sec-internal'; + `, + }); + + await harness.modifyFile('projects/lib/package.json', (content) => { + const pkg = JSON.parse(content); + pkg.exports = { + '.': './src/public-api.ts', + './secondary': './secondary/src/public-api.ts', + }; + + return JSON.stringify(pkg, null, 2); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const primaryDts = harness.readFile('dist/lib/types/lib.d.ts'); + expect(primaryDts).toContain('PublicExportedType'); + expect(primaryDts).not.toContain('UnexportedInternalType'); + + const secondaryDts = harness.readFile('dist/lib/types/lib-secondary.d.ts'); + expect(secondaryDts).toContain('SecondaryPublicType'); + expect(secondaryDts).not.toContain('SecondaryInternalType'); + + expect(harness.hasFileMatch('dist/lib/types', /^rolldown-runtime-/)).toBeFalse(); + }); + it('should automatically add tslib to dependencies when output chunks import tslib', async () => { await harness.writeFile( 'projects/lib/src/public-api.ts', From bd16c477239b0b7b740ee30c6d89f0cfe5c662d4 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:53:53 +0000 Subject: [PATCH 2/5] fixup! perf(@angular/build): optimize library dts bundling and enable dts tree-shaking --- .../src/builders/library/pipeline/bundler.ts | 34 +------------------ .../builders/library/pipeline/bundler_spec.ts | 9 ++--- .../library/tests/behavior/build_spec.ts | 6 +--- 3 files changed, 5 insertions(+), 44 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/bundler.ts b/packages/angular/build/src/builders/library/pipeline/bundler.ts index 31079416bb22..acbdb9a9f6c2 100644 --- a/packages/angular/build/src/builders/library/pipeline/bundler.ts +++ b/packages/angular/build/src/builders/library/pipeline/bundler.ts @@ -245,22 +245,6 @@ function createMemoryFileLoaderPlugin( }; } -/** - * Checks whether a non-entry declaration chunk is empty or only contains - * virtual Rolldown runtime helpers (e.g., `rolldown-runtime-[hash].d.ts`). - */ -function isEmptyDtsChunk(chunk: OutputChunk): boolean { - if (chunk.isEntry) { - return false; - } - - if (chunk.moduleIds.every((id) => id[0] === '\0')) { - return true; - } - - return chunk.code.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '').trim() === ''; -} - /** * Processes Rolldown output assets and chunks, extracting emitted files and computing * the transitive sets of module IDs, output file paths, and tslib imports belonging to @@ -268,35 +252,19 @@ function isEmptyDtsChunk(chunk: OutputChunk): boolean { * * @param output The array of output chunks and assets from Rolldown. * @param dir The destination POSIX output directory prefix. - * @param isDts Whether the output is from a declaration (.d.ts) bundle. * @returns The processed multi-bundle output. */ function processRolldownOutput( output: RolldownOutput['output'], dir: string, - isDts: boolean, ): Omit { const filesToEmit: MemoryOutputFile[] = []; const bundles = new Map(); const chunksByFileName = new Map(); const outputFileNames = new Set(); const entryChunks: OutputChunk[] = []; - const skippedFiles = new Set(); - - if (isDts) { - for (const item of output) { - if (item.type === 'chunk' && isEmptyDtsChunk(item)) { - skippedFiles.add(item.fileName); - skippedFiles.add(`${item.fileName}.map`); - } - } - } for (const item of output) { - if (skippedFiles.has(item.fileName)) { - continue; - } - outputFileNames.add(item.fileName); filesToEmit.push( createMemoryOutputFile( @@ -418,7 +386,7 @@ async function executeMultiBundle( }); return { - ...processRolldownOutput(output, dir, isDts), + ...processRolldownOutput(output, dir), warnings, }; } finally { diff --git a/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts index 5096e4a49d7e..c816ca879c51 100644 --- a/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts +++ b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts @@ -43,7 +43,7 @@ describe('bundleEntryPoints', () => { }; } - it('should tree-shake unexported internal types in .d.ts and skip rolldown-runtime chunks when using export *', async () => { + it('should tree-shake unexported internal types in .d.ts when using export *', async () => { const primaryEntry: NormalizedEntryPoint = { subpath: '.', name: 'my-lib', @@ -78,7 +78,7 @@ describe('bundleEntryPoints', () => { ], [ `${workspaceRoot}/src/public-api.d.ts`, - 'export * from "./internal.js";\nexport * as nsA from "./internal.js";\nexport declare const a = 1;', + 'export * from "./internal.js";\nexport declare const a = 1;', ], [ `${workspaceRoot}/secondary/src/internal.d.ts`, @@ -86,7 +86,7 @@ describe('bundleEntryPoints', () => { ], [ `${workspaceRoot}/secondary/src/public-api.d.ts`, - 'export * from "./internal.js";\nexport * as nsB from "./internal.js";\nexport declare const b = 2;', + 'export * from "./internal.js";\nexport declare const b = 2;', ], ]); @@ -100,9 +100,6 @@ describe('bundleEntryPoints', () => { createOptions(entryPoints), ); - const emittedPaths = output.filesToEmit.map((f) => f.path); - expect(emittedPaths.some((p) => p.includes('rolldown-runtime'))).toBeFalse(); - const primaryDtsFile = output.filesToEmit.find((f) => f.path === 'types/my-lib.d.ts'); expect(primaryDtsFile).toBeDefined(); expect(String(primaryDtsFile?.contents)).toContain('PublicA'); diff --git a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts index 968f38b3b302..3efc3221762a 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts @@ -117,7 +117,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(dtsContent).not.toContain('@__PURE__'); }); - it('should tree-shake unexported internal types from .d.ts and not emit rolldown-runtime chunks when using export *', async () => { + it('should tree-shake unexported internal types from .d.ts when using export *', async () => { await harness.writeFiles({ 'projects/lib/src/lib/internal.ts': ` export interface PublicExportedType { @@ -129,7 +129,6 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => `, 'projects/lib/src/public-api.ts': ` export * from './lib/internal'; - export * as internalNs from './lib/internal'; export * from './lib/lib.service'; export * from './lib/lib.component'; `, @@ -143,7 +142,6 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => `, 'projects/lib/secondary/src/public-api.ts': ` export * from './lib/sec-internal'; - export * as secNs from './lib/sec-internal'; `, }); @@ -171,8 +169,6 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => const secondaryDts = harness.readFile('dist/lib/types/lib-secondary.d.ts'); expect(secondaryDts).toContain('SecondaryPublicType'); expect(secondaryDts).not.toContain('SecondaryInternalType'); - - expect(harness.hasFileMatch('dist/lib/types', /^rolldown-runtime-/)).toBeFalse(); }); it('should automatically add tslib to dependencies when output chunks import tslib', async () => { From d91527bb26faa3cbd12e1811bc87f0f0204e8cfb Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:03:26 +0000 Subject: [PATCH 3/5] fixup! perf(@angular/build): optimize library dts bundling and enable dts tree-shaking --- .../library/tests/behavior/build_spec.ts | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts index 3efc3221762a..0b6de2a26c7f 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts @@ -89,11 +89,10 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(logs.some((l) => l.level === 'warn')).toBeFalse(); }); - it('should preserve JSDoc comments in .d.ts and legal and annotation comments in .mjs bundles', async () => { + it('should preserve JSDoc comments in .d.ts and annotation comments in .mjs bundles', async () => { await harness.writeFile( 'projects/lib/src/public-api.ts', ` - /*! @license Custom license header */ function identity(x: T): T { return x; } /** Public API JSDoc comment */ export const VALUE = /* @__PURE__ */ identity('test'); @@ -108,7 +107,6 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(result?.success).toBeTrue(); const fesmContent = harness.readFile('dist/lib/fesm2022/lib.mjs'); - expect(fesmContent).toContain('/*! @license Custom license header */'); expect(fesmContent).toContain('@__PURE__'); expect(fesmContent).not.toContain('Public API JSDoc comment'); @@ -117,60 +115,6 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(dtsContent).not.toContain('@__PURE__'); }); - it('should tree-shake unexported internal types from .d.ts when using export *', async () => { - await harness.writeFiles({ - 'projects/lib/src/lib/internal.ts': ` - export interface PublicExportedType { - id: string; - } - interface UnexportedInternalType { - secret: number; - } - `, - 'projects/lib/src/public-api.ts': ` - export * from './lib/internal'; - export * from './lib/lib.service'; - export * from './lib/lib.component'; - `, - 'projects/lib/secondary/src/lib/sec-internal.ts': ` - export interface SecondaryPublicType { - flag: boolean; - } - interface SecondaryInternalType { - hidden: string; - } - `, - 'projects/lib/secondary/src/public-api.ts': ` - export * from './lib/sec-internal'; - `, - }); - - await harness.modifyFile('projects/lib/package.json', (content) => { - const pkg = JSON.parse(content); - pkg.exports = { - '.': './src/public-api.ts', - './secondary': './secondary/src/public-api.ts', - }; - - return JSON.stringify(pkg, null, 2); - }); - - harness.useTarget('build', { - ...BASE_OPTIONS, - }); - - const { result } = await harness.executeOnce(); - expect(result?.success).toBeTrue(); - - const primaryDts = harness.readFile('dist/lib/types/lib.d.ts'); - expect(primaryDts).toContain('PublicExportedType'); - expect(primaryDts).not.toContain('UnexportedInternalType'); - - const secondaryDts = harness.readFile('dist/lib/types/lib-secondary.d.ts'); - expect(secondaryDts).toContain('SecondaryPublicType'); - expect(secondaryDts).not.toContain('SecondaryInternalType'); - }); - it('should automatically add tslib to dependencies when output chunks import tslib', async () => { await harness.writeFile( 'projects/lib/src/public-api.ts', From 22c75d98ba254565b35a2a1f2a066d72287d274c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:08:12 +0000 Subject: [PATCH 4/5] fixup! perf(@angular/build): optimize library dts bundling and enable dts tree-shaking --- .../build/src/builders/library/pipeline/bundler.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/bundler.ts b/packages/angular/build/src/builders/library/pipeline/bundler.ts index acbdb9a9f6c2..ce5a61f8e051 100644 --- a/packages/angular/build/src/builders/library/pipeline/bundler.ts +++ b/packages/angular/build/src/builders/library/pipeline/bundler.ts @@ -31,9 +31,13 @@ let rolldownPluginDtsPromise: Promise | undefined; * @returns A promise resolving to the `rolldown-plugin-dts` module namespace. */ function loadRolldownPluginDts(): Promise { - return (rolldownPluginDtsPromise ??= loadRolldown().then( - () => import('rolldown-plugin-dts' as string) as Promise, - )); + return (rolldownPluginDtsPromise ??= (async () => { + // Ensure Rolldown's native binding is initialized via `loadRolldown()` before evaluating + // `rolldown-plugin-dts`, which has top-level imports of `rolldown/experimental`. + await loadRolldown(); + + return import('rolldown-plugin-dts' as string) as Promise; + })()); } /** From de1df2c4399c59a2bc8a4547ba3e5a85b70e2ae4 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:29:40 +0000 Subject: [PATCH 5/5] fixup! perf(@angular/build): optimize library dts bundling and enable dts tree-shaking --- .../builders/library/pipeline/bundler_spec.ts | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts index c816ca879c51..8e8522a26e19 100644 --- a/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts +++ b/packages/angular/build/src/builders/library/pipeline/bundler_spec.ts @@ -100,17 +100,31 @@ describe('bundleEntryPoints', () => { createOptions(entryPoints), ); - const primaryDtsFile = output.filesToEmit.find((f) => f.path === 'types/my-lib.d.ts'); - expect(primaryDtsFile).toBeDefined(); - expect(String(primaryDtsFile?.contents)).toContain('PublicA'); - expect(String(primaryDtsFile?.contents)).not.toContain('UnexportedInternalA'); + expect(output.filesToEmit).toContain( + jasmine.objectContaining({ + path: 'types/my-lib.d.ts', + contents: jasmine.stringContaining('PublicA'), + }), + ); + expect(output.filesToEmit).not.toContain( + jasmine.objectContaining({ + path: 'types/my-lib.d.ts', + contents: jasmine.stringContaining('UnexportedInternalA'), + }), + ); - const secondaryDtsFile = output.filesToEmit.find( - (f) => f.path === 'types/my-lib-secondary.d.ts', + expect(output.filesToEmit).toContain( + jasmine.objectContaining({ + path: 'types/my-lib-secondary.d.ts', + contents: jasmine.stringContaining('PublicB'), + }), + ); + expect(output.filesToEmit).not.toContain( + jasmine.objectContaining({ + path: 'types/my-lib-secondary.d.ts', + contents: jasmine.stringContaining('UnexportedInternalB'), + }), ); - expect(secondaryDtsFile).toBeDefined(); - expect(String(secondaryDtsFile?.contents)).toContain('PublicB'); - expect(String(secondaryDtsFile?.contents)).not.toContain('UnexportedInternalB'); }); it('should preserve legal comments in emitted FESM2022 .mjs bundles', async () => { @@ -141,8 +155,11 @@ describe('bundleEntryPoints', () => { createOptions(entryPoints), ); - const mjsFile = output.filesToEmit.find((f) => f.path === 'fesm2022/my-lib.mjs'); - expect(mjsFile).toBeDefined(); - expect(String(mjsFile?.contents)).toContain('/*! @license MIT Custom Header */'); + expect(output.filesToEmit).toContain( + jasmine.objectContaining({ + path: 'fesm2022/my-lib.mjs', + contents: jasmine.stringContaining('/*! @license MIT Custom Header */'), + }), + ); }); });