@@ -14,58 +14,110 @@ import type { NormalizedLibraryOptions } from '../types';
1414import { type DiskOutputFile , createDiskOutputFile } from './utils' ;
1515
1616/**
17- * Resolves and collects configured library assets to be emitted to disk.
17+ * Result of resolving and collecting library assets for a build iteration.
18+ */
19+ export interface CollectedAssets {
20+ /** Asset files to emit to disk, with POSIX source and destination paths. */
21+ filesToEmit : DiskOutputFile [ ] ;
22+
23+ /** Previously emitted asset files whose source files were deleted and should be removed from output. */
24+ filesToDelete : DiskOutputFile [ ] ;
25+ }
26+
27+ interface AssetMatcher {
28+ asset : NormalizedLibraryOptions [ 'assets' ] [ number ] ;
29+ posixInputPrefix : string ;
30+ isMatch : picomatch . Matcher ;
31+ }
32+
33+ const assetMatcherCache = new WeakMap < NormalizedLibraryOptions [ 'assets' ] , AssetMatcher [ ] > ( ) ;
34+
35+ /**
36+ * Resolves and collects configured library assets to be emitted to or deleted from disk.
1837 *
19- * @param assets The normalized asset patterns with POSIX paths.
20- * @param workspaceRoot The normalized POSIX workspace root directory path.
38+ * @param assets The normalized asset patterns with POSIX input and output paths.
39+ * @param workspaceRoot The workspace root POSIX directory path.
2140 * @param modifiedFiles Optional set of modified POSIX file paths for incremental copying in watch mode.
22- * @returns An array of disk file emission descriptors.
41+ * @param emittedAssets Optional map of previously emitted POSIX source file paths to relative POSIX output paths.
42+ * @returns The collected asset files to emit and delete.
2343 */
2444export async function collectAssetsToEmit (
2545 assets : NormalizedLibraryOptions [ 'assets' ] ,
2646 workspaceRoot : string ,
2747 modifiedFiles ?: ReadonlySet < string > ,
28- ) : Promise < DiskOutputFile [ ] > {
48+ emittedAssets ?: ReadonlyMap < string , string > ,
49+ ) : Promise < CollectedAssets > {
2950 if ( assets . length === 0 || modifiedFiles ?. size === 0 ) {
30- return [ ] ;
51+ return { filesToEmit : [ ] , filesToDelete : [ ] } ;
3152 }
3253
3354 if ( modifiedFiles ) {
34- const matchers = createAssetMatchers ( assets ) ;
55+ const matchers = getAssetMatchers ( assets ) ;
3556 const filesToEmit : DiskOutputFile [ ] = [ ] ;
57+ const filesToDelete : DiskOutputFile [ ] = [ ] ;
3658
3759 for ( const file of modifiedFiles ) {
38- for ( const { asset, isMatch } of matchers ) {
39- const relative = path . posix . relative ( asset . input , file ) ;
40- if ( ! relative || relative . startsWith ( '..' ) || path . posix . isAbsolute ( relative ) ) {
60+ let isFile : boolean | undefined ;
61+
62+ const previousOutput = emittedAssets ?. get ( file ) ;
63+ if ( previousOutput !== undefined ) {
64+ isFile = statSync ( file , { throwIfNoEntry : false } ) ?. isFile ( ) ?? false ;
65+ if ( ! isFile ) {
66+ filesToDelete . push ( createDiskOutputFile ( file , previousOutput ) ) ;
4167 continue ;
4268 }
69+ }
4370
71+ for ( const { asset, posixInputPrefix, isMatch } of matchers ) {
72+ if ( ! file . startsWith ( posixInputPrefix ) ) {
73+ continue ;
74+ }
75+
76+ const relative = file . slice ( posixInputPrefix . length ) ;
4477 if ( ! isMatch ( relative ) ) {
4578 continue ;
4679 }
4780
48- if ( statSync ( file , { throwIfNoEntry : false } ) ?. isFile ( ) ) {
81+ isFile ??= statSync ( file , { throwIfNoEntry : false } ) ?. isFile ( ) ?? false ;
82+ if ( isFile ) {
4983 filesToEmit . push ( createDiskOutputFile ( file , path . posix . join ( asset . output , relative ) ) ) ;
5084 }
5185 }
5286 }
5387
54- return filesToEmit ;
88+ return { filesToEmit, filesToDelete } ;
5589 }
5690
5791 const resolvedAssets = await resolveAssets ( assets , workspaceRoot ) ;
5892
59- return resolvedAssets . map ( ( { source, destination } ) => createDiskOutputFile ( source , destination ) ) ;
93+ return {
94+ filesToEmit : resolvedAssets . map ( ( { source, destination } ) =>
95+ createDiskOutputFile ( source , destination ) ,
96+ ) ,
97+ filesToDelete : [ ] ,
98+ } ;
6099}
61100
62- function createAssetMatchers ( assets : NormalizedLibraryOptions [ 'assets' ] ) {
63- return assets . map ( ( asset ) => {
64- const isMatch = picomatch ( asset . glob , {
65- dot : true ,
66- ignore : [ ...DEFAULT_ASSET_IGNORE , ...( asset . ignore ?? [ ] ) ] ,
101+ /**
102+ * Returns cached or newly compiled picomatch matchers and POSIX input prefixes for each asset configuration.
103+ *
104+ * @param assets The normalized asset patterns with POSIX input and output paths.
105+ * @returns An array of asset matcher descriptors.
106+ */
107+ function getAssetMatchers ( assets : NormalizedLibraryOptions [ 'assets' ] ) : AssetMatcher [ ] {
108+ let matchers = assetMatcherCache . get ( assets ) ;
109+ if ( ! matchers ) {
110+ matchers = assets . map ( ( asset ) => {
111+ const posixInput = asset . input . replace ( / \/ + $ / , '' ) ;
112+ const isMatch = picomatch ( asset . glob , {
113+ dot : true ,
114+ ignore : [ ...DEFAULT_ASSET_IGNORE , ...( asset . ignore ?? [ ] ) ] ,
115+ } ) ;
116+
117+ return { asset, posixInputPrefix : `${ posixInput } /` , isMatch } ;
67118 } ) ;
119+ assetMatcherCache . set ( assets , matchers ) ;
120+ }
68121
69- return { asset, isMatch } ;
70- } ) ;
122+ return matchers ;
71123}
0 commit comments