@@ -484,6 +484,99 @@ class Collection {
484484 return this . assets . get ( resource ) ?. type === Collection . type . script ;
485485 }
486486
487+ /**
488+ * Recover a script entry from Webpack's cached chunk graph.
489+ *
490+ * This is a fallback for the case where Webpack restores a cached template
491+ * module, but the plugin's persistent collection cache is missing the script
492+ * resource that the restored module still requires.
493+ *
494+ * @param {{resource: string, issuer: FileInfo, entry: AssetEntryOptions} } options
495+ * @return {boolean }
496+ */
497+ recoverMissingScript ( { resource, issuer, entry } ) {
498+ const issuerResource = issuer ?. resource ;
499+ const entryFilename = entry ?. filename ;
500+
501+ if ( ! issuerResource || ! entryFilename ) return false ;
502+
503+ let item = this . assets . get ( resource ) ;
504+ if ( item && item . type !== Collection . type . script ) return false ;
505+
506+ const name = item ?. name || this . #findScriptEntryName( resource ) ;
507+ if ( ! name ) return false ;
508+
509+ if ( ! item ) {
510+ item = {
511+ type : Collection . type . script ,
512+ inline : undefined ,
513+ name,
514+ entries : new Map ( ) ,
515+ assets : [ ] ,
516+ } ;
517+ this . assets . set ( resource , item ) ;
518+ } else if ( ! item . name ) {
519+ item . name = name ;
520+ }
521+
522+ let entryFilenames = item . entries . get ( issuerResource ) ;
523+ if ( ! entryFilenames ) {
524+ entryFilenames = new Set ( ) ;
525+ item . entries . set ( issuerResource , entryFilenames ) ;
526+ }
527+
528+ entryFilenames . add ( entryFilename ) ;
529+
530+ if ( entry . id != null ) {
531+ let orderedResources = this . orderedResources . get ( entry . id ) ;
532+ if ( ! orderedResources ) {
533+ orderedResources = new Set ( ) ;
534+ this . orderedResources . set ( entry . id , orderedResources ) ;
535+ }
536+ orderedResources . add ( resource ) ;
537+ }
538+
539+ return true ;
540+ }
541+
542+ /**
543+ * Find an entrypoint containing the script resource.
544+ *
545+ * @param {string } resource The script resource.
546+ * @return {string|null }
547+ */
548+ #findScriptEntryName( resource ) {
549+ const compilation = this . compilation ;
550+ const chunkGraph = compilation ?. chunkGraph ;
551+ const namedChunkGroups =
552+ compilation ?. entrypoints ?. size > 0 ? compilation . entrypoints : compilation ?. namedChunkGroups ;
553+ const [ sourceFile ] = resource . split ( '?' , 1 ) ;
554+
555+ if ( ! chunkGraph || ! namedChunkGroups ) return null ;
556+
557+ for ( const [ name , entrypoint ] of namedChunkGroups ) {
558+ for ( const chunk of entrypoint . chunks ) {
559+ const modules = chunkGraph . getChunkModulesIterable ( chunk ) ;
560+
561+ if ( ! modules ) continue ;
562+
563+ for ( const module of modules ) {
564+ const moduleResource = module . resource ;
565+ if ( ! moduleResource ) continue ;
566+
567+ const [ moduleFile ] = moduleResource . split ( '?' , 1 ) ;
568+ if ( moduleResource === resource || moduleFile === sourceFile ) {
569+ const entry = this . assetEntry ?. entriesByName . get ( name ) ;
570+
571+ if ( ! entry ?. isTemplate ) return name ;
572+ }
573+ }
574+ }
575+ }
576+
577+ return null ;
578+ }
579+
487580 /**
488581 * Whether the collection contains the style file.
489582 *
@@ -1308,6 +1401,7 @@ class Collection {
13081401 this . importStyleRootIssuers . clear ( ) ;
13091402 this . importStyleSources . clear ( ) ;
13101403 this . importStyleIdx = 1000 ;
1404+ this . deserialized = false ;
13111405 }
13121406
13131407 /**
@@ -1341,6 +1435,7 @@ class Collection {
13411435 // the original functions will be recovered by deserialization from the cached object `AssetEntry`
13421436 entry . filenameFn = null ;
13431437 entry . filenameTemplate = null ;
1438+ entry . options = null ;
13441439 }
13451440
13461441 write ( this . assets ) ;
@@ -1352,20 +1447,64 @@ class Collection {
13521447 * @param {Function } read The deserialize function.
13531448 */
13541449 deserialize ( { read } ) {
1355- this . assets = read ( ) ;
1356- this . data = read ( ) ;
1450+ const assets = read ( ) ;
1451+ const data = read ( ) ;
1452+
1453+ if ( ! this . #isDeserializedDataValid( assets , data ) ) {
1454+ this . clear ( ) ;
1455+ return ;
1456+ }
1457+
1458+ this . assets = assets ;
1459+ this . data = data ;
1460+
1461+ const assetEntry = this . assetEntry || this . pluginContext . assetEntry ;
1462+
1463+ if ( ! assetEntry ) {
1464+ this . clear ( ) ;
1465+ return ;
1466+ }
13571467
13581468 for ( let [ , { entry } ] of this . data ) {
1359- const cachedEntry = this . assetEntry . entriesById . get ( entry . id ) ;
1469+ if ( ! entry . id ) continue ;
1470+
1471+ const cachedEntry = assetEntry . entriesById . get ( entry . id ) ;
1472+
1473+ if ( ! cachedEntry ) {
1474+ this . clear ( ) ;
1475+ return ;
1476+ }
13601477
13611478 // recovery original not serializable functions from the object cached in the memory
13621479 entry . filenameFn = cachedEntry . filenameFn ;
13631480 entry . filenameTemplate = cachedEntry . filenameTemplate ;
1481+ entry . options = cachedEntry . options ;
13641482 }
13651483
13661484 this . deserialized = true ;
13671485 }
13681486
1487+ /**
1488+ * @param {Map } assets
1489+ * @param {Map } data
1490+ * @return {boolean }
1491+ */
1492+ #isDeserializedDataValid( assets , data ) {
1493+ if ( ! ( assets instanceof Map ) || ! ( data instanceof Map ) ) return false ;
1494+
1495+ for ( const [ , item ] of assets ) {
1496+ if ( item == null || typeof item !== 'object' ) return false ;
1497+ if ( ! item . type || ! ( item . entries instanceof Map ) ) return false ;
1498+ }
1499+
1500+ for ( const [ , item ] of data ) {
1501+ if ( item == null || typeof item !== 'object' ) return false ;
1502+ if ( item . entry == null || ! Array . isArray ( item . assets ) ) return false ;
1503+ }
1504+
1505+ return true ;
1506+ }
1507+
13691508 isDeserialized ( ) {
13701509 return this . deserialized ;
13711510 }
0 commit comments