Skip to content

Commit

Permalink
Introduce injectStylesAs config for html plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
edoardocavazza committed Mar 6, 2024
1 parent 623f59f commit b190cf1
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-yaks-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chialab/esbuild-plugin-html": patch
---

Introduce `injectStylesAs` config.
11 changes: 10 additions & 1 deletion docs/guide/esbuild-plugin-html.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ The target of the plain scripts build (`type="text/javascript"`).

The target of the ES modules build (`type="module"`).

#### `injectStylesAs`

The method to inject styles in the document when imported in a JavaScript module.
It can be `link` or `script` (default).

#### `minifyOptions`

The options for the minification process. If the `htmlnano` module is installed, the plugin will minify the HTML output.

## How it works

**Esbuild Plugin HTML** instructs esbuild to load a HTML file as entrypoint. It parses the HTML and runs esbuild on scripts, styles, assets and icons.
Expand Down Expand Up @@ -117,7 +126,7 @@ This will result in producing two bundles:

### Styles

It supports both `<link rel="stylesheet">` and `<style>` nodes for styling.
The plugins collects `<link rel="stylesheet">` entrypoints, `<style>` nodes and CSS imports in JavaScript modules.

**Sample**

Expand Down
2 changes: 1 addition & 1 deletion packages/esbuild-plugin-html/lib/collectAssets.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function collectAsset($, element, attribute, options, helpers) {

/**
* Collect and bundle each node with a src reference.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
export async function collectAssets($, dom, options, helpers) {
const builds = await Promise.all([
Expand Down
4 changes: 2 additions & 2 deletions packages/esbuild-plugin-html/lib/collectIcons.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function collectIcon($, element, icon, rel, shortcut, options, help

/**
* Collect and bundle apple icons.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
async function collectAppleIcons($, dom, options, helpers) {
let remove = true;
Expand Down Expand Up @@ -173,7 +173,7 @@ async function collectAppleIcons($, dom, options, helpers) {

/**
* Collect and bundle favicons.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
export async function collectIcons($, dom, options, helpers) {
const { resolve, load } = helpers;
Expand Down
2 changes: 1 addition & 1 deletion packages/esbuild-plugin-html/lib/collectScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export async function collectScreen($, element, screen, options, helpers) {

/**
* Collect and bundle apple screens.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
export async function collectScreens($, dom, options, helpers) {
const splashElement = dom.find('link[rel*="apple-touch-startup-image"]').last();
Expand Down
29 changes: 20 additions & 9 deletions packages/esbuild-plugin-html/lib/collectScripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isRelativeUrl } from '@chialab/node-resolve';
* @param {import('esbuild').Format} format Build format.
* @param {string} type Script type.
* @param {{ [key: string]: string }} attrs Script attrs.
* @param {import('./index.js').BuildOptions} options Build options.
* @param {import('./index.js').CollectOptions<{ injectStylesAs: 'script' | 'link' }>} options Build options.
* @param {import('./index.js').Helpers} helpers Helpers.
* @returns {Promise<import('@chialab/esbuild-rna').OnTransformResult[]>} Plain build.
*/
Expand Down Expand Up @@ -94,12 +94,13 @@ async function innerCollect($, dom, elements, target, format, type, attrs = {},
});

