Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce injectStylesAs config for html plugin #176

Merged
merged 1 commit into from
Jun 14, 2024
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/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
Loading