Skip to content

Commit 19eb206

Browse files
committed
feat: add support the ?inline query for styles imported in JavaScript
1 parent 361ff6c commit 19eb206

14 files changed

Lines changed: 206 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Change log
22

3+
## 3.17.0 (2024-07-23)
4+
5+
- feat: add support the `?inline` query for styles imported in JavaScript:
6+
```js
7+
import './style-a.css?inline'; // the extracted CSS will be injected into HTML
8+
import './style-b.css'; // the extracted CSS will be saved into separate output file
9+
```
10+
311
## 3.16.0 (2024-07-23)
412

513
- feat: add `runtime` option for the `handlebars` preprocessor

package-lock.json

Lines changed: 12 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html-bundler-webpack-plugin",
3-
"version": "3.16.0",
3+
"version": "3.17.0",
44
"description": "HTML bundler plugin for webpack handles a template as an entry point, extracts CSS and JS from their sources referenced in HTML, supports template engines like Eta, EJS, Handlebars, Nunjucks.",
55
"keywords": [
66
"html",
@@ -177,7 +177,7 @@
177177
"nunjucks": "^3.2.4",
178178
"parse5": "^7.1.2",
179179
"postcss-loader": "^8.1.1",
180-
"prettier": "^3.3.2",
180+
"prettier": "^3.3.3",
181181
"prismjs": "^1.29.0",
182182
"pug": "^3.0.3",
183183
"react": "18.3.1",

src/Plugin/AssetCompiler.js

Lines changed: 93 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,6 @@ class AssetCompiler {
10291029
const { createHash } = this.webpack.util;
10301030
const isAutoPublicPath = Option.isAutoPublicPath();
10311031
const publicPath = Option.getPublicPath();
1032-
const inline = Option.getCss().inline;
10331032
const esModule = Collection.isImportStyleEsModule();
10341033
const urlRegex = new RegExp(`${esModule ? baseUri : ''}${urlPathPrefix}(.+?)(?=\\))`, 'g');
10351034
const entry = this.currentEntryPoint;
@@ -1056,19 +1055,24 @@ class AssetCompiler {
10561055
const sources = [];
10571056
const resources = [];
10581057
const imports = [];
1058+
const inlineSources = [];
1059+
const inlineResources = [];
1060+
const inlineImports = [];
10591061
let cssHash = '';
10601062

10611063
// 1. get styles from all nested files imported in the root JS file and sort them
10621064
const modules = Collection.findImportedModules(entry.id, issuer, chunk);
10631065

1064-
// 2. squash styles from all nested files into one file
1066+
// 2. squash styles from all nested files and group by inline/file type
10651067
const uniqueModuleIds = new Set();
10661068
for (const { module } of modules) {
10671069
if (uniqueModuleIds.has(module.debugId)) {
10681070
continue;
10691071
}
10701072

1071-
const isUrl = module.resourceResolveData?.query.includes('url');
1073+
const urlQuery = module.resourceResolveData?.query || '';
1074+
const isUrl = urlQuery.includes('url');
1075+
const isInline = Option.isInlineCss(urlQuery);
10721076
const importData = {
10731077
resource: module.resource,
10741078
assets: [],
@@ -1078,15 +1082,15 @@ class AssetCompiler {
10781082
const { assetsInfo } = module.buildInfo;
10791083

10801084
if (assetsInfo) {
1081-
for (const [assetFile2, asset] of assetsInfo) {
1085+
for (const [assetFile, asset] of assetsInfo) {
10821086
const sourceFilename = asset.sourceFilename;
10831087
const stylePath = path.dirname(module.resource);
10841088

10851089
const data = {
10861090
type: Collection.type.resource,
1087-
inline: false,
1091+
inline: isInline,
10881092
resource: path.resolve(stylePath, sourceFilename),
1089-
assetFile: assetFile2,
1093+
assetFile: assetFile,
10901094
issuer: {
10911095
resource: module.resource,
10921096
},
@@ -1114,20 +1118,26 @@ class AssetCompiler {
11141118
}
11151119

11161120
cssHash += module.buildInfo.hash;
1117-
sources.push(...module._cssSource);
1118-
imports.push(importData);
1119-
resources.push(module.resource);
11201121
uniqueModuleIds.add(module.debugId);
1122+
1123+
if (isInline) {
1124+
inlineSources.push(...module._cssSource);
1125+
inlineResources.push(module.resource);
1126+
inlineImports.push(importData);
1127+
} else {
1128+
sources.push(...module._cssSource);
1129+
resources.push(module.resource);
1130+
imports.push(importData);
1131+
}
11211132
}
11221133

1123-
if (sources.length === 0) continue;
1134+
if (sources.length === 0 && inlineSources.length === 0) continue;
11241135

11251136
// 3. generate output filename
11261137

11271138
// mixin importStyleIdx into hash to generate new hash after changes
11281139
cssHash += Collection.importStyleIdx++;
11291140

1130-
//const hash = this.webpack.util.createHash('md4').update(sources.toString()).digest('hex');
11311141
const hash = createHash('md4').update(cssHash).digest('hex');
11321142
const { isCached, filename } = this.getStyleAsseFile({
11331143
name: issuerEntry.name,
@@ -1137,45 +1147,85 @@ class AssetCompiler {
11371147
useChunkFilename: true,
11381148
});
11391149

1140-
const assetFile = inline ? this.getInlineStyleAsseFile(filename, entryFilename) : filename;
1141-
const outputFilename = inline ? assetFile : Option.getAssetOutputFile(assetFile, entryFilename);
1142-
1143-
Collection.setData(
1144-
entry,
1145-
{ resource: issuer },
1146-
{
1147-
type: Collection.type.style,
1148-
inline,
1149-
imported: true,
1150-
// if style is imported then resource is the array of imported source files
1151-
resource: resources,
1152-
assetFile: outputFilename,
1153-
imports,
1154-
}
1155-
);
1150+
// CSS injected into HTML
1151+
if (inlineSources.length) {
1152+
const assetFile = this.getInlineStyleAsseFile(filename, entryFilename);
1153+
const outputFilename = assetFile;
1154+
1155+
Collection.setData(
1156+
entry,
1157+
{ resource: issuer },
1158+
{
1159+
type: Collection.type.style,
1160+
inline: true,
1161+
imported: true,
1162+
// if style is imported then resource is the array of imported source files
1163+
resource: inlineResources,
1164+
assetFile: outputFilename,
1165+
imports: inlineImports,
1166+
}
1167+
);
11561168

1157-
// skip already processed styles except inlined
1158-
if (isCached && !inline) {
1159-
continue;
1169+
// 4. extracts CSS content from squashed sources
1170+
const issuerFilename = entryFilename;
1171+
1172+
const resolveAssetFile = (match, file) =>
1173+
isAutoPublicPath ? Option.getAssetOutputFile(file, issuerFilename) : path.posix.join(publicPath, file);
1174+
1175+
const cssContent = CssExtractModule.apply(inlineSources, (content) =>
1176+
content.replace(urlRegex, resolveAssetFile)
1177+
);
1178+
1179+
// 5. add extracted CSS file into compilation
1180+
const fileManifest = {
1181+
render: () => cssContent,
1182+
filename: assetFile,
1183+
identifier: `${pluginName}.${chunk.id}`,
1184+
hash,
1185+
};
1186+
1187+
result.push(fileManifest);
11601188
}
11611189

1162-
// 4. extracts CSS content from squashed sources
1163-
const issuerFilename = inline ? entryFilename : assetFile;
1190+
// CSS saved into file
1191+
if (sources.length) {
1192+
const assetFile = filename;
1193+
const outputFilename = Option.getAssetOutputFile(assetFile, entryFilename);
1194+
1195+
Collection.setData(
1196+
entry,
1197+
{ resource: issuer },
1198+
{
1199+
type: Collection.type.style,
1200+
inline: false,
1201+
imported: true,
1202+
// if style is imported then resource is the array of imported source files
1203+
resource: resources,
1204+
assetFile: outputFilename,
1205+
imports,
1206+
}
1207+
);
11641208

1165-
const resolveAssetFile = (match, file) =>
1166-
isAutoPublicPath ? Option.getAssetOutputFile(file, issuerFilename) : path.posix.join(publicPath, file);
1209+
if (!isCached) {
1210+
// 4. extracts CSS content from squashed sources
1211+
const issuerFilename = assetFile;
11671212

1168-
const cssContent = CssExtractModule.apply(sources, (content) => content.replace(urlRegex, resolveAssetFile));
1213+
const resolveAssetFile = (match, file) =>
1214+
isAutoPublicPath ? Option.getAssetOutputFile(file, issuerFilename) : path.posix.join(publicPath, file);
11691215

1170-
// 5. add extracted CSS file into compilation
1171-
const fileManifest = {
1172-
render: () => cssContent,
1173-
filename: assetFile,
1174-
identifier: `${pluginName}.${chunk.id}`,
1175-
hash,
1176-
};
1216+
const cssContent = CssExtractModule.apply(sources, (content) => content.replace(urlRegex, resolveAssetFile));
11771217

1178-
result.push(fileManifest);
1218+
// 5. add extracted CSS file into compilation
1219+
const fileManifest = {
1220+
render: () => cssContent,
1221+
filename: assetFile,
1222+
identifier: `${pluginName}.${chunk.id}`,
1223+
hash,
1224+
};
1225+
1226+
result.push(fileManifest);
1227+
}
1228+
}
11791229
}
11801230
}
11811231

src/Plugin/Collection.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ class Collection {
119119
// note: in inlined style must be no LF character after the open tag, otherwise the mapping will not work
120120
styleTags += `<style>` + source + `</style>${LF}`;
121121
} else {
122+
// note: void elements don't need the closing
123+
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
122124
linkTags += `<link href="${asset.assetFile}" rel="stylesheet">${LF}`;
123125
}
124126
}

src/Plugin/Option.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ class Option {
423423
}
424424

425425
/**
426-
* Whether the CSS resource should be inlined.
426+
* Whether the CSS resource should be inlined, regard of the global css.inline option and the file query.
427427
*
428428
* @param {string} resource The resource file, including a query.
429429
* @return {boolean}
@@ -432,8 +432,10 @@ class Option {
432432
const [, query] = resource.split('?', 2);
433433
const urlParams = new URLSearchParams(query);
434434
const value = urlParams.get('inline');
435+
const hasQueryInline = value != null;
436+
const isInlinedByQuery = this.toBool(value, false, false);
435437

436-
return this.toBool(value, false, this.options.css.inline);
438+
return (this.options.css.inline && (isInlinedByQuery || !hasQueryInline)) || isInlinedByQuery;
437439
}
438440

439441
/**

test/cases/js-import-css-modules-type-css-style-sheet-mix/expected/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
color: indianred;
99
}
1010
</style>
11-
<link href="main.dc4ea4af.css" rel="stylesheet">
11+
<link href="main.5b01e96e.css" rel="stylesheet">
12+
<style>.royal-blue {
13+
border: 5px solid royalblue;
14+
color: royalblue;
15+
}
16+
</style>
1217
<script src="main.9da2f4dd.js" async defer="defer"></script>
1318
</head>
1419
<body>
1520
<h1>Hello World!</h1>
1621
<div class="red">Red</div>
1722
<div class="lime-green">Green</div>
1823
<div class="royal-blue">Blue</div>
24+
<div class="hotpink">Pink</div>
1925
</body>
2026
</html>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.hotpink {
2+
border: 5px solid hotpink;
3+
color: hotpink;
4+
}

test/cases/js-import-css-modules-type-css-style-sheet-mix/expected/main.dc4ea4af.css

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

test/cases/js-import-css-modules-type-css-style-sheet-mix/src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ <h1>Hello World!</h1>
1111
<div class="red">Red</div>
1212
<div class="lime-green">Green</div>
1313
<div class="royal-blue">Blue</div>
14+
<div class="hotpink">Pink</div>
1415
</body>
1516
</html>

0 commit comments

Comments
 (0)