99import type { BuilderContext } from '@angular-devkit/architect' ;
1010import path from 'node:path' ;
1111import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets' ;
12- import { emitFilesToDisk } from '../../../utils/disk-files' ;
12+ import { deleteFilesFromDisk , emitFilesToDisk } from '../../../utils/disk-files' ;
1313import { assertIsError } from '../../../utils/error' ;
14- import type { NormalizedLibraryOptions , SingleBuildState } from '../types' ;
15- import { collectAssetsToEmit } from './assets' ;
14+ import type { BundleResult , NormalizedLibraryOptions , SingleBuildState } from '../types' ;
15+ import { type CollectedAssets , collectAssetsToEmit } from './assets' ;
1616import { bundleEntryPoints } from './bundler' ;
1717import { type LibraryCompilationDiagnostics , compileLibrary } from './compilation' ;
1818import { collectEntryPointsToBundle } from './entry-points' ;
1919import { generatePackageManifests } from './package-manifests' ;
20- import type { DiskOutputFile , OutputFile } from './utils' ;
20+ import type { OutputFile } from './utils' ;
2121
2222/**
2323 * Result of executing a single library build iteration.
@@ -41,6 +41,9 @@ export function createSingleBuildState(): SingleBuildState {
4141 previousBundleResults : new Map ( ) ,
4242 pendingChangedEsmFiles : new Set ( ) ,
4343 pendingChangedDtsFiles : new Set ( ) ,
44+ emittedBundleFiles : new Set ( ) ,
45+ emittedManifestFiles : new Set ( ) ,
46+ emittedAssets : new Map ( ) ,
4447 } ;
4548}
4649
@@ -76,12 +79,12 @@ export interface BuildActionContext {
7679 modifiedFiles ?: Set < string > ;
7780
7881 /**
79- * Asset files to emit to disk for the current build iteration.
82+ * Collected asset changes for the current build iteration.
8083 * Pre-collected in the watch loop to avoid redundant asset matching, or resolved and
8184 * populated by `buildAction` when omitted (such as during the initial build) so the
82- * caller can register their source paths with the file watcher.
85+ * caller can register their POSIX source paths with the file watcher.
8386 */
84- assetsToEmit ?: DiskOutputFile [ ] ;
87+ collectedAssets ?: CollectedAssets ;
8588}
8689
8790/**
@@ -133,8 +136,12 @@ export async function buildAction(actionContext: BuildActionContext): Promise<Bu
133136 }
134137
135138 const filesToEmit : OutputFile [ ] = [ ] ;
139+ const filesToDelete = new Set < string > ( ) ;
140+ let nextBundleFiles : Set < string > | undefined ;
141+ let nextManifestFiles : Set < string > | undefined ;
136142
137143 if ( shouldCompileEntryPoints ) {
144+ const hadEntryPointsChanges = buildState . hasEntryPointsChanges ;
138145 buildState . hasCompilationError = true ;
139146 buildState . hasEntryPointsChanges = false ;
140147
@@ -212,6 +219,18 @@ export async function buildAction(actionContext: BuildActionContext): Promise<Bu
212219 for ( const [ name , bundleResult ] of bundleOutput . bundleResults ) {
213220 buildState . previousBundleResults . set ( name , bundleResult ) ;
214221 }
222+
223+ if (
224+ bundleOutput . bundleResults . size > 0 ||
225+ hadEntryPointsChanges ||
226+ buildState . emittedBundleFiles . size === 0
227+ ) {
228+ nextBundleFiles = collectBundleOutputFiles (
229+ buildState . previousBundleResults . values ( ) ,
230+ buildState . emittedBundleFiles ,
231+ filesToDelete ,
232+ ) ;
233+ }
215234 }
216235
217236 let hasTslibImport = false ;
@@ -223,27 +242,45 @@ export async function buildAction(actionContext: BuildActionContext): Promise<Bu
223242 }
224243
225244 if ( ! buildState . hasEmittedManifests || buildState . hasEmittedTslib !== hasTslibImport ) {
226- filesToEmit . push (
227- ...( await generatePackageManifests (
228- {
229- ...options ,
230- compilationMode : buildState . compilationMode ?? options . compilationMode ?? 'partial' ,
231- } ,
232- isWatchMode ,
233- hasTslibImport ,
234- ) ) ,
245+ const manifestFiles = await generatePackageManifests (
246+ {
247+ ...options ,
248+ compilationMode : buildState . compilationMode ?? options . compilationMode ?? 'partial' ,
249+ } ,
250+ isWatchMode ,
251+ hasTslibImport ,
235252 ) ;
253+
254+ filesToEmit . push ( ...manifestFiles ) ;
255+
256+ nextManifestFiles = new Set ( manifestFiles . map ( ( file ) => file . path ) ) ;
257+ for ( const prevFile of buildState . emittedManifestFiles ) {
258+ if ( ! nextManifestFiles . has ( prevFile ) ) {
259+ filesToDelete . add ( prevFile ) ;
260+ }
261+ }
236262 }
237263
238- const resolvedAssetsToEmit = ( actionContext . assetsToEmit ??= await collectAssetsToEmit (
264+ const collectedAssets = ( actionContext . collectedAssets ??= await collectAssetsToEmit (
239265 options . assets ,
240266 options . workspaceRoot ,
241267 buildState . hasEmittedAssets ? modifiedFiles : undefined ,
268+ buildState . emittedAssets ,
242269 ) ) ;
243- filesToEmit . push ( ...resolvedAssetsToEmit ) ;
270+ filesToEmit . push ( ...collectedAssets . filesToEmit ) ;
244271
245272 await emitFilesToDisk ( filesToEmit , ( file ) => path . posix . join ( options . outputPath , file . path ) ) ;
246273
274+ updateEmittedAssetState ( buildState , collectedAssets , filesToDelete ) ;
275+
276+ if ( filesToDelete . size ) {
277+ await deleteFilesFromDisk (
278+ Array . from ( filesToDelete , ( file ) => path . join ( options . outputPath , file ) ) ,
279+ ) ;
280+ }
281+
282+ buildState . emittedBundleFiles = nextBundleFiles ?? buildState . emittedBundleFiles ;
283+ buildState . emittedManifestFiles = nextManifestFiles ?? buildState . emittedManifestFiles ;
247284 buildState . hasEmittedManifests = true ;
248285 buildState . hasEmittedTslib = hasTslibImport ;
249286 buildState . hasEmittedAssets = true ;
@@ -255,6 +292,79 @@ export async function buildAction(actionContext: BuildActionContext): Promise<Bu
255292 return { errors, warnings, logs } ;
256293}
257294
295+ /**
296+ * Collects the active POSIX bundle output file paths across all entry points and queues any
297+ * previously emitted bundle files that are no longer present for deletion.
298+ *
299+ * @param bundleResults Active bundle results across all entry points.
300+ * @param previousBundleFiles Relative POSIX output paths emitted in the previous build iteration.
301+ * @param filesToDelete Set of relative POSIX output paths queued for deletion.
302+ * @returns The new set of active relative POSIX bundle output file paths.
303+ */
304+ function collectBundleOutputFiles (
305+ bundleResults : Iterable < BundleResult > ,
306+ previousBundleFiles : ReadonlySet < string > ,
307+ filesToDelete : Set < string > ,
308+ ) : Set < string > {
309+ const nextBundleFiles = new Set < string > ( ) ;
310+ for ( const result of bundleResults ) {
311+ if ( result . esmOutputFiles ) {
312+ for ( const file of result . esmOutputFiles ) {
313+ nextBundleFiles . add ( file ) ;
314+ }
315+ }
316+ if ( result . dtsOutputFiles ) {
317+ for ( const file of result . dtsOutputFiles ) {
318+ nextBundleFiles . add ( file ) ;
319+ }
320+ }
321+ }
322+
323+ for ( const prevFile of previousBundleFiles ) {
324+ if ( ! nextBundleFiles . has ( prevFile ) ) {
325+ filesToDelete . add ( prevFile ) ;
326+ }
327+ }
328+
329+ return nextBundleFiles ;
330+ }
331+
332+ /**
333+ * Updates `buildState.emittedAssets` with newly emitted and deleted assets, queuing any
334+ * deleted asset POSIX output paths that are not produced by another active asset for removal.
335+ *
336+ * @param buildState The state preserved across incremental builds.
337+ * @param collectedAssets The collected asset files to emit and delete.
338+ * @param filesToDelete Set of relative POSIX output paths queued for deletion.
339+ */
340+ function updateEmittedAssetState (
341+ buildState : SingleBuildState ,
342+ collectedAssets : CollectedAssets ,
343+ filesToDelete : Set < string > ,
344+ ) : void {
345+ const emittedAssets = buildState . emittedAssets ;
346+ const toDelete = collectedAssets . filesToDelete ;
347+ const toEmit = collectedAssets . filesToEmit ;
348+
349+ for ( const deletedAsset of toDelete ) {
350+ emittedAssets . delete ( deletedAsset . source ) ;
351+ }
352+
353+ for ( const emittedAsset of toEmit ) {
354+ emittedAssets . set ( emittedAsset . source , emittedAsset . path ) ;
355+ }
356+
357+ if ( toDelete . length ) {
358+ const activeAssetOutputs = new Set ( emittedAssets . values ( ) ) ;
359+
360+ for ( const deletedAsset of toDelete ) {
361+ if ( ! activeAssetOutputs . has ( deletedAsset . path ) ) {
362+ filesToDelete . add ( deletedAsset . path ) ;
363+ }
364+ }
365+ }
366+ }
367+
258368/**
259369 * Checks whether any modified file (excluding `package.json`) is part of the
260370 * tracked compilation file set.
0 commit comments