Skip to content

Commit 1cae551

Browse files
committed
feat: add support per-entry js, css and html minify overrides, #188
1 parent 3323ba7 commit 1cae551

File tree

46 files changed

+715
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+715
-34
lines changed

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.23.0 (2026-03-09)
4+
5+
- feat: add per-entry override support for `js.inline`, `css.inline`, and HTML `minify` options
6+
37
## 4.22.0 (2025-11-27)
48

59
- fix: preprocessor options for the default preprocessor

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.22.0",
3+
"version": "4.23.0",
44
"description": "Generates complete single-page or multi-page website from source assets. Built-in support for Markdown, Eta, EJS, Handlebars, Nunjucks, Pug. Alternative to html-webpack-plugin.",
55
"keywords": [
66
"html",

src/Plugin/AssetCompiler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ class AssetCompiler {
13151315
return;
13161316
}
13171317

1318-
const inline = this.collection.isInlineStyle(resource);
1318+
const inline = this.pluginOption.isInlineCss(resource, this.currentEntryPoint);
13191319
const { name } = path.parse(sourceFile);
13201320
const hash = buildInfo.assetInfo?.contenthash || buildInfo.hash;
13211321
const { isCached, filename } = this.getStyleAsseFile({
@@ -1406,7 +1406,7 @@ class AssetCompiler {
14061406

14071407
const urlQuery = module.resourceResolveData?.query || '';
14081408
const isUrl = urlQuery.includes('url');
1409-
const isInline = this.pluginOption.isInlineCss(urlQuery);
1409+
const isInline = this.pluginOption.isInlineCss(urlQuery, entry);
14101410
const importData = {
14111411
resource: module.resource,
14121412
assets: [],

src/Plugin/AssetEntry.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const loader = require.resolve('../Loader');
3535
* @property {string} sourcePath The path where are source files.
3636
* @property {string} outputPath The absolute output path.
3737
* @property {string} publicPath The public path relative to outputPath.
38+
* @property {{js: JsOptions, css: CssOptions, minify: boolean, minifyOptions: Object}} options The normalized runtime options.
3839
* @property {{name: string, type: string}} library Define the output a js file.
3940
* See https://webpack.js.org/configuration/output/#outputlibrary
4041
* @property {boolean|string} [verbose = false] Show an information by handles of the entry in a postprocess.
@@ -455,6 +456,7 @@ class AssetEntry {
455456
if (options == null) continue;
456457

457458
let { verbose, filename: filenameTemplate, sourcePath, outputPath } = options;
459+
const runtimeOptions = this.pluginOption.createEntryRuntimeOptions(entry);
458460

459461
// Note:
460462
// when the entry contains the same source file for many chunks,
@@ -501,6 +503,7 @@ class AssetEntry {
501503
sourcePath,
502504
outputPath,
503505
publicPath: '',
506+
options: runtimeOptions,
504507
library: entry.library,
505508
verbose,
506509
isTemplate: this.pluginOption.isEntry(sourceFile),

src/Plugin/Collection.js

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,10 @@ class Collection {
140140
* @param {AssetEntryOptions} entry
141141
* @param {Array<Object>} styles
142142
* @param {string} LF The new line feed in depends on the minification option.
143+
* @param {Set<string>} externalStyleAssets Styles that must stay as emitted files for other entries.
143144
* @return {string|undefined}
144145
*/
145-
#bindImportedStyles(content, entry, styles, LF) {
146+
async #bindImportedStyles(content, entry, styles, LF, externalStyleAssets) {
146147
const insertPos = this.findStyleInsertPos(content);
147148
if (insertPos < 0) {
148149
noHeadException(entry.resource);
@@ -153,7 +154,7 @@ class Collection {
153154

154155
for (const asset of styles) {
155156
if (asset.inline) {
156-
const source = this.cssExtractModule.getInlineSource(asset.assetFile);
157+
const source = this.cssExtractModule.getInlineSource(asset.assetFile, externalStyleAssets.has(asset.assetFile));
157158

158159
// note: in inlined style must be no LF character after the open tag, otherwise the mapping will not work
159160
styleTags += `<style>` + source + `</style>${LF}`;
@@ -174,19 +175,22 @@ class Collection {
174175
* @param {string} search The original request of a style in the content.
175176
* @param {Object} asset The object of the style.
176177
* @param {string} LF The new line feed in depends on the minification option.
178+
* @param {Set<string>} externalStyleAssets Styles that must stay as emitted files for other entries.
179+
* @param {AssetEntryOptions} entry
177180
* @return {string|boolean} Return content with inlined CSS or false if the content was not modified.
178181
*/
179-
#inlineStyle(content, search, asset, LF) {
182+
async #inlineStyle(content, search, asset, LF, externalStyleAssets, entry) {
180183
const pos = content.indexOf(search);
184+
const searchPos = pos < 0 ? content.indexOf(asset.assetFile) : pos;
181185

182-
if (pos < 0) return false;
186+
if (searchPos < 0) return false;
183187

184-
const source = this.cssExtractModule.getInlineSource(asset.assetFile);
188+
const source = this.cssExtractModule.getInlineSource(asset.assetFile, externalStyleAssets.has(asset.assetFile));
185189
const tagEnd = '>';
186190
const openTag = '<style>';
187191
let closeTag = '</style>';
188-
let tagStartPos = pos;
189-
let tagEndPos = pos + search.length;
192+
let tagStartPos = searchPos;
193+
let tagEndPos = searchPos + (pos < 0 ? asset.assetFile.length : search.length);
190194

191195
while (tagStartPos >= 0 && content.charAt(--tagStartPos) !== '<') {}
192196
tagEndPos = content.indexOf(tagEnd, tagEndPos) + tagEnd.length;
@@ -207,13 +211,18 @@ class Collection {
207211
* @param {string} resource The resource file containing in the content.
208212
* @param {Object} asset The object of the script.
209213
* @param {string} LF The new line feed in depends on the minification option.
214+
* @param {AssetEntryOptions} entry
215+
* @param {Set<string>} externalChunkFiles Chunks that must stay as emitted files for other entries.
210216
* @return {string|boolean} Return content with inlined JS or false if the content was not modified.
211217
*/
212-
#bindScript(content, resource, asset, LF) {
218+
#bindScript(content, resource, asset, LF, entry, externalChunkFiles) {
213219
let pos = content.indexOf(resource);
220+
if (pos < 0 && asset.chunks?.length > 0) {
221+
pos = content.indexOf(asset.chunks[0].assetFile);
222+
}
214223
if (pos < 0) return false;
215224

216-
const { attributeFilter } = this.pluginOption.getJs().inline;
225+
const { attributeFilter } = this.pluginOption.getJs(entry).inline;
217226

218227
const sources = this.compilation.assets;
219228
const { chunks } = asset;
@@ -267,7 +276,9 @@ class Collection {
267276
}
268277

269278
replacement += openTag + code + closeTag;
270-
this.assetTrash.add(chunkFile);
279+
if (!externalChunkFiles.has(chunkFile)) {
280+
this.assetTrash.add(chunkFile);
281+
}
271282
} else {
272283
replacement += beforeTagSrc + assetFile + afterTagSrc;
273284
}
@@ -279,6 +290,38 @@ class Collection {
279290
return content.slice(0, tagStartPos) + replacement + content.slice(tagEndPos);
280291
}
281292

293+
/**
294+
* Collect emitted JS chunks and CSS assets that must remain on disk because
295+
* at least one entry still references them as external files.
296+
*
297+
* This prevents removing an asset while inlining it into one page when the
298+
* same emitted file is still needed by another page.
299+
*
300+
* @return {{externalChunkFiles: Set<string>, externalStyleAssets: Set<string>}}
301+
*/
302+
#collectExternalAssetReferences() {
303+
const externalChunkFiles = new Set();
304+
const externalStyleAssets = new Set();
305+
306+
for (const [, { assets }] of this.data) {
307+
for (const asset of assets) {
308+
if (asset.type === Collection.type.script) {
309+
for (const chunk of [...(asset.chunks || []), ...(asset.children || [])]) {
310+
if (!chunk.inline) {
311+
externalChunkFiles.add(chunk.chunkFile);
312+
}
313+
}
314+
}
315+
316+
if (asset.type === Collection.type.style && asset.inline === false) {
317+
externalStyleAssets.add(asset.assetFile);
318+
}
319+
}
320+
}
321+
322+
return { externalChunkFiles, externalStyleAssets };
323+
}
324+
282325
/**
283326
* Prepare data for script rendering.
284327
*/
@@ -343,7 +386,8 @@ class Collection {
343386
injectedChunks.add(chunkFile);
344387
}
345388

346-
const inline = this.pluginOption.isInlineJs(resource, chunkFile);
389+
const entry = this.data.get(entryFile)?.entry;
390+
const inline = this.pluginOption.isInlineJs(resource, chunkFile, entry);
347391
const assetFile = this.pluginOption.getOutputFilename(chunkFile, entryFile);
348392

349393
splitChunkFiles.add(chunkFile);
@@ -357,7 +401,8 @@ class Collection {
357401
injectedChunks.add(chunkFile);
358402
}
359403

360-
const inline = this.pluginOption.isInlineJs(resource, chunkFile);
404+
const entry = this.data.get(entryFile)?.entry;
405+
const inline = this.pluginOption.isInlineJs(resource, chunkFile, entry);
361406
const assetFile = this.pluginOption.getOutputFilename(chunkFile, entryFile);
362407

363408
splitChunkFiles.add(chunkFile);
@@ -914,9 +959,6 @@ class Collection {
914959
const compilation = this.compilation;
915960
const { RawSource } = compilation.compiler.webpack.sources;
916961
const hasIntegrity = this.pluginOption.isIntegrityEnabled();
917-
const isHtmlMinify = this.pluginOption.isMinify();
918-
const { minifyOptions } = this.pluginOption.get();
919-
const LF = this.pluginOption.getLF();
920962
const hooks = this.hooks;
921963
const promises = [];
922964

@@ -930,6 +972,7 @@ class Collection {
930972

931973
this.#normalizeData();
932974
this.#prepareScriptData();
975+
const { externalChunkFiles, externalStyleAssets } = this.#collectExternalAssetReferences();
933976

934977
// TODO: update this.data.assets[].asset.resource after change the filename in a template
935978
// - e.g. src="./main.js?v=1" => ./main.js?v=123 => WRONG filename is replaced
@@ -959,6 +1002,9 @@ class Collection {
9591002
const resourcePath = entry.resource;
9601003
const entryDirname = path.dirname(entryFilename);
9611004
const importedStyles = [];
1005+
const isHtmlMinify = this.pluginOption.isEntryMinify(entry);
1006+
const minifyOptions = this.pluginOption.getMinifyOptions(entry);
1007+
const LF = this.pluginOption.getLF(entry);
9621008
const parseOptions = new Map();
9631009
let hasInlineSvg = false;
9641010
let content = rawSource.source();
@@ -1046,7 +1092,7 @@ class Collection {
10461092
}
10471093

10481094
// 4. inline JS and CSS
1049-
promise = promise.then((content) => {
1095+
promise = promise.then(async (content) => {
10501096
// TODO:
10511097
// - style: rename output filename `assetFile` into filename or assetFilename
10521098
// - style: add additional filed - assetFile as asset path relative to output.path, not to issuer
@@ -1064,7 +1110,7 @@ class Collection {
10641110
if (imported) {
10651111
importedStyles.push(asset);
10661112
} else if (inline) {
1067-
content = this.#inlineStyle(content, resource, asset, LF) || content;
1113+
content = (await this.#inlineStyle(content, resource, asset, LF, externalStyleAssets, entry)) || content;
10681114
} else {
10691115
// special use case for Pug only e.g.: style(scope='some')=require('./component.css?include')
10701116
const [, query] = resource.split('?');
@@ -1134,7 +1180,7 @@ class Collection {
11341180
}
11351181
}
11361182

1137-
content = this.#bindScript(content, resource, asset, LF) || content;
1183+
content = this.#bindScript(content, resource, asset, LF, entry, externalChunkFiles) || content;
11381184
break;
11391185
}
11401186
}
@@ -1144,7 +1190,9 @@ class Collection {
11441190

11451191
// 5. inject styles imported in JS
11461192
promise = promise.then((content) =>
1147-
importedStyles.length > 0 ? this.#bindImportedStyles(content, entry, importedStyles, LF) || content : content
1193+
importedStyles.length > 0
1194+
? this.#bindImportedStyles(content, entry, importedStyles, LF, externalStyleAssets) || content
1195+
: content
11481196
);
11491197

11501198
// 6. inline SVG

src/Plugin/Modules/CssExtractModule.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ class CssExtractModule {
8888

8989
/**
9090
* @param {string} assetFile The asset filename.
91+
* @param {boolean} keepAsset Keep the emitted CSS file when it is still referenced externally.
9192
* @returns {string}
9293
*/
93-
getInlineSource(assetFile) {
94+
getInlineSource(assetFile, keepAsset = false) {
9495
const sources = this.compilation.assets;
9596
const assetMapFile = assetFile + '.map';
9697
const mapFilename = path.basename(assetMapFile);
@@ -104,7 +105,9 @@ class CssExtractModule {
104105
}
105106

106107
// don't generate css file for inlined styles
107-
this.assetTrash.add(assetFile);
108+
if (!keepAsset) {
109+
this.assetTrash.add(assetFile);
110+
}
108111

109112
return source;
110113
}

0 commit comments

Comments
 (0)