7 Commits

52 changed files with 1717 additions and 834 deletions

View File

@@ -1,86 +1,2 @@
# @rollup/plugin-html ChangeLog
## v1.0.2
_2023-01-20_
### Bugfixes
- fix: types should come first in exports [#1403](https://github.com/rollup/plugins/pull/1403)
## v1.0.1
_2022-10-21_
### Updates
- chore: update rollup dependencies ([3038271](https://github.com/rollup/plugins/commit/303827191ede6b2e4eade96c6968ed16a587683f))
## v1.0.0
_2022-10-09_
### Breaking Changes
- fix: prepare for Rollup 3 [#1294](https://github.com/rollup/plugins/pull/1294)
### Bugfixes
- fix: function signature of html updated [#975](https://github.com/rollup/plugins/pull/975)
## v0.2.3
_2021-02-14_
### Bugfixes
- fix: package.json files (#802)
## v0.2.2
_2021-01-29_
### Updates
- chore: run build before publish (ce65c12)
- chore: upgrade TypeScript (#713)
- refactor: refactor to typescript (#634)
- chore: update dependencies (6c8614c)
## v0.2.1
_2021-01-29_
### Updates
- chore: run build before publish (ce65c12)
- chore: upgrade TypeScript (#713)
- refactor: refactor to typescript (#634)
- chore: update dependencies (6c8614c)
## v0.2.0
_2020-05-02_
### Features
- feat: support custom meta element creation (#308)
### Updates
- test: update snapshots for rollup v2 (c09509f)
## v0.1.1
_2020-01-04_
### Updates
- docs: fix <br> tag (#89)
- test: change tests to serial to avoid weird snapshot conflicts (a492ce7)
## 0.1.0
_2019-11-29_
- First Release
# 0.0.1
Initial private release

182
README.md
View File

@@ -1,33 +1,41 @@
# Work-in-progress changelog
[npm]: https://img.shields.io/npm/v/rollup-plugin-html-entry2
[npm-url]: https://www.npmjs.com/package/rollup-plugin-html-entry2
[size]: https://packagephobia.now.sh/badge?p=rollup-plugin-html-entry2
[size-url]: https://packagephobia.now.sh/result?p=rollup-plugin-html-entry2
[handlebars]: https://www.npmjs.com/package/handlebars
[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
# rollup-plugin-html-entry2
| :warning: WARNING |
|:-------------------------------------------------------------------|
| **Experimental-stage** plugin. Expect bugs and missing features... |
🍣 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`.
A(nother) rollup plugin that tries to teach Rollup to start from an HTML entry, and the use of (multiple) HTML files in general.
The goal is to include assets referenced by the HTML file into the build-process as to copy/inline where appropriate and
optionally optimize them. Without having to seperatly copy them to the output directory.
When building web-applications a HTML-file is simply the logical entry point into your application. \
Inspired (and forked) by the original [@rollup/plugin-html](https://www.npmjs.com/package/@rollup/plugin-html),
this plugin will also allow you to transform the source files by any HTML-templating engine such as [handlebars].
Please see [Supported Output Formats](#supported-output-formats) for information about using this plugin with output formats other than `esm` (`es`).
## Requirements
This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v14.0.0+) and Rollup v1.20.0+.
This plugin requires an [LTS](https://github.com/nodejs/Release) Node version (v18.0.0+) and Rollup v3.?.?+.
## Install
Using npm:
```console
npm install @rollup/plugin-html --save-dev
npm install rollup-plugin-html-entry2 --save-dev
```
## Usage
@@ -35,131 +43,85 @@ npm install @rollup/plugin-html --save-dev
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');
import html from 'rollup-plugin-html-entry2';
module.exports = {
input: 'src/index.js',
export default {
input: 'src/index.html',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [html()]
};
```
!! To use 'import x from y' syntax you might need to set `"type": "module"` in your `package.json`.
[Javascript modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) are the _preferred_ way of writing modern Javascript.
Due to the early stage development of this plugin, old-style CommonJS modules are completely ignored for now.
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'`
### `meta`
Type: `Array[...object]`<br>
Default: `[{ charset: 'utf-8' }]`
Specifies attributes used to create `<meta>` elements. For each array item, provide an object with key-value pairs that represent `<meta>` element attribute names and values.
Specifies the name of the HTML to emit.
### `publicPath`
Type: `String`<br>
Default: `''`
Specifies a path to prepend to all bundle assets (files) in the HTML output.
### `template`
Type: `Function`<br>
Default: `internal function`
Default: `undefined`\
Returns: `String`
Specifies a function that provides the rendered source for the HTML output. The function should be in the form of:
Specifies a transform to be applied before parsing the HTML, this allows you to transform the sourcefile with a templating engine such as [handlebars] first.
```js
const template = ({ attributes, bundle, files, publicPath, title }) => { ... }
```
```javascript
import {rollup} from "rollup";
import handlebars from "handlebars";
import html from "rollup-plugin-html-entry2";
- `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>
${metas}
<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"'
async function build() {
await rollup({
input: 'index.hbs',
plugins: [
html({
transform(src) {
return handlebars.compile(src)({a: 'a'})
}
})
]
});
}
```
## 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.
By default, this plugin supports the `esm` (`es`). Any other format is currently untested as this plugin is in an early state, see [#status](#status)
### `amd`
## Status
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.
This plugin is in an early state. As such not everything that is supported yet, and the options may change.
### `system`
### (Rudimentarily) supported
- Importing JS via `<script src="..." type="module">` tags
- Importing assets using @rollup/plugin-url (which could use an update TBH)
- Compatibility with other plugins such as @rollup/plugin-node-resolve, @rollup/plugin-babel, @rollup/plugin-commonjs, @rollup/plugin-terser and rollup-plugin-livereload
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
### Not (yet) supported
- Inline scripts (i.e `<script>...</script>`)
- Plugins importing CSS files
- CommonJS (cjs) and IIFI output formats. (Is UMD actually ever used?)
- Overriding which tags to ignore/include
- Other (various) plugins such as those for HMR etc
- ...
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.
# Contibuting
## Meta
You can be helpful by testing, proving helpful feedback, expanding the documentation, responding to issues/questions being reported, resolving the many ToDo`s in the code, implementating features...\
[Get in touch](mailto:rollup-plugin-html-entry2@cerxes.net) or just dive into [the code](https://git.cerxes.net/rollup-apps/plugin-html) or [issues](https://git.cerxes.net/rollup-apps/plugin-html/issues)
[CONTRIBUTING](/.github/CONTRIBUTING.md)
[LICENSE (MIT)](/LICENSE)
# Notes
## git.cerxes.net
Once publicly released, the intent is to move the GIT-repository to github. Until that day though, it exists privately on this gitea server and corresponding npm-registry [npm.cerxes.net](https://npm.cerxes.net).\
TODO: change the links once this happens
## Prior work
- [Vite](https://vitejs.dev) seems to have already [done work])(https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts) to handle HTML in rollup.
- [rollup-plugin-html-entry](https://www.npmjs.com/package/rollup-plugin-html-entry) seems to be **dead**. Last version from 2020, there have been many changes to rollup`s plugin capabilities since then
- [@rollup/plugin-html](https://www.npmjs.com/package/@rollup/plugin-html) is where this project was originally forked from. Its main focus was to generate an HTML to serve the resulting bundle. Which is different from supporting HTML as entry point since it did not resolve assets used in the HTML. Besides the project setup, not much of the original has been kept...
- [@web/rollup-plugin-html](https://www.npmjs.com/package/@web/rollup-plugin-html) a plugin with similar intentions as this one (in active development anno 2023).

View File

@@ -1,18 +1,15 @@
{
"name": "@rollup-apps/plugin-html",
"name": "rollup-plugin-html-entry2",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"description": "Creates HTML files to serve Rollup bundles",
"description": "Teaches rollup how to deal with HTML, allows to use HTML-files as entry-points.",
"license": "MIT",
"repository": {
"url": "rollup/plugins",
"directory": "packages/html"
"type": "git",
"url": "https://git.cerxes.net/rollup-apps/plugin-html.git"
},
"author": "Andrew Powell <andrew@shellscape.org>",
"homepage": "https://github.com/rollup/plugins/tree/master/packages/html#readme",
"bugs": "https://github.com/rollup/plugins/issues",
"author": "Miel Truyen <miel.truyen@cerxes.net>",
"homepage": "https://git.cerxes.net/rollup-apps/plugin-html",
"bugs": "https://git.cerxes.net/rollup-apps/plugin-html/issues",
"type": "module",
"engines": {
"node": ">=18"
@@ -24,15 +21,17 @@
"import": "./dist/es/index.js"
},
"publishConfig": {
"registry": "https://npm.cerxes.net"
"registry": "https://npm.cerxes.net",
"access": "public"
},
"scripts": {
"build": "rollup -c",
"prerelease": "pnpm build",
"test": "ava",
"ci:coverage": "nyc pnpm test && nyc report --reporter=text-lcov > coverage.lcov",
"ci:lint": "pnpm build && pnpm lint-staged",
"ci:test": "pnpm test -- --verbose"
"ci:test": "pnpm test -- --verbose",
"test": "ava",
"save-test": "ava --update-snapshots"
},
"files": [
"dist",
@@ -55,6 +54,10 @@
"optional": true
}
},
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"parse5": "^7.1.2"
},
"devDependencies": {
"@types/node": "^18.15.11",
"@rollup/plugin-typescript": "^11.1.0",
@@ -73,7 +76,11 @@
"rollup-plugin-delete": "^2.0.0",
"@babel/preset-typescript": "^7.21.4",
"nyc": "^15.1.0",
"lint-staged": "^13.2.1"
"lint-staged": "^13.2.1",
"handlebars": "^4.7.7",
"@rollup/plugin-url": "^8.0.1",
"chalk": "^5.2.0",
"rollup-plugin-livereload": "^2.0.5"
},
"types": "./types/index.d.ts",
"ava": {
@@ -90,7 +97,8 @@
"js": true
},
"nodeArguments": [
"--loader=ts-node/esm"
"--loader=ts-node/esm",
"--experimental-vm-modules"
]
}
}

136
pnpm-lock.yaml generated
View File

@@ -1,5 +1,13 @@
lockfileVersion: '6.0'
dependencies:
'@rollup/pluginutils':
specifier: ^5.0.1
version: 5.0.2(rollup@3.20.3)
parse5:
specifier: ^7.1.2
version: 7.1.2
devDependencies:
'@babel/core':
specifier: ^7.21.4
@@ -19,15 +27,24 @@ devDependencies:
'@rollup/plugin-typescript':
specifier: ^11.1.0
version: 11.1.0(rollup@3.20.3)(tslib@2.5.0)(typescript@5.0.4)
'@rollup/plugin-url':
specifier: ^8.0.1
version: 8.0.1(rollup@3.20.3)
'@types/node':
specifier: ^18.15.11
version: 18.15.11
ava:
specifier: ^5.2.0
version: 5.2.0
chalk:
specifier: ^5.2.0
version: 5.2.0
del-cli:
specifier: ^5.0.0
version: 5.0.0
handlebars:
specifier: ^4.7.7
version: 4.7.7
lint-staged:
specifier: ^13.2.1
version: 13.2.1
@@ -43,6 +60,9 @@ devDependencies:
rollup-plugin-delete:
specifier: ^2.0.0
version: 2.0.0
rollup-plugin-livereload:
specifier: ^2.0.5
version: 2.0.5
rollup-plugin-postcss:
specifier: ^4.0.2
version: 4.0.2(postcss@8.4.22)(ts-node@10.9.1)
@@ -549,6 +569,21 @@ packages:
typescript: 5.0.4
dev: true
/@rollup/plugin-url@8.0.1(rollup@3.20.3):
resolution: {integrity: sha512-8ajztphXb5e19dk3Iwjtm2eSYJR8jFQubZ8pJ1GG2MBMM7/qUedLnZAN+Vt4jqbcT/m27jfjIBocvrzV0giNRw==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.20.3)
make-dir: 3.1.0
mime: 3.0.0
rollup: 3.20.3
dev: true
/@rollup/pluginutils@5.0.2(rollup@3.20.3):
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
@@ -562,7 +597,6 @@ packages:
estree-walker: 2.0.2
picomatch: 2.3.1
rollup: 3.20.3
dev: true
/@trysound/sax@0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
@@ -587,7 +621,6 @@ packages:
/@types/estree@1.0.0:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/glob@7.2.0:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
@@ -1367,6 +1400,11 @@ packages:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: true
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
@@ -1409,7 +1447,6 @@ packages:
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
@@ -1525,7 +1562,6 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/function-bind@1.1.1:
@@ -1610,6 +1646,19 @@ packages:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
/handlebars@4.7.7:
resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==}
engines: {node: '>=0.4.7'}
hasBin: true
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.17.4
dev: true
/hard-rejection@2.1.0:
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
engines: {node: '>=6'}
@@ -1998,6 +2047,24 @@ packages:
wrap-ansi: 7.0.0
dev: true
/livereload-js@3.4.1:
resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==}
dev: true
/livereload@0.9.3:
resolution: {integrity: sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==}
engines: {node: '>=8.0.0'}
hasBin: true
dependencies:
chokidar: 3.5.3
livereload-js: 3.4.1
opts: 2.0.2
ws: 7.5.9
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: true
/load-json-file@7.0.1:
resolution: {integrity: sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -2161,6 +2228,12 @@ packages:
picomatch: 2.3.1
dev: true
/mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
hasBin: true
dev: true
/mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -2191,6 +2264,10 @@ packages:
kind-of: 6.0.3
dev: true
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: true
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -2205,6 +2282,10 @@ packages:
hasBin: true
dev: true
/neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
/node-preload@0.2.1:
resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==}
engines: {node: '>=8'}
@@ -2314,6 +2395,10 @@ packages:
mimic-fn: 4.0.0
dev: true
/opts@2.0.2:
resolution: {integrity: sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==}
dev: true
/p-defer@1.0.0:
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
engines: {node: '>=4'}
@@ -2444,6 +2529,12 @@ packages:
engines: {node: '>=12'}
dev: true
/parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies:
entities: 4.5.0
dev: false
/path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -2485,7 +2576,6 @@ packages:
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
@@ -3028,6 +3118,16 @@ packages:
del: 5.1.0
dev: true
/rollup-plugin-livereload@2.0.5:
resolution: {integrity: sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==}
engines: {node: '>=8.3'}
dependencies:
livereload: 0.9.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: true
/rollup-plugin-postcss@4.0.2(postcss@8.4.22)(ts-node@10.9.1):
resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==}
engines: {node: '>=10'}
@@ -3064,7 +3164,6 @@ packages:
hasBin: true
optionalDependencies:
fsevents: 2.3.2
dev: true
/run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -3441,6 +3540,14 @@ packages:
hasBin: true
dev: true
/uglify-js@3.17.4:
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
engines: {node: '>=0.8.0'}
hasBin: true
requiresBuild: true
dev: true
optional: true
/update-browserslist-db@1.0.11(browserslist@4.21.5):
resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
hasBin: true
@@ -3489,6 +3596,10 @@ packages:
isexe: 2.0.0
dev: true
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true
/wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@@ -3528,6 +3639,19 @@ packages:
signal-exit: 3.0.7
dev: true
/ws@7.5.9:
resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==}
engines: {node: '>=8.3.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: true
/y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
dev: true

View File

@@ -30,13 +30,14 @@ export function createConfig({ pkg, external = [] }) {
},
strictDeprecations: true,
output: [
{
format: 'cjs',
file: pkg.main,
exports: 'named',
footer: 'module.exports = Object.assign(exports.default, exports);',
sourcemap: true
},
// TODO: cjs output not supported for now
// {
// format: 'cjs',
// file: pkg.main,
// exports: 'named',
// footer: 'module.exports = Object.assign(exports.default, exports);',
// sourcemap: true
// },
{
format: 'es',
file: pkg.module,
@@ -57,7 +58,6 @@ export function createConfig({ pkg, external = [] }) {
...commonOpts,
babelHelpers: "bundled",
}),
// typescript({ sourceMap: true })]
]
};
}

34
src/html-module.ts Normal file
View File

@@ -0,0 +1,34 @@
// The HTML-Module is an internal helper structure to track the processing of an HTML file
// This is intended to be serialized into chunk-meta, so it can be cached. (thus keep any functions and circular references out of it)
// TODO: Actually making this serialiable (check rollupResolved, node, as we might no longer need them)
import type {
ModuleInfo,
ResolvedId,
} from 'rollup';
import type {
LoadedReference
} from "../types/load.d.ts";
import {DefaultTreeAdapterMap} from "parse5";
// Internal type
export type HtmlImport = LoadedReference & {
id: string;
resolved: ResolvedId|null;
// loaded: ModuleInfo|null;
node: DefaultTreeAdapterMap['element'];
referenceId: string|null;
placeholder: string,
index: number;
}
export type HtmlModule = {
// TODO might want to impose an own unique id, in case this changes after multiple builds
id: string;
name: string;
importers: Set<string|undefined>,
imports: HtmlImport[];
assetId?: string|null;
document?: DefaultTreeAdapterMap['document'];
}

View File

@@ -1,133 +1,358 @@
import { extname } from "node:path";
import type { Plugin, NormalizedOutputOptions, OutputBundle, EmittedAsset } from 'rollup';
import type {
Plugin,
OutputBundle,
OutputChunk,
OutputAsset,
NormalizedOutputOptions,
// ModuleInfo,
ResolvedId,
PreRenderedChunk,
RenderedChunk,
} from 'rollup';
import type { RollupHtmlOptions, RollupHtmlTemplateOptions } from '../types/index.d.ts';
import type {
LoadResult,
RollupHtmlOptions,
LoadNodeCallback,
LoadReference, BodyReference, AttributeReference, LoadFunction
} from '../types/index.d.ts';
const getFiles = (bundle: OutputBundle): RollupHtmlTemplateOptions['files'] => {
const result = {} as ReturnType<typeof getFiles>;
for (const file of Object.values(bundle)) {
const { fileName } = file;
const extension = extname(fileName).substring(1);
import {createFilter} from '@rollup/pluginutils';
import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";
import {readFile} from "node:fs/promises"
result[extension] = (result[extension] || []).concat(file);
}
import {makeLoader, makeInlineId} from "./loader.js";
import {HtmlImport, HtmlModule} from "./html-module.js";
return result;
import {dirname} from "node:path";
import posix from "node:path/posix";
import crypto from "node:crypto";
const defaults: RollupHtmlOptions = {
transform: (source: string)=>source,// NO-OP
load: makeLoader(),
resolve: ()=>true,
htmlFileNames: "[name].html",
include: [
'**/*.(html|hbs)',// html or handlebars
]
};
export const makeHtmlAttributes = (attributes: Record<string, any>): string => {
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,
meta,
publicPath,
title
}: RollupHtmlTemplateOptions) => {
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');
const metas = meta
.map((input) => {
const attrs = makeHtmlAttributes(input);
return `<meta${attrs}>`;
})
.join('\n');
return `
<!doctype html>
<html${makeHtmlAttributes(attributes.html)}>
<head>
${metas}
<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',
meta: [{ charset: 'utf-8' }],
publicPath: '',
template: defaultTemplate,
title: 'Rollup Bundle'
};
const modulePrefix = `// <html-module>`;
const moduleSuffix = `// </html-module>`;
export default function html(opts: RollupHtmlOptions = {}): Plugin {
const { attributes, fileName, meta, publicPath, template, title } = Object.assign(
const {
publicPath,
transform,
load,
htmlFileNames,
resolve,
include,
exclude,
} = Object.assign(
{},
defaults,
opts
);
if(publicPath){ throw new Error("TODO, do something with the public path or throw it out of the options. this is just to stop typescript complaining")}
let filter = createFilter(include, exclude, {});
let htmlModules = new Map<string, HtmlModule>();// todo clean this per new build?
let virtualSources = new Map<string, string>();
const pluginName = 'html2';
return {
name: 'html',
name: pluginName,// TODO: Need a better name, original plugin was just named `html` and might still make sense to use in conjunction with this one
async generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
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(
', '
)}`
);
}
resolveId: {
async handler(specifier: string,
importer: string | undefined,
options: { assertions: Record<string, string> }){
if(virtualSources.has(specifier)) return specifier;
if(!filter(specifier)) return;
if (output.format === 'es') {
attributes.script = Object.assign({}, attributes.script, {
type: 'module'
});
}
const files = getFiles(bundle);
const source = await template({
attributes,
bundle,
files,
meta,
publicPath,
title
// Let it be resolved like others (node_modules, project aliases, ..)
const resolved = await this.resolve(specifier, importer, {
skipSelf: true,
...options,
});
const htmlFile: EmittedAsset = {
type: 'asset',
if(resolved){
const moduleId = resolved.id;
const moduleExt = extname(resolved.id);
const moduleName = specifier.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any
const htmlModule : HtmlModule = htmlModules.get(moduleId) ?? {
id: resolved.id,
name: moduleName,
imports: [],
assetId: null,
importers: new Set(),
};
htmlModule.importers.add(importer);
htmlModules.set(htmlModule.id, htmlModule);
// TODO: trigger special handling when imported from a JS file (in which case we want might want to export a module returning the HTML, instead of HTML directly)
return {
...resolved,
meta: {
...resolved.meta,
[pluginName]: {name: specifier}
}
}
}
}
},
load: {
async handler(id: string) {
if(virtualSources.has(id)) return virtualSources.get(id);
if(!filter(id)) return;
// Load
const htmlModule = htmlModules.get(id);
if(htmlModule) {
const contents = await readFile(id, {encoding: "utf-8"});
const htmlSrc = transform ? await transform(contents, {
id,
}) : contents;
// Parse document and store it (TODO: check for watch mode, we should check if it needs reparsing or not)
const document = htmlModule.document = htmlModule.document ?? parseHtml(htmlSrc);
// Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes)
let htmlImports: HtmlImport[] = htmlModule.imports = [];
if (document.childNodes) {
let nodeQueue = document.childNodes;
do {
const nextQueue: DefaultTreeAdapterMap['childNode'][][] = [];
await Promise.all(nodeQueue.map(async (node) => {
const el = (<DefaultTreeAdapterMap['element']>node);
const loadFunction: LoadFunction = async ({
id: sourceId,
source,
name: 'Rollup HTML Asset',
fileName
};
type
})=>{
if(!sourceId){
sourceId = makeInlineId(id, node, 'js');
}
if(source){
virtualSources.set(sourceId, source);
}
this.emitFile(htmlFile);
const resolved = await this.resolve(sourceId, id, {
isEntry: type==='entryChunk',
});
if(!resolved){
throw new Error(`Could not resolve ${sourceId} from ${id}`);
}
const selfInfo = this.getModuleInfo(id);
const importName = (source && selfInfo?.meta[pluginName].name) ? makeInlineId(selfInfo?.meta[pluginName].name, node, extname(sourceId)) : undefined;
const htmlImport: HtmlImport = {
id: <string>sourceId,
resolved: resolved,
// loaded: loaded,
node: el,
type,
source,
referenceId:
(resolved && (['chunk','entryChunk'].includes(type!))) ? this.emitFile({
type: 'chunk', // Might want to adapt, or make configurable (see LoadType)
id: resolved.id,
name: importName,
importer: id,
}) : null,
placeholder: `html-import-${crypto.randomBytes(32).toString('base64')}`,
index: htmlImports.length,
}
htmlImports.push(htmlImport);
return htmlImport.placeholder;
}
let toLoad: LoadResult | undefined = load? await Promise.resolve(load({
node: el,
sourceId: id
}, loadFunction)) : undefined;
if (toLoad !== false) {
let asParent = (<DefaultTreeAdapterMap['parentNode']>node);
if (asParent.childNodes) {
nextQueue.push(asParent.childNodes);
}
}
}));
nodeQueue = nextQueue.flat();
} while (nodeQueue.length > 0);
}
let html = serializeHtml(htmlModule.document).replaceAll(/`/g,'\\\`').replaceAll(/\$\{/g,'\\${');
const moduleImports = [];
for(const htmlImport of htmlImports){
if(htmlImport.type === 'default') {
const assetId: string = `asset${moduleImports.length}`;
moduleImports.push(`import ${assetId} from "${htmlImport.id}";`);// TODO: This is just the easy & safe solution. Would prefer to have recognizable names, and reeuse when something is the exact same resource..
html = html.replace(htmlImport.placeholder, `\${${assetId}}`);// TODO: Should we be worried about windows absolute URLs here?
// }else if(htmlImport.type === 'entryChunk' && htmlImport.referenceId){
// html = html.replace(htmlImport.placeholder, `\${import.meta.ROLLUP_FILE_URL_${htmlImport.referenceId}\}`);
}else{
// TODO: this will probably not do for complicated cases ( presumably no other method then emitting the chunk as file, loading its result but excluding it from the output bundle)
// html = html.replace(htmlImport.placeholder, htmlImport.loaded?.code||htmlImport.source||'');
}
}
// TODO when importing html from .js this will not do. (
const htmlJSModule = [
...moduleImports,
``,
`export const html = \`${html}\`;`,
`export default html;`,
].join('\n');
return {
code: htmlJSModule,
};
}
}
},
outputOptions(options){
return {
...options,
entryFileNames: (chunkInfo)=>{
const htmlModule = chunkInfo.facadeModuleId ? htmlModules.get(chunkInfo.facadeModuleId!) : null;
const defaultOption = options.entryFileNames ?? "[name]-[hash].js";// This default is copied from the docs. TODO: don't like overwrite it this way, can we remove the need for this or fetch the true default?
if(htmlModule){
let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo);
if(fileName) {
return fileName;
}
}
return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo);
},
// TODO do we need to do the same for chunks?? (what if they're dynamically imported?)
}
},
resolveFileUrl(options){
const htmlModule = htmlModules.get(options.moduleId);
if(htmlModule){
// Simply use the relative path in our HTML-fileURLs instead of the default `new URL('${fileName}', document.baseURI).href`)
return `"${options.relativePath}"`;
}
},
banner: {
// Injects a tag so we know where our bundle starts so we can safely ignore other stuff addded via a banner (ie. live-reload)
order:'post',
handler(chunk: RenderedChunk){
if(chunk.facadeModuleId) {
const htmlModule = htmlModules.get(chunk.facadeModuleId);
if (htmlModule) {
return modulePrefix; // Overwrite any added banner with our own
}
}
return '';
}
},
async generateBundle(outputOptions, bundles){
const bundleItems = Object.entries(bundles);
const virtualBundles = new Set<string>();
const facadeToChunk = new Map<string,OutputChunk>();
const htmlResults = new Map<string, {chunk: OutputChunk, htmlModule: HtmlModule}>();
for(const [bundleName, bundle] of bundleItems) {
const chunk = (<OutputChunk>bundle);
if(chunk.facadeModuleId) {
facadeToChunk.set(chunk.facadeModuleId, chunk);
const htmlModule = htmlModules.get(chunk.facadeModuleId);
if(htmlModule){ htmlResults.set(bundleName, {chunk, htmlModule})}
else if(virtualSources.has(chunk.facadeModuleId)){
virtualBundles.add(bundleName);
}
}
}
for(const [bundleName, {chunk, htmlModule}] of htmlResults.entries()){
if(htmlModule. document) {
// Delete the placeholder chunk from the bundle and emit an asset file for the HTML instead.
deleteFromBundle(bundleName, bundles);
// Interpret the module and take its default export (TODO: if [NodeJS vm SourceTextModule](https://nodejs.org/api/vm.html#class-vmsourcetextmodule) ever lands, it would be cleaner to use that one instead of directly importing it)
let htmlContents: string;
// Take out the sourceMapUrl if any (it will not have been written yet and tends to cause a crash, we don't need it anyway))
let sanitizedCode = chunk.code;
// Use the modulePrefix to filter out prepended code that is not relevant for us (like live-reload)
const moduleStart = sanitizedCode.indexOf(modulePrefix);
if(moduleStart>=0){
sanitizedCode = sanitizedCode.slice(moduleStart+modulePrefix.length);
}
// Filter out any sourceMapping url that may have been added
const sourceMapRE = /\/\/# sourceMappingURL=(.+)/.exec(sanitizedCode);
if(sourceMapRE){
sanitizedCode = sanitizedCode.slice(0,sourceMapRE.index)+sanitizedCode.slice(sourceMapRE.index+sourceMapRE[0].length);
}
// Encode into a url that we can import(...)
// const importUrl = `data:text/javascript;base64,${Buffer.from(sanitizedCode).toString('base64')}`; // Safer, but unrecognizable if this throws an error
const importUrl = `data:text/javascript,${encodeURIComponent(sanitizedCode)}`; // Due to needed encoding still hard to read, but it might at least be recognizable by the user if it throws an error
try{
let htmlJsModule = await import(importUrl);
htmlContents = htmlJsModule.default;
}catch(err){
throw new Error([
`Failed to parse resulting HTML-module. Most likely this is due to a plugin that has altered the module in such a way that we cannot easely evaluate it in NodeJS.`,
`The code we tried to evaluate:`,
sanitizedCode.split('\n').map(x=>` ${x}`).join('\n'),
`The error we got:`,
err
].join('\n'))
// TODO: We could try to fallback as follows, but the issues are likely to persist in the end result
// for(const htmlImport of htmlModule.imports){
// if(htmlImport.referenceId) {
// const fileName = this.getFileName(htmlImport.referenceId);
// htmlImport.reference.set(fileName);
// }
// }
// serialized = serializeHtml(htmlModule.document);
}
// Inject the inlined chunks (TODO cleanup)
for(const htmlImport of htmlModule.imports){
const importResult = facadeToChunk.get(htmlImport.resolved?.id!);
if(importResult){
if(htmlImport.type === 'chunk') {
htmlContents = htmlContents.replace(htmlImport.placeholder, importResult.code);
}else if(htmlImport.type === 'entryChunk'){
const relPath = posix.relative(dirname(chunk.fileName), importResult.fileName);
htmlContents = htmlContents.replace(htmlImport.placeholder, relPath);
}
}
}
this.emitFile({
type: 'asset',
name: htmlModule.name,
fileName: chunk.fileName,
source: htmlContents,
});
}else{
throw new Error('something went wrong...');
}
}
for( const bundleName of virtualBundles.keys()){
deleteFromBundle(bundleName, bundles, false);
}
}
};
}
function deleteFromBundle(bundlename: string, bundle: OutputBundle, deleteMap: boolean = true){
delete bundle[bundlename];
if(deleteMap) {
delete bundle[`${bundlename}.map`];// Also delete any generated map files because they don't make any sense. (TODO: there seems to be no better way to detect this?)
}
}

110
src/loader-mappings.ts Normal file
View File

@@ -0,0 +1,110 @@
// The loader parses a DOM node and detects which resource (script, style, image, ...) needs to be loaded from it
import type {
NodeMapping,
} from '../types/load.d.ts';
// TODO: specifying ext makes sense for inlined script to convey as what kind of content this should be treated as (i.e. is the inlined script JSX/Typescript/..., or the inlined style CSS/PCSS/SASS. Might be prerrable to support a 'compile-time' ext-attribute on the node)
// but in the case of href/src references, it makes more sense to add it as a meta-data property (conveying how we expect it to be loaded) and the existing filename left as is.
export const KnownMappings : {[name: string]: NodeMapping} = {
externalScript: {
tagName: 'script',
attr: 'src',
loadType: 'entryChunk' // TODO: assuming entryChunk is always the right option for now. However we might want to switch to just chunk and leave it to the rollup to decide if this script should be inlined or not.
},
inlinedScript: {
tagName: 'script',
body: true,
ext: 'js',
loadType: 'chunk'
},
externalStylesheet: {
tagName: 'link',
match: {
attr: {
rel: 'stylesheet'
},
},
attr: 'href',
},
inlinedStylesheet: {
tagName: 'style',
body: true,
ext: 'css',
},
externalResource: { // i.e favicons.
tagName: 'link',
match: {
attr: {
rel: /^(?!.*stylesheet$)/ // Anything that is not rel="stylesheet",
}
},
attr: 'href'
// Could probably use finetuning, see possible values: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
},
externalImage: {
tagName: 'img',
attr: 'src',
},
link: {
tagName: 'a',
attr: 'href',
},
iframe: {
tagName: 'iframe',
attr: 'src',
},
videoSource: {
tagName: 'source',
attr: 'src'
},
subtitle: {
tagName: 'track',
attr: 'src'
},
audio: {
tagName: 'audio',
attr: 'src'
},
portal: {
tagName: 'portal',
attr: 'src',
},
object: {
tagName: 'object',
attr: 'data'
}
}
export type KnownMappingTypes = keyof typeof KnownMappings;
export const defaultMapping: NodeMapping[] = [
// Scripts
KnownMappings.externalScript,
KnownMappings.inlinedScript,
// Stylesheet
KnownMappings.externalStylesheet,
KnownMappings.inlinedStylesheet,
// Images, svgs
KnownMappings.externalImage,
// Links
// knownMappings.link,
// knownMappings.iframe, // Very unlikely to become a default, but who knows if someone has a valid use for this
// Media
KnownMappings.videoSource,
KnownMappings.subtitle,
KnownMappings.audio,
// Misc
// knownMappings.portal,// <portal src="..."> An experimental feature to replace valid use cases for iframes? Might want to [look into it...](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/portal)
// knownMappings.object,// <object data="..."> Not sure what to do with this, is this still commonly used? Any valid use-case for this? [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)
]

91
src/loader.ts Normal file
View File

@@ -0,0 +1,91 @@
// The loader parses a DOM node and detects which resource (script, style, image, ...) needs to be loaded from it
import type {
LoadResult,
LoadNodeCallback,
LoadReference,
NodeMapping,
AttributeReference, BodyReference, LoadedReference
} from '../types/index.d.ts';
import {parseFragment as parseHtmlFragment, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";
import {KnownMappings, defaultMapping} from "./loader-mappings.js";
/**
* Makes a unique but human-readable name from a path within a HTML file.
* i.e html.body.script0
* @param node
*/
export function makeHtmlPath(node: DefaultTreeAdapterMap['childNode']){
const path = [];
let cur = node;
while(cur?.parentNode){
const parent = cur.parentNode;
const asElement = (<DefaultTreeAdapterMap['element']>cur);
const similarChildNodes = parent.childNodes?.filter(x=>(<DefaultTreeAdapterMap['element']>x).nodeName == cur.nodeName) || [];
const pathName = `${asElement.tagName}${similarChildNodes.length>1? similarChildNodes.indexOf(cur): ''}`;
path.unshift(pathName);
cur = (<DefaultTreeAdapterMap['childNode']>cur.parentNode);
if((<DefaultTreeAdapterMap['element']>cur).tagName==='html'
&& (!cur.parentNode || cur.parentNode?.nodeName === '#document')
&& (!cur.parentNode || cur.parentNode?.childNodes.length===1)
){
break; // Break early, don't include 'html0' if we can prevent it
}
}
return path.join('.');
}
/**
* // TODO check if this works cross platform (windows)
* @param sourceId
* @param node
* @param ext
*/
export function makeInlineId(sourceId: string, node: DefaultTreeAdapterMap['childNode'], ext = '.js'){
return [sourceId, [makeHtmlPath(node), 'js'].join('.')].join('.');
}
export function makeLoader(mappings: NodeMapping[] = defaultMapping){
const fn : LoadNodeCallback = async function ({node, sourceId}, load){
for(const mapping of mappings){
if (mapping.tagName && mapping.tagName !== node.tagName) continue; // No match, skip
if (mapping.match){
if(typeof(mapping.match) === 'function'){
if(!mapping.match(node)) continue;
}else{
if(mapping.match.body && !(node.childNodes?.length>0)) continue; // No match, skip
if(mapping.match.attr) {
for (const [attrName, attrMatch] of Object.entries(mapping.match.attr)) {
if(!node.attrs.find(attr=>{
if(attr.name !== attrName) return false;
if(typeof(attrMatch) === 'string') return attrMatch === attr.value;
if(attrMatch instanceof RegExp) return !!(attrMatch.exec(attr.value));
if(typeof(attrMatch) === 'function') return attrMatch(attr.value);
})) continue; // No match, skip
}
}
}
}
if((<AttributeReference>mapping).attr){
const attr = node.attrs.find(attr=>attr.name === (<AttributeReference>mapping).attr);
if(!attr) continue ;// No match, skip
const placeholder = await load({
id: attr.value,
type: mapping.loadType||'default', // Use the default export unless explicitely mapped differently
});
attr.value = placeholder;
}else if((<BodyReference>mapping).body){
const body = serializeHtml(node); // unlike what you' might expect, this doesn't serialize the <script>-tag itself, only its contents. Which is what we want.
if(!body) continue; // Empty body, skip
const placeholder = await load({
source: body,
type: mapping.loadType||'chunk'
});
node.childNodes = parseHtmlFragment(placeholder).childNodes;
return false;
}
}
}
return fn;
}

View File

@@ -0,0 +1,2 @@
export const b = ()=>'batman';
console.log(b());

View File

@@ -0,0 +1,7 @@
<html>
<head>
</head>
<body>
<script src="./batman.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<html>
<head>
</head>
<body>
<script type="module">
import {b} from "./batman.js";
document.body.appendChild(
document.createTextNode(`Inline script including ${b()}`)
);
</script>
</body>
</html>

View File

@@ -0,0 +1,86 @@
# Snapshot report for `test/basic/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## simple
> Snapshot 1
[
{
code: `const b = ()=>'batman';␊
console.log(b());␊
export { b };␊
//# sourceMappingURL=batman-c7fa228c.js.map␊
`,
fileName: 'batman-c7fa228c.js',
map: SourceMap {
file: 'batman-c7fa228c.js',
mappings: 'AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;',
names: [],
sources: [
'../batman.js',
],
sourcesContent: [
`export const b = ()=>'batman';␊
console.log(b());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'batman-c7fa228c.js.map',
map: undefined,
source: '{"version":3,"file":"batman-c7fa228c.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>\'batman\';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<script src="batman-c7fa228c.js" type="module"></script>␊
</body></html>`,
},
]
## inline-script
> Snapshot 1
[
{
code: undefined,
fileName: 'script.html.body.script.js-e3b82208.js.map',
map: undefined,
source: '{"version":3,"file":"script.html.body.script.js-e3b82208.js","sources":["../batman.js","../script.html.body.script.js"],"sourcesContent":["export const b = ()=>\'batman\';\\nconsole.log(b());\\n","\\n import {b} from \\"./batman.js\\";\\n document.body.appendChild(\\n document.createTextNode(`Inline script including ${b()}`)\\n );\\n "],"names":[],"mappings":"AAAO,MAAM,CAAC,GAAG,IAAI,QAAQ,CAAC;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;ACCJ,QAAQ,CAAC,IAAI,CAAC,WAAW;AACrC,gBAAgB,QAAQ,CAAC,cAAc,CAAC,CAAC,wBAAwB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AACzE,aAAa"}',
},
{
code: undefined,
fileName: 'script.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<script type="module">const b = ()=>'batman';␊
console.log(b());␊
document.body.appendChild(␊
document.createTextNode(\`Inline script including ${b()}\`)␊
);␊
//# sourceMappingURL=script.html.body.script.js-e3b82208.js.map␊
</script>␊
</body></html>`,
},
]

Binary file not shown.

50
test/basic/test.js Normal file
View File

@@ -0,0 +1,50 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import html from "../../src/index.ts";
const output = {
dir: 'output', // Output all files
format: 'es', // iifi and cjs should be added to tests
sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output
};
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('simple', async (t) => {
const bundle = await rollup({
input: 'index.html',
plugins: [
html({
}),
]
});
const code = await getCode(bundle, output, true);
debugPrintOutput('simple',code);
t.snapshot(code);
});
test.serial('inline-script', async (t) => {
const bundle = await rollup({
input: 'script.html',
plugins: [
html({
}),
]
});
const code = await getCode(bundle, output, true);
debugPrintOutput('inline-script',code);
t.snapshot(code);
});
// TODO various parameters
// - format: cjs, iifi, ...
// - sourcemap: inline, false, (and the various exotic sourcemap options)
// Watch mode tests would be its own dir
// ...

View File

View File

@@ -1,2 +0,0 @@
// eslint-disable-next-line
import style from './joker.css';

View File

@@ -1,2 +0,0 @@
// eslint-disable-next-line
import * as batman from './batman.js';

View File

@@ -0,0 +1,4 @@
export const test = ()=>{
return `I'm "annoying" ${"in case we need to test \`string\` escaping."}. Hence this files \'tries\' to include all allowed forms of 'it'`;
}
console.log(test());

View File

@@ -0,0 +1,7 @@
<html>
<head>
</head>
<body>
<script src="./batman.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,60 @@
# Snapshot report for `test/live-reload/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## live-reload
> Snapshot 1
[
{
code: `␊
(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document);␊
const test = ()=>{␊
return \`I'm "annoying" ${"in case we need to test \\\`string\\\` escaping."}. Hence this files \\'tries\\' to include all allowed forms of 'it'\`;␊
};␊
console.log(test());␊
export { test };␊
//# sourceMappingURL=batman-1a5fc364.js.map␊
`,
fileName: 'batman-1a5fc364.js',
map: SourceMap {
file: 'batman-1a5fc364.js',
mappings: ';;AAAY,MAAC,IAAI,GAAG,IAAI;AACxB,IAAI,OAAO,CAAC,eAAe,EAAE,8CAA8C,CAAC,iEAAiE,CAAC,CAAC;AAC/I,EAAC;AACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;;;;',
names: [],
sources: [
'../batman.js',
],
sourcesContent: [
`export const test = ()=>{␊
return \`I'm "annoying" ${"in case we need to test \\\`string\\\` escaping."}. Hence this files \\'tries\\' to include all allowed forms of 'it'\`;␊
}␊
console.log(test());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'batman-1a5fc364.js.map',
map: undefined,
source: '{"version":3,"file":"batman-1a5fc364.js","sources":["../batman.js"],"sourcesContent":["export const test = ()=>{\\n return `I\'m \\"annoying\\" ${\\"in case we need to test \\\\`string\\\\` escaping.\\"}. Hence this files \\\\\'tries\\\\\' to include all allowed forms of \'it\'`;\\n}\\nconsole.log(test());\\n"],"names":[],"mappings":";;AAAY,MAAC,IAAI,GAAG,IAAI;AACxB,IAAI,OAAO,CAAC,eAAe,EAAE,8CAA8C,CAAC,iEAAiE,CAAC,CAAC;AAC/I,EAAC;AACD,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<script src="batman-1a5fc364.js" type="module"></script>␊
</body></html>`,
},
]

Binary file not shown.

42
test/live-reload/test.js Normal file
View File

@@ -0,0 +1,42 @@
import {join, dirname} from "node:path";
import test from "ava";
import {rollup} from "rollup";
import liveReload from "rollup-plugin-livereload";
import {debugPrintOutput, getCode} from "../util/test.js";
import html from "../../src/index.ts";
const output = {
dir: 'output', // Output all files
format: 'es', // iifi and cjs should be added to tests
sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output
};
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('live-reload', async (t) => {
const bundle = await rollup({
input: 'index.html',
plugins: [
html({
}),
liveReload({
verbose: false// this oddly enough prevents it from actually starting the liveserver, which would've left the test to wait indefinatly to close
})
]
});
const code = await getCode(bundle, output, true);
await bundle.close();// Make sure live-reload closes itself
debugPrintOutput('live-reload',code);
t.snapshot(code);
});
// TODO various parameters
// - format: cjs, iifi, ...
// - sourcemap: inline, false, (and the various exotic sourcemap options)
// Watch mode tests would be its own dir
// ...

View File

@@ -0,0 +1,12 @@
<html>
<head>
</head>
<body>
<div id="root"></div>
<script type="module">
import {bootstrap} from "../app/app.js"
import {adminDeps} from "../app/admin-deps.js";
bootstrap(document.getElementById('root'), adminDeps());
</script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
export function adminDeps(){
return "robin!";
}

View File

@@ -0,0 +1,6 @@
export const bootstrap = (el,deps = [])=>{
el.innerHtml = `
<div>I'm "annoying" ${"in case we need to test \`string\` escaping."}. Hence this file \'tries\' to include all allowed forms of 'it'</div>
<div>Deps: ${deps}</div>
`;
}

View File

@@ -0,0 +1,11 @@
<html>
<head>
</head>
<body>
<div id="root"></div>
<script type="module">
import {bootstrap} from "./app/app.js"
bootstrap(document.getElementById('root'), "<none>");
</script>
</body>
</html>

View File

@@ -0,0 +1,100 @@
# Snapshot report for `test/multi-entry/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## multi-entry
> Snapshot 1
[
{
code: `const bootstrap = (el,deps = [])=>{␊
el.innerHtml = \`
<div>I'm "annoying" ${"in case we need to test \\\`string\\\` escaping."}. Hence this file \\'tries\\' to include all allowed forms of 'it'</div>␊
<div>Deps: ${deps}</div>␊
\`;␊
};␊
export { bootstrap as b };␊
//# sourceMappingURL=app-01141b67.js.map␊
`,
fileName: 'app-01141b67.js',
map: SourceMap {
file: 'app-01141b67.js',
mappings: 'AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB,4BAA4B,EAAE,8CAA8C,CAAC;AAC7E,mBAAmB,EAAE,IAAI,CAAC;AAC1B,IAAI,CAAC,CAAC;AACN;;;;',
names: [],
sources: [
'../app/app.js',
],
sourcesContent: [
`export const bootstrap = (el,deps = [])=>{␊
el.innerHtml = \`␊
<div>I'm "annoying" ${"in case we need to test \\\`string\\\` escaping."}. Hence this file \\'tries\\' to include all allowed forms of 'it'</div>␊
<div>Deps: ${deps}</div>␊
\`;␊
}␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'index.html.body.script.js-45303f0f.js.map',
map: undefined,
source: '{"version":3,"file":"index.html.body.script.js-45303f0f.js","sources":["../index.html.body.script.js"],"sourcesContent":["\\n import {bootstrap} from \\"./app/app.js\\"\\n bootstrap(document.getElementById(\'root\'), \\"<none>\\");\\n "],"names":[],"mappings":";;AAEY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC"}',
},
{
code: undefined,
fileName: 'admin/index.html.body.script.js-15dfaff3.js.map',
map: undefined,
source: '{"version":3,"file":"index.html.body.script.js-15dfaff3.js","sources":["../../app/admin-deps.js","../../admin/index.html.body.script.js"],"sourcesContent":["export function adminDeps(){\\n return \\"robin!\\";\\n}\\n","\\n import {bootstrap} from \\"../app/app.js\\"\\n import {adminDeps} from \\"../app/admin-deps.js\\";\\n bootstrap(document.getElementById(\'root\'), adminDeps());\\n "],"names":[],"mappings":";;AAAO,SAAS,SAAS,EAAE;AAC3B,IAAI,OAAO,QAAQ,CAAC;AACpB;;ACCY,SAAS,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC"}',
},
{
code: undefined,
fileName: 'app-01141b67.js.map',
map: undefined,
source: '{"version":3,"file":"app-01141b67.js","sources":["../app/app.js"],"sourcesContent":["export const bootstrap = (el,deps = [])=>{\\n el.innerHtml = `\\n <div>I\'m \\"annoying\\" ${\\"in case we need to test \\\\`string\\\\` escaping.\\"}. Hence this file \\\\\'tries\\\\\' to include all allowed forms of \'it\'</div>\\n <div>Deps: ${deps}</div>\\n `;\\n}\\n"],"names":[],"mappings":"AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB,4BAA4B,EAAE,8CAA8C,CAAC;AAC7E,mBAAmB,EAAE,IAAI,CAAC;AAC1B,IAAI,CAAC,CAAC;AACN;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<div id="root"></div>␊
<script type="module">import { b as bootstrap } from './app-01141b67.js';␊
bootstrap(document.getElementById('root'), "<none>");␊
//# sourceMappingURL=index.html.body.script.js-45303f0f.js.map␊
</script>␊
</body></html>`,
},
{
code: undefined,
fileName: 'admin/index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<div id="root"></div>␊
<script type="module">import { b as bootstrap } from '../app-01141b67.js';␊
function adminDeps(){␊
return "robin!";␊
}␊
bootstrap(document.getElementById('root'), adminDeps());␊
//# sourceMappingURL=index.html.body.script.js-15dfaff3.js.map␊
</script>␊
</body></html>`,
},
]

Binary file not shown.

40
test/multi-entry/test.js Normal file
View File

@@ -0,0 +1,40 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import html from "../../src/index.ts";
const output = {
dir: 'output', // Output all files
format: 'es', // iifi and cjs should be added to tests
sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output
};
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('multi-entry', async (t) => {
const bundle = await rollup({
input: {
['index']: 'index.html',
['admin/index']: 'admin/index.html'
},
plugins: [
html({
}),
]
});
const code = await getCode(bundle, output, true);
debugPrintOutput('multi-entry',code);
t.snapshot(code);
});
// TODO various parameters
// - format: cjs, iifi, ...
// - sourcemap: inline, false, (and the various exotic sourcemap options)
// Watch mode tests would be its own dir
// ...

View File

@@ -1,357 +0,0 @@
# Snapshot report for `test/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## default options
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
</head>␊
<body>␊
<script src="batman.js"></script>␊
</body>␊
</html>`,
},
]
## options
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'batman.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width">␊
<title>Batcave</title>␊
</head>␊
<body>␊
<script src="batcave/batman.js"></script>␊
</body>␊
</html>`,
},
]
## iife
> Snapshot 1
[
{
code: `(function () {␊
'use strict';␊
})();␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
</head>␊
<body>␊
<script src="batman.js"></script>␊
</body>␊
</html>`,
},
]
## esm
> Snapshot 1
[
{
code: `␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
</head>␊
<body>␊
<script src="batman.js" type="module"></script>␊
</body>␊
</html>`,
},
]
## unsupported output format
> Snapshot 1
[
{
code: `'use strict';␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
</head>␊
<body>␊
<script src="batman.js" type="module"></script>␊
</body>␊
</html>`,
},
]
> Snapshot 2
[
{
code: 'EMPTY_BUNDLE',
message: 'Generated an empty chunk: "batman".',
names: [
'batman',
],
toString: Function {},
},
{
code: 'PLUGIN_WARNING',
message: 'plugin-html: The output format \'cjs\' is not directly supported. A custom `template` is probably required. Supported formats include: es, esm, iife, umd',
plugin: 'html',
toString: Function {},
},
]
## css
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'joker.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'joker.css',
map: undefined,
source: Buffer @Uint8Array [
2a207b20 77696474 683a2031 3030253b 207d0a
],
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
<link href="joker.css" rel="stylesheet">␊
</head>␊
<body>␊
<script src="joker.js" type="module"></script>␊
</body>␊
</html>`,
},
]
## attributes
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'joker.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'joker.css',
map: undefined,
source: Buffer @Uint8Array [
2a207b20 77696474 683a2031 3030253b 207d0a
],
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html batsignal="on" lang="bat">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
<link href="joker.css" rel="stylesheet" data-vilian="joker">␊
</head>␊
<body>␊
<script src="joker.js" defer="true"></script>␊
</body>␊
</html>`,
},
]
## imports
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'robin.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `␊
<!doctype html>␊
<html lang="en">␊
<head>␊
<meta charset="utf-8">␊
<title>Rollup Bundle</title>␊
</head>␊
<body>␊
<script src="robin.js" type="module"></script>␊
</body>␊
</html>`,
},
]
## template
> Snapshot 1
[
{
code: `(function (factory) {␊
typeof define === 'function' && define.amd ? define(factory) :␊
factory();␊
})((function () { 'use strict';␊
}));␊
`,
fileName: 'batman.js',
map: null,
source: undefined,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: '<html><body><main></main></body></html>',
},
]

Binary file not shown.

View File

@@ -0,0 +1,2 @@
export const b = ()=>'batman';
console.log(b());

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path style="fill:none;stroke:#00ff0d;stroke-width:5;stroke-linecap:square;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" d="M4.1 14.72 16 26.31 28.38 5.09"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,8 @@
<html>
<head>
<meta data-test="{{a}}"/>
</head>
<body>
<script src="./batman.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
<html>
<head>
<link rel="icon" href="./icon.svg">
</head>
<body>
<script src="./batman.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
# Snapshot report for `test/templating/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## handlebars
> Snapshot 1
[
{
code: `const b = ()=>'batman';␊
console.log(b());␊
export { b };␊
//# sourceMappingURL=batman-c7fa228c.js.map␊
`,
fileName: 'batman-c7fa228c.js',
map: SourceMap {
file: 'batman-c7fa228c.js',
mappings: 'AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;',
names: [],
sources: [
'../batman.js',
],
sourcesContent: [
`export const b = ()=>'batman';␊
console.log(b());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'batman-c7fa228c.js.map',
map: undefined,
source: '{"version":3,"file":"batman-c7fa228c.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>\'batman\';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
<meta data-test="a">␊
</head>␊
<body>␊
<script src="batman-c7fa228c.js" type="module"></script>␊
</body></html>`,
},
]

Binary file not shown.

43
test/templating/test.js Normal file
View File

@@ -0,0 +1,43 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js";
import html from "../../src/index.ts";
import handlebars from "handlebars";
const output = {
dir: 'output', // Output all files
format: 'es', // iifi and cjs should be added to tests
sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output
};
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('handlebars', async (t) => {
const bundle = await rollup({
input: 'index.hbs',
plugins: [
html({
transform(src){
return handlebars.compile(src)({a:'a'})
}
})
]
});
const code = await getCode(bundle, output, true);
debugPrintOutput('handlebars',code);
t.snapshot(code);
});
// TODO various parameters
// - format: cjs, iifi, ...
// - sourcemap: inline, false, (and the various exotic sourcemap options)
// Watch mode tests would be its own dir
// ...

View File

@@ -1,124 +0,0 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import css from "rollup-plugin-postcss";
import { getCode } from "./util/test.js";
import html from "../src/index.ts";
// const read = (file = 'index.html') => readFileSync(join('output/', file), 'utf-8');
const output = { dir: 'output', format: 'umd' };
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('default options', async (t) => {
const bundle = await rollup({
input: 'batman.js',
plugins: [html()]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});
test.serial('options', async (t) => {
const bundle = await rollup({
input: 'batman.js',
plugins: [
html({
fileName: 'batman.html',
publicPath: 'batcave/',
title: 'Batcave',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'minimum-scale=1, initial-scale=1, width=device-width' }
]
})
]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});
test.serial('iife', async (t) => {
const bundle = await rollup({
input: 'batman.js',
plugins: [html()]
});
const code = await getCode(bundle, { dir: 'output', format: 'iife' }, true);
t.snapshot(code);
});
test.serial('esm', async (t) => {
const bundle = await rollup({
input: 'batman.js',
plugins: [html()]
});
const code = await getCode(bundle, { dir: 'output', format: 'es' }, true);
t.snapshot(code);
});
test.serial('unsupported output format', async (t) => {
const warnings = [];
const bundle = await rollup({
input: 'batman.js',
onwarn: (warning) => warnings.push(warning),
plugins: [html()]
});
const code = await getCode(bundle, { dir: 'output', format: 'cjs' }, true);
t.snapshot(code);
t.snapshot(warnings);
});
test.serial('css', async (t) => {
const bundle = await rollup({
input: 'joker.js',
plugins: [css({ extract: true }), html()]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});
test.serial('attributes', async (t) => {
const bundle = await rollup({
input: 'joker.js',
plugins: [
css({ extract: true }),
html({
attributes: {
html: { batsignal: 'on', lang: 'bat' },
link: { 'data-vilian': 'joker' },
script: { defer: true }
}
})
]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});
test.serial('imports', async (t) => {
const bundle = await rollup({
input: 'robin.js',
plugins: [html()]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});
test.serial('template', async (t) => {
const bundle = await rollup({
input: 'batman.js',
plugins: [
html({
template: () => '<html><body><main></main></body></html>'
})
]
});
const code = await getCode(bundle, output, true);
t.snapshot(code);
});

View File

@@ -0,0 +1,2 @@
export const b = ()=>'batman';
console.log(b());

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path style="fill:none;stroke:#00ff0d;stroke-width:5;stroke-linecap:square;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" d="M4.1 14.72 16 26.31 28.38 5.09"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,8 @@
<html>
<head>
<link rel="icon" href="./icon.svg">
</head>
<body>
<script src="./batman.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
# Snapshot report for `test/url-plugin/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## copied-assets
> Snapshot 1
[
{
code: `const b = ()=>'batman';␊
console.log(b());␊
export { b };␊
//# sourceMappingURL=batman-c7fa228c.js.map␊
`,
fileName: 'batman-c7fa228c.js',
map: SourceMap {
file: 'batman-c7fa228c.js',
mappings: 'AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;',
names: [],
sources: [
'../batman.js',
],
sourcesContent: [
`export const b = ()=>'batman';␊
console.log(b());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'batman-c7fa228c.js.map',
map: undefined,
source: '{"version":3,"file":"batman-c7fa228c.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>\'batman\';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
<link rel="icon" href="fb585fdb6db313c9.svg">␊
</head>␊
<body>␊
<script src="batman-c7fa228c.js" type="module"></script>␊
</body></html>`,
},
]
## inlined-assets
> Snapshot 1
[
{
code: `const b = ()=>'batman';␊
console.log(b());␊
export { b };␊
//# sourceMappingURL=batman-c7fa228c.js.map␊
`,
fileName: 'batman-c7fa228c.js',
map: SourceMap {
file: 'batman-c7fa228c.js',
mappings: 'AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;',
names: [],
sources: [
'../batman.js',
],
sourcesContent: [
`export const b = ()=>'batman';␊
console.log(b());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'batman-c7fa228c.js.map',
map: undefined,
source: '{"version":3,"file":"batman-c7fa228c.js","sources":["../batman.js"],"sourcesContent":["export const b = ()=>\'batman\';\\nconsole.log(b());\\n"],"names":[],"mappings":"AAAY,MAAC,CAAC,GAAG,IAAI,SAAS;AAC9B,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;;;;"}',
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
<link rel="icon" href="data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%20%20%3Cpath%20style%3D%22fill%3Anone%3Bstroke%3A%2300ff0d%3Bstroke-width%3A5%3Bstroke-linecap%3Asquare%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A1%22%20d%3D%22M4.1%2014.72%2016%2026.31%2028.38%205.09%22%2F%3E%3C%2Fsvg%3E">␊
</head>␊
<body>␊
<script src="batman-c7fa228c.js" type="module"></script>␊
</body></html>`,
},
]

Binary file not shown.

67
test/url-plugin/test.js Normal file
View File

@@ -0,0 +1,67 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url";
import {debugPrintOutput, getCode} from "../util/test.js";
import html from "../../src/index.ts";
const output = {
dir: 'output', // Output all files
format: 'es', // iifi and cjs should be added to tests
sourcemap: true,// Test if #sourcemapUrl is not accidentally included in the html-output
};
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
const defaultAssetInclude = [
'**/*.(png|jpg|jpeg|gif|ico|svg)',// images, svg
'**/*.(woff|woff2|eot|ttf|otf)',// fonts
'**/*.(webm|mp4)',// video
];
test.serial('copied-assets', async (t) => {
const bundle = await rollup({
input: 'index.html',
plugins: [
html({
}),
urlPlugin({
include: defaultAssetInclude,
limit: 0,// Never inline something
}),
],
});
const code = await getCode(bundle, output, true);
debugPrintOutput('copied-assets',code);
t.snapshot(code);
});
test.serial('inlined-assets', async (t) => {
const bundle = await rollup({
input: 'index.html',
plugins: [
html({
}),
urlPlugin({
include: defaultAssetInclude,
limit: Number.MAX_SAFE_INTEGER,// Always inline things
}),
]
});
const code = await getCode(bundle, output, true);
debugPrintOutput('inlined-assets',code);
t.snapshot(code);
});
// TODO various parameters
// - format: cjs, iifi, ...
// - sourcemap: inline, false, (and the various exotic sourcemap options)
// Watch mode tests would be its own dir
// ...

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import process from "node:process";
import chalk from "chalk";
/**
* @param {import('rollup').RollupBuild} bundle
@@ -22,6 +23,35 @@ export const getCode = async (bundle, outputOptions, allFiles = false) => {
return code;
};
export const debugPrintOutput = async (header, files) => {
const out = [];
const headFn = chalk.bgCyan;
const headPadding = header.split('').map(x=>'#').join('');
out.push(...[
headFn(`##${headPadding}##`),
headFn(`# ${header} #`),
headFn(`##${headPadding}##`),
]);
const fileHeadFn = chalk.blue;
const fileContentFn = chalk.blackBright;
out.push(...(files.map(file=>{
return [
fileHeadFn(`${file.fileName}:`),
fileContentFn(`${file.code??file.source}`),
'',
]
}).flat()));
out.push(...[
headFn(`##${headPadding}##`),
]);
process.env.DEBUG? console.log(out.join('\n')) : null;
};
/**
* @param {import('rollup').RollupBuild} bundle
* @param {import('rollup').OutputOptions} [outputOptions]

View File

@@ -4,8 +4,9 @@
"esModuleInterop": true,
"noEmit": true,
"noEmitOnError": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
// Surpress errors about unused stuff: doesn't solve any bugs and is just annyoing during development, leave warning about this to the IDE or linters
"noUnusedLocals": false,
"noUnusedParameters": false,
"pretty": true,
"sourceMap": true,
"strict": true,

62
types/index.d.ts vendored
View File

@@ -1,24 +1,58 @@
import type { Plugin, OutputChunk, OutputAsset, OutputBundle } from 'rollup';
import type {Plugin, OutputChunk, OutputAsset, OutputBundle, TransformModuleJSON, } from 'rollup';
import {FilterPattern} from "@rollup/pluginutils";
import type {DefaultTreeAdapterMap} from "parse5";
import {PreRenderedChunk} from "rollup";
export interface RollupHtmlTemplateOptions {
title: string;
attributes: Record<string, any>;
publicPath: string;
meta: Record<string, any>[];
bundle: OutputBundle;
files: Record<string, (OutputChunk | OutputAsset)[]>;
import type {LoadNodeCallback} from "./load.d.ts";
export type * from "./load.d.ts"
import type {ResolveCallback} from "./resolve.d.ts";
export type * from "./resolve.d.ts"
export interface RollupHtmlTransformContext {
id?: string;
// bundle: OutputBundle;
// files: Record<string, (OutputChunk | OutputAsset)[]>;
}
export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>;
export interface RollupHtmlOptions {
title?: string;
attributes?: Record<string, any>;
fileName?: string;
meta?: Record<string, any>[];
publicPath?: string;
template?: (templateoptions?: RollupHtmlTemplateOptions) => string;
/**
* Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames).
*/
htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string);
/**
* Transform a source file passed into this plugin to HTML. For example: a handlebars transform
* ```
* transform(source){
* return handlebars.compile(source)({myVar:'example'})
* }
* ```
*/
transform?: TransformCallback;
/**
* Detect which references (<a href="...">, <img src="...">) to resolve from a HTML node.
* This rarely needs to be overloaded, but can be used to support non-native attributes used by custom-elements.
*
* Return false to skip any further processing on this node. Use the load function to add any resources from this node, and replace the import with a placeholder so the plugin knows where to inject the end result
*/
load?: LoadNodeCallback;
/**
* Callback to filter which references actually need to be resolved. Here you can filter out:
* - Links to extensions that don't need to be handled through rollup
* - Resources that are external to the app (for example non-relative paths)
* - Page navigation within the app
*
* Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id)
*/
resolve?: ResolveCallback;
include?: FilterPattern;
exclude?: FilterPattern
}
export function makeHtmlAttributes(attributes: Record<string, string>): string;
/**
* A Rollup plugin which creates HTML files to serve Rollup bundles.

79
types/load.d.ts vendored Normal file
View File

@@ -0,0 +1,79 @@
import type {DefaultTreeAdapterMap} from "parse5";
// Load hook types
export interface RollupHtmlLoadContext {
node: DefaultTreeAdapterMap['element'];
sourceId: string;
}
export type AttributeReference = {
attr: string;
};
export type BodyReference = {
/**
* Indiciate this is an inlined reference (node body)
*/
body: boolean;
/**
* Describes what the content type is. I.e 'js' for inlined <script>, 'css' for inlined <style>
*/
ext?: string;
};
/**
* Describes how a resource should be loaded.
*/
export type LoadReference = AttributeReference | BodyReference
/**
* Indicate how to load this resource:
* - 'default' uses the default export of the referenced id
* - 'chunk' use the rendered chunk of this file (e.g inlined JS)
* - 'entryChunk' mark this resource as its own entry-chunk and use its rendered output path
* // TODO: add a type 'asset' here, in which we use rollups emitFile({type:'asset'} feature (which reduces the need for plugin-url, and probably makes more sense as the default option instead of 'default' in zero-config scenarios)
*/
export type LoadType = 'default'|'chunk'|'entryChunk';
export type LoadedReference = (
{
// External (virtual) reference
id: string; // path/url referenced. Or identifier for the virtual source
source?: string; // Source to use for this id, for inlined chunks
} | {
// Inline
id?: string; // A unique identifier for snippet
source: string; // Source to use for this id, for inlined chunks
}
) & {
type?: LoadType
};
export type LoadResult = undefined|void|false;
export type LoadFunction = (reference: LoadedReference)=>Promise<string>
export type LoadNodeCallback = (loadContext: RollupHtmlLoadContext, load: LoadFunction) => LoadResult|Promise<LoadResult>;
// Make load hook mapping
/**
* Describes which DOM nodes to extract references from
*/
export type NodeMapping = {
tagName?: string;
/** Filter to specific properties to DOM node must have nodes. TODO allowing a callback here probably makes sense */
match?: ({
/** Whether the element must have a non-null body */
body?: boolean
/** Which additional attributes the element must have to match */
attr?: {[attrName: string]: (string|RegExp|((value:string)=>boolean))}
} | ((el: DefaultTreeAdapterMap['element'])=>boolean));
/**
* Indicate how to load this resource:
* - 'default' uses the default export of the referenced id
* - 'chunk' use the rendered chunk of this file (e.g inlined JS)
* - 'entryChunk' mark this resource as its own entry-chunk and use its rendered output path
*/
loadType?: LoadType
} & LoadReference;

8
types/resolve.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type {DefaultTreeAdapterMap} from "parse5";
export interface RollupHtmlResolveContext {
node: DefaultTreeAdapterMap['element'];
sourceId: string;
}
export type ResolveResult = string|true|undefined|void|false;
export type ResolveCallback = (id: string, resolveContext: RollupHtmlResolveContext) => ResolveResult|Promise<ResolveResult>;