From 35ee5d91e772a32a1c2981ad0cac5b134d364a39 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:25:50 +0000 Subject: [PATCH 1/7] fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers - Log uncaught errors in `executeBuild` and `loadPackageJson` in `builder.ts`, and track `hasPackageJsonError` in the watch loop so `loadPackageJson` is retried on subsequent watch events until `package.json` is fixed. - Watch `styleIncludePaths` in `setupWatcher` and only call `resetSassWorkerPoolCaches()` when stylesheet files are added or removed or `package.json` changes, rather than on every in-place stylesheet edit. - Lazily initialize `#assetMatchers` in `BuilderState.collectAssets` only on incremental watch builds rather than unconditionally in the constructor. - Reset `compilationInstance` and clear cached `esmFiles`/`dtsFiles` and `previousBundleResults` when `compilationMode` changes, and mark entry point DTS files dirty when `declarationMap` changes. - Prune non-asset paths from `#pendingAssetFiles` in `BuilderState.collectAssets` after `collectAssetsToEmit` completes. --- .../src/builders/library/builder-state.ts | 75 +++++++- .../builders/library/builder-state_spec.ts | 170 +++++++++++++++++- .../build/src/builders/library/builder.ts | 31 +++- .../builders/library/pipeline/compilation.ts | 69 +++++-- .../library/tests/behavior/watch_spec.ts | 106 +++++++++++ 5 files changed, 417 insertions(+), 34 deletions(-) diff --git a/packages/angular/build/src/builders/library/builder-state.ts b/packages/angular/build/src/builders/library/builder-state.ts index 9a468c2fb543..39f6aa04e279 100644 --- a/packages/angular/build/src/builders/library/builder-state.ts +++ b/packages/angular/build/src/builders/library/builder-state.ts @@ -32,7 +32,7 @@ export class BuilderState { readonly #watchedCompilationFiles = new Set(); readonly #pendingCompilationFiles = new Set(); readonly #pendingAssetFiles = new Set(); - readonly #assetMatchers?: readonly AssetMatcher[]; + #assetMatchers?: readonly AssetMatcher[]; #emittedBundleFiles = new Set(); #emittedManifestFiles = new Set(); @@ -44,6 +44,7 @@ export class BuilderState { #singleProgramCache?: SingleProgramCache; #compilationMode?: 'partial' | 'full'; + #declarationMap?: boolean; #hasEmittedTslib = false; /** Map of entry point names to their previous bundle results. */ @@ -80,8 +81,6 @@ export class BuilderState { this.#watchedCompilationFiles.add(entryPoint.entryFilePath); } } - - this.#assetMatchers = options?.assets ? createAssetMatchers(options.assets) : undefined; } /** Cached unified TypeScript and Angular compilation state. */ @@ -94,6 +93,11 @@ export class BuilderState { return this.#compilationMode; } + /** Effective declarationMap setting resolved from builder options and tsconfig. */ + get declarationMap(): boolean | undefined { + return this.#declarationMap; + } + /** Whether the last emitted root package.json included a tslib dependency. */ get hasEmittedTslib(): boolean { return this.#hasEmittedTslib; @@ -298,10 +302,49 @@ export class BuilderState { */ recordCompilationOutput(output: LibraryCompilationOutput): void { this.#pendingCompilationFiles.clear(); + const lastCache = this.#singleProgramCache; + const lastCompilationMode = this.#compilationMode; + const lastDeclarationMap = this.#declarationMap; this.#singleProgramCache = output.cache; - if (this.#compilationMode !== output.compilationMode) { - this.#compilationMode = output.compilationMode; + this.#compilationMode = output.compilationMode; + this.#declarationMap = output.declarationMap; + + if (lastCompilationMode !== output.compilationMode) { this.hasEmittedManifests = false; + if (lastCompilationMode !== undefined) { + if (lastCache && lastCache.esmFiles !== output.esmFiles) { + lastCache.esmFiles.clear(); + } + if (lastCache && lastCache.dtsFiles !== output.dtsFiles) { + lastCache.dtsFiles.clear(); + } + for (const prev of this.previousBundleResults.values()) { + for (const file of prev.esmModuleIds) { + this.pendingChangedEsmFiles.add(file); + } + for (const file of prev.dtsModuleIds) { + this.pendingChangedDtsFiles.add(file); + } + } + this.previousBundleResults.clear(); + for (const file of output.esmFiles.keys()) { + this.pendingChangedEsmFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); + } + for (const file of output.dtsFiles.keys()) { + this.pendingChangedDtsFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); + } + } + } + + if (lastDeclarationMap !== undefined && lastDeclarationMap !== output.declarationMap) { + for (const prev of this.previousBundleResults.values()) { + for (const file of prev.dtsModuleIds) { + this.pendingChangedDtsFiles.add(file); + } + } + for (const file of output.dtsFiles.keys()) { + this.pendingChangedDtsFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); + } } for (const file of output.referencedFiles) { @@ -344,7 +387,7 @@ export class BuilderState { * @param modifiedFiles Optional set of modified POSIX file paths in watch mode. * @returns The collected asset files to emit and delete. */ - collectAssets( + async collectAssets( options: Pick, modifiedFiles?: ReadonlySet, ): Promise { @@ -356,13 +399,29 @@ export class BuilderState { effectiveModifiedFiles = this.#pendingAssetFiles; } - return collectAssetsToEmit( + if (effectiveModifiedFiles && effectiveModifiedFiles.size > 0 && options.assets.length > 0) { + this.#assetMatchers ??= createAssetMatchers(options.assets); + } + + const result = await collectAssetsToEmit( options.assets, options.workspaceRoot, this.#emittedAssets, effectiveModifiedFiles, this.#assetMatchers, ); + + if (effectiveModifiedFiles) { + this.#pendingAssetFiles.clear(); + for (const file of result.filesToEmit) { + this.#pendingAssetFiles.add(file.source); + } + for (const file of result.filesToDelete) { + this.#pendingAssetFiles.add(file); + } + } + + return result; } /** @@ -460,6 +519,8 @@ export class BuilderState { await this.#singleProgramCache?.compilationInstance.close?.(); this.#singleProgramCache = undefined; this.#compilationMode = undefined; + this.#declarationMap = undefined; + this.#assetMatchers = undefined; this.#hasEmittedTslib = false; this.#hasEmittedAssets = false; this.hasCompilationError = false; diff --git a/packages/angular/build/src/builders/library/builder-state_spec.ts b/packages/angular/build/src/builders/library/builder-state_spec.ts index da7420afc3e5..6a83b559fde6 100644 --- a/packages/angular/build/src/builders/library/builder-state_spec.ts +++ b/packages/angular/build/src/builders/library/builder-state_spec.ts @@ -172,18 +172,106 @@ describe('BuilderState', () => { ).toBeFalse(); }); - it('should invalidate hasEmittedManifests when compilationMode changes', () => { + it('should invalidate hasEmittedManifests and compiled bundle state when compilationMode changes', () => { const state = new BuilderState(); state.hasEmittedManifests = true; - state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); + const firstCache = createMockCache(); + firstCache.esmFiles.set('/src/index.js', 'export const a = 1;'); + firstCache.dtsFiles.set('/src/index.d.ts', 'export declare const a: number;'); + + state.recordCompilationOutput( + createMockCompilationOutput({ + compilationMode: 'partial', + cache: firstCache, + esmFiles: firstCache.esmFiles, + dtsFiles: firstCache.dtsFiles, + }), + ); expect(state.hasEmittedManifests).toBeFalse(); + state.recordBundleOutput( + new Map([ + [ + '.', + { + esmModuleIds: new Set(['/src/index.js']), + dtsModuleIds: new Set(['/src/index.d.ts']), + esmOutputFiles: new Set(['fesm2022/my-lib.mjs']), + dtsOutputFiles: new Set(['types/my-lib.d.ts']), + hasTslibImport: false, + }, + ], + ]), + ); state.hasEmittedManifests = true; - state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); + + state.recordCompilationOutput( + createMockCompilationOutput({ + compilationMode: 'partial', + cache: firstCache, + esmFiles: firstCache.esmFiles, + dtsFiles: firstCache.dtsFiles, + }), + ); expect(state.hasEmittedManifests).toBeTrue(); + expect(state.previousBundleResults.size).toBe(1); + + const secondCache = createMockCache(); + secondCache.esmFiles.set('/src/index.js', 'export const a = 2;'); + secondCache.dtsFiles.set('/src/index.d.ts', 'export declare const a: number;'); - state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'full' })); + state.recordCompilationOutput( + createMockCompilationOutput({ + compilationMode: 'full', + cache: secondCache, + esmFiles: secondCache.esmFiles, + dtsFiles: secondCache.dtsFiles, + }), + ); expect(state.hasEmittedManifests).toBeFalse(); + expect(state.previousBundleResults.size).toBe(0); + expect(firstCache.esmFiles.size).toBe(0); + expect(firstCache.dtsFiles.size).toBe(0); + expect(state.pendingChangedEsmFiles).toEqual(new Set(['/src/index.js'])); + expect(state.pendingChangedDtsFiles).toEqual(new Set(['/src/index.d.ts'])); + }); + + it('should mark entry point DTS files dirty when declarationMap changes', () => { + const state = new BuilderState(); + state.recordCompilationOutput( + createMockCompilationOutput({ + declarationMap: false, + dtsFiles: new Map([['/src/index.d.ts', 'export declare const a: number;']]), + }), + ); + state.recordBundleOutput( + new Map([ + [ + '.', + { + esmModuleIds: new Set(['/src/index.js']), + dtsModuleIds: new Set(['/src/index.d.ts']), + esmOutputFiles: new Set(['fesm2022/my-lib.mjs']), + dtsOutputFiles: new Set(['types/my-lib.d.ts']), + hasTslibImport: false, + }, + ], + ]), + ); + expect(state.pendingChangedDtsFiles.size).toBe(0); + + state.recordCompilationOutput( + createMockCompilationOutput({ + declarationMap: true, + dtsFiles: new Map([ + ['/src/index.d.ts', 'export declare const a: number;'], + ['/src/index.d.ts.map', '{}'], + ]), + }), + ); + expect(state.declarationMap).toBeTrue(); + expect(state.pendingChangedDtsFiles).toEqual(new Set(['/src/index.d.ts'])); + expect(state.pendingChangedEsmFiles.size).toBe(0); }); }); @@ -332,6 +420,80 @@ describe('BuilderState', () => { } }); + it('should lazily create asset matchers only on incremental watch asset collection', async () => { + const baseTmpDir = process.env['TEST_TMPDIR']; + assert(baseTmpDir, 'TEST_TMPDIR is not set'); + const tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-builder-state-lazy-')); + + try { + const readmePath = join(tempRoot, 'README.md'); + await writeFile(readmePath, '# Initial'); + + let globAccessCount = 0; + const asset = { + get glob() { + globAccessCount++; + + return 'README.md'; + }, + input: tempRoot, + output: '.', + }; + const options = { + workspaceRoot: tempRoot, + assets: [asset], + }; + + // Constructing BuilderState should not compile asset matchers + const state = new BuilderState(options); + expect(globAccessCount).toBe(0); + + state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] }); + expect(globAccessCount).toBe(0); + + // First incremental watch collection initializes asset matchers once + const firstCollect = await state.collectAssets(options, new Set([readmePath])); + expect(firstCollect.filesToEmit).toHaveSize(1); + expect(globAccessCount).toBe(1); + + // Subsequent incremental watch collections reuse cached asset matchers + const secondCollect = await state.collectAssets(options, new Set([readmePath])); + expect(secondCollect.filesToEmit).toHaveSize(1); + expect(globAccessCount).toBe(1); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }); + + it('should prune non-asset paths from pendingAssetFiles after collectAssets', async () => { + const baseTmpDir = process.env['TEST_TMPDIR']; + assert(baseTmpDir, 'TEST_TMPDIR is not set'); + const tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-builder-state-prune-')); + + try { + const options = { + workspaceRoot: tempRoot, + assets: [{ glob: '**/*.txt', input: tempRoot, output: '.' }], + }; + const state = new BuilderState(options); + state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] }); + + // Pass a path that does not exist on disk yet, so it produces no asset output on iteration 1 + const transientPath = join(tempRoot, 'transient.txt'); + const firstCollect = await state.collectAssets(options, new Set([transientPath])); + expect(firstCollect.filesToEmit).toHaveSize(0); + + // Now create transientPath on disk, but trigger collectAssets with only a non-asset .ts file. + // Because transientPath was pruned after the first collectAssets call, it should not be emitted. + await writeFile(transientPath, 'created later'); + const nonAssetFile = join(tempRoot, 'src/index.ts'); + const secondCollect = await state.collectAssets(options, new Set([nonAssetFile])); + expect(secondCollect.filesToEmit).toHaveSize(0); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }); + it('should close compilationInstance and clear cached state on dispose', async () => { const closeSpy = jasmine.createSpy('close').and.resolveTo(); const state = new BuilderState({ packageJsonPath }); diff --git a/packages/angular/build/src/builders/library/builder.ts b/packages/angular/build/src/builders/library/builder.ts index 1e5d08b55b5f..ff5d260094d7 100644 --- a/packages/angular/build/src/builders/library/builder.ts +++ b/packages/angular/build/src/builders/library/builder.ts @@ -8,6 +8,7 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { logging } from '@angular-devkit/core'; +import { existsSync } from 'node:fs'; import type { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets'; import { resetSassWorkerPoolCaches, @@ -119,6 +120,7 @@ export async function* executeLibraryBuilder( signal, watchFiles: [ ...buildState.watchedCompilationFiles, + ...normalizedOptions.styleIncludePaths, ...normalizedOptions.assets.map((asset) => asset.input), ], }); @@ -209,6 +211,7 @@ async function executeBuild( return output; } catch (error) { assertIsError(error); + logger.error(error.message); logBuildResult(logger, startTime, false); return { success: false, error: error.message }; @@ -243,6 +246,7 @@ async function* runWatchLoop( signal: AbortSignal, ): AsyncIterableIterator { const { packageJsonPath, clearScreen } = options; + let hasPackageJsonError = false; for await (const changes of watcher) { if (signal.aborted) { @@ -258,29 +262,40 @@ async function* runWatchLoop( stylesheetBundler.invalidate(rawChangedFiles); const changedFiles = buildState.getAffectedFiles(rawChangedFiles); - for (const posixFile of changedFiles) { - if (/\.(?:scss|sass)$/i.test(posixFile)) { - resetSassWorkerPoolCaches(); - break; - } + const shouldResetSassCache = + changedFiles.has(packageJsonPath) || + rawChangedFiles.some((file) => file.endsWith('/package.json')) || + Array.from(changes.added).some((file) => /\.(?:scss|sass|css)$/i.test(file)) || + Array.from(changes.removed).some((file) => /\.(?:scss|sass|css)$/i.test(file)) || + Array.from(changes.modified).some( + (file) => /\.(?:scss|sass|css)$/i.test(file) && !existsSync(file), + ); + + if (shouldResetSassCache) { + resetSassWorkerPoolCaches(); } - // Check if package.json was modified - if (changedFiles.has(packageJsonPath)) { + // Check if package.json was modified or failed to reload on a previous iteration + if (hasPackageJsonError || changedFiles.has(packageJsonPath)) { buildState.hasEmittedManifests = false; try { const packageJson = await loadPackageJson(packageJsonPath); updateWatchedEntryPoints(packageJson, options, buildState); + hasPackageJsonError = false; } catch (error) { assertIsError(error); + hasPackageJsonError = true; await buildState.singleProgramCache?.compilationInstance.update?.(changedFiles); if (buildState.shouldCompileEntryPoints(packageJsonPath, changedFiles)) { buildState.hasCompilationError = true; } + const message = `Failed to reload 'package.json': ${error.message}`; + logger.error(message); + yield { success: false, - error: `Failed to reload 'package.json': ${error.message}`, + error: message, }; continue; } diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index 1c026c5e9fec..4b6e4c92724e 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -54,7 +54,7 @@ export async function compileLibrary( rootFiles.push(ep.entryFilePath); } - const compilationInstance = + let compilationInstance = cached?.compilationInstance ?? (await createAngularCompilation('aot', false)); try { @@ -114,21 +114,45 @@ export async function compileLibrary( processWebWorker: () => '', }; - const { referencedFiles, compilerOptions } = await compilationInstance.initialize( + const compilerOptionOverrides = { + sourcemap: true, + preserveSymlinks, + rootFiles, + declarationMap, + compilationMode, + paths: entryPathsMap, + cachePath: cacheOptions.enabled ? cacheOptions.path : undefined, + }; + + let { referencedFiles, compilerOptions } = await compilationInstance.initialize( tsConfigPath, hostOptions, - { - sourcemap: true, - preserveSymlinks, - rootFiles, - declarationMap, - compilationMode, - paths: entryPathsMap, - cachePath: cacheOptions.enabled ? cacheOptions.path : undefined, - }, + compilerOptionOverrides, 'library', ); + const effectiveCompilationMode = + compilerOptions.compilationMode === 'full' ? 'full' : 'partial'; + const hasCompilationModeChanged = + buildState.compilationMode !== undefined && + buildState.compilationMode !== effectiveCompilationMode; + + if (hasCompilationModeChanged && cached) { + await compilationInstance.close?.(); + compilationInstance = await createAngularCompilation('aot', false); + hostOptions.modifiedFiles = undefined; + stylesheetWarnings.length = 0; + stylesheetErrors.length = 0; + failedFiles.clear(); + ({ referencedFiles, compilerOptions } = await compilationInstance.initialize( + tsConfigPath, + hostOptions, + compilerOptionOverrides, + 'library', + )); + } + + const effectiveDeclarationMap = !!compilerOptions.declarationMap; const hasStylesheetErrors = stylesheetErrors.length > 0; const emittedFiles = hasStylesheetErrors ? [] : await compilationInstance.emitAffectedFiles(); const diagnosePromise = runDiagnosticsAndFormat( @@ -141,11 +165,26 @@ export async function compileLibrary( const esmFiles = cached?.esmFiles ?? new Map(); const dtsFiles = cached?.dtsFiles ?? new Map(); + if (hasCompilationModeChanged) { + esmFiles.clear(); + dtsFiles.clear(); + } else if ( + buildState.declarationMap !== undefined && + buildState.declarationMap !== effectiveDeclarationMap && + !effectiveDeclarationMap + ) { + for (const key of dtsFiles.keys()) { + if (key.endsWith('.map')) { + dtsFiles.delete(key); + } + } + } + const changedEsmFiles = new Set(); const changedDtsFiles = new Set(); const posixReferencedFiles = new Set(referencedFiles.map(toPosixPath)); - if (cached && effectiveModifiedFiles) { + if (cached && !hasCompilationModeChanged && effectiveModifiedFiles) { for (const modifiedFile of effectiveModifiedFiles) { if (posixReferencedFiles.has(modifiedFile)) { continue; @@ -199,11 +238,11 @@ export async function compileLibrary( }, hasStylesheetErrors, diagnosePromise, - declarationMap: !!compilerOptions.declarationMap, - compilationMode: compilerOptions.compilationMode === 'full' ? 'full' : 'partial', + declarationMap: effectiveDeclarationMap, + compilationMode: effectiveCompilationMode, }; } catch (error) { - if (!cached) { + if (!cached || compilationInstance !== cached.compilationInstance) { await compilationInstance.close?.(); } throw error; diff --git a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts index add8b5151d81..e2ecce85ffd9 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts @@ -847,5 +847,111 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => }, ]); }); + + it('should retry reloading broken package.json when another file changes and recover once fixed', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + + // Break package.json with invalid JSON + await harness.writeFile('projects/lib/package.json', '{ invalid json'); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(result?.error).toContain("Failed to reload 'package.json'"); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining("Failed to reload 'package.json'"), + }), + ); + + // Modify a source file while package.json is still broken + await harness.writeFile( + 'projects/lib/src/lib/lib.component.ts', + ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'lib-after-pkg-error', + template: 'After Pkg Error', + }) + export class LibComponent {} + `, + ); + }, + async ({ result, logs }) => { + expect(result?.success).toBeFalse(); + expect(result?.error).toContain("Failed to reload 'package.json'"); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining("Failed to reload 'package.json'"), + }), + ); + + // Fix package.json + await harness.writeFile( + 'projects/lib/package.json', + JSON.stringify( + { + name: 'lib', + version: '0.0.1', + exports: { + '.': './src/public-api.ts', + }, + }, + null, + 2, + ), + ); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('lib-after-pkg-error'); + }, + ]); + }); + + it('should recompile bundles when compilationMode and declarationMap change in tsconfig in watch mode', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + watch: true, + }); + + await harness.executeWithCases([ + async ({ result }) => { + expect(result?.success).toBeTrue(); + expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('ɵɵngDeclareComponent'); + expect(harness.hasFile('dist/lib/types/lib.d.ts.map')).toBeFalse(); + + await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => { + const tsconfig = JSON.parse(content); + tsconfig.compilerOptions = { + ...tsconfig.compilerOptions, + declarationMap: true, + }; + tsconfig.angularCompilerOptions = { + ...tsconfig.angularCompilerOptions, + compilationMode: 'full', + }; + + return JSON.stringify(tsconfig, null, 2); + }); + }, + ({ result }) => { + expect(result?.success).toBeTrue(); + const mjs = harness.readFile('dist/lib/fesm2022/lib.mjs'); + expect(mjs).toContain('ɵɵdefineComponent'); + expect(mjs).not.toContain('ɵɵngDeclareComponent'); + expect(harness.hasFile('dist/lib/types/lib.d.ts.map')).toBeTrue(); + }, + ]); + }); }); }); From 445d59e94999284675874ec5b0181ff50ff2f435 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:35:56 +0000 Subject: [PATCH 2/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- .../builders/library/pipeline/compilation.ts | 141 ++++++++++-------- 1 file changed, 82 insertions(+), 59 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index 4b6e4c92724e..1d9fa89d24ef 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -163,66 +163,17 @@ export async function compileLibrary( buildState, ); - const esmFiles = cached?.esmFiles ?? new Map(); - const dtsFiles = cached?.dtsFiles ?? new Map(); - if (hasCompilationModeChanged) { - esmFiles.clear(); - dtsFiles.clear(); - } else if ( - buildState.declarationMap !== undefined && - buildState.declarationMap !== effectiveDeclarationMap && - !effectiveDeclarationMap - ) { - for (const key of dtsFiles.keys()) { - if (key.endsWith('.map')) { - dtsFiles.delete(key); - } - } - } - - const changedEsmFiles = new Set(); - const changedDtsFiles = new Set(); const posixReferencedFiles = new Set(referencedFiles.map(toPosixPath)); - - if (cached && !hasCompilationModeChanged && effectiveModifiedFiles) { - for (const modifiedFile of effectiveModifiedFiles) { - if (posixReferencedFiles.has(modifiedFile)) { - continue; - } - - const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ''); - if (basePathWithoutExt === modifiedFile || existsSync(modifiedFile)) { - continue; - } - - for (const ext of EMITTED_EXTENSIONS) { - const outputPath = `${basePathWithoutExt}${ext}`; - const mapPath = `${outputPath}.map`; - if (esmFiles.delete(outputPath)) { - changedEsmFiles.add(outputPath); - } - if (dtsFiles.delete(outputPath)) { - changedDtsFiles.add(outputPath); - } - esmFiles.delete(mapPath); - dtsFiles.delete(mapPath); - } - } - } - - for (const { filename, contents } of emittedFiles) { - const normalized = toPosixPath(filename); - const isMap = normalized.endsWith('.map'); - const isDts = isMap ? isDeclarationSourceMapFile(normalized) : isDeclarationFile(normalized); - const targetFiles = isDts ? dtsFiles : esmFiles; - - if (targetFiles.get(normalized) !== contents) { - targetFiles.set(normalized, contents); - (isDts ? changedDtsFiles : changedEsmFiles).add( - isMap ? normalized.slice(0, -4) : normalized, - ); - } - } + const { esmFiles, dtsFiles, changedEsmFiles, changedDtsFiles } = updateEmittedFileMaps( + emittedFiles, + cached, + posixReferencedFiles, + effectiveModifiedFiles, + hasCompilationModeChanged, + buildState.declarationMap !== undefined && + buildState.declarationMap !== effectiveDeclarationMap && + !effectiveDeclarationMap, + ); return { esmFiles, @@ -290,3 +241,75 @@ async function runDiagnosticsAndFormat( executionResult.addWarnings(await formatMessages(warnings, { kind: 'warning', color: colors })); } } + +function updateEmittedFileMaps( + emittedFiles: Iterable<{ filename: string; contents: string }>, + cached: BuilderState[singleProgramCache], + posixReferencedFiles: ReadonlySet, + effectiveModifiedFiles: ReadonlySet | undefined, + hasCompilationModeChanged: boolean, + removeDtsMaps: boolean, +): { + esmFiles: Map; + dtsFiles: Map; + changedEsmFiles: Set; + changedDtsFiles: Set; +} { + const esmFiles = cached?.esmFiles ?? new Map(); + const dtsFiles = cached?.dtsFiles ?? new Map(); + if (hasCompilationModeChanged) { + esmFiles.clear(); + dtsFiles.clear(); + } else if (removeDtsMaps) { + for (const key of dtsFiles.keys()) { + if (key.endsWith(.map)) { + dtsFiles.delete(key); + } + } + } + + const changedEsmFiles = new Set(); + const changedDtsFiles = new Set(); + + if (cached && !hasCompilationModeChanged && effectiveModifiedFiles) { + for (const modifiedFile of effectiveModifiedFiles) { + if (posixReferencedFiles.has(modifiedFile)) { + continue; + } + + const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ); + if (basePathWithoutExt === modifiedFile || existsSync(modifiedFile)) { + continue; + } + + for (const ext of EMITTED_EXTENSIONS) { + const outputPath = `${basePathWithoutExt}${ext}`; + const mapPath = `${outputPath}.map`; + if (esmFiles.delete(outputPath)) { + changedEsmFiles.add(outputPath); + } + if (dtsFiles.delete(outputPath)) { + changedDtsFiles.add(outputPath); + } + esmFiles.delete(mapPath); + dtsFiles.delete(mapPath); + } + } + } + + for (const { filename, contents } of emittedFiles) { + const normalized = toPosixPath(filename); + const isMap = normalized.endsWith(.map); + const isDts = isMap ? isDeclarationSourceMapFile(normalized) : isDeclarationFile(normalized); + const targetFiles = isDts ? dtsFiles : esmFiles; + + if (targetFiles.get(normalized) !== contents) { + targetFiles.set(normalized, contents); + (isDts ? changedDtsFiles : changedEsmFiles).add( + isMap ? normalized.slice(0, -4) : normalized, + ); + } + } + + return { esmFiles, dtsFiles, changedEsmFiles, changedDtsFiles }; +} From 9fa6dfd8153c3658079578b9ca3be7920c78ad3a Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:36:51 +0000 Subject: [PATCH 3/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- .../builders/library/pipeline/compilation.ts | 95 ++++++++++--------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index 1d9fa89d24ef..edcbdeba1968 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -201,50 +201,11 @@ export async function compileLibrary( } /** - * Formats stylesheet bundling diagnostics and TypeScript/Angular diagnostics - * and records them on the build state's execution result. - * - * @param compilationInstance The active Angular compilation instance. - * @param stylesheetErrors List of stylesheet bundling errors. - * @param stylesheetWarnings List of stylesheet bundling warnings. - * @param colors Whether diagnostic messages should be formatted with ANSI colors. - * @param buildState The build state whose execution result receives the formatted diagnostics. + * Updates cached in-memory ESM and DTS maps with newly emitted files and prunes deleted files. */ -async function runDiagnosticsAndFormat( - compilationInstance: AngularCompilation, - stylesheetErrors: PartialMessage[], - stylesheetWarnings: PartialMessage[], - colors: boolean, - buildState: BuilderState, -): Promise { - const { executionResult } = buildState; - const errors = [...stylesheetErrors]; - const warnings: PartialMessage[] = []; - - if (errors.length === 0 && useTypeChecking) { - const diagnostics = await compilationInstance.diagnoseFiles(); - if (diagnostics.errors?.length) { - errors.push(...diagnostics.errors); - } - if (diagnostics.warnings?.length) { - warnings.push(...diagnostics.warnings); - } - } - - warnings.push(...stylesheetWarnings); - - if (errors.length > 0) { - executionResult.addErrors(await formatMessages(errors, { kind: 'error', color: colors })); - } - - if (warnings.length > 0) { - executionResult.addWarnings(await formatMessages(warnings, { kind: 'warning', color: colors })); - } -} - function updateEmittedFileMaps( emittedFiles: Iterable<{ filename: string; contents: string }>, - cached: BuilderState[singleProgramCache], + cached: BuilderState['singleProgramCache'], posixReferencedFiles: ReadonlySet, effectiveModifiedFiles: ReadonlySet | undefined, hasCompilationModeChanged: boolean, @@ -262,7 +223,7 @@ function updateEmittedFileMaps( dtsFiles.clear(); } else if (removeDtsMaps) { for (const key of dtsFiles.keys()) { - if (key.endsWith(.map)) { + if (key.endsWith('.map')) { dtsFiles.delete(key); } } @@ -277,7 +238,7 @@ function updateEmittedFileMaps( continue; } - const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ); + const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ''); if (basePathWithoutExt === modifiedFile || existsSync(modifiedFile)) { continue; } @@ -299,17 +260,57 @@ function updateEmittedFileMaps( for (const { filename, contents } of emittedFiles) { const normalized = toPosixPath(filename); - const isMap = normalized.endsWith(.map); + const isMap = normalized.endsWith('.map'); const isDts = isMap ? isDeclarationSourceMapFile(normalized) : isDeclarationFile(normalized); const targetFiles = isDts ? dtsFiles : esmFiles; if (targetFiles.get(normalized) !== contents) { targetFiles.set(normalized, contents); - (isDts ? changedDtsFiles : changedEsmFiles).add( - isMap ? normalized.slice(0, -4) : normalized, - ); + (isDts ? changedDtsFiles : changedEsmFiles).add(isMap ? normalized.slice(0, -4) : normalized); } } return { esmFiles, dtsFiles, changedEsmFiles, changedDtsFiles }; } + +/** + * Formats stylesheet bundling diagnostics and TypeScript/Angular diagnostics + * and records them on the build state's execution result. + * + * @param compilationInstance The active Angular compilation instance. + * @param stylesheetErrors List of stylesheet bundling errors. + * @param stylesheetWarnings List of stylesheet bundling warnings. + * @param colors Whether diagnostic messages should be formatted with ANSI colors. + * @param buildState The build state whose execution result receives the formatted diagnostics. + */ +async function runDiagnosticsAndFormat( + compilationInstance: AngularCompilation, + stylesheetErrors: PartialMessage[], + stylesheetWarnings: PartialMessage[], + colors: boolean, + buildState: BuilderState, +): Promise { + const { executionResult } = buildState; + const errors = [...stylesheetErrors]; + const warnings: PartialMessage[] = []; + + if (errors.length === 0 && useTypeChecking) { + const diagnostics = await compilationInstance.diagnoseFiles(); + if (diagnostics.errors?.length) { + errors.push(...diagnostics.errors); + } + if (diagnostics.warnings?.length) { + warnings.push(...diagnostics.warnings); + } + } + + warnings.push(...stylesheetWarnings); + + if (errors.length > 0) { + executionResult.addErrors(await formatMessages(errors, { kind: 'error', color: colors })); + } + + if (warnings.length > 0) { + executionResult.addWarnings(await formatMessages(warnings, { kind: 'warning', color: colors })); + } +} From 5f47925c5ff67ea0b5acfdb3b208bc9382dbbced Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:37:42 +0000 Subject: [PATCH 4/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- .../build/src/builders/library/builder.ts | 21 +++++-------------- .../library/tests/behavior/watch_spec.ts | 16 ++------------ 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/packages/angular/build/src/builders/library/builder.ts b/packages/angular/build/src/builders/library/builder.ts index ff5d260094d7..78d0ad1546b0 100644 --- a/packages/angular/build/src/builders/library/builder.ts +++ b/packages/angular/build/src/builders/library/builder.ts @@ -8,7 +8,6 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import type { logging } from '@angular-devkit/core'; -import { existsSync } from 'node:fs'; import type { ComponentStylesheetBundler } from '../../tools/esbuild/angular/component-stylesheets'; import { resetSassWorkerPoolCaches, @@ -211,7 +210,6 @@ async function executeBuild( return output; } catch (error) { assertIsError(error); - logger.error(error.message); logBuildResult(logger, startTime, false); return { success: false, error: error.message }; @@ -262,16 +260,10 @@ async function* runWatchLoop( stylesheetBundler.invalidate(rawChangedFiles); const changedFiles = buildState.getAffectedFiles(rawChangedFiles); - const shouldResetSassCache = - changedFiles.has(packageJsonPath) || - rawChangedFiles.some((file) => file.endsWith('/package.json')) || - Array.from(changes.added).some((file) => /\.(?:scss|sass|css)$/i.test(file)) || - Array.from(changes.removed).some((file) => /\.(?:scss|sass|css)$/i.test(file)) || - Array.from(changes.modified).some( - (file) => /\.(?:scss|sass|css)$/i.test(file) && !existsSync(file), - ); - - if (shouldResetSassCache) { + const hasStyleChanges = Array.from(changedFiles).some((file) => + /\.(?:scss|sass|css)$/i.test(file), + ); + if (hasStyleChanges) { resetSassWorkerPoolCaches(); } @@ -290,12 +282,9 @@ async function* runWatchLoop( buildState.hasCompilationError = true; } - const message = `Failed to reload 'package.json': ${error.message}`; - logger.error(message); - yield { success: false, - error: message, + error: `Failed to reload 'package.json': ${error.message}`, }; continue; } diff --git a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts index e2ecce85ffd9..120d5489287d 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts @@ -861,15 +861,9 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => // Break package.json with invalid JSON await harness.writeFile('projects/lib/package.json', '{ invalid json'); }, - async ({ result, logs }) => { + async ({ result }) => { expect(result?.success).toBeFalse(); expect(result?.error).toContain("Failed to reload 'package.json'"); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringContaining("Failed to reload 'package.json'"), - }), - ); // Modify a source file while package.json is still broken await harness.writeFile( @@ -885,15 +879,9 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => `, ); }, - async ({ result, logs }) => { + async ({ result }) => { expect(result?.success).toBeFalse(); expect(result?.error).toContain("Failed to reload 'package.json'"); - expect(logs).toContain( - jasmine.objectContaining({ - level: 'error', - message: jasmine.stringContaining("Failed to reload 'package.json'"), - }), - ); // Fix package.json await harness.writeFile( From cae948d0928197fbd0566b921c517120a6d9bf23 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:41:13 +0000 Subject: [PATCH 5/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- packages/angular/build/src/builders/library/builder.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/angular/build/src/builders/library/builder.ts b/packages/angular/build/src/builders/library/builder.ts index 78d0ad1546b0..4735eeea3093 100644 --- a/packages/angular/build/src/builders/library/builder.ts +++ b/packages/angular/build/src/builders/library/builder.ts @@ -260,11 +260,11 @@ async function* runWatchLoop( stylesheetBundler.invalidate(rawChangedFiles); const changedFiles = buildState.getAffectedFiles(rawChangedFiles); - const hasStyleChanges = Array.from(changedFiles).some((file) => - /\.(?:scss|sass|css)$/i.test(file), - ); - if (hasStyleChanges) { - resetSassWorkerPoolCaches(); + for (const posixFile of changedFiles) { + if (/\.(?:scss|sass|css)$/i.test(posixFile)) { + resetSassWorkerPoolCaches(); + break; + } } // Check if package.json was modified or failed to reload on a previous iteration From 961ee0c78ae737ce8be0edf17d3443409415ff5c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:01:10 +0000 Subject: [PATCH 6/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- .../src/builders/library/builder-state.ts | 70 +--------- .../builders/library/builder-state_spec.ts | 129 +----------------- .../builders/library/pipeline/compilation.ts | 10 -- 3 files changed, 12 insertions(+), 197 deletions(-) diff --git a/packages/angular/build/src/builders/library/builder-state.ts b/packages/angular/build/src/builders/library/builder-state.ts index 39f6aa04e279..24257782b333 100644 --- a/packages/angular/build/src/builders/library/builder-state.ts +++ b/packages/angular/build/src/builders/library/builder-state.ts @@ -44,7 +44,6 @@ export class BuilderState { #singleProgramCache?: SingleProgramCache; #compilationMode?: 'partial' | 'full'; - #declarationMap?: boolean; #hasEmittedTslib = false; /** Map of entry point names to their previous bundle results. */ @@ -67,7 +66,7 @@ export class BuilderState { constructor( options?: Partial< - Pick + Pick >, ) { if (options?.tsConfigPath) { @@ -93,11 +92,6 @@ export class BuilderState { return this.#compilationMode; } - /** Effective declarationMap setting resolved from builder options and tsconfig. */ - get declarationMap(): boolean | undefined { - return this.#declarationMap; - } - /** Whether the last emitted root package.json included a tslib dependency. */ get hasEmittedTslib(): boolean { return this.#hasEmittedTslib; @@ -302,49 +296,10 @@ export class BuilderState { */ recordCompilationOutput(output: LibraryCompilationOutput): void { this.#pendingCompilationFiles.clear(); - const lastCache = this.#singleProgramCache; - const lastCompilationMode = this.#compilationMode; - const lastDeclarationMap = this.#declarationMap; this.#singleProgramCache = output.cache; - this.#compilationMode = output.compilationMode; - this.#declarationMap = output.declarationMap; - - if (lastCompilationMode !== output.compilationMode) { + if (this.#compilationMode !== output.compilationMode) { + this.#compilationMode = output.compilationMode; this.hasEmittedManifests = false; - if (lastCompilationMode !== undefined) { - if (lastCache && lastCache.esmFiles !== output.esmFiles) { - lastCache.esmFiles.clear(); - } - if (lastCache && lastCache.dtsFiles !== output.dtsFiles) { - lastCache.dtsFiles.clear(); - } - for (const prev of this.previousBundleResults.values()) { - for (const file of prev.esmModuleIds) { - this.pendingChangedEsmFiles.add(file); - } - for (const file of prev.dtsModuleIds) { - this.pendingChangedDtsFiles.add(file); - } - } - this.previousBundleResults.clear(); - for (const file of output.esmFiles.keys()) { - this.pendingChangedEsmFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); - } - for (const file of output.dtsFiles.keys()) { - this.pendingChangedDtsFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); - } - } - } - - if (lastDeclarationMap !== undefined && lastDeclarationMap !== output.declarationMap) { - for (const prev of this.previousBundleResults.values()) { - for (const file of prev.dtsModuleIds) { - this.pendingChangedDtsFiles.add(file); - } - } - for (const file of output.dtsFiles.keys()) { - this.pendingChangedDtsFiles.add(file.endsWith('.map') ? file.slice(0, -4) : file); - } } for (const file of output.referencedFiles) { @@ -387,7 +342,7 @@ export class BuilderState { * @param modifiedFiles Optional set of modified POSIX file paths in watch mode. * @returns The collected asset files to emit and delete. */ - async collectAssets( + collectAssets( options: Pick, modifiedFiles?: ReadonlySet, ): Promise { @@ -399,29 +354,17 @@ export class BuilderState { effectiveModifiedFiles = this.#pendingAssetFiles; } - if (effectiveModifiedFiles && effectiveModifiedFiles.size > 0 && options.assets.length > 0) { + if (effectiveModifiedFiles?.size && options.assets.length > 0) { this.#assetMatchers ??= createAssetMatchers(options.assets); } - const result = await collectAssetsToEmit( + return collectAssetsToEmit( options.assets, options.workspaceRoot, this.#emittedAssets, effectiveModifiedFiles, this.#assetMatchers, ); - - if (effectiveModifiedFiles) { - this.#pendingAssetFiles.clear(); - for (const file of result.filesToEmit) { - this.#pendingAssetFiles.add(file.source); - } - for (const file of result.filesToDelete) { - this.#pendingAssetFiles.add(file); - } - } - - return result; } /** @@ -519,7 +462,6 @@ export class BuilderState { await this.#singleProgramCache?.compilationInstance.close?.(); this.#singleProgramCache = undefined; this.#compilationMode = undefined; - this.#declarationMap = undefined; this.#assetMatchers = undefined; this.#hasEmittedTslib = false; this.#hasEmittedAssets = false; diff --git a/packages/angular/build/src/builders/library/builder-state_spec.ts b/packages/angular/build/src/builders/library/builder-state_spec.ts index 6a83b559fde6..312301b09df2 100644 --- a/packages/angular/build/src/builders/library/builder-state_spec.ts +++ b/packages/angular/build/src/builders/library/builder-state_spec.ts @@ -172,106 +172,18 @@ describe('BuilderState', () => { ).toBeFalse(); }); - it('should invalidate hasEmittedManifests and compiled bundle state when compilationMode changes', () => { + it('should invalidate hasEmittedManifests when compilationMode changes', () => { const state = new BuilderState(); state.hasEmittedManifests = true; - const firstCache = createMockCache(); - firstCache.esmFiles.set('/src/index.js', 'export const a = 1;'); - firstCache.dtsFiles.set('/src/index.d.ts', 'export declare const a: number;'); - - state.recordCompilationOutput( - createMockCompilationOutput({ - compilationMode: 'partial', - cache: firstCache, - esmFiles: firstCache.esmFiles, - dtsFiles: firstCache.dtsFiles, - }), - ); + state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); expect(state.hasEmittedManifests).toBeFalse(); - state.recordBundleOutput( - new Map([ - [ - '.', - { - esmModuleIds: new Set(['/src/index.js']), - dtsModuleIds: new Set(['/src/index.d.ts']), - esmOutputFiles: new Set(['fesm2022/my-lib.mjs']), - dtsOutputFiles: new Set(['types/my-lib.d.ts']), - hasTslibImport: false, - }, - ], - ]), - ); state.hasEmittedManifests = true; - - state.recordCompilationOutput( - createMockCompilationOutput({ - compilationMode: 'partial', - cache: firstCache, - esmFiles: firstCache.esmFiles, - dtsFiles: firstCache.dtsFiles, - }), - ); + state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'partial' })); expect(state.hasEmittedManifests).toBeTrue(); - expect(state.previousBundleResults.size).toBe(1); - - const secondCache = createMockCache(); - secondCache.esmFiles.set('/src/index.js', 'export const a = 2;'); - secondCache.dtsFiles.set('/src/index.d.ts', 'export declare const a: number;'); - state.recordCompilationOutput( - createMockCompilationOutput({ - compilationMode: 'full', - cache: secondCache, - esmFiles: secondCache.esmFiles, - dtsFiles: secondCache.dtsFiles, - }), - ); + state.recordCompilationOutput(createMockCompilationOutput({ compilationMode: 'full' })); expect(state.hasEmittedManifests).toBeFalse(); - expect(state.previousBundleResults.size).toBe(0); - expect(firstCache.esmFiles.size).toBe(0); - expect(firstCache.dtsFiles.size).toBe(0); - expect(state.pendingChangedEsmFiles).toEqual(new Set(['/src/index.js'])); - expect(state.pendingChangedDtsFiles).toEqual(new Set(['/src/index.d.ts'])); - }); - - it('should mark entry point DTS files dirty when declarationMap changes', () => { - const state = new BuilderState(); - state.recordCompilationOutput( - createMockCompilationOutput({ - declarationMap: false, - dtsFiles: new Map([['/src/index.d.ts', 'export declare const a: number;']]), - }), - ); - state.recordBundleOutput( - new Map([ - [ - '.', - { - esmModuleIds: new Set(['/src/index.js']), - dtsModuleIds: new Set(['/src/index.d.ts']), - esmOutputFiles: new Set(['fesm2022/my-lib.mjs']), - dtsOutputFiles: new Set(['types/my-lib.d.ts']), - hasTslibImport: false, - }, - ], - ]), - ); - expect(state.pendingChangedDtsFiles.size).toBe(0); - - state.recordCompilationOutput( - createMockCompilationOutput({ - declarationMap: true, - dtsFiles: new Map([ - ['/src/index.d.ts', 'export declare const a: number;'], - ['/src/index.d.ts.map', '{}'], - ]), - }), - ); - expect(state.declarationMap).toBeTrue(); - expect(state.pendingChangedDtsFiles).toEqual(new Set(['/src/index.d.ts'])); - expect(state.pendingChangedEsmFiles.size).toBe(0); }); }); @@ -393,7 +305,7 @@ describe('BuilderState', () => { workspaceRoot: tempRoot, assets: [{ glob: 'README.md', input: tempRoot, output: '.' }], }; - const state = new BuilderState(options); + const state = new BuilderState(); // Initial build emits README.md const initialAssets = await state.collectAssets(options); @@ -445,7 +357,7 @@ describe('BuilderState', () => { }; // Constructing BuilderState should not compile asset matchers - const state = new BuilderState(options); + const state = new BuilderState(); expect(globAccessCount).toBe(0); state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] }); @@ -465,35 +377,6 @@ describe('BuilderState', () => { } }); - it('should prune non-asset paths from pendingAssetFiles after collectAssets', async () => { - const baseTmpDir = process.env['TEST_TMPDIR']; - assert(baseTmpDir, 'TEST_TMPDIR is not set'); - const tempRoot = await mkdtemp(join(baseTmpDir, 'angular-cli-builder-state-prune-')); - - try { - const options = { - workspaceRoot: tempRoot, - assets: [{ glob: '**/*.txt', input: tempRoot, output: '.' }], - }; - const state = new BuilderState(options); - state.recordEmittedOutputs({ filesToEmit: [], filesToDelete: [] }); - - // Pass a path that does not exist on disk yet, so it produces no asset output on iteration 1 - const transientPath = join(tempRoot, 'transient.txt'); - const firstCollect = await state.collectAssets(options, new Set([transientPath])); - expect(firstCollect.filesToEmit).toHaveSize(0); - - // Now create transientPath on disk, but trigger collectAssets with only a non-asset .ts file. - // Because transientPath was pruned after the first collectAssets call, it should not be emitted. - await writeFile(transientPath, 'created later'); - const nonAssetFile = join(tempRoot, 'src/index.ts'); - const secondCollect = await state.collectAssets(options, new Set([nonAssetFile])); - expect(secondCollect.filesToEmit).toHaveSize(0); - } finally { - await rm(tempRoot, { recursive: true, force: true }); - } - }); - it('should close compilationInstance and clear cached state on dispose', async () => { const closeSpy = jasmine.createSpy('close').and.resolveTo(); const state = new BuilderState({ packageJsonPath }); diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index edcbdeba1968..40545834baa5 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -170,9 +170,6 @@ export async function compileLibrary( posixReferencedFiles, effectiveModifiedFiles, hasCompilationModeChanged, - buildState.declarationMap !== undefined && - buildState.declarationMap !== effectiveDeclarationMap && - !effectiveDeclarationMap, ); return { @@ -209,7 +206,6 @@ function updateEmittedFileMaps( posixReferencedFiles: ReadonlySet, effectiveModifiedFiles: ReadonlySet | undefined, hasCompilationModeChanged: boolean, - removeDtsMaps: boolean, ): { esmFiles: Map; dtsFiles: Map; @@ -221,12 +217,6 @@ function updateEmittedFileMaps( if (hasCompilationModeChanged) { esmFiles.clear(); dtsFiles.clear(); - } else if (removeDtsMaps) { - for (const key of dtsFiles.keys()) { - if (key.endsWith('.map')) { - dtsFiles.delete(key); - } - } } const changedEsmFiles = new Set(); From 7bb9b5e357336c25e53785086c6c6a497673d2c0 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:03:11 +0000 Subject: [PATCH 7/7] fixup! fix(@angular/build): improve library watch mode state invalidation and lazy asset matchers --- .../builders/library/pipeline/compilation.ts | 169 ++++++------------ .../library/tests/behavior/watch_spec.ts | 36 ---- 2 files changed, 58 insertions(+), 147 deletions(-) diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index 40545834baa5..1c026c5e9fec 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -54,7 +54,7 @@ export async function compileLibrary( rootFiles.push(ep.entryFilePath); } - let compilationInstance = + const compilationInstance = cached?.compilationInstance ?? (await createAngularCompilation('aot', false)); try { @@ -114,45 +114,21 @@ export async function compileLibrary( processWebWorker: () => '', }; - const compilerOptionOverrides = { - sourcemap: true, - preserveSymlinks, - rootFiles, - declarationMap, - compilationMode, - paths: entryPathsMap, - cachePath: cacheOptions.enabled ? cacheOptions.path : undefined, - }; - - let { referencedFiles, compilerOptions } = await compilationInstance.initialize( + const { referencedFiles, compilerOptions } = await compilationInstance.initialize( tsConfigPath, hostOptions, - compilerOptionOverrides, + { + sourcemap: true, + preserveSymlinks, + rootFiles, + declarationMap, + compilationMode, + paths: entryPathsMap, + cachePath: cacheOptions.enabled ? cacheOptions.path : undefined, + }, 'library', ); - const effectiveCompilationMode = - compilerOptions.compilationMode === 'full' ? 'full' : 'partial'; - const hasCompilationModeChanged = - buildState.compilationMode !== undefined && - buildState.compilationMode !== effectiveCompilationMode; - - if (hasCompilationModeChanged && cached) { - await compilationInstance.close?.(); - compilationInstance = await createAngularCompilation('aot', false); - hostOptions.modifiedFiles = undefined; - stylesheetWarnings.length = 0; - stylesheetErrors.length = 0; - failedFiles.clear(); - ({ referencedFiles, compilerOptions } = await compilationInstance.initialize( - tsConfigPath, - hostOptions, - compilerOptionOverrides, - 'library', - )); - } - - const effectiveDeclarationMap = !!compilerOptions.declarationMap; const hasStylesheetErrors = stylesheetErrors.length > 0; const emittedFiles = hasStylesheetErrors ? [] : await compilationInstance.emitAffectedFiles(); const diagnosePromise = runDiagnosticsAndFormat( @@ -163,14 +139,51 @@ export async function compileLibrary( buildState, ); + const esmFiles = cached?.esmFiles ?? new Map(); + const dtsFiles = cached?.dtsFiles ?? new Map(); + const changedEsmFiles = new Set(); + const changedDtsFiles = new Set(); const posixReferencedFiles = new Set(referencedFiles.map(toPosixPath)); - const { esmFiles, dtsFiles, changedEsmFiles, changedDtsFiles } = updateEmittedFileMaps( - emittedFiles, - cached, - posixReferencedFiles, - effectiveModifiedFiles, - hasCompilationModeChanged, - ); + + if (cached && effectiveModifiedFiles) { + for (const modifiedFile of effectiveModifiedFiles) { + if (posixReferencedFiles.has(modifiedFile)) { + continue; + } + + const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ''); + if (basePathWithoutExt === modifiedFile || existsSync(modifiedFile)) { + continue; + } + + for (const ext of EMITTED_EXTENSIONS) { + const outputPath = `${basePathWithoutExt}${ext}`; + const mapPath = `${outputPath}.map`; + if (esmFiles.delete(outputPath)) { + changedEsmFiles.add(outputPath); + } + if (dtsFiles.delete(outputPath)) { + changedDtsFiles.add(outputPath); + } + esmFiles.delete(mapPath); + dtsFiles.delete(mapPath); + } + } + } + + for (const { filename, contents } of emittedFiles) { + const normalized = toPosixPath(filename); + const isMap = normalized.endsWith('.map'); + const isDts = isMap ? isDeclarationSourceMapFile(normalized) : isDeclarationFile(normalized); + const targetFiles = isDts ? dtsFiles : esmFiles; + + if (targetFiles.get(normalized) !== contents) { + targetFiles.set(normalized, contents); + (isDts ? changedDtsFiles : changedEsmFiles).add( + isMap ? normalized.slice(0, -4) : normalized, + ); + } + } return { esmFiles, @@ -186,83 +199,17 @@ export async function compileLibrary( }, hasStylesheetErrors, diagnosePromise, - declarationMap: effectiveDeclarationMap, - compilationMode: effectiveCompilationMode, + declarationMap: !!compilerOptions.declarationMap, + compilationMode: compilerOptions.compilationMode === 'full' ? 'full' : 'partial', }; } catch (error) { - if (!cached || compilationInstance !== cached.compilationInstance) { + if (!cached) { await compilationInstance.close?.(); } throw error; } } -/** - * Updates cached in-memory ESM and DTS maps with newly emitted files and prunes deleted files. - */ -function updateEmittedFileMaps( - emittedFiles: Iterable<{ filename: string; contents: string }>, - cached: BuilderState['singleProgramCache'], - posixReferencedFiles: ReadonlySet, - effectiveModifiedFiles: ReadonlySet | undefined, - hasCompilationModeChanged: boolean, -): { - esmFiles: Map; - dtsFiles: Map; - changedEsmFiles: Set; - changedDtsFiles: Set; -} { - const esmFiles = cached?.esmFiles ?? new Map(); - const dtsFiles = cached?.dtsFiles ?? new Map(); - if (hasCompilationModeChanged) { - esmFiles.clear(); - dtsFiles.clear(); - } - - const changedEsmFiles = new Set(); - const changedDtsFiles = new Set(); - - if (cached && !hasCompilationModeChanged && effectiveModifiedFiles) { - for (const modifiedFile of effectiveModifiedFiles) { - if (posixReferencedFiles.has(modifiedFile)) { - continue; - } - - const basePathWithoutExt = modifiedFile.replace(/(?:\.d\.[cm]?ts|\.[cm]?[jt]sx?)$/i, ''); - if (basePathWithoutExt === modifiedFile || existsSync(modifiedFile)) { - continue; - } - - for (const ext of EMITTED_EXTENSIONS) { - const outputPath = `${basePathWithoutExt}${ext}`; - const mapPath = `${outputPath}.map`; - if (esmFiles.delete(outputPath)) { - changedEsmFiles.add(outputPath); - } - if (dtsFiles.delete(outputPath)) { - changedDtsFiles.add(outputPath); - } - esmFiles.delete(mapPath); - dtsFiles.delete(mapPath); - } - } - } - - for (const { filename, contents } of emittedFiles) { - const normalized = toPosixPath(filename); - const isMap = normalized.endsWith('.map'); - const isDts = isMap ? isDeclarationSourceMapFile(normalized) : isDeclarationFile(normalized); - const targetFiles = isDts ? dtsFiles : esmFiles; - - if (targetFiles.get(normalized) !== contents) { - targetFiles.set(normalized, contents); - (isDts ? changedDtsFiles : changedEsmFiles).add(isMap ? normalized.slice(0, -4) : normalized); - } - } - - return { esmFiles, dtsFiles, changedEsmFiles, changedDtsFiles }; -} - /** * Formats stylesheet bundling diagnostics and TypeScript/Angular diagnostics * and records them on the build state's execution result. diff --git a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts index 120d5489287d..d1a41b244320 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/watch_spec.ts @@ -905,41 +905,5 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => }, ]); }); - - it('should recompile bundles when compilationMode and declarationMap change in tsconfig in watch mode', async () => { - harness.useTarget('build', { - ...BASE_OPTIONS, - watch: true, - }); - - await harness.executeWithCases([ - async ({ result }) => { - expect(result?.success).toBeTrue(); - expect(harness.readFile('dist/lib/fesm2022/lib.mjs')).toContain('ɵɵngDeclareComponent'); - expect(harness.hasFile('dist/lib/types/lib.d.ts.map')).toBeFalse(); - - await harness.modifyFile('projects/lib/tsconfig.lib.json', (content) => { - const tsconfig = JSON.parse(content); - tsconfig.compilerOptions = { - ...tsconfig.compilerOptions, - declarationMap: true, - }; - tsconfig.angularCompilerOptions = { - ...tsconfig.angularCompilerOptions, - compilationMode: 'full', - }; - - return JSON.stringify(tsconfig, null, 2); - }); - }, - ({ result }) => { - expect(result?.success).toBeTrue(); - const mjs = harness.readFile('dist/lib/fesm2022/lib.mjs'); - expect(mjs).toContain('ɵɵdefineComponent'); - expect(mjs).not.toContain('ɵɵngDeclareComponent'); - expect(harness.hasFile('dist/lib/types/lib.d.ts.map')).toBeTrue(); - }, - ]); - }); }); });