From c5a2e68e068759ef1c8cf425edb721717701c7c6 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:21:45 +0000 Subject: [PATCH 1/3] perf(@angular/build): overlap library asset resolution and deduplicate output mkdir calls - Start `buildState.collectAssets(options, modifiedFiles)` before `compileLibrary` so asset globbing overlaps with TypeScript/Angular compilation, Rolldown bundling, and manifest generation. - Execute `deleteFilesFromDisk` and `emitFilesToDisk` concurrently via `Promise.all` since deleted and emitted paths are disjoint. - Cache ancestor directory promises created by `mkdir(..., { recursive: true })` in `emitFilesToDisk` so parent directories (such as `outputPath`) do not incur redundant `mkdir` syscalls when subdirectories (`fesm2022`, `types`) are created first. - Return early when `result.hasErrors` is true before adding `bundleOutput.warnings` so secondary bundler warnings are not logged alongside compilation errors. - Include `options.packageJsonPath` in the error message when `verifyAllowedDependencies` fails. --- .../builders/library/pipeline/build-action.ts | 27 +++++++++++-------- .../library/tests/behavior/build_spec.ts | 10 ++++--- .../allowed-non-peer-dependencies_spec.ts | 4 ++- .../angular/build/src/utils/disk-files.ts | 8 +++++- .../build/src/utils/disk-files_spec.ts | 4 +++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/build-action.ts b/packages/angular/build/src/builders/library/pipeline/build-action.ts index 9d579e58a76a..493d2720f63a 100644 --- a/packages/angular/build/src/builders/library/pipeline/build-action.ts +++ b/packages/angular/build/src/builders/library/pipeline/build-action.ts @@ -71,6 +71,13 @@ export async function buildAction(actionContext: BuildActionContext): Promise {}); + } + const filesToEmit: OutputFile[] = []; if (shouldCompileEntryPoints) { @@ -121,10 +128,10 @@ export async function buildAction(actionContext: BuildActionContext): Promise path.posix.join(options.outputPath, file.path)); - const filesToDelete = buildState.recordEmittedOutputs(collectedAssets, manifestFiles); - if (filesToDelete.size) { - await deleteFilesFromDisk( + await Promise.all([ + deleteFilesFromDisk( Array.from(filesToDelete, (file) => path.posix.join(options.outputPath, file)), - ); - } + ), + emitFilesToDisk(filesToEmit, (file) => path.posix.join(options.outputPath, file.path)), + ]); } catch (error) { assertIsError(error); result.addError(error.message); @@ -178,7 +183,7 @@ function verifyAllowedDependencies( options: NormalizedLibraryOptions, result: ExecutionResult, ): boolean { - const { packageJson, allowedNonPeerDependencies } = options; + const { packageJson, packageJsonPath, allowedNonPeerDependencies } = options; const dependencies = { ...(packageJson.dependencies ?? {}), ...(packageJson.optionalDependencies ?? {}), @@ -189,7 +194,7 @@ function verifyAllowedDependencies( if (!allowedNonPeerDependencies.some((regex) => regex.test(dep))) { result.addError( `Dependency '${dep}' must be explicitly allowed using the 'allowedNonPeerDependencies' option, ` + - `or moved to 'peerDependencies' in 'package.json'.`, + `or moved to 'peerDependencies' in '${packageJsonPath}'.`, ); isValid = false; } 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 be6c1d3d3f94..c4b55f36e1d8 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 @@ -86,7 +86,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => const { result, logs } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs.some((l) => l.level === 'warn')).toBeFalse(); + expect(logs).not.toContain(jasmine.objectContaining({ level: 'warn' })); }); it('should preserve JSDoc comments in .d.ts and annotation comments in .mjs bundles', async () => { @@ -204,10 +204,13 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => ); }); - it('should log failure on build error', async () => { + it('should log failure on build error without secondary bundler warnings', async () => { await harness.writeFile( 'projects/lib/src/public-api.ts', - `export const title: number = 'invalid type';`, + ` + export const title: number = 'invalid type'; + export function runDynamic(code: string) { return eval(code); } + `, ); harness.useTarget('build', { @@ -230,6 +233,7 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => ), }), ); + expect(logs).not.toContain(jasmine.objectContaining({ level: 'warn' })); expect(logs).not.toContain( jasmine.objectContaining({ message: jasmine.stringMatching(/Output location:/), diff --git a/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts b/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts index c75699d068a3..d72923c85ba0 100644 --- a/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts @@ -29,7 +29,9 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => expect(logs).toContain( jasmine.objectContaining({ level: 'error', - message: jasmine.stringMatching(/lodash-es.*allowedNonPeerDependencies/), + message: jasmine.stringMatching( + /lodash-es.*allowedNonPeerDependencies.*projects\/lib\/package\.json/, + ), }), ); }); diff --git a/packages/angular/build/src/utils/disk-files.ts b/packages/angular/build/src/utils/disk-files.ts index dbbe34ee919e..219e45128ccc 100644 --- a/packages/angular/build/src/utils/disk-files.ts +++ b/packages/angular/build/src/utils/disk-files.ts @@ -50,7 +50,13 @@ export async function emitFilesToDisk( let mkdirPromise = directoryCreationPromises.get(fileBasePath); if (!mkdirPromise) { mkdirPromise = mkdir(fileBasePath, { recursive: true }); - directoryCreationPromises.set(fileBasePath, mkdirPromise); + for ( + let currentDir = fileBasePath; + !directoryCreationPromises.has(currentDir); + currentDir = dirname(currentDir) + ) { + directoryCreationPromises.set(currentDir, mkdirPromise); + } } await mkdirPromise; } diff --git a/packages/angular/build/src/utils/disk-files_spec.ts b/packages/angular/build/src/utils/disk-files_spec.ts index 4a9422bd4373..0f7ec40face4 100644 --- a/packages/angular/build/src/utils/disk-files_spec.ts +++ b/packages/angular/build/src/utils/disk-files_spec.ts @@ -39,6 +39,8 @@ describe('disk-files', () => { contents: new TextEncoder().encode('mem-1'), }, { origin: 'memory', path: 'nested/a/mem2.txt', contents: 'mem-2' }, + { origin: 'memory', path: 'nested/parent.txt', contents: 'parent-mem' }, + { origin: 'memory', path: 'root.txt', contents: 'root-mem' }, { origin: 'disk', path: 'nested/b/disk1.txt', source: sourceFile }, { origin: 'disk', path: 'nested/c/disk2.txt', source: sourceFile }, { origin: 'memory', path: 'skipped/file.txt', contents: 'should-not-exist' }, @@ -51,6 +53,8 @@ describe('disk-files', () => { expect(await readFile(join(outDir, 'nested/a/mem1.txt'), 'utf-8')).toBe('mem-1'); expect(await readFile(join(outDir, 'nested/a/mem2.txt'), 'utf-8')).toBe('mem-2'); + expect(await readFile(join(outDir, 'nested/parent.txt'), 'utf-8')).toBe('parent-mem'); + expect(await readFile(join(outDir, 'root.txt'), 'utf-8')).toBe('root-mem'); expect(await readFile(join(outDir, 'nested/b/disk1.txt'), 'utf-8')).toBe('from-disk'); expect(await readFile(join(outDir, 'nested/c/disk2.txt'), 'utf-8')).toBe('from-disk'); expect(existsSync(join(outDir, 'skipped/file.txt'))).toBeFalse(); From 30362f70650dd485882e3d1570dc1ebe416506f0 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:02:26 +0000 Subject: [PATCH 2/3] fixup! perf(@angular/build): overlap library asset resolution and deduplicate output mkdir calls --- .../builders/library/pipeline/build-action.ts | 39 ++++++++++++------- .../angular/build/src/utils/disk-files.ts | 8 +--- .../build/src/utils/disk-files_spec.ts | 4 -- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/build-action.ts b/packages/angular/build/src/builders/library/pipeline/build-action.ts index 493d2720f63a..ebc264b3c8d0 100644 --- a/packages/angular/build/src/builders/library/pipeline/build-action.ts +++ b/packages/angular/build/src/builders/library/pipeline/build-action.ts @@ -71,13 +71,8 @@ export async function buildAction(actionContext: BuildActionContext): Promise {}); - } - + let collectedAssetsPromise: CollectedAssets | Promise | undefined = + actionContext.collectedAssets; const filesToEmit: OutputFile[] = []; if (shouldCompileEntryPoints) { @@ -106,6 +101,13 @@ export async function buildAction(actionContext: BuildActionContext): Promise {}); + collectedAssetsPromise = promise; + } + const itemsToBundle = collectEntryPointsToBundle(options, buildState); let bundleOutput: BundleEntryPointsOutput; try { @@ -152,17 +154,26 @@ export async function buildAction(actionContext: BuildActionContext): Promise + path.posix.join(options.outputPath, file.path), + ); + const filesToDelete = buildState.recordEmittedOutputs(collectedAssets, manifestFiles); - await Promise.all([ - deleteFilesFromDisk( - Array.from(filesToDelete, (file) => path.posix.join(options.outputPath, file)), - ), - emitFilesToDisk(filesToEmit, (file) => path.posix.join(options.outputPath, file.path)), - ]); + if (filesToDelete.size) { + await Promise.all([ + emitPromise, + deleteFilesFromDisk( + Array.from(filesToDelete, (file) => path.posix.join(options.outputPath, file)), + ), + ]); + } else { + await emitPromise; + } } catch (error) { assertIsError(error); result.addError(error.message); diff --git a/packages/angular/build/src/utils/disk-files.ts b/packages/angular/build/src/utils/disk-files.ts index 219e45128ccc..dbbe34ee919e 100644 --- a/packages/angular/build/src/utils/disk-files.ts +++ b/packages/angular/build/src/utils/disk-files.ts @@ -50,13 +50,7 @@ export async function emitFilesToDisk( let mkdirPromise = directoryCreationPromises.get(fileBasePath); if (!mkdirPromise) { mkdirPromise = mkdir(fileBasePath, { recursive: true }); - for ( - let currentDir = fileBasePath; - !directoryCreationPromises.has(currentDir); - currentDir = dirname(currentDir) - ) { - directoryCreationPromises.set(currentDir, mkdirPromise); - } + directoryCreationPromises.set(fileBasePath, mkdirPromise); } await mkdirPromise; } diff --git a/packages/angular/build/src/utils/disk-files_spec.ts b/packages/angular/build/src/utils/disk-files_spec.ts index 0f7ec40face4..4a9422bd4373 100644 --- a/packages/angular/build/src/utils/disk-files_spec.ts +++ b/packages/angular/build/src/utils/disk-files_spec.ts @@ -39,8 +39,6 @@ describe('disk-files', () => { contents: new TextEncoder().encode('mem-1'), }, { origin: 'memory', path: 'nested/a/mem2.txt', contents: 'mem-2' }, - { origin: 'memory', path: 'nested/parent.txt', contents: 'parent-mem' }, - { origin: 'memory', path: 'root.txt', contents: 'root-mem' }, { origin: 'disk', path: 'nested/b/disk1.txt', source: sourceFile }, { origin: 'disk', path: 'nested/c/disk2.txt', source: sourceFile }, { origin: 'memory', path: 'skipped/file.txt', contents: 'should-not-exist' }, @@ -53,8 +51,6 @@ describe('disk-files', () => { expect(await readFile(join(outDir, 'nested/a/mem1.txt'), 'utf-8')).toBe('mem-1'); expect(await readFile(join(outDir, 'nested/a/mem2.txt'), 'utf-8')).toBe('mem-2'); - expect(await readFile(join(outDir, 'nested/parent.txt'), 'utf-8')).toBe('parent-mem'); - expect(await readFile(join(outDir, 'root.txt'), 'utf-8')).toBe('root-mem'); expect(await readFile(join(outDir, 'nested/b/disk1.txt'), 'utf-8')).toBe('from-disk'); expect(await readFile(join(outDir, 'nested/c/disk2.txt'), 'utf-8')).toBe('from-disk'); expect(existsSync(join(outDir, 'skipped/file.txt'))).toBeFalse(); From b0921fa033f9f87490186cc244b688360d383fb6 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:52:41 +0000 Subject: [PATCH 3/3] fixup! perf(@angular/build): overlap library asset resolution and deduplicate output mkdir calls --- .../builders/library/pipeline/build-action.ts | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/build-action.ts b/packages/angular/build/src/builders/library/pipeline/build-action.ts index ebc264b3c8d0..54239a71477e 100644 --- a/packages/angular/build/src/builders/library/pipeline/build-action.ts +++ b/packages/angular/build/src/builders/library/pipeline/build-action.ts @@ -71,8 +71,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise | undefined = - actionContext.collectedAssets; + let collectedAssets = actionContext.collectedAssets; const filesToEmit: OutputFile[] = []; if (shouldCompileEntryPoints) { @@ -101,23 +100,17 @@ export async function buildAction(actionContext: BuildActionContext): Promise {}); - collectedAssetsPromise = promise; - } - const itemsToBundle = collectEntryPointsToBundle(options, buildState); let bundleOutput: BundleEntryPointsOutput; try { - [bundleOutput] = await Promise.all([ + [bundleOutput, , collectedAssets] = await Promise.all([ bundleEntryPoints(itemsToBundle, esmFiles, dtsFiles, { ...options, declarationMap, compilationMode, }), diagnosePromise, + collectedAssets ?? buildState.collectAssets(options, modifiedFiles), ]); } catch (error) { assertIsError(error); @@ -154,26 +147,17 @@ export async function buildAction(actionContext: BuildActionContext): Promise - path.posix.join(options.outputPath, file.path), - ); - const filesToDelete = buildState.recordEmittedOutputs(collectedAssets, manifestFiles); - if (filesToDelete.size) { - await Promise.all([ - emitPromise, - deleteFilesFromDisk( - Array.from(filesToDelete, (file) => path.posix.join(options.outputPath, file)), - ), - ]); - } else { - await emitPromise; + await deleteFilesFromDisk( + Array.from(filesToDelete, (file) => path.posix.join(options.outputPath, file)), + ); } + + await emitFilesToDisk(filesToEmit, (file) => path.posix.join(options.outputPath, file.path)); } catch (error) { assertIsError(error); result.addError(error.message);