Skip to content

Commit 1b0443a

Browse files
committed
feat: add integrity attribute for preload tags, #145
1 parent 179667c commit 1b0443a

25 files changed

+257
-29
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.16.0 (2025-01-28)
4+
5+
- feat: add integrity attribute for preload tags if integrity option on, #145
6+
37
## 4.15.3 (2025-01-27)
48

59
- fix: compilation fails if used the integrity option with the publicPath as an external URL

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.3",
3+
"version": "4.16.0",
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/Collection.js

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const Preload = require('./Preload');
77
const { noHeadException } = require('./Messages/Exception');
88

99
/** @typedef {import('webpack').Compilation} Compilation */
10-
/** @typedef {import("webpack/lib/Entrypoint")} Entrypoint */
11-
/** @typedef {import("webpack/lib/ChunkGroup")} ChunkGroup */
10+
/** @typedef {import('webpack/lib/Entrypoint')} Entrypoint */
11+
/** @typedef {import('webpack/lib/ChunkGroup')} ChunkGroup */
1212

1313
/**
1414
* @typedef {Object} CollectionData
@@ -57,6 +57,10 @@ class Collection {
5757
/** @type {Dependency} */
5858
dependency = null;
5959

60+
/**
61+
* Map of assets by source file.
62+
* @type {Map<string, any>}
63+
*/
6064
assets = new Map();
6165

6266
/** @type {Map<string, {entry: AssetEntryOptions, assets: Array<{}>} >} Entries data */
@@ -913,10 +917,15 @@ class Collection {
913917
return Promise.resolve();
914918
}
915919

920+
/**
921+
* Map of assets by output file.
922+
* @type {Map<string, any>}
923+
*/
924+
const assetsCache = new Map();
925+
916926
const entryDirname = path.dirname(entryFilename);
917927
const importedStyles = [];
918928
const parseOptions = new Map();
919-
const assetIntegrity = new Map();
920929
let hasInlineSvg = false;
921930
let content = rawSource.source();
922931

