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

chore: add @rollup/plugin-html #57

Merged
merged 10 commits into from
Nov 29, 2019
10 changes: 5 additions & 5 deletions packages/auto-install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ npm install @rollup/plugin-inject --save-dev
Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files) and import the plugin:

```js
import auto from "@rollup/plugin-auto-install";
import resolve from "rollup-plugin-node-resolve";
import auto from '@rollup/plugin-auto-install';
import resolve from 'rollup-plugin-node-resolve';

export default {
input: "src/index.js",
input: 'src/index.js',
output: {
dir: "output",
format: "cjs"
dir: 'output',
format: 'cjs'
},
plugins: [auto(), resolve()]
};
Expand Down
153 changes: 153 additions & 0 deletions packages/html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
[npm]: https://img.shields.io/npm/v/@rollup/plugin-html
[npm-url]: https://www.npmjs.com/package/@rollup/plugin-html
[size]: https://packagephobia.now.sh/badge?p=@rollup/plugin-html
[size-url]: https://packagephobia.now.sh/result?p=@rollup/plugin-html

[![npm][npm]][npm-url]
[![size][size]][size-url]
[![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com)

# @rollup/plugin-html

🍣 A Rollup plugin which creates HTML files to serve Rollup bundles.

Please see [Supported Output Formats](#supported-output-formats) for information about using this plugin with output formats other than `esm` (`es`), `iife`, and `umd`.

## Requirements

This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v8.0.0+) and Rollup v1.20.0+.

## Install

Using npm:

```console
npm install @rollup/plugin-html --save-dev
```

## Usage

Create a `rollup.config.js` [configuration file](https://www.rollupjs.org/guide/en/#configuration-files) and import the plugin:

```js
const html = require('@rollup/plugin-html');

module.exports = {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [html()]
};
```

Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#command-line-reference) or the [API](https://www.rollupjs.org/guide/en/#javascript-api).

Once run successfully, an HTML file should be written to the bundle output destination.

## Options

### `attributes`

Type: `Object`<br>
Default: `{ html: { lang: 'en' }, link: null, script: null }`

Specifies additional attributes for `html`, `link`, and `script` elements. For each property, provide an object with key-value pairs that represent an HTML element attribute name and value. By default, the `html` element is rendered with an attribute of `lang="en"`.

_Note: If using the `es` / `esm` output format, `{ type: 'module'}` is automatically added to `attributes.script`._

### `fileName`

Type: `String`<br>
Default: `'index.html'`

Specifies the name of the HTML to emit.

### `publicPath`

Type: `String`<bri3ty3
Default: `''`

Specifies a path to prepend to all bundle assets (files) in the HTML output.

### `template`

Type: `Function`<br>
Default: `internal function`
Returns: `String`

Specifies a function that provides the rendered source for the HTML output. The function should be in the form of:

```js
const template = ({ attributes, bundle, files, publicPath, title }) => { ... }
```

- `attributes`: Corresponds to the `attributes` option passed to the plugin
- `bundle`: An `Object` containing key-value pairs of [`AssetInfo` or `ChunkInfo`](https://rollupjs.org/guide/en/#generatebundle)
- `files`: An `Array` of `AssetInfo` or `ChunkInfo` containing any entry (`isEntry: true`) files, and any asset (`isAsset: true`) files in the bundle that will be emitted
- `publicPath`: Corresponds to the `publicPath` option passed to the plugin
- `title`: Corresponds to the `title` option passed to the plugin

By default this is handled internally and produces HTML in the following format:

```html
<!DOCTYPE html>
<html ${attributes}>
<head>
<meta charset="utf-8" />
<title>${title}</title>
${links}
</head>
<body>
${scripts}
</body>
</html>
```

Where `${links}` represents all `<link ..` tags for CSS and `${scripts}` represents all `<script...` tags for JavaScript files.

### `title`

Type: `String`<br>
Default: `'Rollup Bundle'`

Specifies the HTML document title.

## Exports

### `makeHtmlAttributes(attributes)`

Parameters: `attributes`, Type: `Object`<br>
Returns: `String`

Consumes an object with key-value pairs that represent an HTML element attribute name and value. The function returns all pairs as a space-separated string of valid HTML element attributes. e.g.

```js
const { makeHtmlAttributes } = require('@rollup/plugin-html');

