@@ -129,6 +129,38 @@ const OPTIONS_FORM_KEYS = new Set(["external", "sideEffects"]);
129129
130130const PLUGIN_NAME = "ExternalModuleFactoryPlugin" ;
131131
132+ /**
133+ * An externals array with each run of strings merged into one set.
134+ * @typedef {object } GroupedExternals
135+ * @property {Externals[] } source the entries the grouping was built from
136+ * @property {(Externals | Set<string>)[] } entries the entries, each run of strings as one set
137+ * @property {number[] } ends for each entry, the index in the array just past it
138+ */
139+
140+ /**
141+ * Whether matching an externals entry can call user code, which may change the array.
142+ * Only a set of strings is matched without it: a RegExp may override `test`, an object may hold a `byLayer` function.
143+ * @param {Externals | Set<string> } entry a grouped entry
144+ * @returns {boolean } true unless the entry is a set of strings
145+ */
146+ const mayRunUserCode = ( entry ) => ! ( entry instanceof Set ) ;
147+
148+ /**
149+ * Whether an externals array still holds the entries it was grouped from.
150+ * An externals function may change the array while requests are matched.
151+ * @param {Externals[] } externals externals array
152+ * @param {GroupedExternals } grouped its grouping
153+ * @returns {boolean } true when the grouping is current
154+ */
155+ const isCurrentGrouping = ( externals , grouped ) => {
156+ const { source } = grouped ;
157+ if ( externals . length !== source . length ) return false ;
158+ for ( let i = 0 ; i < source . length ; i ++ ) {
159+ if ( externals [ i ] !== source [ i ] ) return false ;
160+ }
161+ return true ;
162+ } ;
163+
132164class ExternalModuleFactoryPlugin {
133165 /**
134166 * Creates an instance of ExternalModuleFactoryPlugin.
@@ -148,6 +180,47 @@ class ExternalModuleFactoryPlugin {
148180 */
149181 apply ( normalModuleFactory ) {
150182 const globalType = this . type ;
183+ /** @type {WeakMap<Externals[], GroupedExternals> } */
184+ const groupedArrays = new WeakMap ( ) ;
185+
186+ /**
187+ * Merges each run of request strings in an externals array into one set.
188+ * A string entry always externalizes the request as written, so which string of a run matched never matters.
189+ * @param {Externals[] } externals externals array
190+ * @returns {GroupedExternals } the entries with each run of strings as a set, regrouped when the array changed
191+ */
192+ const getGroupedArray = ( externals ) => {
193+ const cached = groupedArrays . get ( externals ) ;
194+ if ( cached !== undefined && isCurrentGrouping ( externals , cached ) ) {
195+ return cached ;
196+ }
197+ /** @type {GroupedExternals["entries"] } */
198+ const entries = [ ] ;
199+ /** @type {number[] } */
200+ const ends = [ ] ;
201+ /** @type {Set<string> | undefined } */
202+ let strings ;
203+ for ( let i = 0 ; i < externals . length ; i ++ ) {
204+ const item = externals [ i ] ;
205+ if ( typeof item === "string" ) {
206+ if ( strings === undefined ) {
207+ strings = new Set ( ) ;
208+ entries . push ( strings ) ;
209+ ends . push ( i + 1 ) ;
210+ }
211+ strings . add ( item ) ;
212+ ends [ ends . length - 1 ] = i + 1 ;
213+ } else {
214+ strings = undefined ;
215+ entries . push ( item ) ;
216+ ends . push ( i + 1 ) ;
217+ }
218+ }
219+ /** @type {GroupedExternals } */
220+ const grouped = { source : [ ...externals ] , entries, ends } ;
221+ groupedArrays . set ( externals , grouped ) ;
222+ return grouped ;
223+ } ;
151224 normalModuleFactory . hooks . factorize . tapAsync (
152225 PLUGIN_NAME ,
153226 ( data , callback ) => {
@@ -162,6 +235,7 @@ class ExternalModuleFactoryPlugin {
162235 ? dependency . originalRequest
163236 : request ;
164237 const hasOriginalRequest = originalRequest !== request ;
238+ const alternateRequest = getAlternateCoreModuleRequest ( request ) ;
165239
166240 /** @typedef {(err?: Error | null, externalModule?: ExternalModule) => void } HandleExternalCallback */
167241
@@ -322,12 +396,16 @@ class ExternalModuleFactoryPlugin {
322396 if (
323397 externals === request ||
324398 ( hasOriginalRequest && externals === originalRequest ) ||
325- externals === getAlternateCoreModuleRequest ( request )
399+ externals === alternateRequest
326400 ) {
327401 return handleExternal ( originalRequest , undefined , callback ) ;
328402 }
329403 } else if ( Array . isArray ( externals ) ) {
330- let i = 0 ;
404+ const grouped = getGroupedArray ( externals ) ;
405+ const { entries, ends } = grouped ;
406+ let groupIndex = 0 ;
407+ // The index into `externals` once a non-string entry changed the array.
408+ let liveIndex = - 1 ;
331409 const next = ( ) => {
332410 /** @type {boolean | undefined } */
333411 let asyncFlag ;
@@ -351,8 +429,37 @@ class ExternalModuleFactoryPlugin {
351429
352430 do {
353431 asyncFlag = true ;
354- if ( i >= externals . length ) return callback ( ) ;
355- handleExternals ( externals [ i ++ ] , handleExternalsAndCallback ) ;
432+ if (
433+ liveIndex === - 1 &&
434+ groupIndex > 0 &&
435+ mayRunUserCode ( entries [ groupIndex - 1 ] ) &&
436+ ! isCurrentGrouping ( externals , grouped )
437+ ) {
438+ liveIndex = ends [ groupIndex - 1 ] ;
439+ }
440+ if ( liveIndex !== - 1 ) {
441+ if ( liveIndex >= externals . length ) return callback ( ) ;
442+ handleExternals (
443+ externals [ liveIndex ++ ] ,
444+ handleExternalsAndCallback
445+ ) ;
446+ continue ;
447+ }
448+ if ( groupIndex >= entries . length ) return callback ( ) ;
449+ const item = entries [ groupIndex ++ ] ;
450+ if ( item instanceof Set ) {
451+ if (
452+ item . has ( request ) ||
453+ ( hasOriginalRequest && item . has ( originalRequest ) ) ||
454+ ( alternateRequest !== undefined &&
455+ item . has ( alternateRequest ) )
456+ ) {
457+ return handleExternal ( originalRequest , undefined , callback ) ;
458+ }
459+ asyncFlag = false ;
460+ } else {
461+ handleExternals ( item , handleExternalsAndCallback ) ;
462+ }
356463 } while ( ! asyncFlag ) ;
357464 asyncFlag = false ;
358465 } ;
@@ -470,7 +577,6 @@ class ExternalModuleFactoryPlugin {
470577 ) ;
471578 }
472579 // Fall back to the `node:`-prefixed/unprefixed core module form.
473- const alternateRequest = getAlternateCoreModuleRequest ( request ) ;
474580 if (
475581 alternateRequest !== undefined &&
476582 Object . prototype . hasOwnProperty . call (
0 commit comments