@@ -82,30 +82,181 @@ interface MultiBundleOutput {
8282}
8383
8484/**
85- * Checks whether an entry point's previous output files included shared (non-entry) chunks .
85+ * Checks whether two sets share at least one common element .
8686 */
87- function hasSharedChunks (
88- outputFiles : ReadonlySet < string > | undefined ,
89- dir : string ,
90- bundleName : string ,
91- extension : 'mjs' | 'd.ts' ,
92- ) : boolean {
93- if ( ! outputFiles || outputFiles . size === 0 ) {
94- return false ;
87+ function hasSetIntersection ( a : ReadonlySet < string > , b : ReadonlySet < string > ) : boolean {
88+ if ( a . size > b . size ) {
89+ return hasSetIntersection ( b , a ) ;
90+ }
91+
92+ for ( const item of a ) {
93+ if ( b . has ( item ) ) {
94+ return true ;
95+ }
96+ }
97+
98+ return false ;
99+ }
100+
101+ /**
102+ * Expands the selected entry points to include any available entry points whose module IDs
103+ * transitively overlap with the selected set.
104+ */
105+ function expandOverlappingEntryPoints (
106+ selectedEntryPoints : readonly NormalizedEntryPoint [ ] ,
107+ allEntryPoints : readonly NormalizedEntryPoint [ ] ,
108+ isAvailable : ( ep : NormalizedEntryPoint ) => boolean ,
109+ getModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
110+ ) : readonly NormalizedEntryPoint [ ] {
111+ if ( selectedEntryPoints . length >= allEntryPoints . length ) {
112+ return selectedEntryPoints ;
113+ }
114+
115+ let trackedModuleIds : ReadonlySet < string > | undefined ;
116+ let mutableTrackedIds : Set < string > | undefined ;
117+
118+ if ( selectedEntryPoints . length === 1 ) {
119+ trackedModuleIds = getModuleIds ( selectedEntryPoints [ 0 ] ) ;
120+ } else {
121+ mutableTrackedIds = new Set < string > ( ) ;
122+ for ( const ep of selectedEntryPoints ) {
123+ const ids = getModuleIds ( ep ) ;
124+ if ( ids ) {
125+ for ( const id of ids ) {
126+ mutableTrackedIds . add ( id ) ;
127+ }
128+ }
129+ }
130+ trackedModuleIds = mutableTrackedIds ;
131+ }
132+
133+ if ( ! trackedModuleIds || trackedModuleIds . size === 0 ) {
134+ return selectedEntryPoints ;
95135 }
96136
97- const entryFile = `${ dir } /${ bundleName } .${ extension } ` ;
98- const entryMapFile = `${ entryFile } .map` ;
137+ let selectedNames : Set < string > | undefined ;
138+ const singleSelectedName =
139+ selectedEntryPoints . length === 1 ? selectedEntryPoints [ 0 ] . name : undefined ;
140+
141+ let added = true ;
142+ while ( added ) {
143+ added = false ;
144+ for ( const ep of allEntryPoints ) {
145+ if ( singleSelectedName !== undefined && ! selectedNames ) {
146+ if ( ep . name === singleSelectedName ) {
147+ continue ;
148+ }
149+ } else {
150+ selectedNames ??= new Set ( selectedEntryPoints . map ( ( item ) => item . name ) ) ;
151+ if ( selectedNames . has ( ep . name ) ) {
152+ continue ;
153+ }
154+ }
99155
100- for ( const file of outputFiles ) {
101- if ( file !== entryFile && file !== entryMapFile ) {
156+ const ids = getModuleIds ( ep ) ;
157+ if ( ids && hasSetIntersection ( ids , trackedModuleIds ) && isAvailable ( ep ) ) {
158+ selectedNames ??= new Set ( selectedEntryPoints . map ( ( item ) => item . name ) ) ;
159+ selectedNames . add ( ep . name ) ;
160+ mutableTrackedIds ??= new Set ( trackedModuleIds ) ;
161+ trackedModuleIds = mutableTrackedIds ;
162+ for ( const id of ids ) {
163+ mutableTrackedIds . add ( id ) ;
164+ }
165+ added = true ;
166+ }
167+ }
168+ }
169+
170+ return selectedNames && selectedNames . size > selectedEntryPoints . length
171+ ? allEntryPoints . filter ( ( ep ) => selectedNames . has ( ep . name ) )
172+ : selectedEntryPoints ;
173+ }
174+
175+ /**
176+ * Checks whether any bundled entry point acquired a module ID that was not present in its
177+ * previous bundle result.
178+ */
179+ function hasNewlyAddedModules (
180+ bundledEntryPoints : readonly NormalizedEntryPoint [ ] ,
181+ output : MultiBundleOutput ,
182+ getPreviousModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
183+ ) : boolean {
184+ for ( const ep of bundledEntryPoints ) {
185+ const newIds = output . bundles . get ( ep . bundleName ) ?. moduleIds ;
186+ if ( ! newIds || newIds . size === 0 ) {
187+ continue ;
188+ }
189+
190+ const prevIds = getPreviousModuleIds ( ep ) ;
191+ if ( ! prevIds ) {
102192 return true ;
103193 }
194+
195+ for ( const id of newIds ) {
196+ if ( ! prevIds . has ( id ) ) {
197+ return true ;
198+ }
199+ }
104200 }
105201
106202 return false ;
107203}
108204
205+ /**
206+ * Bundles dirty entry points for a single mode (ESM or DTS), automatically expanding the entry
207+ * point set before or after bundling when module IDs overlap across entry points.
208+ */
209+ async function bundleModeWithSharedModules (
210+ dirtyEntryPoints : readonly NormalizedEntryPoint [ ] ,
211+ allEntryPoints : readonly NormalizedEntryPoint [ ] ,
212+ files : ReadonlyMap < string , string > ,
213+ dtsMode : boolean ,
214+ getPreviousModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
215+ bundleFn : (
216+ entryPoints : readonly NormalizedEntryPoint [ ] ,
217+ files : ReadonlyMap < string , string > ,
218+ options : NormalizedLibraryOptions ,
219+ ) => Promise < MultiBundleOutput > ,
220+ options : NormalizedLibraryOptions ,
221+ ) : Promise < MultiBundleOutput > {
222+ if ( dirtyEntryPoints . length === 0 ) {
223+ return { filesToEmit : [ ] , bundles : new Map ( ) , warnings : [ ] } ;
224+ }
225+
226+ if ( dirtyEntryPoints . length >= allEntryPoints . length ) {
227+ return bundleFn ( dirtyEntryPoints , files , options ) ;
228+ }
229+
230+ const isAvailable = ( ep : NormalizedEntryPoint ) : boolean =>
231+ files . has ( resolveEntryInputPath ( ep . entryFilePath , dtsMode ) ) ;
232+
233+ const initialEntryPoints = expandOverlappingEntryPoints (
234+ dirtyEntryPoints ,
235+ allEntryPoints ,
236+ isAvailable ,
237+ getPreviousModuleIds ,
238+ ) ;
239+ const output = await bundleFn ( initialEntryPoints , files , options ) ;
240+
241+ if (
242+ initialEntryPoints . length < allEntryPoints . length &&
243+ hasNewlyAddedModules ( initialEntryPoints , output , getPreviousModuleIds )
244+ ) {
245+ const expandedEntryPoints = expandOverlappingEntryPoints (
246+ initialEntryPoints ,
247+ allEntryPoints ,
248+ isAvailable ,
249+ ( ep ) => output . bundles . get ( ep . bundleName ) ?. moduleIds ?? getPreviousModuleIds ( ep ) ,
250+ ) ;
251+
252+ if ( expandedEntryPoints . length > initialEntryPoints . length ) {
253+ return bundleFn ( expandedEntryPoints , files , options ) ;
254+ }
255+ }
256+
257+ return output ;
258+ }
259+
109260/**
110261 * Collects all normalized entry points from options and bundle input items.
111262 */
@@ -128,7 +279,7 @@ function getAllEntryPoints(
128279
129280/**
130281 * Bundles the compiled in-memory JavaScript and declaration files for all dirty entry points
131- * using at most 2 Rolldown instances total (1 for all .mjs bundles, 1 for all .d.ts bundles).
282+ * using at most 2 Rolldown instances per pass (1 for all .mjs bundles, 1 for all .d.ts bundles).
132283 *
133284 * @param items The collection of entry point inputs with change statuses.
134285 * @param esmFiles Map of virtual POSIX ESM output file paths to their content.
@@ -151,16 +302,15 @@ export async function bundleEntryPoints(
151302
152303 const allEntryPoints = getAllEntryPoints ( items , options ) ;
153304 const itemsByName = new Map ( items . map ( ( item ) => [ item . entryPoint . name , item ] ) ) ;
305+ const getPreviousResult = ( name : string ) : BundleResult | undefined =>
306+ itemsByName . get ( name ) ?. previousBundleResult ?? previousBundleResults ?. get ( name ) ;
154307
155- let esmEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
156- let dtsEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
157- let hasSharedEsmChunks = false ;
158- let hasSharedDtsChunks = false ;
308+ const esmEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
309+ const dtsEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
159310
160311 for ( const item of items ) {
161312 const { entryPoint } = item ;
162- const previousBundleResult =
163- item . previousBundleResult ?? previousBundleResults ?. get ( entryPoint . name ) ;
313+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
164314 if ( item . hasEsmChanges || ! previousBundleResult ) {
165315 esmEntryPoints . push ( entryPoint ) ;
166316 }
@@ -169,50 +319,25 @@ export async function bundleEntryPoints(
169319 }
170320 }
171321
172- for ( const entryPoint of allEntryPoints ) {
173- const previousBundleResult =
174- itemsByName . get ( entryPoint . name ) ?. previousBundleResult ??
175- previousBundleResults ?. get ( entryPoint . name ) ;
176- if ( ! previousBundleResult ) {
177- continue ;
178- }
179-
180- hasSharedEsmChunks ||= hasSharedChunks (
181- previousBundleResult . esmOutputFiles ,
182- FESM_OUTPUT_DIR ,
183- entryPoint . bundleName ,
184- 'mjs' ,
185- ) ;
186- hasSharedDtsChunks ||= hasSharedChunks (
187- previousBundleResult . dtsOutputFiles ,
188- TYPES_OUTPUT_DIR ,
189- entryPoint . bundleName ,
190- 'd.ts' ,
191- ) ;
192- }
193-
194- if (
195- esmEntryPoints . length > 0 &&
196- hasSharedEsmChunks &&
197- esmEntryPoints . length < allEntryPoints . length
198- ) {
199- esmEntryPoints = allEntryPoints . filter ( ( ep ) =>
200- esmFiles . has ( ep . entryFilePath . replace ( / \. ( m ? ) t s $ / , '.$1js' ) ) ,
201- ) ;
202- }
203- if (
204- dtsEntryPoints . length > 0 &&
205- hasSharedDtsChunks &&
206- dtsEntryPoints . length < allEntryPoints . length
207- ) {
208- dtsEntryPoints = allEntryPoints . filter ( ( ep ) =>
209- dtsFiles . has ( ep . entryFilePath . replace ( / \. ( m ? t s ) $ / , '.d.$1' ) ) ,
210- ) ;
211- }
212-
213322 const [ esmOutput , dtsOutput ] = await Promise . all ( [
214- bundleAllEsm ( esmEntryPoints , esmFiles , options ) ,
215- bundleAllDts ( dtsEntryPoints , dtsFiles , options ) ,
323+ bundleModeWithSharedModules (
324+ esmEntryPoints ,
325+ allEntryPoints ,
326+ esmFiles ,
327+ false ,
328+ ( ep ) => getPreviousResult ( ep . name ) ?. esmModuleIds ,
329+ bundleAllEsm ,
330+ options ,
331+ ) ,
332+ bundleModeWithSharedModules (
333+ dtsEntryPoints ,
334+ allEntryPoints ,
335+ dtsFiles ,
336+ true ,
337+ ( ep ) => getPreviousResult ( ep . name ) ?. dtsModuleIds ,
338+ bundleAllDts ,
339+ options ,
340+ ) ,
216341 ] ) ;
217342
218343 for ( const entryPoint of allEntryPoints ) {
@@ -223,8 +348,7 @@ export async function bundleEntryPoints(
223348 continue ;
224349 }
225350
226- const previousBundleResult =
227- item ?. previousBundleResult ?? previousBundleResults ?. get ( entryPoint . name ) ;
351+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
228352 bundleResults . set ( entryPoint . name , {
229353 esmModuleIds : esmBundle ?. moduleIds ?? previousBundleResult ?. esmModuleIds ?? new Set ( ) ,
230354 dtsModuleIds : dtsBundle ?. moduleIds ?? previousBundleResult ?. dtsModuleIds ?? new Set ( ) ,
@@ -241,6 +365,15 @@ export async function bundleEntryPoints(
241365 } ;
242366}
243367
368+ /**
369+ * Resolves the compiled in-memory entry file path for a TypeScript entry point.
370+ */
371+ function resolveEntryInputPath ( entryFilePath : string , dtsMode : boolean ) : string {
372+ return dtsMode
373+ ? entryFilePath . replace ( / \. ( m ? t s ) $ / , '.d.$1' )
374+ : entryFilePath . replace ( / \. ( m ? ) t s $ / , '.$1js' ) ;
375+ }
376+
244377/**
245378 * Generates the Rolldown input mapping object from the specified entry points.
246379 *
@@ -254,9 +387,7 @@ function resolveEntryInputMap(
254387) : Record < string , string > {
255388 const input : Record < string , string > = { } ;
256389 for ( const { bundleName, entryFilePath } of entryPoints ) {
257- input [ bundleName ] = dtsMode
258- ? entryFilePath . replace ( / \. ( m ? t s ) $ / , '.d.$1' )
259- : entryFilePath . replace ( / \. ( m ? ) t s $ / , '.$1js' ) ;
390+ input [ bundleName ] = resolveEntryInputPath ( entryFilePath , dtsMode ) ;
260391 }
261392
262393 return input ;
0 commit comments