makeHtmlAttributes({ lang: 'en', 'data-batcave': 'secret' });
// -> 'lang="en" data-batcave="secret"'
```

## Supported Output Formats

By default, this plugin supports the `esm` (`es`), `iife`, and `umd` [output formats](https://rollupjs.org/guide/en/#outputformat), as those are most commonly used as browser bundles. Other formats can be used, but will require using the [`template`](#template) option to specify a custom template function which renders the unique requirements of other formats.

### `amd`

Will likely require use of RequireJS semantics, which allows only for a single entry `<script>` tag. If more entry chunks are emitted, these need to be loaded via a proxy file. RequireJS would also need to be a dependency and added to the build: https://requirejs.org/docs/start.html.

### `system`

Would require a separate `<script>` tag first that adds the `s.js` minimal loader. Loading modules might then resemble: `<script>System.import('./batman.js')</script>`.

## Attribution

This plugin was inspired by and is based upon [mini-html-webpack-plugin](https://github.com/styleguidist/mini-html-webpack-plugin) by Juho Vepsäläinen and Artem Sapegin, with permission.

## Meta

[CONTRIBUTING](/.github/CONTRIBUTING.md)

[LICENSE (MIT)](/LICENSE)
106 changes: 106 additions & 0 deletions packages/html/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { extname } = require('path');
shellscape marked this conversation as resolved.
Show resolved Hide resolved

const getFiles = (bundle) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should ONLY get entry point files (isEntry: true) and ignore the rest. Reason:

  • For IIFE and UMD formats, there will only be a single entry point anyway
  • All other formats import their dependencies themselves. If you add all files as script tags, you will at least destroy dynamic lazy loading of chunks (if it works at all).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree here. Various assets can be added to a bundle's files via plugins, and we should pick those up. CSS is the major example here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function puts all files .js files into an array and throws away all meta information. It is virtually impossible to distiguish the auto-generated chunks from the entry points. This makes the plugin rather useless for code-splitting IMO. Either you provide some meta information, or you ignore the non-entry chunks. I would be very confused if a CSS plugin was broken if you ignored the auto-generated chunks, as they are still imported by the main JavaScript. And maybe this even breaks CSS plugins in so far as maybe their chunks are meant to be loaded dynamically together with their content while you make them load up-front instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes the case for providing the bundle metadata to the template option function for complex builds that the default template doesn't suit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be sufficient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With regard to my initial comment, maybe I mis-stated what I mean: This function should of course return all assets, but with regard to chunks, it should only return entry chunks, as all non-entry chunks (read: .js files) will be loaded automatically by the entry chunks, no additional work necessary. But additionally having the bundle available to the template still would not hurt.

const files = Object.values(bundle).filter(
(file) => file.isEntry || file.type === 'asset' || file.isAsset
);
const result = {};
for (const file of files) {
const { fileName } = file;
const extension = extname(fileName).substring(1);
result[extension] = (result[extension] || []).concat(file);
}

return result;
};

const makeHtmlAttributes = (attributes) => {
if (!attributes) {
return '';
}

const keys = Object.keys(attributes);
// eslint-disable-next-line no-param-reassign
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
};

const defaultTemplate = async ({ attributes, files, publicPath, title }) => {
const scripts = (files.js || [])
.map(({ fileName }) => {
const attrs = makeHtmlAttributes(attributes.script);
return `<script src="${publicPath}${fileName}"${attrs}></script>`;
})
.join('\n');

const links = (files.css || [])
.map(({ fileName }) => {
const attrs = makeHtmlAttributes(attributes.link);
return `<link href="${publicPath}${fileName}" rel="stylesheet"${attrs}>`;
})
.join('\n');

return `
<!doctype html>
<html${makeHtmlAttributes(attributes.html)}>
<head>
<meta charset="utf-8">
<title>${title}</title>
${links}
</head>
<body>
${scripts}
</body>
</html>`;
};

const supportedFormats = ['es', 'esm', 'iife', 'umd'];

const defaults = {
attributes: {
link: null,
html: { lang: 'en' },
script: null
},
fileName: 'index.html',
publicPath: '',
template: defaultTemplate,
title: 'Rollup Bundle'
};

const html = (opts = {}) => {
const { attributes, fileName, publicPath, template, title } = Object.assign({}, defaults, opts);
return {
name: 'html',

async generateBundle(output, bundle) {
if (!supportedFormats.includes(output.format) && !opts.template) {
this.warn(
`plugin-html: The output format '${
output.format
}' is not directly supported. A custom \`template\` is probably required. Supported formats include: ${supportedFormats.join(
', '
)}`
);
}

if (output.format === 'esm' || output.format === 'es') {
attributes.script = Object.assign({}, attributes.script, { type: 'module' });
}

const files = getFiles(bundle);
const source = await template({ attributes, bundle, files, publicPath, title });

const htmlFile = {
type: 'asset',
source,
name: 'Rollup HTML Asset',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name will be ignored as a fileName is provided.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. We need to update the Rollup docs as this is a copy/paste/modify from them. I'll see about doing that this week.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is written in the first paragraph after the code block here: https://rollupjs.org/guide/en/#thisemitfileemittedfile-emittedchunk--emittedasset--string

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did read that, but with the code sample above that having both and both being optional, I had assumed it would be prudent to set name in the event that something was inspecting it down the line. Some extra verbiage there could be useful to point out there's no need to set the other property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. It does not hurt adding the name, it is just ignored.

fileName
};

this.emitFile(htmlFile);
}
};
};

module.exports = html;
module.exports.makeHtmlAttributes = makeHtmlAttributes;
56 changes: 56 additions & 0 deletions packages/html/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@rollup/plugin-html",
"version": "0.1.0",
"publishConfig": {
"access": "public"
},
"description": "Creates HTML files to serve Rollup bundles",
"license": "MIT",
"repository": "rollup/plugins",
"author": "Andrew Powell <andrew@shellscape.org>",
"homepage": "https://github.com/rollup/plugins/packages/beep",
"bugs": "https://github.com/rollup/plugins/issues",
"main": "lib/index.js",
"engines": {
"node": ">= 8.0.0"
},
"scripts": {
"ci:coverage": "nyc pnpm run test && nyc report --reporter=text-lcov > coverage.lcov",
"ci:lint": "pnpm run lint && pnpm run security",
"ci:lint:commits": "commitlint --from=${CIRCLE_BRANCH} --to=${CIRCLE_SHA1}",
"ci:test": "pnpm run test -- --verbose",
"lint": "pnpm run lint:js && pnpm run lint:docs && pnpm run lint:package",
"lint:docs": "prettier --single-quote --write *.md",
"lint:js": "eslint --fix --cache lib test",
"lint:package": "prettier --write package.json --plugin=prettier-plugin-package",
"prepublishOnly": "pnpm run lint",
"security": "echo 'pnpm needs `npm audit` support'",
"test": "ava"
},
"files": [
"lib",
"README.md",
"LICENSE"
],
"keywords": [
"rollup",
"plugin",
"html",
"template"
],
"peerDependencies": {
"rollup": "^1.20.0"
},
"devDependencies": {
"rollup": "^1.27.5",
"rollup-plugin-postcss": "^2.0.3"
},
"ava": {
"files": [
"!**/fixtures/**",
"!**/helpers/**",
"!**/recipes/**",
"!**/types.ts"
]
}
}
Empty file.
1 change: 1 addition & 0 deletions packages/html/test/fixtures/joker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* { width: 100%; }
2 changes: 2 additions & 0 deletions packages/html/test/fixtures/joker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line
import style from './joker.css';
2 changes: 2 additions & 0 deletions packages/html/test/fixtures/robin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line
import * as batman from './batman.js';
Loading