Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/yellow-garlics-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@web/rollup-plugin-html': minor
---

add CSS bundling and minification
41 changes: 35 additions & 6 deletions docs/docs/building/rollup-plugin-html.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export default {
};
```

### Minification
### HTML minification

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

Expand All @@ -292,6 +292,31 @@ export default {
};
```

### CSS bundling and minification

It's implemented via [Lightning CSS](https://lightningcss.dev/).
It works when `extractAssets: true` and CSS files are extracted.

CSS bundling is enabled by default.
Set `bundleCss: false` to disable it.

CSS minification can be enabled by setting `minifyCss: true`.

```js
import { rollupPluginHTML as html } from '@web/rollup-plugin-html';

export default {
input: 'index.html',
output: { dir: 'dist' },
plugins: [
// add HTML plugin
html({
minifyCss: true,
}),
],
};
```

### Social Media Tags

Some social media tags require full absolute URLs (e.g. https://domain.com/guide/).
Expand Down Expand Up @@ -379,11 +404,11 @@ export interface InputHTMLOptions {
export interface RollupPluginHTMLOptions {
/** HTML file(s) to use as input. If not set, uses rollup input option. */
input?: string | InputHTMLOptions | (string | InputHTMLOptions)[];
/** HTML file glob patterns or patterns to ignore */
/** HTML file glob pattern or patterns to ignore */
exclude?: string | string[];
/** Whether to minify the output HTML. */
/** Whether to minify the output HTML. Defaults to false. */
minify?: boolean;
/** Whether to preserve or flatten the directory structure of the HTML file. */
/** Whether to preserve or flatten the directory structure of the HTML file. Defaults to true. */
flattenOutput?: boolean;
/** Directory to resolve absolute paths relative to, and to use as base for non-flatted filename output. */
rootDir?: string;
Expand All @@ -393,13 +418,17 @@ export interface RollupPluginHTMLOptions {
transformAsset?: TransformAssetFunction | TransformAssetFunction[];
/** Transform HTML file before output. */
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
/** Whether to extract and bundle assets referenced in HTML. Defaults to true. */
/** Whether to extract and bundle assets referenced in HTML and CSS. Defaults to true. */
extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css';
/** Whether to bundle extracted CSS assets. Bundling is done via Lightning CSS. Defaults to true. */
bundleCss?: boolean;
/** Whether to minify extracted CSS assets. Minificaiton is done via Lightning CSS. Defaults to false. */
minifyCss?: boolean;
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
externalAssets?: string | string[];
/** Define a full absolute url to your site (e.g. https://domain.com) */
absoluteBaseUrl?: string;
/** 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. */
/** 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. */
absoluteSocialMediaUrls?: boolean;
/** Should a service worker registration script be injected. Defaults to false. */
injectServiceWorker?: boolean;
Expand Down
10 changes: 7 additions & 3 deletions packages/rollup-plugin-html/src/RollupPluginHTMLOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export interface RollupPluginHTMLOptions {
input?: string | InputHTMLOptions | (string | InputHTMLOptions)[];
/** HTML file glob pattern or patterns to ignore */
exclude?: string | string[];
/** Whether to minify the output HTML. */
/** Whether to minify the output HTML. Defaults to false. */
minify?: boolean;
/** Whether to preserve or flatten the directory structure of the HTML file. */
/** Whether to preserve or flatten the directory structure of the HTML file. Defaults to true. */
flattenOutput?: boolean;
/** Directory to resolve absolute paths relative to, and to use as base for non-flatted filename output. */
rootDir?: string;
Expand All @@ -29,11 +29,15 @@ export interface RollupPluginHTMLOptions {
transformHtml?: TransformHtmlFunction | TransformHtmlFunction[];
/** Whether to extract and bundle assets referenced in HTML and CSS. Defaults to true. */
extractAssets?: boolean | 'legacy-html' | 'legacy-html-and-css';
/** Whether to bundle extracted CSS assets. Bundling is done via Lightning CSS. Defaults to true. */
bundleCss?: boolean;
/** Whether to minify extracted CSS assets. Minificaiton is done via Lightning CSS. Defaults to false. */
minifyCss?: boolean;
/** Whether to ignore assets referenced in HTML and CSS with glob patterns. */
externalAssets?: string | string[];
/** Define a full absolute url to your site (e.g. https://domain.com) */
absoluteBaseUrl?: string;
/** 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. */
/** 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. */
absoluteSocialMediaUrls?: boolean;
/** Should a service worker registration script be injected. Defaults to false. */
injectServiceWorker?: boolean;
Expand Down
17 changes: 9 additions & 8 deletions packages/rollup-plugin-html/src/output/emitAssets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PluginContext } from 'rollup';
import path from 'path';
import { transform } from 'lightningcss';
import { bundleAsync, transform } from 'lightningcss';
import fs from 'fs';

import { InputAsset, InputData } from '../input/InputData';
Expand Down Expand Up @@ -41,6 +41,8 @@ export async function emitAssets(
options: RollupPluginHTMLOptions,
) {
const extractAssets = options.extractAssets ?? true;
const bundleCss = options.bundleCss ?? true;
const minifyCss = options.minifyCss ?? false;
const extractAssetsLegacyCss = options.extractAssets === 'legacy-html-and-css';
const emittedStaticAssets = new Map<string, string>();
const emittedHashedAssets = new Map<string, string>();
Expand Down Expand Up @@ -89,11 +91,10 @@ export async function emitAssets(
const emittedExternalAssets = new Map();
if (asset.hashed) {
if (basename.endsWith('.css') && extractAssets) {
let updatedCssSource = false;
const { code } = await transform({
filename: basename,
const { code } = await (bundleCss ? bundleAsync : transform)({
filename: asset.filePath,
code: asset.content,
minify: false,
minify: minifyCss,
visitor: {
Url: url => {
// Support foo.svg#bar
Expand Down Expand Up @@ -150,13 +151,13 @@ export async function emitAssets(
}
}
}
updatedCssSource = true;
return url;
},
},
});
if (updatedCssSource) {
source = Buffer.from(code);
const codeBuffer = Buffer.from(code);
if (!asset.content.equals(codeBuffer)) {
source = codeBuffer;
}
}

Expand Down
129 changes: 123 additions & 6 deletions packages/rollup-plugin-html/test/rollup-plugin-html.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1288,7 +1288,7 @@ describe('rollup-plugin-html', () => {
'assets/image-a-BCCvKrTe.svg',
'assets/image-b-C4stzVZW.svg',
'assets/image-c-DPeYetg3.svg',
'assets/styles-CF2Iy5n1.css',
'assets/styles-Bh7Pnjui.css',
'assets/x-DDGg8O6h.css',
'assets/y-DJTrnPH3.css',
'assets/webmanifest-BkrOR1WG.json',
Expand All @@ -1302,7 +1302,7 @@ describe('rollup-plugin-html', () => {
<link rel="icon" type="image/png" sizes="32x32" href="assets/image-b-BgQHKcRn.png" />
<link rel="manifest" href="assets/webmanifest-BkrOR1WG.json" />
<link rel="mask-icon" href="assets/image-a-BCCvKrTe.svg" color="#3f93ce" />
<link rel="stylesheet" href="assets/styles-CF2Iy5n1.css" />
<link rel="stylesheet" href="assets/styles-Bh7Pnjui.css" />
<link rel="stylesheet" href="assets/x-DDGg8O6h.css" />
<link rel="stylesheet" href="assets/y-DJTrnPH3.css" />
<meta property="og:image" content="assets/image-c-DPeYetg3.svg" />
Expand Down Expand Up @@ -1384,7 +1384,7 @@ describe('rollup-plugin-html', () => {
'assets/image-c-C4yLPiIL.png',
'assets/image-a.svg',
'assets/image-b-C4stzVZW.svg',
'assets/styles-CF2Iy5n1.css',
'assets/styles-Bh7Pnjui.css',
'assets/x-DDGg8O6h.css',
'assets/y-DJTrnPH3.css',
'assets/webmanifest.json',
Expand All @@ -1398,7 +1398,7 @@ describe('rollup-plugin-html', () => {
<link rel="icon" type="image/png" sizes="32x32" href="assets/image-b.png" />
<link rel="manifest" href="assets/webmanifest.json" />
<link rel="mask-icon" href="assets/image-a.svg" color="#3f93ce" />
<link rel="stylesheet" href="assets/styles-CF2Iy5n1.css" />
<link rel="stylesheet" href="assets/styles-Bh7Pnjui.css" />
<link rel="stylesheet" href="assets/x-DDGg8O6h.css" />
<link rel="stylesheet" href="assets/y-DJTrnPH3.css" />
</head>
Expand Down Expand Up @@ -2046,7 +2046,7 @@ describe('rollup-plugin-html', () => {
expect(Object.keys(assets)).to.have.lengthOf(4);

expect(assets).to.have.keys([
'assets/styles-CF2Iy5n1.css',
'assets/styles-Bh7Pnjui.css',
'assets/foo-CxmWeBHm.svg',
'assets/image-b-C4stzVZW.svg',
'x/index.html',
Expand All @@ -2055,7 +2055,7 @@ describe('rollup-plugin-html', () => {
expect(assets['x/index.html']).to.equal(html`
<html>
<head>
<link rel="stylesheet" href="../assets/styles-CF2Iy5n1.css" />
<link rel="stylesheet" href="../assets/styles-Bh7Pnjui.css" />
</head>
<body>
<img src="../assets/foo-CxmWeBHm.svg" />
Expand Down Expand Up @@ -3189,4 +3189,121 @@ describe('rollup-plugin-html', () => {
}
`);
});

it('can minify extracted CSS', async () => {
const rootDir = createApp({
'styles.css': css`
p {
font-weight: bold;
}
`,
});

const config = {
plugins: [
rollupPluginHTML({
rootDir,
input: {
html: html`
<html>
<head>
<link rel="stylesheet" href="./styles.css" />
</head>
<body></body>
</html>
`,
},
minifyCss: true,
}),
],
};

const build = await rollup(config);
const { chunks, assets, assetsUnformatted } = await generateTestBundle(build, outputConfig);

expect(Object.keys(chunks)).to.have.lengthOf(1);
expect(Object.keys(assets)).to.have.lengthOf(2);

expect(assets).to.have.keys(['assets/styles-DPU2l-t7.css', 'index.html']);

expect(assets['index.html']).to.equal(html`
<html>
<head>
<link rel="stylesheet" href="assets/styles-DPU2l-t7.css" />
</head>
<body></body>
</html>
`);

expect(assetsUnformatted['assets/styles-DPU2l-t7.css']).to.equal('p{font-weight:700}');
});

it('can bundle extracted CSS', async () => {
const rootDir = createApp({
'themes/theme-light.css': css`
body {
background-color: #fff;
}
`,
'themes/theme-dark.css': css`
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}
`,
'styles.css': css`
@import './themes/theme-light.css';
@import './themes/theme-dark.css';
`,
});

const config = {
plugins: [
rollupPluginHTML({
rootDir,
input: {
html: html`
<html>
<head>
<link rel="stylesheet" href="./styles.css" />
</head>
<body></body>
</html>
`,
},
bundleCss: true,
}),
],
};

const build = await rollup(config);
const { chunks, assets } = await generateTestBundle(build, outputConfig);

expect(Object.keys(chunks)).to.have.lengthOf(1);
expect(Object.keys(assets)).to.have.lengthOf(2);

expect(assets).to.have.keys(['assets/styles-Dc1pq4Qu.css', 'index.html']);

expect(assets['index.html']).to.equal(html`
<html>
<head>
<link rel="stylesheet" href="assets/styles-Dc1pq4Qu.css" />
</head>
<body></body>
</html>
`);

expect(assets['assets/styles-Dc1pq4Qu.css']).to.equal(css`
body {
background-color: #fff;
}

@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}
`);
});
});
6 changes: 5 additions & 1 deletion packages/rollup-plugin-html/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function generateTestBundle(build: RollupBuild, outputConfig: Outpu
const { output } = await build.generate(outputConfig);
const chunks: Record<string, string> = {};
const assets: Record<string, string | Uint8Array> = {};
const assetsUnformatted: Record<string, string | Uint8Array> = {};

for (const file of output) {
const filename = file.fileName;
Expand All @@ -57,17 +58,20 @@ export async function generateTestBundle(build: RollupBuild, outputConfig: Outpu
chunks[filename] = formatter ? formatter(file.code) : file.code;
} else if (file.type === 'asset') {
let code = file.source;
let codeUnformatted = file.source;
if (typeof code !== 'string' && filename.endsWith('.css')) {
code = Buffer.from(code).toString('utf8');
codeUnformatted = Buffer.from(codeUnformatted).toString('utf8');
}
if (typeof code === 'string' && formatter) {
code = formatter(code);
}
assets[filename] = code;
assetsUnformatted[filename] = codeUnformatted;
}
}

return { output, chunks, assets };
return { output, chunks, assets, assetsUnformatted };
}

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