@@ -81,50 +81,224 @@ interface MultiBundleOutput {
8181 warnings : string [ ] ;
8282}
8383
84+ /**
85+ * Expands the selected entry points to include any available entry points whose module IDs
86+ * transitively overlap with the selected set.
87+ */
88+ function expandOverlappingEntryPoints (
89+ selectedEntryPoints : readonly NormalizedEntryPoint [ ] ,
90+ allEntryPoints : readonly NormalizedEntryPoint [ ] ,
91+ isAvailable : ( ep : NormalizedEntryPoint ) => boolean ,
92+ getModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
93+ ) : readonly NormalizedEntryPoint [ ] {
94+ assert (
95+ selectedEntryPoints . length <= allEntryPoints . length ,
96+ 'Selected entry points cannot exceed all entry points.' ,
97+ ) ;
98+ if ( selectedEntryPoints . length === allEntryPoints . length ) {
99+ return selectedEntryPoints ;
100+ }
101+
102+ const selected = new Set ( selectedEntryPoints ) ;
103+ for ( const ep of selected ) {
104+ const moduleIds = getModuleIds ( ep ) ;
105+ if ( ! moduleIds ?. size ) {
106+ continue ;
107+ }
108+
109+ for ( const candidate of allEntryPoints ) {
110+ if ( selected . has ( candidate ) || ! isAvailable ( candidate ) ) {
111+ continue ;
112+ }
113+
114+ const candidateIds = getModuleIds ( candidate ) ;
115+ if ( candidateIds && ! moduleIds . isDisjointFrom ( candidateIds ) ) {
116+ selected . add ( candidate ) ;
117+ }
118+ }
119+ }
120+
121+ return selected . size > selectedEntryPoints . length
122+ ? allEntryPoints . filter ( ( ep ) => selected . has ( ep ) )
123+ : selectedEntryPoints ;
124+ }
125+
126+ /**
127+ * Checks whether any bundled entry point acquired a module ID that was not present in its
128+ * previous bundle result.
129+ */
130+ function hasNewlyAddedModules (
131+ bundledEntryPoints : readonly NormalizedEntryPoint [ ] ,
132+ output : MultiBundleOutput ,
133+ getPreviousModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
134+ ) : boolean {
135+ for ( const ep of bundledEntryPoints ) {
136+ const bundle = output . bundles . get ( ep . bundleName ) ;
137+ assert ( bundle , `Expected bundle output for entry point "${ ep . bundleName } ".` ) ;
138+
139+ const prevIds = getPreviousModuleIds ( ep ) ;
140+ if ( ! prevIds ) {
141+ return true ;
142+ }
143+
144+ for ( const id of bundle . moduleIds ) {
145+ if ( ! prevIds . has ( id ) ) {
146+ return true ;
147+ }
148+ }
149+ }
150+
151+ return false ;
152+ }
153+
154+ /**
155+ * Bundles dirty entry points for a single mode (ESM or DTS), automatically expanding the entry
156+ * point set before or after bundling when module IDs overlap across entry points.
157+ */
158+ async function bundleModeWithSharedModules (
159+ dirtyEntryPoints : readonly NormalizedEntryPoint [ ] ,
160+ allEntryPoints : readonly NormalizedEntryPoint [ ] ,
161+ files : ReadonlyMap < string , string > ,
162+ dtsMode : boolean ,
163+ getPreviousModuleIds : ( ep : NormalizedEntryPoint ) => ReadonlySet < string > | undefined ,
164+ bundleFn : (
165+ entryPoints : readonly NormalizedEntryPoint [ ] ,
166+ files : ReadonlyMap < string , string > ,
167+ options : NormalizedLibraryOptions ,
168+ ) => Promise < MultiBundleOutput > ,
169+ options : NormalizedLibraryOptions ,
170+ ) : Promise < MultiBundleOutput > {
171+ if ( dirtyEntryPoints . length === 0 ) {
172+ return { filesToEmit : [ ] , bundles : new Map ( ) , warnings : [ ] } ;
173+ }
174+
175+ if ( dirtyEntryPoints . length === allEntryPoints . length ) {
176+ return bundleFn ( dirtyEntryPoints , files , options ) ;
177+ }
178+
179+ const isAvailable = ( ep : NormalizedEntryPoint ) : boolean =>
180+ files . has ( resolveEntryInputPath ( ep . entryFilePath , dtsMode ) ) ;
181+
182+ const initialEntryPoints = expandOverlappingEntryPoints (
183+ dirtyEntryPoints ,
184+ allEntryPoints ,
185+ isAvailable ,
186+ getPreviousModuleIds ,
187+ ) ;
188+ const output = await bundleFn ( initialEntryPoints , files , options ) ;
189+
190+ if (
191+ initialEntryPoints . length < allEntryPoints . length &&
192+ hasNewlyAddedModules ( initialEntryPoints , output , getPreviousModuleIds )
193+ ) {
194+ const expandedEntryPoints = expandOverlappingEntryPoints (
195+ initialEntryPoints ,
196+ allEntryPoints ,
197+ isAvailable ,
198+ ( ep ) => output . bundles . get ( ep . bundleName ) ?. moduleIds ?? getPreviousModuleIds ( ep ) ,
199+ ) ;
200+
201+ if ( expandedEntryPoints . length > initialEntryPoints . length ) {
202+ return bundleFn ( expandedEntryPoints , files , options ) ;
203+ }
204+ }
205+
206+ return output ;
207+ }
208+
209+ /**
210+ * Collects all normalized entry points from options and bundle input items.
211+ */
212+ function getAllEntryPoints (
213+ items : readonly BundleEntryPointInput [ ] ,
214+ options : NormalizedLibraryOptions ,
215+ ) : NormalizedEntryPoint [ ] {
216+ const byName = new Map < string , NormalizedEntryPoint > ( ) ;
217+ if ( options . entryPoints ) {
218+ for ( const ep of options . entryPoints . values ( ) ) {
219+ byName . set ( ep . name , ep ) ;
220+ }
221+ }
222+ for ( const item of items ) {
223+ byName . set ( item . entryPoint . name , item . entryPoint ) ;
224+ }
225+
226+ return Array . from ( byName . values ( ) ) ;
227+ }
228+
84229/**
85230 * 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).
231+ * using at most 2 Rolldown instances per pass (1 for all .mjs bundles, 1 for all .d.ts bundles).
87232 *
88233 * @param items The collection of entry point inputs with change statuses.
89234 * @param esmFiles Map of virtual POSIX ESM output file paths to their content.
90235 * @param dtsFiles Map of virtual POSIX TypeScript declaration file paths to their content.
91236 * @param options The normalized library builder options with POSIX paths.
237+ * @param previousBundleResults Optional map of entry point names to their previous bundle results.
92238 * @returns A promise resolving to the bundled output files and bundle cache results.
93239 */
94240export async function bundleEntryPoints (
95241 items : readonly BundleEntryPointInput [ ] ,
96242 esmFiles : ReadonlyMap < string , string > ,
97243 dtsFiles : ReadonlyMap < string , string > ,
98244 options : NormalizedLibraryOptions ,
245+ previousBundleResults ?: ReadonlyMap < string , BundleResult > ,
99246) : Promise < BundleEntryPointsOutput > {
100247 const bundleResults = new Map < string , BundleResult > ( ) ;
101248 if ( items . length === 0 ) {
102249 return { filesToEmit : [ ] , bundleResults, warnings : [ ] } ;
103250 }
104251
252+ const allEntryPoints = getAllEntryPoints ( items , options ) ;
253+ const itemsByName = new Map ( items . map ( ( item ) => [ item . entryPoint . name , item ] ) ) ;
254+ const getPreviousResult = ( name : string ) : BundleResult | undefined =>
255+ itemsByName . get ( name ) ?. previousBundleResult ?? previousBundleResults ?. get ( name ) ;
256+
105257 const esmEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
106258 const dtsEntryPoints : NormalizedEntryPoint [ ] = [ ] ;
107259
108260 for ( const item of items ) {
109- if ( item . hasEsmChanges || ! item . previousBundleResult ) {
110- esmEntryPoints . push ( item . entryPoint ) ;
261+ const { entryPoint } = item ;
262+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
263+ if ( item . hasEsmChanges || ! previousBundleResult ) {
264+ esmEntryPoints . push ( entryPoint ) ;
111265 }
112- if ( item . hasDtsChanges || ! item . previousBundleResult ) {
113- dtsEntryPoints . push ( item . entryPoint ) ;
266+ if ( item . hasDtsChanges || ! previousBundleResult ) {
267+ dtsEntryPoints . push ( entryPoint ) ;
114268 }
115269 }
116270
117271 const [ esmOutput , dtsOutput ] = await Promise . all ( [
118- bundleAllEsm ( esmEntryPoints , esmFiles , options ) ,
119- bundleAllDts ( dtsEntryPoints , dtsFiles , options ) ,
272+ bundleModeWithSharedModules (
273+ esmEntryPoints ,
274+ allEntryPoints ,
275+ esmFiles ,
276+ false ,
277+ ( ep ) => getPreviousResult ( ep . name ) ?. esmModuleIds ,
278+ bundleAllEsm ,
279+ options ,
280+ ) ,
281+ bundleModeWithSharedModules (
282+ dtsEntryPoints ,
283+ allEntryPoints ,
284+ dtsFiles ,
285+ true ,
286+ ( ep ) => getPreviousResult ( ep . name ) ?. dtsModuleIds ,
287+ bundleAllDts ,
288+ options ,
289+ ) ,
120290 ] ) ;
121291
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 ) ;
292+ for ( const entryPoint of allEntryPoints ) {
293+ const item = itemsByName . get ( entryPoint . name ) ;
294+ const esmBundle = esmOutput . bundles . get ( entryPoint . bundleName ) ;
295+ const dtsBundle = dtsOutput . bundles . get ( entryPoint . bundleName ) ;
296+ if ( ! item && ! esmBundle && ! dtsBundle ) {
297+ continue ;
298+ }
126299
127- bundleResults . set ( name , {
300+ const previousBundleResult = getPreviousResult ( entryPoint . name ) ;
301+ bundleResults . set ( entryPoint . name , {
128302 esmModuleIds : esmBundle ?. moduleIds ?? previousBundleResult ?. esmModuleIds ?? new Set ( ) ,
129303 dtsModuleIds : dtsBundle ?. moduleIds ?? previousBundleResult ?. dtsModuleIds ?? new Set ( ) ,
130304 esmOutputFiles : esmBundle ?. outputFiles ?? previousBundleResult ?. esmOutputFiles ?? new Set ( ) ,
@@ -140,6 +314,15 @@ export async function bundleEntryPoints(
140314 } ;
141315}
142316
317+ /**
318+ * Resolves the compiled in-memory entry file path for a TypeScript entry point.
319+ */
320+ function resolveEntryInputPath ( entryFilePath : string , dtsMode : boolean ) : string {
321+ return dtsMode
322+ ? entryFilePath . replace ( / \. ( m ? t s ) $ / , '.d.$1' )
323+ : entryFilePath . replace ( / \. ( m ? ) t s $ / , '.$1js' ) ;
324+ }
325+
143326/**
144327 * Generates the Rolldown input mapping object from the specified entry points.
145328 *
@@ -153,9 +336,7 @@ function resolveEntryInputMap(
153336) : Record < string , string > {
154337 const input : Record < string , string > = { } ;
155338 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' ) ;
339+ input [ bundleName ] = resolveEntryInputPath ( entryFilePath , dtsMode ) ;
159340 }
160341
161342 return input ;
@@ -174,6 +355,8 @@ function createMemoryFileLoaderPlugin(
174355 extensions : readonly string [ ] ,
175356 includeMap : boolean ,
176357) : Plugin {
358+ const isDts = extensions === DTS_EXTENSIONS ;
359+
177360 return {
178361 name : 'memory-file-loader' ,
179362 resolveId : {
@@ -187,28 +370,27 @@ function createMemoryFileLoaderPlugin(
187370 return files . has ( id ) ? { id, external : false } : undefined ;
188371 }
189372
190- if ( id [ 0 ] !== '.' && ! path . posix . isAbsolute ( id ) ) {
373+ if ( id [ 0 ] !== '.' && ! path . isAbsolute ( id ) ) {
191374 return { id, external : true } ;
192375 }
193376
194377 const resolved = id [ 0 ] === '.' ? path . posix . join ( path . posix . dirname ( importer ) , id ) : id ;
378+ const normalizedResolved = resolved . replace ( / \/ + $ / , '' ) ;
195379
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- }
380+ if ( files . has ( normalizedResolved ) ) {
381+ return { id : normalizedResolved , external : false } ;
382+ }
383+
384+ const base = normalizedResolved . replace ( / \. m ? j s $ / , '' ) ;
385+ if ( isDts && normalizedResolved . endsWith ( '.mjs' ) && files . has ( `${ base } .d.mts` ) ) {
386+ return { id : `${ base } .d.mts` , external : false } ;
208387 }
209388
210- if ( resolvedCandidate ) {
211- return { id : resolvedCandidate , external : false } ;
389+ for ( const ext of extensions ) {
390+ const candidate = base + ext ;
391+ if ( files . has ( candidate ) ) {
392+ return { id : candidate , external : false } ;
393+ }
212394 }
213395
214396 return undefined ;
0 commit comments