Skip to content

Commit 6a51951

Browse files
authored
Merge pull request #3027 from modernweb-dev/feat/css-bundling-and-minification
feat(rollup-plugin-html): add CSS bundling and minification
2 parents f920237 + 3a9c97b commit 6a51951

6 files changed

Lines changed: 184 additions & 24 deletions

File tree

.changeset/yellow-garlics-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web/rollup-plugin-html': minor
3+
---
4+
5+
add CSS bundling and minification

docs/docs/building/rollup-plugin-html.md

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export default {
273273
};
274274
```
275275

276-
### Minification
276+
### HTML minification
277277

278278
Set the minify option to do default HTML minification. If you need custom options, you can implement your own minifier using the `transformHtml` option.
279279

@@ -292,6 +292,31 @@ export default {
292292
};
293293
```
294294

295+
### CSS bundling and minification
296+
297+
It's implemented via [Lightning CSS](https://lightningcss.dev/).
298+
It works when `extractAssets: true` and CSS files are extracted.
299+
300+
CSS bundling is enabled by default.
301+
Set `bundleCss: false` to disable it.
302+
303+
CSS minification can be enabled by setting `minifyCss: true`.
304+
305+
```js
306+
import { rollupPluginHTML as html } from '@web/rollup-plugin-html';
307+
308+
export default {
309+
input: 'index.html',
310+
output: { dir: 'dist' },
311+
plugins: [
312+
// add HTML plugin
313+
html({
314+
minifyCss: true,
315+
}),
316+
],
317+
};
318+
```
319+
295320
### Social Media Tags
296321

