@@ -87,6 +87,12 @@ class AssetEntry {
8787 // entry: { __prefix__index: './index.html', index: './index.js' }
8888 entryNamePrefix = '__bundler-plugin-entry__' ;
8989
90+ /**
91+ * Unique last index for each file with the same name.
92+ * @type {Object<file: string, index: number> }
93+ */
94+ uniqueEntryNameIndex = { } ;
95+
9096 /**
9197 *
9298 * @param {{} } pluginOption
@@ -219,18 +225,6 @@ class AssetEntry {
219225 return files ;
220226 }
221227
222- /**
223- * Whether the entry is unique.
224- *
225- * @param {string } name The name of the entry.
226- * @param {string } file The source file.
227- * @return {boolean }
228- */
229- isUnique ( name , file ) {
230- const entry = this . entriesByName . get ( name ) ;
231- return ! entry || entry . sourceFile === file ;
232- }
233-
234228 /**
235229 * Whether the output filename is a template entrypoint.
236230 *
@@ -513,7 +507,8 @@ class AssetEntry {
513507 isStyle : this . pluginOption . isStyle ( sourceFile ) ,
514508 } ;
515509
516- this . #add( entry , assetEntryOptions ) ;
510+ this . #cacheEntry( assetEntryOptions ) ;
511+ this . #setFilename( entry , assetEntryOptions ) ;
517512 }
518513 }
519514
@@ -539,7 +534,7 @@ class AssetEntry {
539534
540535 this . removedEntries . delete ( file ) ;
541536
542- if ( this . #exists ( name , file ) ) return ;
537+ if ( this . #hasEntry ( name , file ) ) return ;
543538
544539 const entries = { } ;
545540 const entrypoint = { import : [ file ] , filename : '[name].html' } ;
@@ -594,19 +589,23 @@ class AssetEntry {
594589 /**
595590 * Add a script to webpack compilation.
596591 *
597- * @param {string } name
592+ * @param {string|null } name The entry name.
598593 * @param {string } importFile The resource, including a query.
599594 * @param {string } filenameTemplate
600595 * @param {string } context
601596 * @param {string } issuer
602- * @return {boolean } Return true if new file was added, if a file exists then return false .
597+ * @return {string } Return the unique entry name .
603598 */
604599 addToCompilation ( { name, importFile, filenameTemplate, context, issuer } ) {
600+ name = this . #getUniqueEntryName( name , importFile ) ;
601+
605602 // skip duplicate entries
606- if ( this . #exists ( name , importFile ) ) {
607- return false ;
603+ if ( this . #hasEntry ( name , importFile ) ) {
604+ return name ;
608605 }
609606
607+ const compiler = this . compiler ;
608+ const compilation = this . compilation ;
610609 let [ sourceFile ] = importFile . split ( '?' , 1 ) ;
611610
612611 const entryOptions = {
@@ -636,33 +635,69 @@ class AssetEntry {
636635 isTemplate : false ,
637636 } ;
638637
639- this . #add( entryOptions , assetEntryOptions ) ;
640- this . compilationEntryNames . add ( name ) ;
641-
642- const compiler = this . compiler ;
643- const compilation = this . compilation ;
644638 const { EntryPlugin } = compiler . webpack ;
645639 const entryDependency = EntryPlugin . createDependency ( importFile , { name } ) ;
646640
647- compilation . addEntry ( context , entryDependency , entryOptions , ( err , module ) => {
648- if ( err ) throw new Error ( err ) ;
641+ this . #cacheEntry( assetEntryOptions ) ;
642+ this . compilationEntryNames . add ( name ) ;
643+
644+ compilation . addEntry ( context , entryDependency , entryOptions , ( error , module ) => {
645+ if ( error ) {
646+ throw new Error ( error . toString ( ) ) ;
647+ }
648+
649+ // fix #143: if the same entry already exists, it caused Webpack error:
650+ // "Conflicting entry option filename () => {...} vs () => {...}".
651+ // This is a Webpack issue, occurs unpredictably, after a random number of runs
652+ // when the sequence of processing modules is changed sporadically.
653+ if ( module ) {
654+ // assign a custom filename function only after the module has been successfully created
655+ this . #setFilename( entryOptions , assetEntryOptions ) ;
656+ }
649657 } ) ;
650658
651659 // add missing dependencies after rebuild
652660 if ( PluginService . isWatchMode ( compiler ) ) {
653661 new EntryPlugin ( context , importFile , { name } ) . apply ( compiler ) ;
654662 }
655663
656- return true ;
664+ return name ;
657665 }
658666
659667 /**
668+ * @param {string|null } name The entry name.
669+ * @param {string } file The source file.
670+ * @return {string } Return unique entry name.
671+ */
672+ #getUniqueEntryName( name , file ) {
673+ if ( ! name ) {
674+ name = path . parse ( file ) . name ;
675+ }
676+
677+ let uniqueName = name ;
678+ let hasEntry = this . compilation . entries . has ( name ) ;
679+
680+ // the entrypoint name must be unique, if already exists then add an index: `main` => `main.1`, etc.
681+ if ( hasEntry ) {
682+ // create unique name
683+ if ( ! this . uniqueEntryNameIndex [ name ] ) {
684+ this . uniqueEntryNameIndex [ name ] = 1 ;
685+ }
686+ uniqueName += '.' + this . uniqueEntryNameIndex [ name ] ++ ;
687+ }
688+
689+ return uniqueName ;
690+ }
691+
692+ /**
693+ * Sets the filename as a function for a given entry.
694+ * This function processes the filename template and applies adjustments.
695+ *
660696 * @param {EntryOptions } entry The Webpack entry option object.
661697 * @param {AssetEntryOptions } assetEntryOptions
662- * @private
663698 */
664- #add ( entry , assetEntryOptions ) {
665- const { name , originalName, filenameTemplate, outputPath } = assetEntryOptions ;
699+ #setFilename ( entry , assetEntryOptions ) {
700+ const { originalName, filenameTemplate, outputPath } = assetEntryOptions ;
666701
667702 if ( path . isAbsolute ( outputPath ) ) {
668703 assetEntryOptions . publicPath = path . relative ( this . pluginOption . getWebpackOutputPath ( ) , outputPath ) ;
@@ -675,6 +710,7 @@ class AssetEntry {
675710 let filename = filenameTemplate ;
676711
677712 if ( isFunction ( filenameTemplate ) ) {
713+ // type PathData: https://webpack.js.org/configuration/output/#template-strings
678714 // clone the pathData object to modify the chunk object w/o side effects in the main compilation
679715 const pathDataCloned = { ...pathData } ;
680716 pathDataCloned . chunk = { ...pathDataCloned . chunk } ;
@@ -687,6 +723,9 @@ class AssetEntry {
687723 if ( pathDataCloned . filename == null ) {
688724 pathDataCloned . filename = assetEntryOptions . sourceFile ;
689725 }
726+ // note: resource is full path including URL queries
727+ // TODO: undocumented non-standard additional property, should be perhaps into filename?
728+ pathDataCloned . resource = assetEntryOptions . resource ;
690729
691730 filename = filenameTemplate ( pathDataCloned , assetInfo ) ;
692731 }
@@ -698,11 +737,19 @@ class AssetEntry {
698737 return filename ;
699738 } ;
700739
701- entry . filename = filenameFn ;
702- assetEntryOptions . filenameFn = filenameFn ;
740+ entry . filename = assetEntryOptions . filenameFn = filenameFn ;
741+ }
742+
743+ /**
744+ * Save already added entry to cache.
745+ *
746+ * @param {AssetEntryOptions } assetEntryOptions
747+ */
748+ #cacheEntry( assetEntryOptions ) {
749+ const { name, id } = assetEntryOptions ;
703750
704751 this . entriesByName . set ( name , assetEntryOptions ) ;
705- this . entriesById . set ( assetEntryOptions . id , assetEntryOptions ) ;
752+ this . entriesById . set ( id , assetEntryOptions ) ;
706753 }
707754
708755 /**
@@ -713,8 +760,9 @@ class AssetEntry {
713760 * @return {boolean }
714761 * @private
715762 */
716- #exists ( name , importFile ) {
763+ #hasEntry ( name , importFile ) {
717764 const entry = this . entriesByName . get ( name ) ;
765+
718766 return entry != null && entry . importFile === importFile ;
719767 }
720768
@@ -723,6 +771,7 @@ class AssetEntry {
723771 * Called only once when the plugin is applied.
724772 */
725773 clear ( ) {
774+ this . uniqueEntryNameIndex = { } ;
726775 this . idIndex = 1 ;
727776 this . data . clear ( ) ;
728777 this . entriesByName . clear ( ) ;
@@ -734,6 +783,16 @@ class AssetEntry {
734783 * Called before each new compilation after changes, in the serve/watch mode.
735784 */
736785 reset ( ) {
786+ // don't clear the uniqueEntryNameIndex
787+ // test case:
788+ // there are 3 entries: home.html, news.html and about.html
789+ // 1. add the `script.js` to the home.html => script.js
790+ // 2. add the `script.js` to the news.html => script.1.js
791+ // 3. add the `script.js` to the about.html => script.2.js
792+ // but when the index is cleared, then after adding the file with the same name will be not unique
793+ // and is generated a js file having a wrong content
794+ //this.uniqueEntryNameIndex = {};
795+
737796 for ( const entryName of this . compilationEntryNames ) {
738797 const entry = this . entriesByName . get ( entryName ) ;
739798 if ( entry ) {
0 commit comments