@@ -1004,35 +1013,40 @@ class Collection {
10041013

10051014
const assetContent = compilation.assets[pathname].source();
10061015
asset.integrity = Integrity.getIntegrity(compilation, assetContent, pathname);
1007-
assetIntegrity.set(asset.assetFile, asset.integrity);
1016+
assetsCache.set(asset.assetFile, asset);
10081017

10091018
if (!parseOptions.has(type)) {
10101019
parseOptions.set(type, {
10111020
tag: 'link',
10121021
attributes: ['href'],
1013-
filter: ({ attribute, attributes }) =>
1014-
!attributes.hasOwnProperty('integrity') &&
1015-
attribute === 'href' &&
1016-
attributes.rel === 'stylesheet',
1022+
// disable ignoring tags already containing integrity attribute
1023+
// filter: ({ attribute, attributes }) =>
1024+
// !attributes.hasOwnProperty('integrity') &&
1025+
// attribute === 'href' &&
1026+
// attributes.rel === 'stylesheet',
10171027
});
10181028
}
10191029
}
10201030
break;
10211031
case Collection.type.script:
10221032
// 1.2 compute JS integrity
10231033
if (hasIntegrity) {
1024-
for (const chunk of asset.chunks) {
1034+
// combine sync and async (dynamic imported) chunks into one array
1035+
const chunks = [...asset.chunks, ...asset.children];
1036+
1037+
// sync chunks
1038+
for (const chunk of chunks) {
10251039
if (!chunk.inline) {
10261040
const assetContent = compilation.assets[chunk.chunkFile].source();
10271041
chunk.integrity = Integrity.getIntegrity(compilation, assetContent, chunk.chunkFile);
1028-
assetIntegrity.set(chunk.assetFile, chunk.integrity);
1042+
assetsCache.set(chunk.assetFile, chunk);
10291043

10301044
if (!parseOptions.has(type)) {
10311045
parseOptions.set(type, {
10321046
tag: 'script',
10331047
attributes: ['src'],
1034-
filter: ({ attribute, attributes }) =>
1035-
!attributes.hasOwnProperty('integrity') && attribute === 'src',
1048+
// disable ignoring tags already containing integrity attribute
1049+
// filter: ({ attribute, attributes }) => !attributes.hasOwnProperty('integrity') && attribute === 'src',
10361050
});
10371051
}
10381052
}
@@ -1057,18 +1071,12 @@ class Collection {
10571071
hasInlineSvg ? this.assetInline.inlineSvg(content, entryFilename) : content
10581072
);
10591073

1060-
// 7. inject preloads
1061-
if (this.pluginOption.isPreload()) {
1062-
promise = promise.then(
1063-
(content) => this.preload.insertPreloadAssets(content, entry.filename, this.data) || content
1064-
);
1065-
}
1066-
1067-
// 8. inject integrity
1074+
// 7. inject integrity
10681075
if (hasIntegrity) {
10691076
promise = promise.then((content) => {
10701077
// 2. parse generated html for `link` and `script` tags
10711078
const parsedResults = [];
1079+
const crossorigin = this.pluginOption.getCrossorigin();
10721080

10731081
for (const opts of parseOptions.values()) {
10741082
parsedResults.push(...HtmlParser.parseTag(content, opts));
@@ -1079,15 +1087,23 @@ class Collection {
10791087
let pos = 0;
10801088
let output = '';
10811089

1082-
for (const { tag, parsedAttrs, attrs, startPos, endPos } of parsedResults) {
1090+
for (const { type, tag, parsedAttrs, attrs, startPos, endPos } of parsedResults) {
10831091
if (!attrs || parsedAttrs.length < 1) continue;
10841092

10851093
const assetFile = attrs.href || attrs.src;
1086-
const integrity = assetIntegrity.get(assetFile);
1094+
const asset = assetsCache.get(assetFile) || {};
1095+
const { integrity } = asset;
1096+
const integrityOrigin = attrs.integrity;
1097+
1098+
// update the integrity that was calculated to the original value parsed in HTML
1099+
if (integrityOrigin) {
1100+
asset.integrity = integrityOrigin;
1101+
continue;
1102+
}
10871103

10881104
if (integrity) {
10891105
attrs.integrity = integrity;
1090-
attrs.crossorigin = this.pluginOption.webpackOptions.output.crossOriginLoading || 'anonymous';
1106+
attrs.crossorigin = crossorigin;
10911107

10921108
let attrsStr = '';
10931109
for (const attrName in attrs) {
@@ -1104,6 +1120,13 @@ class Collection {
11041120
});
11051121
}
11061122

1123+
// 8. inject preloads containing integrity attribute
1124+
if (this.pluginOption.isPreload()) {
1125+
promise = promise.then(
1126+
(content) => this.preload.insertPreloadAssets(content, entry.filename, this.data) || content
1127+
);
1128+
}
1129+
11071130
// 9. beforeEmit hook allows plugins to change the html after chunks and inlined assets are injected
11081131
promise = promise.then((content) => hooks.beforeEmit.promise(content, compileEntry) || content);
11091132

src/Plugin/Option.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,13 @@ class Option {
655655
return this.webpackPublicPath;
656656
}
657657

658+
/**
659+
* @return {string}
660+
*/
661+
getCrossorigin() {
662+
return this.webpackOptions.output.crossOriginLoading || 'anonymous';
663+
}
664+
658665
/**
659666
* Get the output path of the asset.
660667
*

src/Plugin/Preload.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class Preload {
103103
return;
104104
}
105105

106+
const crossorigin = this.pluginOption.getCrossorigin();
106107
const preloadAssets = new Map();
107108
const LF = this.pluginOption.getLF();
108109
const indent = LF + detectIndent(content, insertPos - 1);
@@ -150,13 +151,14 @@ class Preload {
150151
if (conf) {
151152
if (Array.isArray(item.chunks)) {
152153
// js
153-
for (let { chunkFile, assetFile } of item.chunks) {
154+
for (let { chunkFile, assetFile, integrity } of item.chunks) {
154155
// sourceFiles contain only one file
155156
let sourceFiles = [item.resource];
156157
let outputFile = assetFile;
157158

158159
if (this.pluginOption.applyAdvancedFiler({ sourceFiles, outputFile }, conf._opts.filter)) {
159-
preloadAssets.set(assetFile, conf._opts);
160+
let props = this.createPreloadAttributes(conf, { integrity: integrity, crossorigin });
161+
preloadAssets.set(assetFile, props);
160162
}
161163
}
162164
} else {
@@ -167,19 +169,21 @@ class Preload {
167169
let outputFile = item.assetFile;
168170

169171
if (this.pluginOption.applyAdvancedFiler({ sourceFiles, outputFile }, conf._opts.filter)) {
170-
preloadAssets.set(item.assetFile, conf._opts);
172+
let props = this.createPreloadAttributes(conf, { integrity: item.integrity, crossorigin });
173+
preloadAssets.set(item.assetFile, props);
171174
}
172175
}
173176

174177
// dynamic imported modules, asyncChunks
175178
if (Array.isArray(item.children)) {
176-
for (let { chunkFile, assetFile, sourceFile } of item.children) {
179+
for (let { chunkFile, assetFile, sourceFile, integrity } of item.children) {
177180
// sourceFiles contain only one file
178181
let sourceFiles = [sourceFile];
179182
let outputFile = assetFile;
180183

181184
if (this.pluginOption.applyAdvancedFiler({ sourceFiles, outputFile }, conf._opts.filter)) {
182-
preloadAssets.set(assetFile, conf._opts);
185+
let props = this.createPreloadAttributes(conf, { integrity: integrity, crossorigin });
186+
preloadAssets.set(assetFile, props);
183187
}
184188
}
185189
}
@@ -241,6 +245,22 @@ class Preload {
241245
}
242246
}
243247

248+
/**
249+
* @param {Object} conf
250+
* @param {string|undefined} integrity
251+
* @param {string} crossorigin
252+
* @return {Object} Returns preload attributes. It my contains integrity if exists.
253+
*/
254+
createPreloadAttributes(conf, { integrity, crossorigin }) {
255+
let opts = { ...conf._opts };
256+
257+
if (integrity) {
258+
opts.attrs = { ...opts.attrs, integrity, crossorigin };
259+
}
260+
261+
return opts;
262+
}
263+
244264
/**
245265
* Find start position in the content to insert generated tags.
246266
*
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.no-integrity {
2+
color: darkviolet;
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h1 {
2+
color: red;
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
h2 {
2+
color: green;
3+
}
1.22 KB
Loading
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Test</title>
5+
6+
<!-- load source style -->
7+
<link rel="preload" href="js/main.bundle.js" as="script" integrity="sha384-23S4uFexi9GT+stEnRetHYPEQOOD3z3yZUBXGyN8P9jkE3pPEspH27bEf8ujdiqX" crossorigin="use-credentials">
8+
<link rel="preload" href="js/473.chunk.js" as="script" integrity="sha384-mDb7vARsl+J8e3qKFBZsGuOHa7WVAxWgXzgY16BOgxXthejkBcdERN1yvwQkn790" crossorigin="use-credentials">
9+
<link rel="preload" href="js/vendor.bundle.js" as="script" integrity="sha384-KOHLjGo0rQ+C8SOiWbOw8KN2r6NlfN+E6Jlre5dqkZJLv2NCk3EzaQClYi7qJBtc" crossorigin="use-credentials">
10+
<link rel="preload" href="js/no-integrity.bundle.js" as="script" integrity="vendor-predefined-js" crossorigin="use-credentials">
11+
<link rel="preload" href="css/style.bundle.css" as="style" integrity="sha384-gaDmgJjLpipN1Jmuc98geFnDjVqWn1fixlG0Ab90qFyUIJ4ARXlKBsMGumxTSu7E" crossorigin="use-credentials">
12+
<link rel="preload" href="css/vendor.bundle.css" as="style" integrity="sha384-S5He9W/ycWxLrmqRI+6B4V1TOIe0rK0NKdUpD8M61yjawdrgmpxiC/EwmNGkUkcd" crossorigin="use-credentials">
13+
<link rel="preload" href="css/no-integrity.bundle.css" as="style" integrity="vendor-predefined-css" crossorigin="use-credentials">
14+
<link rel="preload" href="img/image.697ef306.png" as="image" type="image/png">
15+
<link href="css/style.bundle.css" rel="stylesheet" integrity="sha384-gaDmgJjLpipN1Jmuc98geFnDjVqWn1fixlG0Ab90qFyUIJ4ARXlKBsMGumxTSu7E" crossorigin="use-credentials">
16+
17+
<!-- load source script -->
18+
<script src="js/main.bundle.js" defer="defer" integrity="sha384-23S4uFexi9GT+stEnRetHYPEQOOD3z3yZUBXGyN8P9jkE3pPEspH27bEf8ujdiqX" crossorigin="use-credentials"></script>
19+
20+
<!-- test: mixed order of css and js files -->
21+
<link href="css/vendor.bundle.css" rel="stylesheet" integrity="sha384-S5He9W/ycWxLrmqRI+6B4V1TOIe0rK0NKdUpD8M61yjawdrgmpxiC/EwmNGkUkcd" crossorigin="use-credentials">
22+
<script src="js/vendor.bundle.js" defer="defer" integrity="sha384-KOHLjGo0rQ+C8SOiWbOw8KN2r6NlfN+E6Jlre5dqkZJLv2NCk3EzaQClYi7qJBtc" crossorigin="use-credentials"></script>
23+
24+
<link href="css/no-integrity.bundle.css" integrity="vendor-predefined-css" rel="stylesheet">
25+
26+
<!-- test: already exists integrity attribute -->
27+
<script src="js/no-integrity.bundle.js" integrity="vendor-predefined-js" defer="defer"></script>
28+
29+
<!-- test: not processed script, because the attribute is invalid -->
30+
<script x-src="./not-processed-script.js" defer="defer"></script>
31+
32+
<!-- test: not processed script, because it's loaded via url -->
33+
<script src="//cdn-script.js" defer="defer"></script>
34+
</head>
35+
<body>
36+
<h1>Hello World!</h1>
37+
38+
<!-- load source image -->
39+
<img src="img/image.697ef306.png">
40+
</body>
41+
</html>

0 commit comments

Comments
 (0)