Skip to content

Commit

Permalink
Merge pull request #168 from chialab/minify-html
Browse files Browse the repository at this point in the history
Support HTML minification with `htmlnano`
  • Loading branch information
edoardocavazza authored Mar 5, 2024
2 parents c1130ac + 550fdcb commit 0bf6ad9
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-shirts-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chialab/esbuild-plugin-html": patch
---

Support HTML minification with `htmlnano`.
37 changes: 37 additions & 0 deletions docs/guide/esbuild-plugin-html.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,40 @@ This will result in:
```

It also update `<link rel="manifest">` content if found.

## Minify

The plugin will honor the `minify` option from esbuild if the `htmlnano` module is installed.

::: code-group

```sh[npm]
npm i -D @chialab/esbuild-plugin-html htmlnano
```

```sh[yarn]
yarn add -D @chialab/esbuild-plugin-html htmlnano
```

```sh[pnpm]
pnpm add -D @chialab/esbuild-plugin-html htmlnano
```

:::

Configuration can be passed with the `minifyOptions` property. Please refer to the [htmlnano documentation](https://htmlnano.netlify.app/modules) for more information.

```js
import htmlPlugin from '@chialab/esbuild-plugin-html';
import esbuild from 'esbuild';

await esbuild.build({
plugins: [
htmlPlugin({
minifyOptions: {
collapseWhitespace: true,
},
}),
],
});
```
2 changes: 1 addition & 1 deletion packages/esbuild-plugin-html/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ esbuild.build({
sourcemap: true,
format: 'esm',
platform: 'node',
external: ['@chialab/esbuild-rna', '@chialab/node-resolve'],
external: ['@chialab/esbuild-rna', '@chialab/node-resolve', 'htmlnano'],
banner: {
js: `import { dirname as __pathDirname } from 'path';
import { createRequire as __moduleCreateRequire } from 'module';
Expand Down
33 changes: 30 additions & 3 deletions packages/esbuild-plugin-html/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const loadHtml = /** @type {typeof cheerio.load} */ (cheerio.load || cheerio.def
* @property {string} [entryNames]
* @property {string} [chunkNames]
* @property {string} [assetNames]
* @property {import('htmlnano').HtmlnanoOptions} [minifyOptions]
*/

/**
Expand Down Expand Up @@ -53,15 +54,15 @@ const loadHtml = /** @type {typeof cheerio.load} */ (cheerio.load || cheerio.def
* @param {PluginOptions} options
* @returns An esbuild plugin.
*/
export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020' } = {}) {
export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020', minifyOptions = {} } = {}) {
/**
* @type {import('esbuild').Plugin}
*/
const plugin = {
name: 'html',
setup(pluginBuild) {
const build = useRna(plugin, pluginBuild);
const { write = true } = build.getOptions();
const { write = true, minify = false } = build.getOptions();
const outDir = build.getOutDir();
const cwd = build.getWorkingDir();
if (!outDir) {
Expand All @@ -77,6 +78,11 @@ export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020' }
const entryPoints = [];
const workingDir = build.getWorkingDir();

/**
* @type {import('esbuild').Message[]}
*/
const warnings = [];

build.onStart(() => {
entryPoints.splice(0, entryPoints.length);
});
Expand Down Expand Up @@ -248,8 +254,28 @@ export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020' }
results.push(...(await collectStyles($, root, collectOptions, helpers)));
results.push(...(await collectScripts($, root, collectOptions, helpers)));

let resultHtml = $.html().replace(/\n\s*$/gm, '');
if (minify) {
await import('htmlnano')
.then(async ({ default: htmlnano }) => {
resultHtml = (await htmlnano.process(resultHtml, minifyOptions)).html;
})
.catch(() => {
warnings.push({
id: 'missing-htmlnano',
pluginName: 'html',
text: `Unable to load "htmlnano" module for HTML minification.`,
location: null,
notes: [],
detail: '',
});
});
} else {
resultHtml = beautify.html(resultHtml);
}

return {
code: beautify.html($.html().replace(/\n\s*$/gm, '')),
code: resultHtml,
loader: 'file',
watchFiles: results.reduce((acc, result) => {
if (!result) {
Expand All @@ -266,6 +292,7 @@ export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020' }
}
return acc;
}, /** @type {string[]} */ ([])),
warnings,
};
});
},
Expand Down
9 changes: 9 additions & 0 deletions packages/esbuild-plugin-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
"@chialab/esbuild-rna": "^0.18.0",
"@chialab/node-resolve": "^0.18.0"
},
"peerDependencies": {
"htmlnano": "^2.0.0"
},
"peerDependenciesMeta": {
"htmlnano": {
"optional": true
}
},
"devDependencies": {
"@jimp/custom": "^0.22.0",
"@jimp/jpeg": "^0.22.0",
Expand All @@ -44,6 +52,7 @@
"@types/js-beautify": "^1.13.3",
"cheerio": "^1.0.0-rc.12",
"esbuild": "^0.19.0",
"htmlnano": "^2.1.0",
"js-beautify": "^1.14.0",
"rimraf": "^5.0.1",
"typescript": "^5.0.0"
Expand Down
64 changes: 64 additions & 0 deletions packages/esbuild-plugin-html/test/test.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1341,4 +1341,68 @@ html {
expect(js.path).endsWith(path.join(path.sep, 'out', 'index.js'));
expect(css.path).endsWith(path.join(path.sep, 'out', 'index.css'));
});

it('should minify html', async () => {
const { outputFiles } = await esbuild.build({
absWorkingDir: fileURLToPath(new URL('.', import.meta.url)),
entryPoints: [fileURLToPath(new URL('fixture/index.iife.html', import.meta.url))],
sourceRoot: '/',
publicPath: '/public',
entryNames: '[dir]/[name]',
chunkNames: '[name]',
outdir: 'out',
format: 'esm',
bundle: true,
minify: true,
write: false,
plugins: [htmlPlugin()],
});

const [index] = outputFiles;
expect(index.text).to.be
.equal(`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script type="application/javascript">(function() {
function loadStyle(url) {
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = url;
document.head.appendChild(l);
}
loadStyle('/public/index.css');
}());</script></head><body> <script src="/public/index.js" type="application/javascript"></script> </body></html>`);
});

it('should minify html with minify option', async () => {
const { outputFiles } = await esbuild.build({
absWorkingDir: fileURLToPath(new URL('.', import.meta.url)),
entryPoints: [fileURLToPath(new URL('fixture/index.iife.html', import.meta.url))],
sourceRoot: '/',
publicPath: '/public',
entryNames: '[dir]/[name]',
chunkNames: '[name]',
outdir: 'out',
format: 'esm',
bundle: true,
minify: true,
write: false,
plugins: [
htmlPlugin({
minifyOptions: {
removeAttributeQuotes: true,
},
}),
],
});

const [index] = outputFiles;
expect(index.text).to.be
.equal(`<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width, initial-scale=1.0"><title>Document</title><script type=application/javascript>(function() {
function loadStyle(url) {
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = url;
document.head.appendChild(l);
}
loadStyle('/public/index.css');
}());</script></head><body> <script src=/public/index.js type=application/javascript></script> </body></html>`);
});
});
Loading

0 comments on commit 0bf6ad9

Please sign in to comment.