297322
Some social media tags require full absolute URLs (e.g. https://domain.com/guide/).
@@ -379,11 +404,11 @@ export interface InputHTMLOptions {
379404
export interface RollupPluginHTMLOptions {
380405
/** HTML file(s) to use as input. If not set, uses rollup input option. */
381406
input?: string | InputHTMLOptions | (string | InputHTMLOptions)[];
382-
/** HTML file glob patterns or patterns to ignore */
407+
/** HTML file glob pattern or patterns to ignore */
383408
exclude?: string | string[];
384-
/** Whether to minify the output HTML. */
409+
/** Whether to minify the output HTML. Defaults to false. */
385410
minify?: boolean;
386-
/** Whether to preserve or flatten the directory structure of the HTML file. */
411+
/** Whether to preserve or flatten the directory structure of the HTML file. Defaults to true. */
387412
flattenOutput?: boolean;
388413
/** Directory to resolve absolute paths relative to, and to use as base for non-flatted filename output. */
389414
rootDir?: string;
@@ -393,13 +418,17 @@ export interface RollupPluginHTMLOptions {
393418
transformAsset?: TransformAssetFunction | TransformAssetFunction[];
394419
/** Transform HTML file before output. */
395420
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
396-
/** Whether to extract and bundle assets referenced in HTML. Defaults to true. */
421+
/** Whether to extract and bundle assets referenced in HTML and CSS. Defaults to true. */
397422
extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css';
423+
/** Whether to bundle extracted CSS assets. Bundling is done via Lightning CSS. Defaults to true. */
424+
bundleCss?: boolean;
425+
/** Whether to minify extracted CSS assets. Minificaiton is done via Lightning CSS. Defaults to false. */
426+
minifyCss?: boolean;
398427
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
399428
externalAssets?: string | string[];
400429
/** Define a full absolute url to your site (e.g. https://domain.com) */
401430
absoluteBaseUrl?: string;
402-
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Default to true. */
431+
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Defaults to true. */
403432
absoluteSocialMediaUrls?: boolean;
404433
/** Should a service worker registration script be injected. Defaults to false. */
405434
injectServiceWorker?: boolean;

packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export interface RollupPluginHTMLOptions {
1515
input?: string | InputHTMLOptions | (string | InputHTMLOptions)[];
1616
/** HTML file glob pattern or patterns to ignore */
1717
exclude?: string | string[];
18-
/** Whether to minify the output HTML. */
18+
/** Whether to minify the output HTML. Defaults to false. */
1919
minify?: boolean;
20-
/** Whether to preserve or flatten the directory structure of the HTML file. */
20+
/** Whether to preserve or flatten the directory structure of the HTML file. Defaults to true. */
2121
flattenOutput?: boolean;
2222
/** Directory to resolve absolute paths relative to, and to use as base for non-flatted filename output. */
2323
rootDir?: string;
@@ -29,11 +29,15 @@ export interface RollupPluginHTMLOptions {
2929
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
3030
/** Whether to extract and bundle assets referenced in HTML and CSS. Defaults to true. */
3131
extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css';
32+
/** Whether to bundle extracted CSS assets. Bundling is done via Lightning CSS. Defaults to true. */
33+
bundleCss?: boolean;
34+
/** Whether to minify extracted CSS assets. Minificaiton is done via Lightning CSS. Defaults to false. */
35+
minifyCss?: boolean;
3236
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
3337
externalAssets?: string | string[];
3438
/** Define a full absolute url to your site (e.g. https://domain.com) */
3539
absoluteBaseUrl?: string;
36-
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Default to true. */
40+
/** Whether to set full absolute urls for ['meta[property=og:image]', 'link[rel=canonical]', 'meta[property=og:url]'] or not. Requires a absoluteBaseUrl to be set. Defaults to true. */
3741
absoluteSocialMediaUrls?: boolean;
3842
/** Should a service worker registration script be injected. Defaults to false. */
3943
injectServiceWorker?: boolean;

packages/rollup-plugin-html/src/output/emitAssets.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PluginContext } from 'rollup';
22
import path from 'path';
3-
import { transform } from 'lightningcss';
3+
import { bundleAsync, transform } from 'lightningcss';
44
import fs from 'fs';
55

66
import { InputAsset, InputData } from '../input/InputData';
@@ -41,6 +41,8 @@ export async function emitAssets(
4141
options: RollupPluginHTMLOptions,
4242
) {
4343
const extractAssets = options.extractAssets ?? true;
44+
const bundleCss = options.bundleCss ?? true;
45+
const minifyCss = options.minifyCss ?? false;
4446
const extractAssetsLegacyCss = options.extractAssets === 'legacy-html-and-css';
4547
const emittedStaticAssets = new Map<string, string>();
4648
const emittedHashedAssets = new Map<string, string>();
@@ -89,11 +91,10 @@ export async function emitAssets(
8991
const emittedExternalAssets = new Map();
9092
if (asset.hashed) {
9193
if (basename.endsWith('.css') && extractAssets) {
92-
let updatedCssSource = false;
93-
const { code } = await transform({
94-
filename: basename,
94+
const { code } = await (bundleCss ? bundleAsync : transform)({
95+
filename: asset.filePath,
9596
code: asset.content,
96-
minify: false,
97+
minify: minifyCss,
9798
visitor: {
9899
Url: url => {
99100
// Support foo.svg#bar
@@ -150,13 +151,13 @@ export async function emitAssets(
150151
}
151152
}
152153
}
153-
updatedCssSource = true;
154154
return url;
155155
},
156156
},
157157
});
158-
if (updatedCssSource) {
159-
source = Buffer.from(code);
158+
const codeBuffer = Buffer.from(code);
159+
if (!asset.content.equals(codeBuffer)) {
160+
source = codeBuffer;
160161
}
161162
}
162163

packages/rollup-plugin-html/test/rollup-plugin-html.test.ts

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,7 +1288,7 @@ describe('rollup-plugin-html', () => {
12881288
'assets/image-a-BCCvKrTe.svg',
12891289
'assets/image-b-C4stzVZW.svg',
12901290
'assets/image-c-DPeYetg3.svg',
1291-
'assets/styles-CF2Iy5n1.css',
1291+
'assets/styles-Bh7Pnjui.css',
12921292
'assets/x-DDGg8O6h.css',
12931293
'assets/y-DJTrnPH3.css',
12941294
'assets/webmanifest-BkrOR1WG.json',
@@ -1302,7 +1302,7 @@ describe('rollup-plugin-html', () => {
13021302
<link rel="icon" type="image/png" sizes="32x32" href="assets/image-b-BgQHKcRn.png" />
13031303
<link rel="manifest" href="assets/webmanifest-BkrOR1WG.json" />
13041304
<link rel="mask-icon" href="assets/image-a-BCCvKrTe.svg" color="#3f93ce" />
1305-
<link rel="stylesheet" href="assets/styles-CF2Iy5n1.css" />
1305+
<link rel="stylesheet" href="assets/styles-Bh7Pnjui.css" />
13061306
<link rel="stylesheet" href="assets/x-DDGg8O6h.css" />
13071307
<link rel="stylesheet" href="assets/y-DJTrnPH3.css" />
13081308
<meta property="og:image" content="assets/image-c-DPeYetg3.svg" />
@@ -1384,7 +1384,7 @@ describe('rollup-plugin-html', () => {
13841384
'assets/image-c-C4yLPiIL.png',
13851385
'assets/image-a.svg',
13861386
'assets/image-b-C4stzVZW.svg',
1387-
'assets/styles-CF2Iy5n1.css',
1387+
'assets/styles-Bh7Pnjui.css',
13881388
'assets/x-DDGg8O6h.css',
13891389
'assets/y-DJTrnPH3.css',
13901390
'assets/webmanifest.json',
@@ -1398,7 +1398,7 @@ describe('rollup-plugin-html', () => {
13981398
<link rel="icon" type="image/png" sizes="32x32" href="assets/image-b.png" />
13991399
<link rel="manifest" href="assets/webmanifest.json" />
14001400
<link rel="mask-icon" href="assets/image-a.svg" color="#3f93ce" />
1401-
<link rel="stylesheet" href="assets/styles-CF2Iy5n1.css" />
1401+
<link rel="stylesheet" href="assets/styles-Bh7Pnjui.css" />
14021402
<link rel="stylesheet" href="assets/x-DDGg8O6h.css" />
14031403
<link rel="stylesheet" href="assets/y-DJTrnPH3.css" />
14041404
</head>
@@ -2046,7 +2046,7 @@ describe('rollup-plugin-html', () => {
20462046
expect(Object.keys(assets)).to.have.lengthOf(4);
20472047

20482048
expect(assets).to.have.keys([
2049-
'assets/styles-CF2Iy5n1.css',
2049+
'assets/styles-Bh7Pnjui.css',
20502050
'assets/foo-CxmWeBHm.svg',
20512051
'assets/image-b-C4stzVZW.svg',
20522052
'x/index.html',
@@ -2055,7 +2055,7 @@ describe('rollup-plugin-html', () => {
20552055
expect(assets['x/index.html']).to.equal(html`
20562056
<html>
20572057
<head>
2058-
<link rel="stylesheet" href="../assets/styles-CF2Iy5n1.css" />
2058+
<link rel="stylesheet" href="../assets/styles-Bh7Pnjui.css" />
20592059
</head>
20602060
<body>
20612061
<img src="../assets/foo-CxmWeBHm.svg" />
@@ -3189,4 +3189,121 @@ describe('rollup-plugin-html', () => {
31893189
}
31903190
`);
31913191
});
3192+
3193+
it('can minify extracted CSS', async () => {
3194+
const rootDir = createApp({
3195+
'styles.css': css`
3196+
p {
3197+
font-weight: bold;
3198+
}
3199+
`,
3200+
});
3201+
3202+
const config = {
3203+
plugins: [
3204+
rollupPluginHTML({
3205+
rootDir,
3206+
input: {
3207+
html: html`
3208+
<html>
3209+
<head>
3210+
<link rel="stylesheet" href="./styles.css" />
3211+
</head>
3212+
<body></body>
3213+
</html>
3214+
`,
3215+
},
3216+
minifyCss: true,
3217+
}),
3218+
],
3219+
};
3220+
3221+
const build = await rollup(config);
3222+
const { chunks, assets, assetsUnformatted } = await generateTestBundle(build, outputConfig);
3223+
3224+
expect(Object.keys(chunks)).to.have.lengthOf(1);
3225+
expect(Object.keys(assets)).to.have.lengthOf(2);
3226+
3227+
expect(assets).to.have.keys(['assets/styles-DPU2l-t7.css', 'index.html']);
3228+
3229+
expect(assets['index.html']).to.equal(html`
3230+
<html>
3231+
<head>
3232+
<link rel="stylesheet" href="assets/styles-DPU2l-t7.css" />
3233+
</head>
3234+
<body></body>
3235+
</html>
3236+
`);
3237+
3238+
expect(assetsUnformatted['assets/styles-DPU2l-t7.css']).to.equal('p{font-weight:700}');
3239+
});
3240+
3241+
it('can bundle extracted CSS', async () => {
3242+
const rootDir = createApp({
3243+
'themes/theme-light.css': css`
3244+
body {
3245+
background-color: #fff;
3246+
}
3247+
`,
3248+
'themes/theme-dark.css': css`
3249+
@media (prefers-color-scheme: dark) {
3250+
body {
3251+
background-color: #000;
3252+
}
3253+
}
3254+
`,
3255+
'styles.css': css`
3256+
@import './themes/theme-light.css';
3257+
@import './themes/theme-dark.css';
3258+
`,
3259+
});
3260+
3261+
const config = {
3262+
plugins: [
3263+
rollupPluginHTML({
3264+
rootDir,
3265+
input: {
3266+
html: html`
3267+
<html>
3268+
<head>
3269+
<link rel="stylesheet" href="./styles.css" />
3270+
</head>
3271+
<body></body>
3272+
</html>
3273+
`,
3274+
},
3275+
bundleCss: true,
3276+
}),
3277+
],
3278+
};
3279+
3280+
const build = await rollup(config);
3281+
const { chunks, assets } = await generateTestBundle(build, outputConfig);
3282+
3283+
expect(Object.keys(chunks)).to.have.lengthOf(1);
3284+
expect(Object.keys(assets)).to.have.lengthOf(2);
3285+
3286+
expect(assets).to.have.keys(['assets/styles-Dc1pq4Qu.css', 'index.html']);
3287+
3288+
expect(assets['index.html']).to.equal(html`
3289+
<html>
3290+
<head>
3291+
<link rel="stylesheet" href="assets/styles-Dc1pq4Qu.css" />
3292+
</head>
3293+
<body></body>
3294+
</html>
3295+
`);
3296+
3297+
expect(assets['assets/styles-Dc1pq4Qu.css']).to.equal(css`
3298+
body {
3299+
background-color: #fff;
3300+
}
3301+
3302+
@media (prefers-color-scheme: dark) {
3303+
body {
3304+
background-color: #000;
3305+
}
3306+
}
3307+
`);
3308+
});
31923309
});

packages/rollup-plugin-html/test/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function generateTestBundle(build: RollupBuild, outputConfig: Outpu
4949
const { output } = await build.generate(outputConfig);
5050
const chunks: Record<string, string> = {};
5151
const assets: Record<string, string | Uint8Array> = {};
52+
const assetsUnformatted: Record<string, string | Uint8Array> = {};
5253

5354
for (const file of output) {
5455
const filename = file.fileName;
@@ -57,17 +58,20 @@ export async function generateTestBundle(build: RollupBuild, outputConfig: Outpu
5758
chunks[filename] = formatter ? formatter(file.code) : file.code;
5859
} else if (file.type === 'asset') {
5960
let code = file.source;
61+
let codeUnformatted = file.source;
6062
if (typeof code !== 'string' && filename.endsWith('.css')) {
6163
code = Buffer.from(code).toString('utf8');
64+
codeUnformatted = Buffer.from(codeUnformatted).toString('utf8');
6265
}
6366
if (typeof code === 'string' && formatter) {
6467
code = formatter(code);
6568
}
6669
assets[filename] = code;
70+
assetsUnformatted[filename] = codeUnformatted;
6771
}
6872
}
6973

70-
return { output, chunks, assets };
74+
return { output, chunks, assets, assetsUnformatted };
7175
}
7276

7377
export function createApp(structure: Record<string, string | Buffer | object>) {

0 commit comments

Comments
 (0)