Skip to content

Commit 0a6b81b

Browse files
committed
fix: unpredictably Webpack compilation fails after a random number of runs, #143
1 parent 4a165dc commit 0a6b81b

17 files changed

Lines changed: 174 additions & 158 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 4.15.2 (2025-01-22)
4+
5+
- fix: unpredictably Webpack compilation fails after a random number of runs, #143
6+
37
## 4.15.1 (2025-01-21)
48

59
- fix: improve the handling exceptions in the integrity module, #143

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "4.15.1",
3+
"version": "4.15.2",
44
"description": "Generates complete single-page or multi-page website from source assets. Build-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.",
55
"keywords": [
66
"html",

src/Plugin/AssetEntry.js

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {

src/Plugin/Collection.js

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ class Collection {
6666
/** @type {Map<string, string | Array<string>>} The map of source file to output file */
6767
manifest = new Map();
6868

69-
/**
70-
* Unique last index for each file with the same name.
71-
* @type {Object<file: string, index: number>}
72-
*/
73-
index = {};
74-
7569
importStyleEsModule = true;
7670
orderedResources = new Map();
7771
importStyleRootIssuers = new Set();
@@ -113,20 +107,21 @@ class Collection {
113107
}
114108

115109
/**
116-
* @param {string} name
110+
* @param {string?} name The entry name.
117111
* @param {string} resource
118112
* @param {string} issuer
113+
* @return {string} Return the unique entry name.
119114
*/
120115
#addToCompilation({ name, resource, issuer }) {
121116
const entry = {
122-
name,
117+
name: name,
123118
importFile: resource,
124119
filenameTemplate: this.pluginOption.getJs().filename,
125120
context: path.dirname(issuer),
126121
issuer,
127122
};
128123

129-
this.assetEntry.addToCompilation(entry);
124+
return this.assetEntry.addToCompilation(entry);
130125
}
131126

132127
/**
@@ -512,26 +507,6 @@ class Collection {
512507
return null;
513508
}
514509

515-
/**
516-
* @param {string} file The source file of script.
517-
* @return {string } Return unique assetFile
518-
*/
519-
createUniqueName(file) {
520-
const { name } = path.parse(file);
521-
let uniqueName = name;
522-
523-
// the entrypoint name must be unique, if already exists then add an index: `main` => `main.1`, etc.
524-
if (!this.assetEntry.isUnique(name, file)) {
525-
// create unique name
526-
if (!this.index[name]) {
527-
this.index[name] = 1;
528-
}
529-
uniqueName = name + '.' + this.index[name]++;
530-
}
531-
532-
return uniqueName;
533-
}
534-
535510
/**
536511
* Find styles from all nested JS files.
537512
*
@@ -722,8 +697,7 @@ class Collection {
722697
name = this.assets.get(resource)?.name;
723698

724699
if (!name) {
725-
name = this.createUniqueName(resource);
726-
this.#addToCompilation({ name, resource, issuer });
700+
name = this.#addToCompilation({ resource, issuer });
727701
}
728702

729703
inline = undefined;
@@ -1172,7 +1146,6 @@ class Collection {
11721146
* Called only once when the plugin is applied.
11731147
*/
11741148
clear() {
1175-
this.index = {};
11761149
this.data.clear();
11771150
this.assets.clear();
11781151
this.orderedResources.clear();
@@ -1186,16 +1159,6 @@ class Collection {
11861159
* Called before each new compilation after changes, in the serve/watch mode.
11871160
*/
11881161
reset() {
1189-
// don't clear the index
1190-
// test case:
1191-
// there are 3 entries: home.html, news.html and about.html
1192-
// 1. add the `script.js` to the home.html => script.js
1193-
// 2. add the `script.js` to the news.html => script.1.js
1194-
// 3. add the `script.js` to the about.html => script.2.js
1195-
// but when the index is cleared, then after adding the file with the same name will be not unique
1196-
// and is generated a js file having a wrong content
1197-
//this.index = {};
1198-
11991162
// don't delete entry data, clear only assets
12001163
this.data.forEach((item, key) => {
12011164
if (item.assets != null) item.assets = [];

test/cases/integrity-issue-143/expected/css/style.7947baa4.css

Lines changed: 0 additions & 3 deletions
This file was deleted.

test/cases/integrity-issue-143/expected/index.html

Lines changed: 0 additions & 11 deletions
This file was deleted.

test/cases/integrity-issue-143/expected/main.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/cases/integrity-issue-143/src/chunk.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

test/cases/integrity-issue-143/src/index.html

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)