@@ -81,50 +81,275 @@ interface MultiBundleOutput {
8181 warnings : string [ ] ;
8282}
8383
84+ /**
85+ * Checks whether two sets share at least one common element.
86+ */
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 ;
135+ }
136+
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+ }
155+
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 ) {
192+ return true ;
193+ }
194+
195+ for ( const id of newIds ) {
196+ if ( ! prevIds . has ( id ) ) {
197+ return true ;
198+ }
199+ }
200+ }
201+
202+ return false ;
203+ }
204+
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+
260+ /**
261+ * Collects all normalized entry points from options and bundle input items.
262+ */
263+ function getAllEntryPoints (
264+ items : readonly BundleEntryPointInput [ ] ,
265+ options : NormalizedLibraryOptions ,
266+ ) : NormalizedEntryPoint [ ] {
267+ const byName = new Map < string , NormalizedEntryPoint > ( ) ;
268+ if ( options . entryPoints ) {
269+ for ( const ep of options . entryPoints . values ( ) ) {
270+ byName . set ( ep . name , ep ) ;
271+ }
272+ }
273+ for ( const item of items ) {
274+ byName . set ( item . entryPoint . name , item . entryPoint ) ;
275+ }
276+
277+ return Array . from ( byName . values ( ) ) ;
278+ }
279+
84280/**
85281 * Bundles the compiled in-memory JavaScript and declaration files for all dirty entry points
86- * 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).
87283 *
88284 * @param items The collection of entry point inputs with change statuses.
89285 * @param esmFiles Map of virtual POSIX ESM output file paths to their content.
90286 * @param dtsFiles Map of virtual POSIX TypeScript declaration file paths to their content.
91287 * @param options The normalized library builder options with POSIX paths.
288+ * @param previousBundleResults Optional map of entry point names to their previous bundle results.
92289 * @returns A promise resolving to the bundled output files and bundle cache results.
93290 */
94291export async function bundleEntryPoints (
95292 items : readonly BundleEntryPointInput [ ] ,
96293 esmFiles : ReadonlyMap < string , string > ,
97294 dtsFiles : ReadonlyMap < string , string > ,
98295 options : NormalizedLibraryOptions ,
296+ previousBundleResults ?: ReadonlyMap < string , BundleResult > ,
99297) : Promise < BundleEntryPointsOutput > {
100298 const bundleResults = new Map < string , BundleResult > ( ) ;
101299 if ( items . length === 0 ) {
102300 return { filesToEmit : [ ] , bundleResults, warnings : [ ] } ;
103301 }
104302
303+ const allEntryPoints = getAllEntryPoints ( items , options ) ;
304+ 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 ) ;
307+
105308 const esmEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
106309 const dtsEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
107310
108311 for ( const item of items ) {
109- if ( item . hasEsmChanges || ! item . previousBundleResult ) {
110- esmEntryPoints . push ( item . entryPoint ) ;
312+ const { entryPoint } = item ;
313+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
314+ if ( item . hasEsmChanges || ! previousBundleResult ) {
315+ esmEntryPoints . push ( entryPoint ) ;
111316 }
112- if ( item . hasDtsChanges || ! item . previousBundleResult ) {
113- dtsEntryPoints . push ( item . entryPoint ) ;
317+ if ( item . hasDtsChanges || ! previousBundleResult ) {
318+ dtsEntryPoints . push ( entryPoint ) ;
114319 }
115320 }
116321
117322 const [ esmOutput , dtsOutput ] = await Promise . all ( [
118- bundleAllEsm ( esmEntryPoints , esmFiles , options ) ,
119- 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+ ) ,
120341 ] ) ;
121342
122- for ( const { entryPoint, previousBundleResult } of items ) {
123- const { bundleName, name } = entryPoint ;
124- const esmBundle = esmOutput . bundles . get ( bundleName ) ;
125- const dtsBundle = dtsOutput . bundles . get ( bundleName ) ;
343+ for ( const entryPoint of allEntryPoints ) {
344+ const item = itemsByName . get ( entryPoint . name ) ;
345+ const esmBundle = esmOutput . bundles . get ( entryPoint . bundleName ) ;
346+ const dtsBundle = dtsOutput . bundles . get ( entryPoint . bundleName ) ;
347+ if ( ! item && ! esmBundle && ! dtsBundle ) {
348+ continue ;
349+ }
126350
127- bundleResults . set ( name , {
351+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
352+ bundleResults . set ( entryPoint . name , {
128353 esmModuleIds : esmBundle ?. moduleIds ?? previousBundleResult ?. esmModuleIds ?? new Set ( ) ,
129354 dtsModuleIds : dtsBundle ?. moduleIds ?? previousBundleResult ?. dtsModuleIds ?? new Set ( ) ,
130355 esmOutputFiles : esmBundle ?. outputFiles ?? previousBundleResult ?. esmOutputFiles ?? new Set ( ) ,
@@ -140,6 +365,15 @@ export async function bundleEntryPoints(
140365 } ;
141366}
142367
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+
143377/**
144378 * Generates the Rolldown input mapping object from the specified entry points.
145379 *
@@ -153,9 +387,7 @@ function resolveEntryInputMap(
153387) : Record < string , string > {
154388 const input : Record < string , string > = { } ;
155389 for ( const { bundleName, entryFilePath } of entryPoints ) {
156- input [ bundleName ] = dtsMode
157- ? entryFilePath . replace ( / \. ( [ c m ] ? t s ) $ / , '.d.$1' )
158- : entryFilePath . replace ( / \. ( [ c m ] ? ) t s $ / , '.$1js' ) ;
390+ input [ bundleName ] = resolveEntryInputPath ( entryFilePath , dtsMode ) ;
159391 }
160392
161393 return input ;
@@ -174,6 +406,8 @@ function createMemoryFileLoaderPlugin(
174406 extensions : readonly string [ ] ,
175407 includeMap : boolean ,
176408) : Plugin {
409+ const isDts = extensions === DTS_EXTENSIONS ;
410+
177411 return {
178412 name : 'memory-file-loader' ,
179413 resolveId : {
@@ -187,28 +421,27 @@ function createMemoryFileLoaderPlugin(
187421 return files . has ( id ) ? { id, external : false } : undefined ;
188422 }
189423
190- if ( id [ 0 ] !== '.' && ! path . posix . isAbsolute ( id ) ) {
424+ if ( id [ 0 ] !== '.' && ! path . isAbsolute ( id ) ) {
191425 return { id, external : true } ;
192426 }
193427
194428 const resolved = id [ 0 ] === '.' ? path . posix . join ( path . posix . dirname ( importer ) , id ) : id ;
429+ const normalizedResolved = resolved . replace ( / \/ + $ / , '' ) ;
195430
196- let resolvedCandidate : string | undefined ;
197- if ( files . has ( resolved ) ) {
198- resolvedCandidate = resolved ;
199- } else {
200- const base = resolved . replace ( / \. [ c m ] ? j s $ / , '' ) ;
201- for ( const ext of extensions ) {
202- const candidate = base + ext ;
203- if ( files . has ( candidate ) ) {
204- resolvedCandidate = candidate ;
205- break ;
206- }
207- }
431+ if ( files . has ( normalizedResolved ) ) {
432+ return { id : normalizedResolved , external : false } ;
208433 }
209434
210- if ( resolvedCandidate ) {
211- return { id : resolvedCandidate , external : false } ;
435+ const base = normalizedResolved . replace ( / \. m ? j s $ / , '' ) ;
436+ if ( isDts && normalizedResolved . endsWith ( '.mjs' ) && files . has ( `${ base } .d.mts` ) ) {
437+ return { id : `${ base } .d.mts` , external : false } ;
438+ }
439+
440+ for ( const ext of extensions ) {
441+ const candidate = base + ext ;
442+ if ( files . has ( candidate ) ) {
443+ return { id : candidate , external : false } ;
444+ }
212445 }
213446
214447 return undefined ;
0 commit comments