if (styleFiles.length) {
const script = $('<script>');
for (const attrName in attrs) {
$(script).attr(attrName, attrs[attrName]);
}
$(script).attr('type', type);
$(script).html(`(function() {
if (options.injectStylesAs === 'script') {
const script = $('<script>');
for (const attrName in attrs) {
$(script).attr(attrName, attrs[attrName]);
}
$(script).attr('type', type);
$(script).html(`(function() {
function loadStyle(url) {
var l = document.createElement('link');
l.rel = 'stylesheet';
Expand All @@ -115,15 +116,25 @@ ${styleFiles
})
.join('\n')}
}());`);
dom.find('head').append(script);
dom.find('head').append(script);
} else {
styleFiles.forEach((outName) => {
const fullOutFile = path.join(options.workingDir, outName);
const outputPath = helpers.resolveRelativePath(fullOutFile, options.entryDir, '');
const link = $('<link>');
link.attr('rel', 'stylesheet');
link.attr('href', outputPath);
dom.find('head').append(link);
});
}
}

return [result];
}

/**
* Collect and bundle each <script> reference.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{ injectStylesAs: 'script' | 'link' }>}
*/
export async function collectScripts($, dom, options, helpers) {
const moduleElements = dom.find('script[src][type="module"], script[type="module"]:not([src])').get();
Expand Down
2 changes: 1 addition & 1 deletion packages/esbuild-plugin-html/lib/collectStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isRelativeUrl } from '@chialab/node-resolve';

/**
* Collect and bundle each <link> reference.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
export async function collectStyles($, dom, options, helpers) {
const elements = dom
Expand Down
2 changes: 1 addition & 1 deletion packages/esbuild-plugin-html/lib/collectWebManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const MANIFEST_ICONS = [

/**
* Collect and bundle webmanifests.
* @type {import('./index').Collector}
* @type {import('./index').Collector<{}>}
*/
export async function collectWebManifest($, dom, options, helpers) {
const htmlElement = dom.find('html');
Expand Down
28 changes: 25 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 {'link' | 'script'} [injectStylesAs]
* @property {import('htmlnano').HtmlnanoOptions} [minifyOptions]
*/

Expand All @@ -33,6 +34,11 @@ const loadHtml = /** @type {typeof cheerio.load} */ (cheerio.load || cheerio.def
* @property {(string | string[])[]} target
*/

/**
* @typedef {BuildOptions & T} CollectOptions
* @template {object} T
*/

/**
* @typedef {Object} Helpers
* @property {(ext: string, suggestion?: string) => string} createEntry
Expand All @@ -46,15 +52,21 @@ const loadHtml = /** @type {typeof cheerio.load} */ (cheerio.load || cheerio.def
*/

/**
* @typedef {($: import('cheerio').CheerioAPI, dom: import('cheerio').Cheerio<import('cheerio').Document>, options: BuildOptions, helpers: Helpers) => Promise<import('@chialab/esbuild-rna').OnTransformResult[]>} Collector
* @typedef {($: import('cheerio').CheerioAPI, dom: import('cheerio').Cheerio<import('cheerio').Document>, options: CollectOptions<T>, helpers: Helpers) => Promise<import('@chialab/esbuild-rna').OnTransformResult[]>} Collector
* @template {object} T
*/

/**
* A HTML loader plugin for esbuild.
* @param {PluginOptions} options
* @returns An esbuild plugin.
*/
export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020', minifyOptions = {} } = {}) {
export default function ({
scriptsTarget = 'es2015',
modulesTarget = 'es2020',
minifyOptions = {},
injectStylesAs = 'script',
} = {}) {
/**
* @type {import('esbuild').Plugin}
*/
Expand Down Expand Up @@ -252,7 +264,17 @@ export default function ({ scriptsTarget = 'es2015', modulesTarget = 'es2020', m
results.push(...(await collectIcons($, root, collectOptions, helpers)));
results.push(...(await collectAssets($, root, collectOptions, helpers)));
results.push(...(await collectStyles($, root, collectOptions, helpers)));
results.push(...(await collectScripts($, root, collectOptions, helpers)));
results.push(
...(await collectScripts(
$,
root,
{
...collectOptions,
injectStylesAs,
},
helpers
))
);

let resultHtml = $.html().replace(/\n\s*$/gm, '');
if (minify) {
Expand Down
62 changes: 62 additions & 0 deletions packages/esbuild-plugin-html/test/test.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,68 @@ describe('esbuild-plugin-html', () => {
<script src="index-33TQGLB6.js" type="application/javascript"></script>
</body>
</html>`);

expect(js.path).endsWith(path.join(path.sep, 'out', 'index-33TQGLB6.js'));
expect(js.text).toBe(`"use strict";
(() => {
// fixture/lib.js
var log = console.log.bind(console);
// fixture/index.js
window.addEventListener("load", () => {
log("test");
});
})();
`);

expect(css.path).endsWith(path.join(path.sep, 'out', 'index-UMVLUHQU.css'));
expect(css.text).toBe(`/* fixture/index.css */
html,
body {
margin: 0;
padding: 0;
}
`);
});

test('should bundle webapp with scripts injecting links', 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: '/',
chunkNames: '[name]-[hash]',
outdir: 'out',
format: 'esm',
bundle: true,
write: false,
plugins: [
htmlPlugin({
injectStylesAs: 'link',
}),
],
});

const [index, js, css] = outputFiles;

expect(outputFiles).toHaveLength(3);

expect(index.path).endsWith(path.join(path.sep, 'out', 'index.iife.html'));
expect(index.text).toBe(`<!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>
<link rel="stylesheet" href="index-UMVLUHQU.css">
</head>
<body>
<script src="index-33TQGLB6.js" type="application/javascript"></script>
</body>
</html>`);

expect(js.path).endsWith(path.join(path.sep, 'out', 'index-33TQGLB6.js'));
Expand Down

0 comments on commit b190cf1

Please sign in to comment.