18 Commits

Author SHA1 Message Date
3b540d0c48 chore: reowrked tests to use a runBrowserTest to allow previewing the results in a browser 2024-02-17 21:12:54 +01:00
1c55b894c9 test: refactoring tests 2024-01-27 20:54:59 +01:00
3e46055845 test: added a react application test (in the browser) 2024-01-07 15:28:17 +01:00
e96c2248ee test: added testing via a browser (puppeteer) 2023-12-09 23:35:28 +01:00
2adfbee74b chore: remove outdated todos and commented code 2023-11-28 02:11:20 +01:00
71a377417d feat: initial support for importing html from js 2023-11-26 22:15:40 +01:00
afd4a3c9ae feat: added initial sourcemapping attempt and use transform hook to parse and transform instead of load 2023-11-25 22:44:31 +01:00
980d33c48e chore: documentation 2023-11-25 20:23:13 +01:00
5c1e528304 chore: update for node 20 LTS 2023-11-23 20:07:10 +01:00
5d2a45ef81 fix: missing test snapshots
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-13 02:03:26 +02:00
6e50208557 feat: support rewriting urls
Some checks failed
continuous-integration/drone/push Build is failing
2023-06-13 02:02:41 +02:00
48dcdefee1 0.0.5: fix, try to resolve sensible entrynames even if files were imported with an absolute path
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-20 23:39:14 +02:00
ba09aaf915 0.0.4: increment version number for fix: fix entryNames of sources files included by html in a subdir (they didnt keep their relative address...)
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-20 23:17:02 +02:00
9bf026f0c3 0.0.3: fix entryNames of sources files included by html in a subdir (they didnt keep their relative address...)
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-20 23:14:59 +02:00
a784abc1b0 Build-fix: update snapshotting code to generate a predictable (A-Z) ordering of the snapshotted files
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-20 21:57:36 +02:00
52c104f781 0.0.3: Fixed a bug that showed up in watch mode
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-20 21:05:37 +02:00
b18ac5c361 0.0.2: Package increment
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-02 16:16:40 +02:00
ba07649981 0.0.2: Updated docs to reflect current state
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-02 15:47:23 +02:00
68 changed files with 4615 additions and 1211 deletions

View File

@@ -1,2 +1,28 @@
# 0.0.2
Private release update. Added experimental support for:
- multiple-entrypoints (i.e index.html and admin/index.html)
- Inlined scripts (i.e <script type="module">...</script>)
# 0.0.1 # 0.0.1
Initial private release Initial private release
# Open issues / Short-term ToDo's:
- Implement importing style (#1 linking to a pcss, #2 inlined style)
- Importing html as a JSModule
- Testing on a windows machine and fix whatever issues with paths that come out of it
- Code clean-up / Watch-mode support
- Properly use 'meta' property, and supporting caching
- Supporting 'assets' directly (LoadType) using emitFile({type:'asset',...}). Removes the need for @rollup/plugin-url in small projects (altough it is still the preferred way of including assets)
- Getting rid of the module evaluation step if possible
- Clean up our API, keeping in mind the configurability desired:
- resolving language for inline script/style
- excluding non-relative imports (ie unpkg stuff etc)
- customizing how to import certain things (LoadType)
- support for typescript (might not need extra work, but it should be integrated in tests)
- cjs & iifi supported in tests

View File

@@ -10,10 +10,13 @@
[![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com) [![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com)
# rollup-plugin-html-entry2 # rollup-plugin-html-entry2
| :warning: WARNING | | :warning: WARNING |
|:-------------------------------------------------------------------| |:----------------------------------------------------------------------------------------------------------------------|
| **Experimental-stage** plugin. Expect bugs and missing features... | | **Experimental-stage** plugin. Expect bugs and missing features... |
| :warning: WARNING |
| :------------------------------------------------------------------- |
| **Renaming** Name might change in the future. Consider rollup-plugin-html-bundler |
| (because we're basically transforming rollup into a tool for bundling html, might not even contain any JS in the end) |
A(nother) rollup plugin that tries to teach Rollup to start from an HTML entry, and the use of (multiple) HTML files in general. 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 The goal is to include assets referenced by the HTML file into the build-process as to copy/inline where appropriate and
@@ -64,7 +67,7 @@ Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#comma
### `template` ### `template`
Type: `Function`<br> Type: `Function`\
Default: `undefined`\ Default: `undefined`\
Returns: `String` Returns: `String`
@@ -94,27 +97,28 @@ async function build() {
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) 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)
## Status ## Status
This plugin is in an early state. As such not everything that is supported yet, and the options may change.
### (Rudimentarily) supported ### (Rudimentarily) supported
- Importing JS via `<script src="..." type="module">` tags - Importing JS via `<script src="..." type="module">` tags
- Importing assets using @rollup/plugin-url (which could use an update TBH) - 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 - Compatibility with other plugins such as @rollup/plugin-node-resolve, @rollup/plugin-babel, @rollup/plugin-commonjs, @rollup/plugin-terser and rollup-plugin-livereload
- Inline scripts (i.e `<script>...</script>`)
### Not (yet) supported ### Not (yet/properly) supported
- Inline scripts (i.e `<script>...</script>`) - Sourcemaps (inlined script) (dev-note: we're already including magic-string for this, but do not use it yet, neeeds refactoring)
- Plugins importing CSS files - Plugins importing CSS files
- CommonJS (cjs) and IIFI output formats. (Is UMD actually ever used?) - CommonJS (cjs) and IIFI output formats. (Is UMD actually ever used?)
- Overriding which tags to ignore/include - Overriding which DOM-nodes and resulting URLS to ignore/include (in a clean way)
- Other (various) plugins such as those for HMR etc - Other (various) plugins such as typescript, or those for HMR etc
- ... - ...
# Contibuting # Contibuting
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...\ 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) [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).
See also the ToDo-list at the end of the [changelog](./CHANGELOG.md)
# Notes # Notes
## git.cerxes.net ## git.cerxes.net

View File

@@ -1,6 +1,6 @@
{ {
"name": "rollup-plugin-html-entry2", "name": "rollup-plugin-html-entry2",
"version": "0.0.1", "version": "0.0.6",
"description": "Teaches rollup how to deal with HTML, allows to use HTML-files as entry-points.", "description": "Teaches rollup how to deal with HTML, allows to use HTML-files as entry-points.",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -12,7 +12,7 @@
"bugs": "https://git.cerxes.net/rollup-apps/plugin-html/issues", "bugs": "https://git.cerxes.net/rollup-apps/plugin-html/issues",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}, },
"main": "dist/es/index.js", "main": "dist/es/index.js",
"module": "./dist/es/index.js", "module": "./dist/es/index.js",
@@ -30,8 +30,8 @@
"ci:coverage": "nyc pnpm test && nyc report --reporter=text-lcov > coverage.lcov", "ci:coverage": "nyc pnpm test && nyc report --reporter=text-lcov > coverage.lcov",
"ci:lint": "pnpm build && pnpm lint-staged", "ci:lint": "pnpm build && pnpm lint-staged",
"ci:test": "pnpm test -- --verbose", "ci:test": "pnpm test -- --verbose",
"test": "ava", "test": "NODE_OPTIONS='--import tsx' ava",
"save-test": "ava --update-snapshots" "save-test": "NODE_OPTIONS='--import tsx' ava --update-snapshots"
}, },
"files": [ "files": [
"dist", "dist",
@@ -55,32 +55,44 @@
} }
}, },
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.5",
"magic-string": "^0.30.5",
"parse5": "^7.1.2" "parse5": "^7.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.15.11", "@babel/core": "^7.23.3",
"@rollup/plugin-typescript": "^11.1.0", "@babel/plugin-syntax-import-assertions": "^7.23.3",
"postcss": "^8.4.22", "@babel/preset-typescript": "^7.23.3",
"rollup": "^3.20.3", "@babel/preset-env": "^7.23.6",
"rollup-plugin-postcss": "^4.0.2", "@babel/preset-react": "^7.23.3",
"typescript": "^5.0.4", "@rollup/plugin-babel": "^6.0.4",
"del-cli": "^5.0.0", "@rollup/plugin-node-resolve": "^15.2.3",
"tslib": "^2.5.0", "@rollup/plugin-typescript": "^11.1.5",
"ava": "^5.2.0", "@rollup/plugin-terser": "^0.4.4",
"ts-node": "^10.9.1", "@rollup/plugin-url": "^8.0.2",
"@babel/core": "^7.21.4", "@rollup/plugin-commonjs": "^25.0.7",
"@babel/plugin-syntax-import-assertions": "^7.20.0", "@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-babel": "^6.0.3", "@types/node": "^18.18.12",
"@rollup/plugin-node-resolve": "^15.0.2", "@types/react": "^18.2.0",
"rollup-plugin-delete": "^2.0.0", "@types/react-dom": "^18.2.0",
"@babel/preset-typescript": "^7.21.4", "ava": "^5.3.1",
"chalk": "^5.3.0",
"del-cli": "^5.1.0",
"handlebars": "^4.7.8",
"lint-staged": "^13.3.0",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"lint-staged": "^13.2.1", "postcss": "^8.4.31",
"handlebars": "^4.7.7", "rollup": "^3.29.4",
"@rollup/plugin-url": "^8.0.1", "rollup-plugin-delete": "^2.0.0",
"chalk": "^5.2.0", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-livereload": "^2.0.5" "rollup-plugin-postcss": "^4.0.2",
"tslib": "^2.6.2",
"tsx": "^4.4.0",
"typescript": "^5.3.2",
"puppeteer": "^21.5.2",
"mime": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}, },
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"ava": { "ava": {
@@ -97,7 +109,6 @@
"js": true "js": true
}, },
"nodeArguments": [ "nodeArguments": [
"--loader=ts-node/esm",
"--experimental-vm-modules" "--experimental-vm-modules"
] ]
} }

3468
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +0,0 @@
This method provides the ability to reference external css/js files for the generated html, and supports adjusting the file loading sequence.
when using it:
```js
import html from '@rollup/plugin-html';
import templateExternalFiles from '@rollup/plugin-html/recipes/external-files';
import postcss from 'rollup-plugin-postcss';
export default [
{
input: ['demo/demo.ts'],
output: [{ file: 'dist/demo.js' }],
plugins: [
postcss({
extract: 'demo.css',
minimize: false,
use: ['sass'],
extensions: ['.scss', '.css']
}),
html({
title: 'sdk demo page',
publicPath: './',
fileName: 'demo.html',
attributes: { html: { lang: 'zh-cn' } },
template: templateExternalFiles([
{ type: 'js', file: 'example1.js', pos: 'before' },
{ type: 'js', file: 'example2.js', pos: 'before' },
{ type: 'js', file: 'example3.js' },
{ type: 'js', file: 'example4.js', pos: 'before' },
{ type: 'css', file: 'example1.css', pos: 'before' },
{ type: 'css', file: 'example2.css', pos: 'before' },
{ type: 'css', file: 'example3.css' },
{ type: 'css', file: 'example4.css', pos: 'before' }
])
})
]
}
];
```
The content of the generated html file:
```html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<title>sdk demo page</title>
<link href="./example1.css" rel="stylesheet" />
<link href="./example2.css" rel="stylesheet" />
<link href="./example4.css" rel="stylesheet" />
<link href="./demo.css" rel="stylesheet" />
<link href="./example3.css" rel="stylesheet" />
</head>
<body>
<script src="./example1.js"></script>
<script src="./example2.js"></script>
<script src="./example4.js"></script>
<script src="./demo.js"></script>
<script src="./example3.js"></script>
</body>
</html>
```

View File

@@ -1,76 +0,0 @@
/**
* Provides the ability to reference external css/js files for the generated html
* Method source once issues: https://github.com/rollup/plugins/issues/755
* @param {Array} externals List of external files.
* The format is: [{ type: 'js', file: '//xxxx1.js', pos: 'before' }, { type: 'css', file: '//xxxx1.css' }]
*
* @return {Function} The templae method required by plugin-html
*/
export default function htmlTemplate(externals) {
return ({ attributes, files, meta, publicPath, title }) => {
let scripts = [...(files.js || [])];
let links = [...(files.css || [])];
// externals = [{ type: 'js', file: '//xxxx1.js', pos: 'before' }, { type: 'css', file: '//xxxx1.css' }]
if (Array.isArray(externals)) {
const beforeLinks = [];
const beforeScripts = [];
externals.forEach((node) => {
let fileList;
const isCssFile = node.type === 'css';
if (node.pos === 'before') {
fileList = isCssFile ? beforeLinks : beforeScripts;
} else {
fileList = isCssFile ? links : scripts;
}
fileList.push({ fileName: node.file });
});
scripts = beforeScripts.concat(scripts);
links = beforeLinks.concat(links);
}
scripts = scripts
.map(({ fileName }) => {
const attrs = makeHtmlAttributes(attributes.script);
return `<script src="${publicPath}${fileName}"${attrs}></script>`;
})
.join('\n');
links = links
.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>`;
};
}
function 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]}"`), '');
}

View File

@@ -1,4 +1,4 @@
import { extname } from "node:path";
import type { import type {
Plugin, Plugin,
@@ -19,17 +19,24 @@ import type {
LoadReference, BodyReference, AttributeReference, LoadFunction LoadReference, BodyReference, AttributeReference, LoadFunction
} from '../types/index.d.ts'; } from '../types/index.d.ts';
// createFilter function is a utility that constructs a filter function from include/exclude patterns.
import {createFilter} from '@rollup/pluginutils'; import {createFilter} from '@rollup/pluginutils';
// parse5 package is used for parsing HTML.
import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5"; import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";
import {readFile} from "node:fs/promises" // magic-string to transform code and keeping a sourcemap aligned
import MagicString from "magic-string";
// nodejs imports (io, path)
import path, { extname, dirname } from "node:path";
import {readFile} from "node:fs/promises"
import posix from "node:path/posix";
import crypto from "node:crypto";
// utilities
import {makeLoader, makeInlineId} from "./loader.js"; import {makeLoader, makeInlineId} from "./loader.js";
import {HtmlImport, HtmlModule} from "./html-module.js"; import {HtmlImport, HtmlModule} from "./html-module.js";
import {dirname} from "node:path";
import posix from "node:path/posix";
import crypto from "node:crypto";
const defaults: RollupHtmlOptions = { const defaults: RollupHtmlOptions = {
transform: (source: string)=>source,// NO-OP transform: (source: string)=>source,// NO-OP
@@ -44,10 +51,17 @@ const defaults: RollupHtmlOptions = {
const modulePrefix = `// <html-module>`; const modulePrefix = `// <html-module>`;
const moduleSuffix = `// </html-module>`; const moduleSuffix = `// </html-module>`;
/**
* Creates a Rollup plugin that transforms HTML files.
*
* @param {RollupHtmlOptions} opts - The options for the plugin.
* @returns {Plugin} - The Rollup plugin.
*/
export default function html(opts: RollupHtmlOptions = {}): Plugin { export default function html(opts: RollupHtmlOptions = {}): Plugin {
const { const {
publicPath, publicPath,
transform, transform,
rewriteUrl,
load, load,
htmlFileNames, htmlFileNames,
resolve, resolve,
@@ -61,12 +75,42 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
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")} 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 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'; // TODO, we need to clear all these properly at sme point to avoid odd bugs in watch mode
let virtualSources = new Map<string, string>();
let addedEntries = new Map<string, string>();
let entryNames = new Map<string,string>();
const pluginName = 'html2'; // TODO: Need a better name, and work to strip everything noted below except the short summary
/**
* Short summary:
* Intercepts the loading of the html files and parses it with parse5.
* The parsed result is iterated to check for external references that need to be including in the rollup build (via for example @rollup/plugin-url).
* A .js version of the html file is returned to rollup, optionally including a few imports left for rollup to resolve
* When the result is generated the rollup result for the html file and any of its inlined assets are stripped from the output.
* and replaced with a html file.
*
* Caveats:
* - to get the resulting html content file we're evaluating the resulting JS module and take its default export
* This evaluation step is done in the host NodeJS context, which might screw up things that expect a browser context
* [warn] other plugins such as CJS transformer and hot-reload can severely screw this up.
* - to fix the naming of resulting html files, and behave properly when files are entryPoints or not... we're fighting rollup alot
* issues are likely...
*
*
* Rework by testing a stripped down version with JS imports?
* - the logic in load should be moved to a transform, properly use rollups ability to specify the plugin should run 'pre' other hooks and see if that allows us to intercept before a commonjs or some other tool horribly transpiles our code
* - we might need to know which output is being used to properly extract the html back from the result? (in case of not being included in a JS file)
*/
return { return {
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 name: pluginName,
// Track html entrypoints
buildStart(options){
entryNames = new Map(Object.entries(typeof(options.input)==='object'?options.input:{[options.input]:[options.input]})
.map(([k,v])=>[v,k])
);
},
resolveId: { resolveId: {
async handler(specifier: string, async handler(specifier: string,
@@ -82,25 +126,21 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
}); });
if(resolved){ if(resolved){
const moduleId = resolved.id;
const moduleExt = extname(resolved.id); const moduleExt = extname(resolved.id);
const moduleName = specifier.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any 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 { return {
...resolved, ...resolved,
meta: { meta: {
...resolved.meta, ...resolved.meta,
[pluginName]: {name: specifier} [pluginName]: {
specifier: specifier,
id: resolved.id,
name: moduleName,
imports: [],
assetId: null,
importers: new Set(),
}
} }
} }
} }
@@ -108,127 +148,178 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
}, },
load: { load: {
async handler(id: string) { async handler(id: string) {
if(virtualSources.has(id)) return virtualSources.get(id); if (virtualSources.has(id)) return virtualSources.get(id);
if(!filter(id)) return; // if (!filter(id)) return;
}
},
transform: {
order: 'pre',
async handler(...args){
const [code, id] = args;
if (!filter(id)) return;
// Load // parse
const htmlModule = htmlModules.get(id); const moduleInfo = this.getModuleInfo(id);
if(htmlModule) { const moduleMeta = moduleInfo!.meta ?? {};
const contents = await readFile(id, {encoding: "utf-8"}); let htmlModule = moduleMeta[pluginName];
if(!htmlModule){
const htmlSrc = transform ? await transform(contents, { const moduleExt = extname(id);
id, const moduleName = id.replace(new RegExp(`${moduleExt}\$`),''); // strip extension of the name if any
}) : contents; htmlModule = moduleMeta[pluginName] = {
id: id,
// Parse document and store it (TODO: check for watch mode, we should check if it needs reparsing or not) name: moduleName,
const document = htmlModule.document = htmlModule.document ?? parseHtml(htmlSrc); imports: [],
assetId: null,
// Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes) importers: new Set(),
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,
type
})=>{
if(!sourceId){
sourceId = makeInlineId(id, node, 'js');
}
if(source){
virtualSources.set(sourceId, source);
}
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,
};
} }
const contents = code;
const htmlSrc = transform ? await transform(contents, {
id,
}) : contents;
// Parse document and store it
const document = htmlModule.document = parseHtml(htmlSrc);
// TODO working on this: to preserve sourcemaps as much as possible we're starting the magic string on the raw html source
// question is if we need to though. sourcemaps only make sense for inlined bits of code
//let htmlJS = new MagicString(htmlSrc);// This is where we want to go!
// 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,
type
})=>{
if(!sourceId){
sourceId = makeInlineId(id, node, 'js');
}
if(source){
virtualSources.set(sourceId, source);
}
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);
let entryName: string|undefined = undefined;
const parentName = entryNames.get(id)??selfInfo?.meta[pluginName].name;
if(type==='entryChunk'){
entryName= posix.join(posix.dirname(parentName),sourceId);
entryName = entryName.slice(0,-(posix.extname(entryName).length)); // Cut off the extension (TODO, is this wise?)
}
const importName = (source && selfInfo?.meta[pluginName].name)
? makeInlineId(parentName, node, extname(sourceId))
: entryName;
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,
}
// if(entryName){
// addedEntries.set(resolved.id, entryName);// (we could do this using meta?)
// }
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);
}
// Beware leak of AST (we're starting MagicString on a parsed and modified version of the HTML file, sourcemappings in the HTML file will be off. (can't add a sourcemap for a html file anyway, unless it is outputted as JS module)
let htmlJS = new MagicString(serializeHtml(htmlModule.document));
htmlJS.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..
htmlJS = htmlJS.replace(htmlImport.placeholder, `\${${assetId}}`);// TODO: Should we be worried about windows absolute URLs here?
}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||'');
}
}
// Import all dependencies and wrap the HTML in a `...`, assign to a var and export (escaping any ` characters in the HTML)
htmlJS.prepend([
...moduleImports,
`export const html = \``
].join('\n')).append([
`\`;`,
`export default html;`,
].join('\n'));
const map = htmlJS.generateMap({
source: id,
file: `${id}.map`,
includeContent: true,
hires: 'boundary'
});
return {
code: htmlJS.toString(),
map: map.toString(),
meta: moduleMeta,
};
} }
}, },
outputOptions(options){ outputOptions(options){
return { return {
...options, ...options,
entryFileNames: (chunkInfo)=>{ entryFileNames: (chunkInfo)=>{
const htmlModule = chunkInfo.facadeModuleId ? htmlModules.get(chunkInfo.facadeModuleId!) : null; const moduleInfo = chunkInfo.facadeModuleId? this.getModuleInfo(chunkInfo.facadeModuleId) : null;
const htmlModule = moduleInfo?.meta?.[pluginName];
// const htmlModule = chunkInfo.facadeModuleId ? htmlModules.get(chunkInfo.facadeModuleId!) : null;
const addedEntry = chunkInfo.facadeModuleId ? addedEntries.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? 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){ if(htmlModule){
let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo); let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo);
if(fileName) { if(fileName) {
return fileName; return fileName;
} }
}else if(addedEntry){
return addedEntry;
} }
return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo); return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo);
}, },
@@ -236,7 +327,8 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
} }
}, },
resolveFileUrl(options){ resolveFileUrl(options){
const htmlModule = htmlModules.get(options.moduleId); const moduleInfo = this.getModuleInfo(options.moduleId);
const htmlModule = moduleInfo?.meta?.[pluginName];
if(htmlModule){ if(htmlModule){
// Simply use the relative path in our HTML-fileURLs instead of the default `new URL('${fileName}', document.baseURI).href`) // Simply use the relative path in our HTML-fileURLs instead of the default `new URL('${fileName}', document.baseURI).href`)
return `"${options.relativePath}"`; return `"${options.relativePath}"`;
@@ -247,7 +339,9 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
order:'post', order:'post',
handler(chunk: RenderedChunk){ handler(chunk: RenderedChunk){
if(chunk.facadeModuleId) { if(chunk.facadeModuleId) {
const htmlModule = htmlModules.get(chunk.facadeModuleId); const moduleInfo = chunk.facadeModuleId? this.getModuleInfo(chunk.facadeModuleId) : null;
const htmlModule = moduleInfo?.meta?.[pluginName];
// const htmlModule = htmlModules.get(chunk.facadeModuleId);
if (htmlModule) { if (htmlModule) {
return modulePrefix; // Overwrite any added banner with our own return modulePrefix; // Overwrite any added banner with our own
} }
@@ -265,7 +359,11 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
const chunk = (<OutputChunk>bundle); const chunk = (<OutputChunk>bundle);
if(chunk.facadeModuleId) { if(chunk.facadeModuleId) {
facadeToChunk.set(chunk.facadeModuleId, chunk); facadeToChunk.set(chunk.facadeModuleId, chunk);
const htmlModule = htmlModules.get(chunk.facadeModuleId);
const moduleInfo = this.getModuleInfo(chunk.facadeModuleId);
const htmlModule = moduleInfo?.meta?.[pluginName];
// const htmlModule = htmlModules.get(chunk.facadeModuleId);
if(htmlModule){ htmlResults.set(bundleName, {chunk, htmlModule})} if(htmlModule){ htmlResults.set(bundleName, {chunk, htmlModule})}
else if(virtualSources.has(chunk.facadeModuleId)){ else if(virtualSources.has(chunk.facadeModuleId)){
virtualBundles.add(bundleName); virtualBundles.add(bundleName);
@@ -328,7 +426,12 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
htmlContents = htmlContents.replace(htmlImport.placeholder, importResult.code); htmlContents = htmlContents.replace(htmlImport.placeholder, importResult.code);
}else if(htmlImport.type === 'entryChunk'){ }else if(htmlImport.type === 'entryChunk'){
const relPath = posix.relative(dirname(chunk.fileName), importResult.fileName); const relPath = posix.relative(dirname(chunk.fileName), importResult.fileName);
htmlContents = htmlContents.replace(htmlImport.placeholder, relPath); const rootPath = path.posix.join(dirname(chunk.fileName), relPath);
const rewritten = rewriteUrl? await Promise.resolve(rewriteUrl(relPath, {
from: chunk.fileName,
rootPath,
})): relPath;
htmlContents = htmlContents.replace(htmlImport.placeholder, rewritten);
} }
} }
} }

View File

@@ -46,9 +46,17 @@ export function makeInlineId(sourceId: string, node: DefaultTreeAdapterMap['chil
return [sourceId, [makeHtmlPath(node), 'js'].join('.')].join('.'); return [sourceId, [makeHtmlPath(node), 'js'].join('.')].join('.');
} }
/**
* Creates a loader function that maps node types and attributes to load operations.
*
* @param mappings - An array of NodeMapping objects specifying how to map and load different nodes.
* @returns A LoadNodeCallback function that can be used to load nodes based on the mappings.
*/
export function makeLoader(mappings: NodeMapping[] = defaultMapping){ export function makeLoader(mappings: NodeMapping[] = defaultMapping){
const fn : LoadNodeCallback = async function ({node, sourceId}, load){ const fn : LoadNodeCallback = async function ({node, sourceId}, load){
for(const mapping of mappings){ for(const mapping of mappings){
// Test the mapping for a match
if (mapping.tagName && mapping.tagName !== node.tagName) continue; // No match, skip if (mapping.tagName && mapping.tagName !== node.tagName) continue; // No match, skip
if (mapping.match){ if (mapping.match){
if(typeof(mapping.match) === 'function'){ if(typeof(mapping.match) === 'function'){
@@ -67,7 +75,10 @@ export function makeLoader(mappings: NodeMapping[] = defaultMapping){
} }
} }
} }
// If we've gotten this far its a valid mapping. (either inline or a src/href attribute)
if((<AttributeReference>mapping).attr){ if((<AttributeReference>mapping).attr){
// Mapped on attribute, resolve its src or href (or whatever was returned)
const attr = node.attrs.find(attr=>attr.name === (<AttributeReference>mapping).attr); const attr = node.attrs.find(attr=>attr.name === (<AttributeReference>mapping).attr);
if(!attr) continue ;// No match, skip if(!attr) continue ;// No match, skip
const placeholder = await load({ const placeholder = await load({
@@ -76,6 +87,7 @@ export function makeLoader(mappings: NodeMapping[] = defaultMapping){
}); });
attr.value = placeholder; attr.value = placeholder;
}else if((<BodyReference>mapping).body){ }else if((<BodyReference>mapping).body){
// Mapped as body, use the contents of the DOM element
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. 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 if(!body) continue; // Empty body, skip
const placeholder = await load({ const placeholder = await load({

View File

@@ -60,9 +60,9 @@ Generated by [AVA](https://avajs.dev).
[ [
{ {
code: undefined, code: undefined,
fileName: 'script.html.body.script.js-e3b82208.js.map', fileName: 'script.body.script.js-e3b82208.js.map',
map: undefined, 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"}', source: '{"version":3,"file":"script.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, code: undefined,
@@ -77,7 +77,7 @@ Generated by [AVA](https://avajs.dev).
document.body.appendChild(␊ document.body.appendChild(␊
document.createTextNode(\`Inline script including ${b()}\`)␊ document.createTextNode(\`Inline script including ${b()}\`)␊
);␊ );␊
//# sourceMappingURL=script.html.body.script.js-e3b82208.js.map␊ //# sourceMappingURL=script.body.script.js-e3b82208.js.map␊
</script>␊ </script>␊

Binary file not shown.

View File

@@ -2,7 +2,7 @@ import {join, dirname} from "node:path";
import test from "ava"; import test from "ava";
import { rollup } from "rollup"; import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js"; import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts"; import html from "../../src/index.ts";
@@ -25,7 +25,7 @@ test.serial('simple', async (t) => {
}), }),
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('simple',code); debugPrintOutput('simple',code);
t.snapshot(code); t.snapshot(code);
}); });
@@ -38,7 +38,7 @@ test.serial('inline-script', async (t) => {
}), }),
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('inline-script',code); debugPrintOutput('inline-script',code);
t.snapshot(code); t.snapshot(code);
}); });

View File

@@ -0,0 +1,13 @@
export async function app({root}){
const states = ['started', 'tick', 'ended'];
for(let state of states){
const text = `App ${state}`;
console.log(`Test my sourcemap: ${text}`);
root.innerHTML = `<div style="align-self: center"><b>${text}</b></div>`;
await new Promise((resolve,reject)=>
setTimeout(()=>resolve(), 10)
);
}
}

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<title>HTML5 Logo</title>
<path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/>
<path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/>
<path fill="#f16529" d="M256 480.5V131H404.3L376 447"/>
<path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/>
<path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Test bundle!
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="./assets/logo-sq.svg">
{{{ head }}}
</head>
<body>
<div id="root">Here the app should load!</div>
<script src="./index.mjs" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
// Dynamically loads libraries and bootstraps the application
(async ()=>{
// Add a loader here if any
const root = document.getElementById('root')
if(root) root.innerHTML= `<div style="align-self: center">My app has loaded!!</div>`;
try {
// Load app
const [
appModule,
] = await Promise.all([
import("./app.mjs"),
]);
console.log("Bootstrapped, ready to go!");
// Wait for DOM to be ready
if(document.readyState === 'loading') {
await new Promise((resolve)=>document.addEventListener('DOMContentLoaded', resolve));
}
// Start the app!
await appModule.app({root});
}catch(err){
console.error(err);
}
})()

View File

@@ -0,0 +1,42 @@
# Snapshot report for `test/evaluated-web-bundle/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## web-bundle
> Snapshot 1
{
console: [
'[log] Bootstrapped, ready to go!',
'[log] Test my sourcemap: App started',
'[log] Test my sourcemap: App tick',
'[log] Test my sourcemap: App ended',
],
errors: [],
html: `<html lang="en"><head>␊
<meta charset="UTF-8">␊
<title>␊
Test bundle!␊
</title>␊
<meta name="viewport" content="width=device-width, initial-scale=1">␊
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20512%20512%22%3E%20%20%20%20%3Ctitle%3EHTML5%20Logo%3C%2Ftitle%3E%20%20%20%20%3Cpath%20d%3D%22M108.4%200h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206%2023h-20.3V0h63.7v23H229v46h-23M259.5%200h24.1l14.8%2024.3L313.2%200h24.1v69h-23V34.8l-16.1%2024.8l-16.1-24.8v34.2h-22.6M348.7%200h23v46.2h32.6V69h-55.6%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23e44d26%22%20d%3D%22M107.6%20471l-33-370.4h362.8l-33%20370.2L255.7%20512%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23f16529%22%20d%3D%22M256%20480.5V131H404.3L376%20447%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23ebebeb%22%20d%3D%22M142%20176.3h114v45.4h-64.2l4.2%2046.5h60v45.3H154.4M156.4%20336.3H202l3.2%2036.3%2050.8%2013.6v47.4l-93.2-26%22%2F%3E%20%20%20%20%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M369.6%20176.3H255.8v45.4h109.6M361.3%20268.2H255.8v45.4h56l-5.3%2059-50.7%2013.6v47.2l93-25.8%22%2F%3E%3C%2Fsvg%3E">␊
<title>I'm cool!</title>␊
</head>␊
<body>␊
<div id="root"><div style="align-self: center"><b>App ended</b></div></div>␊
<script src="index.js" type="module"></script>␊
</body></html>`,
requestsFailed: [],
responses: [
'200 http://localhost/index.html',
'200 http://localhost/index.js',
'200 http://localhost/app.js',
],
}

Binary file not shown.

View File

@@ -0,0 +1,53 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url";
import html from "../../src/index.ts";
import {runBrowserTest} from "../util/browser-test.ts";
import {fileURLToPath} from "node:url";
import handlebars from "handlebars";
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('web-bundle', async (t) => {
const out = await runBrowserTest({
input: 'index.hbs',
treeshake: 'smallest',
plugins: [
html({
transform(src) {
return handlebars.compile(src)({
head: `<title>I'm cool!</title>`
});
}
}),
urlPlugin({
include: defaultAssetInclude,
}),
],
}, {
path: 'index.html',
log: t.log,
},{
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
chunkFileNames: '[name].js',
entryFileNames: '[name].[extname]',
assetFileNames: '[name].[extname]',
});
t.snapshot(out);
// await bundle.generate(output);
});

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,11 @@
<html>
<head>
<link rel="icon" href="./icon.svg">
<!-- TODO: support for css imports are yet to be added (as simple assets or through a preprocessor-->
<!-- <link rel="stylesheet" href="./joker.css">-->
</head>
<body>
<!-- TODO: this shouldn't have been commented out, but our plugin fails if it is included (which shoudn't happen!!) -->
<!--<script src="./batman.js" type="module"></script>-->
</body>
</html>

View File

@@ -0,0 +1,5 @@
import html from "./index.html"
export function render(){
return html;
}

View File

@@ -0,0 +1 @@
* { width: 100%; }

View File

@@ -0,0 +1,75 @@
# Snapshot report for `test/js-import/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## js-import
> Snapshot 1
[
{
code: `var asset0 = "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";␊
const html = \`<html><head>␊
<link rel="icon" href="${asset0}">␊
<!-- TODO: support for css imports are yet to be added (as simple assets or through a preprocessor-->␊
<!-- <link rel="stylesheet" href="./joker.css">-->␊
</head>␊
<body>␊
<!-- TODO: this shouldn't have been commented out, but our plugin fails if it is included (which shoudn't happen!!) -->␊
<!--<script src="./batman.js" type="module"></script>-->␊
</body></html>\`;␊
function render(){␊
return html;␊
}␊
export { render };␊
//# sourceMappingURL=index-3d1ca61b.js.map␊
`,
fileName: 'index-3d1ca61b.js',
map: SourceMap {
file: 'index-3d1ca61b.js',
mappings: 'AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACRN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;',
names: [],
sources: [
'../icon.svg',
'../index.html',
'../index.js',
],
sourcesContent: [
'export default "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"',
`<html>␊
<head>␊
<link rel="icon" href="./icon.svg">␊
<!-- TODO: support for css imports are yet to be added (as simple assets or through a preprocessor-->␊
<!-- <link rel="stylesheet" href="./joker.css">-->␊
</head>␊
<body>␊
<!-- TODO: this shouldn't have been commented out, but our plugin fails if it is included (which shoudn't happen!!) -->␊
<!--<script src="./batman.js" type="module"></script>-->␊
</body>␊
</html>␊
`,
`import html from "./index.html"␊
export function render(){␊
return html;␊
}␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'index-3d1ca61b.js.map',
map: undefined,
source: '{"version":3,"file":"index-3d1ca61b.js","sources":["../icon.svg","../index.html","../index.js"],"sourcesContent":["export default \\"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\\"","<html>\\n <head>\\n <link rel=\\"icon\\" href=\\"./icon.svg\\">\\n <!-- TODO: support for css imports are yet to be added (as simple assets or through a preprocessor-->\\n<!-- <link rel=\\"stylesheet\\" href=\\"./joker.css\\">-->\\n </head>\\n <body>\\n <!-- TODO: this shouldn\'t have been commented out, but our plugin fails if it is included (which shoudn\'t happen!!) -->\\n <!--<script src=\\"./batman.js\\" type=\\"module\\"></script>-->\\n </body>\\n</html>\\n","import html from \\"./index.html\\"\\n\\nexport function render(){\\n return html;\\n}\\n"],"names":[],"mappings":"AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACRN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"}',
},
]

Binary file not shown.

51
test/js-import/test.js Normal file
View File

@@ -0,0 +1,51 @@
import {join, dirname} from "node:path";
import test from "ava";
import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/index.ts";
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";
import urlPlugin from "@rollup/plugin-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('js-import', async (t) => {
const bundle = await rollup({
input: 'index.js',
plugins: [
html({
}),
// Test with assets
urlPlugin({
include: defaultAssetInclude,
limit: Number.MAX_SAFE_INTEGER,// Always inline things
}),
]
});
const code = await getCode(bundle, output);
debugPrintOutput('js-import',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,36 @@
import {createRoot} from "react-dom/client";
import {StrictMode, useEffect, useState} from "react";
const states = ['started', 'tick', 'ended'];
export function App(){
const [state, setState] = useState(states[0])
useEffect(()=>{
let timeout: any;
let nextState = states[states.indexOf(state)+1];
if(nextState) {
timeout = setTimeout(() => {
console.log(`Test my sourcemap: ${nextState}`);
setState(nextState)
}, 10);
}
return ()=>{
if(timeout) {
clearTimeout(timeout);
}
}
}, [state])
return (<div style={{alignSelf: "center"}}>
<b>{state}</b>
</div>);
}
export async function start({root: rootContainer}: {root: HTMLElement}){
if(!rootContainer) throw new Error("Missing root element");
const root = createRoot(rootContainer);
root.render(<StrictMode>
<App />
</StrictMode>);
}

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<title>HTML5 Logo</title>
<path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/>
<path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/>
<path fill="#f16529" d="M256 480.5V131H404.3L376 447"/>
<path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/>
<path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1,14 @@
export default {
presets: [
["@babel/preset-env", {
shippedProposals: true,
}],
["@babel/preset-typescript", {
}],
["@babel/preset-react", {
development: process.env.BABEL_ENV === "development",
runtime: "automatic"
}]
],
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
Test bundle!
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="./assets/logo-sq.svg">
{{{ head }}}
</head>
<body>
<div id="root">Here the app should load!</div>
<script src="./index.mjs" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
// Dynamically loads libraries and bootstraps the application
(async ()=>{
// Add a loader here if any
const root = document.getElementById('root')
if(root) root.innerHTML= `<div style="align-self: center">My app has loaded!!</div>`;
try {
// Load app
const [
appModule,
] = await Promise.all([
import("./app.tsx"),
]);
console.log("Bootstrapped, ready to go!");
// Wait for DOM to be ready
if(document.readyState === 'loading') {
await new Promise((resolve)=>document.addEventListener('DOMContentLoaded', resolve));
}
// Start the app!
await appModule.start({root});
}catch(err){
console.error(err);
}
})()

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "ESNext",
"moduleResolution": "Bundler",
"module": "ESNext",
"strict": true,
"paths":{
"react": ["./node_modules/@types/react"]
},
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": true,
"allowUnusedLabels": true,
"noUnusedLocals": false
},
}

85
test/jsx-web-app/test.js Normal file
View File

@@ -0,0 +1,85 @@
import {join, dirname} from "node:path";
import test from "ava";
// Rollup * plugins
import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url";
import nodeResolve from "@rollup/plugin-node-resolve";
import babelPlugin from "@rollup/plugin-babel";
import commonJsPlugin from "@rollup/plugin-commonjs";
import typescriptPlugin from "@rollup/plugin-typescript";
import replacePlugin from "@rollup/plugin-replace";
import html from "../../src/index.ts";
import {runBrowserTest} from "../util/browser-test.ts";
import {fileURLToPath} from "node:url";
import handlebars from "handlebars";
// import {debugPrintOutput, getCode, runBrowserTest} from "../util/index.ts";
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('web-bundle', async (t) => {
const out = await runBrowserTest({
input: 'index.hbs',
treeshake: 'smallest',
plugins: [
html({
transform(src) {
return handlebars.compile(src)({
head: `<title>I'm cool!</title>`
});
}
}),
nodeResolve({
extensions: ['.js', '.mjs', '.jsx', '.ts', '.tsx'],
browser: true,
}),
commonJsPlugin({
}),
typescriptPlugin({
sourceMap: true,
// exclude: 'node_modules/**',
noEmitOnError: true,
outputToFilesystem: false,
noForceEmit: true,
jsx: "preserve",
}),
babelPlugin({
extensions: ['.js', '.mjs', '.jsx', '.ts', '.tsx'],
babelHelpers: "bundled",
}),
replacePlugin({
preventAssignment: false,
'process.env.NODE_ENV': process.env.NODE_ENV==='production'?`'${process.env.NODE_ENV}'` : '"development"'
}),
urlPlugin({
include: defaultAssetInclude,
}),
],
}, {
path: 'index.html',
log: t.log,
},{
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
chunkFileNames: '[name].js',
entryFileNames: '[name].[extname]',
assetFileNames: '[name].[extname]',
});
t.snapshot(out);
// const code = await getCode(bundle, output);
// debugPrintOutput('jsx-web-app',code);
});

View File

@@ -3,7 +3,7 @@ import {join, dirname} from "node:path";
import test from "ava"; import test from "ava";
import {rollup} from "rollup"; import {rollup} from "rollup";
import liveReload from "rollup-plugin-livereload"; import liveReload from "rollup-plugin-livereload";
import {debugPrintOutput, getCode} from "../util/test.js"; import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts"; import html from "../../src/index.ts";
@@ -29,7 +29,7 @@ test.serial('live-reload', async (t) => {
}) })
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
await bundle.close();// Make sure live-reload closes itself await bundle.close();// Make sure live-reload closes itself
debugPrintOutput('live-reload',code); debugPrintOutput('live-reload',code);
t.snapshot(code); t.snapshot(code);

View File

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

View File

@@ -8,5 +8,6 @@
import {adminDeps} from "../app/admin-deps.js"; import {adminDeps} from "../app/admin-deps.js";
bootstrap(document.getElementById('root'), adminDeps()); bootstrap(document.getElementById('root'), adminDeps());
</script> </script>
<script src="./batman.js" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -9,6 +9,64 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1 > Snapshot 1
[ [
{
code: `const b = ()=>'batman';␊
console.log(b());␊
export { b };␊
//# sourceMappingURL=batman-c7fa228c.js.map␊
`,
fileName: 'admin/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: [
'../../admin/batman.js',
],
sourcesContent: [
`export const b = ()=>'batman';␊
console.log(b());␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'admin/batman-c7fa228c.js.map',
map: undefined,
source: '{"version":3,"file":"batman-c7fa228c.js","sources":["../../admin/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: 'admin/index.body.script0.js-15dfaff3.js.map',
map: undefined,
source: '{"version":3,"file":"index.body.script0.js-15dfaff3.js","sources":["../../app/admin-deps.js","../../admin/index.html.body.script0.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: '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.body.script0.js-15dfaff3.js.map␊
</script>␊
<script src="batman-c7fa228c.js" type="module"></script>␊
</body></html>`,
},
{ {
code: `const bootstrap = (el,deps = [])=>{␊ code: `const bootstrap = (el,deps = [])=>{␊
el.innerHtml = \` el.innerHtml = \`
@@ -41,24 +99,18 @@ Generated by [AVA](https://avajs.dev).
}, },
source: undefined, 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, code: undefined,
fileName: 'app-01141b67.js.map', fileName: 'app-01141b67.js.map',
map: undefined, 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;;;;"}', 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.body.script.js-45303f0f.js.map',
map: undefined,
source: '{"version":3,"file":"index.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, code: undefined,
fileName: 'index.html', fileName: 'index.html',
@@ -70,28 +122,7 @@ Generated by [AVA](https://avajs.dev).
<script type="module">import { b as bootstrap } from './app-01141b67.js';␊ <script type="module">import { b as bootstrap } from './app-01141b67.js';␊
bootstrap(document.getElementById('root'), "<none>");␊ bootstrap(document.getElementById('root'), "<none>");␊
//# sourceMappingURL=index.html.body.script.js-45303f0f.js.map␊ //# sourceMappingURL=index.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>␊ </script>␊

View File

@@ -1,8 +1,8 @@
import {join, dirname} from "node:path"; import {resolve, join, dirname} from "node:path";
import test from "ava"; import test from "ava";
import { rollup } from "rollup"; import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js"; import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts"; import html from "../../src/index.ts";
@@ -21,14 +21,14 @@ test.serial('multi-entry', async (t) => {
const bundle = await rollup({ const bundle = await rollup({
input: { input: {
['index']: 'index.html', ['index']: 'index.html',
['admin/index']: 'admin/index.html' ['admin/index']: resolve(__dirname,'fixtures','admin/index.html'),
}, },
plugins: [ plugins: [
html({ html({
}), }),
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('multi-entry',code); debugPrintOutput('multi-entry',code);
t.snapshot(code); t.snapshot(code);
}); });

View File

@@ -0,0 +1,5 @@
export const bootstrap = (el,deps = [])=>{
el.innerHtml = `
<div>load the app</div>
`;
}

View File

@@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
<div id="root"></div>
<script src="./app.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
<div id="root"></div>
<script src="./admin/app.js" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,74 @@
# Snapshot report for `test/rewrite-url/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## rewrite-url
> Snapshot 1
[
{
code: `const bootstrap = (el,deps = [])=>{␊
el.innerHtml = \`
<div>load the app</div>␊
\`;␊
};␊
export { bootstrap };␊
//# sourceMappingURL=app-88ed8fd6.js.map␊
`,
fileName: 'admin/app-88ed8fd6.js',
map: SourceMap {
file: 'app-88ed8fd6.js',
mappings: 'AAAY,MAAC,SAAS,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,GAAG;AACzC,IAAI,EAAE,CAAC,SAAS,GAAG,CAAC;AACpB;AACA,IAAI,CAAC,CAAC;AACN;;;;',
names: [],
sources: [
'../../admin/app.js',
],
sourcesContent: [
`export const bootstrap = (el,deps = [])=>{␊
el.innerHtml = \`␊
<div>load the app</div>␊
\`;␊
}␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'admin/app-88ed8fd6.js.map',
map: undefined,
source: '{"version":3,"file":"app-88ed8fd6.js","sources":["../../admin/app.js"],"sourcesContent":["export const bootstrap = (el,deps = [])=>{\\n el.innerHtml = `\\n <div>load the app</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;AACA,IAAI,CAAC,CAAC;AACN;;;;"}',
},
{
code: undefined,
fileName: 'admin/index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<div id="root"></div>␊
<script src="/admin/app-88ed8fd6.js" type="module"></script>␊
</body></html>`,
},
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<div id="root"></div>␊
<script src="/admin/app-88ed8fd6.js" type="module"></script>␊
</body></html>`,
},
]

Binary file not shown.

64
test/rewrite-url/test.js Normal file
View File

@@ -0,0 +1,64 @@
import {resolve, join, dirname} from "node:path";
import test from "ava";
import {runBrowserTest} from "../util/index.ts";
import html from "../../src/index.ts";
import {fileURLToPath} from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('rewrite-url', async (t) => {
const out = await runBrowserTest({
input: {
['index']: 'index.html',
['admin/index']: resolve(__dirname,'fixtures','admin/index.html'),
['admin/app']: resolve(__dirname,'fixtures','admin/app.js'),
},
plugins: [
html({
rewriteUrl(relative, {rootPath, from}){
return `/${rootPath}`;
}
}),
],
},{
log: t.log,
filterOutput:{
// TODO: Currently only need the "await getCode(bundle, output);" as output
},
path: '/admin'
}, {
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
});
t.snapshot(out.code); // Snapshot the result code
// const bundle = await rollup({
// input: {
// ['index']: 'index.html',
// ['admin/index']: resolve(__dirname,'fixtures','admin/index.html'),
// ['admin/app']: resolve(__dirname,'fixtures','admin/app.js'),
// },
// plugins: [
// html({
// rewriteUrl(relative, {rootPath, from}){
// return `/${rootPath}`;
// }
// }),
// ]
// });
// const code = await getCode(bundle, output);
// debugPrintOutput('rewrite-url',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

@@ -3,7 +3,7 @@ import {join, dirname} from "node:path";
import test from "ava"; import test from "ava";
import { rollup } from "rollup"; import { rollup } from "rollup";
import {debugPrintOutput, getCode} from "../util/test.js"; import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts"; import html from "../../src/index.ts";
import handlebars from "handlebars"; import handlebars from "handlebars";
@@ -30,7 +30,7 @@ test.serial('handlebars', async (t) => {
}) })
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('handlebars',code); debugPrintOutput('handlebars',code);
t.snapshot(code); t.snapshot(code);
}); });

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

@@ -4,7 +4,7 @@ import test from "ava";
import { rollup } from "rollup"; import { rollup } from "rollup";
import urlPlugin from "@rollup/plugin-url"; import urlPlugin from "@rollup/plugin-url";
import {debugPrintOutput, getCode} from "../util/test.js"; import {debugPrintOutput, getCode} from "../util/index.ts";
import html from "../../src/index.ts"; import html from "../../src/index.ts";
@@ -37,7 +37,7 @@ test.serial('copied-assets', async (t) => {
}), }),
], ],
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('copied-assets',code); debugPrintOutput('copied-assets',code);
t.snapshot(code); t.snapshot(code);
}); });
@@ -55,7 +55,7 @@ test.serial('inlined-assets', async (t) => {
}), }),
] ]
}); });
const code = await getCode(bundle, output, true); const code = await getCode(bundle, output);
debugPrintOutput('inlined-assets',code); debugPrintOutput('inlined-assets',code);
t.snapshot(code); t.snapshot(code);
}); });

129
test/util/browser-test.ts Normal file
View File

@@ -0,0 +1,129 @@
import {Plugin, InputPluginOption, RollupOptions, OutputOptions, RollupOutput} from "rollup";
import {TestOptions as BrowserTestOptions, TestOutput as PuppeteerTestOutput} from "./puppeteer-run-test.js";
import { rollup } from "rollup";
import serveTest, {LogCallback} from "./serve-test.js";
import type {ExecutionContext} from "ava";
import {getCode, TestOutput} from "./code-output.ts";
// /**
// * The AVA context used to test (ie t.snapshot(..) )
// */
// t: ExecutionContext
//
//
// filterOutput:{
// html: true,
// console: ['log','error','warn'],// TODO: or warning? need to check what possible values are
// errors: true, // again don't know possible values
// responses: true, // interesting to see what other values were requested
// requestsFailed: true, // will probably also be replicated into console errors, but helpful to have if imports werent found
// }
// try{
// // Track requests, errors and console
// page.on('console', message => {
// let [type, text] = [message.type(), message.text()];
// if(replaceHost){
// text = text.replaceAll(hostUrl, replaceHostWith!);
// }
// if((<any>filterOutput.console)?.includes?.(<any>type) ?? (filterOutput.console === true)){// TODO: add callback option
// output.console?.push(`[${type}] ${text}`);
// }
// }).on('pageerror', ({ message }) => {
// let text = message;
// if(replaceHost){
// text = text.replaceAll(hostUrl, replaceHostWith!);
// }
// if(filterOutput.errors === true) {// TODO add callback option
// output.errors?.push(text)
// }
// }).on('response', response => {
// let [status, url] = [response.status(), response.url()]
// if(replaceHost){
// url = url.replaceAll(hostUrl, replaceHostWith!);
// }
// if(filterOutput.responses === true) {// TODO add callback option
// output.responses?.push(`${status} ${url}`)
// }
// }).on('requestfailed', request => {
// let [failure, url] = [request.failure()?.errorText, request.url()];
// if(replaceHost){
// failure = failure?.replaceAll(hostUrl, replaceHostWith!);
// url = url.replaceAll(hostUrl, replaceHostWith!);
// }
// if(filterOutput.requestsFailed === true) {// TODO add callback option
// output.requestsFailed?.push(`${failure} ${url}`)
// }
// });
// testOptions.t?.snapshot?.(testOutput);
export interface OutputFilterOptions {
html?: boolean
console?: ('log'|'error'|'warn')[] | true
errors?: boolean, // again don't know possible values
responses?: boolean, // interesting to see what other values were requested
requestsFailed?: boolean, // will probably also be replicated into console errors, but helpful to have if imports werent found
}
export interface BrowserTestInput extends BrowserTestOptions{
log?: LogCallback;
/**
* Optionally specify what to filter from the output
*/
filterOutput?: OutputFilterOptions;
}
export interface BrowserTestOutput extends PuppeteerTestOutput{
code: TestOutput[];
}
export async function runBrowserTest(
build: RollupOptions,
test?: BrowserTestInput | false,
output?: OutputOptions
) : Promise<Partial<BrowserTestOutput>>{
const resolvedPlugins = await Promise.resolve(build.plugins||null);
let pluginsArray : InputPluginOption[] = [];
if(resolvedPlugins && resolvedPlugins instanceof Array){
pluginsArray = resolvedPlugins
}else if(resolvedPlugins){
pluginsArray = [resolvedPlugins];
}
let testOutput: Partial<BrowserTestOutput> = {};
const bundle = await rollup({
...build,
plugins: [
...pluginsArray,
// TODO check if browser output is requested (either for snapshot or for testing)
...(test? [serveTest({
// TODO: intercept output from the serveTest? (and include as one bit in output options below, for snapshotting)
...test,
log: test.log ?? console.log,
onResult: (output)=>{
testOutput = {...testOutput, ...output};
}
})]: [])
]
});
// TODO make configurable?
const generated = await bundle.generate({
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
chunkFileNames: '[name].js',
entryFileNames: '[name].mjs',
assetFileNames: '[name].[extname]',
});
if(output){
testOutput.code = await getCode(bundle, output);
}
return testOutput
}

26
test/util/code-output.ts Normal file
View File

@@ -0,0 +1,26 @@
import type {RollupBuild, OutputOptions, OutputAsset, OutputChunk, SourceMap} from "rollup";
export interface TestOutput{
code: string,
fileName: string,
source: any,
map: any
}
export const getCode = async (bundle: RollupBuild, outputOptions: OutputOptions): Promise<TestOutput[]> => {
const { output } = await bundle.generate(outputOptions || { format: 'cjs', exports: 'auto' });
return output.sort((a,b)=> {
if(a.fileName === b.fileName && (<OutputAsset>a).source !== (<OutputAsset>b).source){ return (<OutputAsset>a).source<(<OutputAsset>b).source?-1:1}
return a.fileName < b.fileName ? -1 : (a.fileName > b.fileName? 1 : 0);
}).map(chunk=> {
const { code, map } = (<OutputChunk>chunk);
const { fileName, source } = (<OutputAsset>chunk);
return {
code,
fileName,
source,
map
};
});
};

5
test/util/debug-mode.ts Normal file
View File

@@ -0,0 +1,5 @@
import inspector from 'node:inspector';
export function isInDebugMode() {
return (inspector.url() !== undefined) || process.env.DEBUG;
}

8
test/util/index.ts Normal file
View File

@@ -0,0 +1,8 @@
// TODO: this should be the main module used, other should be imported manually if exceptions are needed?
export * from "./browser-test.ts";
export * from "./code-output.ts";
export * from "./print-code-output.ts";
export * from "./serve-test.ts";
// export * from './misc.js';

View File

@@ -1,56 +1,7 @@
// This is still from the old rollup plugin we forked from. For now not used.
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import chalk from "chalk";
/**
* @param {import('rollup').RollupBuild} bundle
* @param {import('rollup').OutputOptions} [outputOptions]
*/
export const getCode = async (bundle, outputOptions, allFiles = false) => {
const { output } = await bundle.generate(outputOptions || { format: 'cjs', exports: 'auto' });
if (allFiles) {
return output.map(({ code, fileName, source, map }) => {
return {
code,
fileName,
source,
map
};
});
}
const [{ code }] = output;
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').RollupBuild} bundle

View File

@@ -0,0 +1,32 @@
import process from "node:process";
import chalk from "chalk";
import {TestOutput} from "./code-output.ts";
export const debugPrintOutput = async (header: string, files: TestOutput[]) => {
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;
};

View File

@@ -0,0 +1,133 @@
/**
* Puppeteer + from-memory devServer rollup plugin to open the result in a webpage en output the result
* (after an optional series of commands to the puppeteer Page)
*/
import puppeteer, {Page} from "puppeteer";
import {fileURLToPath, URL} from "node:url";
import {isInDebugMode} from "./debug-mode.ts";
export type PageTestCallback = (page: Page)=>Promise<void>;
export interface TestOptions {
path: string
cb: PageTestCallback
replaceHost: boolean
replaceHostWith?: string
}
const defaultOptions: Partial<TestOptions> = {
path: 'index.html',
cb: async (page: Page)=>{
await page.waitForNetworkIdle({});
},
replaceHost: true,
replaceHostWith: `http://localhost`,
}
export interface TestOutput{
html: string,
console: string[],
errors: string[],
responses: string[],
requestsFailed: string[],
}
/**
* Opens a page in a puppeteer browser and return the resulting HTML and logmessages produced.
* Optionally a callback can be provided to simulate user interactions on the page before returning the HTML
* When DEBUG mode is detected, puppeteer headless mode will be disabled allowing you to inspect the page if you set a breakpoint
*
* @param opts
* @param hostUrl
*/
export async function puppeteerRunTest(opts: Partial<TestOptions>, hostUrl: string){
const options : TestOptions = (<TestOptions>{
...defaultOptions,
...opts,
});
const {
path,
cb,
replaceHost,
replaceHostWith,
} = options;
const browser = await puppeteer.launch({
headless: isInDebugMode()? false : 'new',
});
const page = await browser.newPage();
let output : TestOutput = {
html: '',
console: [],
errors: [],
responses: [],
requestsFailed: []
};
let errored = false;
try {
// Track requests, errors and console
page.on('console', message => {
let [type, text] = [message.type(), message.text()];
if (replaceHost) {
text = text.replaceAll(hostUrl, replaceHostWith!);
}
output.console?.push(`[${type}] ${text}`);
}).on('pageerror', ({message}) => {
let text = message;
if (replaceHost) {
text = text.replaceAll(hostUrl, replaceHostWith!);
}
output.errors?.push(text);
}).on('response', response => {
let [status, url] = [response.status(), response.url()]
if (replaceHost) {
url = url.replaceAll(hostUrl, replaceHostWith!);
}
output.responses?.push(`${status} ${url}`);
}).on('requestfailed', request => {
let [failure, url] = [request.failure()?.errorText, request.url()];
if (replaceHost) {
failure = failure?.replaceAll(hostUrl, replaceHostWith!);
url = url.replaceAll(hostUrl, replaceHostWith!);
}
output.requestsFailed?.push(`${failure} ${url}`);
});
const url = new URL(path??'', hostUrl);
await page.goto(url.href);
if (!cb) {
await page.waitForNetworkIdle({});
} else {
await cb(page);
}
const htmlHandle = await page.$('html');
const html = await page.evaluate(html => html?.outerHTML ?? html?.innerHTML, htmlHandle);
// Add the final html
output.html = html || '';
return output;
}catch(err){
errored = true;
throw err;
}finally{
if(isInDebugMode() && !errored){
console.log(`DEBUG MODE ENABLED, Close the puppeteer browsertab to continue!\n${import.meta.url}:144`);
await new Promise((resolve)=>{
page.on('close', ()=>{
console.log("Page closed");
resolve(null);
})
});
}else{
await page.close();
}
await browser.close();
}
}

289
test/util/serve-test.ts Normal file
View File

@@ -0,0 +1,289 @@
/**
* Puppeteer + from-memory devServer rollup plugin to open the result in a webpage en output the result
* (after an optional series of commands to the puppeteer Page)
*/
import {puppeteerRunTest, PageTestCallback, TestOutput} from "./puppeteer-run-test.ts";
import {isInDebugMode} from "./debug-mode.ts";
import {resolve, posix} from "node:path";
import fs from "node:fs/promises";
import type {Stats} from "node:fs";
import { createServer as createHttpsServer } from 'https'
import { createServer} from 'http'
import { Mime } from 'mime/lite'
import standardTypes from 'mime/types/standard.js'
import otherTypes from 'mime/types/other.js'
import type {NormalizedOutputOptions, OutputAsset, OutputBundle, OutputChunk, Plugin} from 'rollup';
import type {
IncomingHttpHeaders, OutgoingHttpHeaders,
IncomingMessage, ServerResponse,
Server
} from 'http'
import type { ServerOptions } from 'https'
import test, {ExecutionContext} from "ava";
import {createReadStream} from "fs";
type TypeMap = {
[key: string]: string[];
};
type ErrorCodeException = Error & {code: string};
export type TestResultCallback = (output: TestOutput)=>void;
export type LogCallback = (...args: string[])=>void;
export interface ServeTestOptions {
/**
* Change the path to be opened when the test is started
* Remember to start with a slash, e.g. `'/different/page'`
*/
path?: string
/**
* Fallback to serving from a specified srcDir, this allows setting breakpoints on sourcecode and test the sourcemaps
*/
srcDir?: string|boolean;
/**
* A callback to manually take control of the page and simulate user interactions
*/
cb?: PageTestCallback;
/**
* Set to `true` to return index.html (200) instead of error page (404)
* or path to fallback page
*/
historyApiFallback?: boolean | string
/**
* Change the host of the server (default: `'localhost'`)
*/
host?: string
/**
* Change the port that the server will listen on (default: `10001`)
*/
port?: number | string
/**
* By default server will be served over HTTP (https: `false`). It can optionally be served over HTTPS.
*/
https?: ServerOptions | false
/**
* Set custom response headers
*/
headers?:
| IncomingHttpHeaders
| OutgoingHttpHeaders
| {
// i.e. Parameters<OutgoingMessage["setHeader"]>
[name: string]: number | string | ReadonlyArray<string>
}
/**
* Set custom mime types, usage https://github.com/broofa/mime#mimedefinetypemap-force--false
*/
mimeTypes?: TypeMap
/**
* Execute function after server has begun listening
*/
onListening?: (server: Server) => void
}
export interface RollupServeTestOptions extends ServeTestOptions{
/**
* A callback to run when a test has been run
*/
onResult?: TestResultCallback;
/**
* Callback to log messages
*/
log?: LogCallback;
}
/**
* Serve your rolled up bundle like webpack-dev-server
* @param {import('..').RollupServeOptions} options
*/
export default function serveTest (options: RollupServeTestOptions ): Plugin {
const mime = new Mime(standardTypes, otherTypes)
const testOptions = {
port: 0,
headers: {},
historyApiFallback: true,
srcDir: '', // Serve source dir as fallback (for sourcemaps / debugging)
onListening: function noop (){},
...options||{},
https: options.https??false,
mimeTypes: options.mimeTypes? mime.define(options.mimeTypes, true): false
}
let server : Server;
let bundle : OutputBundle = {};
const logTest = (msg: string, mode: 'info'|'warn' = 'info')=>{
if(isInDebugMode()){
console.log(msg);
}
const modeColor = {
green: 32,
info: 34,
warn: 33,
}[mode];
testOptions.log?.(`\u001b[${modeColor}m${msg}\u001b[0m`);
}
const requestListener = async (request: IncomingMessage, response: ServerResponse) => {
// Remove querystring
const unsafePath = decodeURI(request.url!.split('?')[0])
// Don't allow path traversal
const urlPath = posix.normalize(unsafePath)
for(const [key, value] of Object.entries((<OutgoingHttpHeaders>testOptions.headers))){
response.setHeader(key, value!);
}
function urlToFilePath(url:string){
return url[0]==='/'?url.slice(1):url;
}
let filePath = urlToFilePath(urlPath);
let absPath: string | undefined = undefined;
let stats: Stats | undefined = undefined;
if(!bundle[filePath]){
if(testOptions.srcDir || testOptions.srcDir===''){
try{
absPath = resolve(<string>testOptions.srcDir||'',filePath);
stats = await fs.stat(absPath);
}catch(err){
// File not found
}
}
if(!(stats?.isFile()) && testOptions.historyApiFallback) {
const fallbackPath = typeof testOptions.historyApiFallback === 'string'
? testOptions.historyApiFallback
: '/index.html';
if(bundle[urlToFilePath(fallbackPath)]){
filePath = urlToFilePath(fallbackPath);
}
}
}
const mimeType = mime.getType(filePath!);
if(bundle[filePath]) {
let file: OutputChunk | OutputAsset = bundle[filePath];
const content = (<OutputChunk>file).code || (<OutputAsset>file).source; // Todo might need to read a source file;
response.writeHead(200, {'Content-Type': mimeType || 'text/plain'});
response.end(content, 'utf-8');
logTest(`[200] ${request.url}`);
return;
}else if(stats?.isFile()){
response.writeHead(200, {
'Content-Type': mimeType || 'text/plain',
'Content-Length': stats.size,
'Last-Modified': stats.mtime.toUTCString()
});
const content = await fs.readFile(absPath!);
response.end(content);
response.end();
logTest(`[200] ${request.url} (src)`);
}else{
response.writeHead(404)
response.end(
'404 Not Found' + '\n\n' + filePath,
'utf-8'
)
logTest(`[404] ${request.url}`, "warn");
return;
}
}
function closeServerOnTermination () {
const terminationSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP']
terminationSignals.forEach(signal => {
process.on(signal, () => {
if (server) {
server.close()
process.exit()
}
})
})
}
// release previous server instance if rollup is reloading configuration in watch mode
// @ts-ignore
if (server) {
server.close()
} else {
closeServerOnTermination()
}
// If HTTPS options are available, create an HTTPS server
server = testOptions.https
? createHttpsServer(testOptions.https, requestListener)
: createServer(requestListener)
server.listen(
typeof(testOptions.port)==='string'? Number.parseInt(testOptions.port):testOptions.port,
testOptions.host,
undefined,
() => testOptions.onListening?.(server)
)
testOptions.port = (<any>server.address())?.port ?? testOptions.port;
// Assemble url for error and info messages
const url = (testOptions.https ? 'https' : 'http') + '://' + (testOptions.host || 'localhost') + ':' + testOptions.port
// Handle common server errors
server.on('error', e => {
if ((<ErrorCodeException>e).code === 'EADDRINUSE') {
console.error(url + ' is in use, either stop the other server or use a different port.')
process.exit()
} else {
throw e
}
})
let first = true
return {
name: 'serve',
generateBundle: {
order: 'post',
async handler(options, output){
bundle = output;
if (first) {
first = false
const testOutput = await puppeteerRunTest({
...testOptions
}, url);
testOptions.onResult?.(testOutput);
}
}
},
closeBundle (){
// Done with the bundle
}
}
}

45
test/util/test.d.ts vendored
View File

@@ -1,45 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import type { RollupBuild, OutputOptions, OutputChunk, OutputAsset } from 'rollup';
import type { Assertions } from 'ava';
interface GetCode {
(bundle: RollupBuild, outputOptions?: OutputOptions | null, allFiles?: false): Promise<string>;
(bundle: RollupBuild, outputOptions: OutputOptions | null | undefined, allFiles: true): Promise<
Array<{
code: OutputChunk['code'] | undefined;
fileName: OutputChunk['fileName'] | OutputAsset['fileName'];
source: OutputAsset['source'] | undefined;
}>
>;
}
export const getCode: GetCode;
export function getFiles(
bundle: RollupBuild,
outputOptions?: OutputOptions
): Promise<
{
fileName: string;
content: any;
}[]
>;
export function evaluateBundle(bundle: RollupBuild): Promise<Pick<NodeModule, 'exports'>>;
export function getImports(bundle: RollupBuild): Promise<string[]>;
export function getResolvedModules(bundle: RollupBuild): Promise<Record<string, string>>;
export function onwarn(warning: string | any): void;
export function testBundle(
t: Assertions,
bundle: RollupBuild,
options: { inject: Record<string, any>; options: Record<string, any> }
): Promise<{
code: string;
error?: any;
result?: any;
module: Pick<NodeModule, 'exports'>;
}>;

View File

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

View File

@@ -0,0 +1,3 @@
export const a = 1; // DO NOT CHANGE ME HERE, but in ../test.js

View File

@@ -0,0 +1,53 @@
# Snapshot report for `test/watch/test.js`
The actual snapshot is saved in `test.js.snap`.
Generated by [AVA](https://avajs.dev).
## watch
> Snapshot 1
[
{
code: undefined,
fileName: 'index.html',
map: undefined,
source: `<html><head>␊
</head>␊
<body>␊
<script src="watched-file-8c4729c5.js" type="module"></script>␊
</body></html>`,
},
{
code: `const a = 2; // If i show up as a changed file, then the watch test has gone wrong!␊
export { a };␊
//# sourceMappingURL=watched-file-8c4729c5.js.map␊
`,
fileName: 'watched-file-8c4729c5.js',
map: SourceMap {
file: 'watched-file-8c4729c5.js',
mappings: 'AACgB,MAAC,CAAC,GAAG,EAAE;;;;',
names: [],
sources: [
'../watched-file.js',
],
sourcesContent: [
`␊
export const a = 2; // If i show up as a changed file, then the watch test has gone wrong!␊
`,
],
version: 3,
},
source: undefined,
},
{
code: undefined,
fileName: 'watched-file-8c4729c5.js.map',
map: undefined,
source: '{"version":3,"file":"watched-file-8c4729c5.js","sources":["../watched-file.js"],"sourcesContent":["\\n export const a = 2; // If i show up as a changed file, then the watch test has gone wrong!\\n "],"names":[],"mappings":"AACgB,MAAC,CAAC,GAAG,EAAE;;;;"}',
},
]

Binary file not shown.

97
test/watch/test.js Normal file
View File

@@ -0,0 +1,97 @@
import {join, dirname} from "node:path";
import test from "ava";
import * as rollup from "rollup";
import {debugPrintOutput, getCode} from "../util/index.ts";
import {resolve} from "node:path";
import {writeFile} from "node:fs/promises";
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";
import {pathToFileURL} from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
process.chdir(join(__dirname, 'fixtures'));
test.serial('watch', async (t) => {
const origContent = `
export const a = 1; // DO NOT CHANGE ME HERE, but in ../test.js
`;
const changeContent = `
export const a = 2; // If i show up as a changed file, then the watch test has gone wrong!
`
const path = resolve(__dirname, 'fixtures/watched-file.js');
await writeFile(path, origContent, {encoding: 'utf-8'});
const watcher = rollup.watch({
input: 'index.html',
output,
plugins: [
html({
}),
],
watch: {
skipWrite: true,
}
});
const steps = [
async (bundle)=>{
await writeFile(path, changeContent, {encoding: 'utf-8'});
// Just wait on the watch mode to pick up on the changes
},
async (bundle)=>{
const code = await getCode(bundle, output);
debugPrintOutput('watch',code);
// Reset the source file
await writeFile(path, origContent, {encoding: 'utf-8'});
// Assert the output is what we exapect;
t.snapshot(code);
watcher
},
];
await new Promise((resolve, reject)=>{
watcher.on('event', async (event) => {
const {result} = event;
switch (event.code) {
case "START":
t.log(`WATCH STARTED`);
break;
case "BUNDLE_START":
t.log(`REBUILDING...`);
break;
case "BUNDLE_END":
t.log(`Rebuilt...`);
const cb = steps.shift();
const generated = await result.generate(output);
const cbResult = await cb(result);
if(steps.length===0){
watcher.close();
resolve();
}
break;
case "ERROR":
reject(event.error);
break;
}
if (result) {
result.close();
}
});
});
});

View File

@@ -13,7 +13,8 @@
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"allowJs": true "allowJs": true,
"allowImportingTsExtensions": true
}, },
"exclude": ["dist", "node_modules", "test/types"], "exclude": ["dist", "node_modules", "test/types"],
"include": ["src/**/*", "types/**/*"], "include": ["src/**/*", "types/**/*"],

24
types/index.d.ts vendored
View File

@@ -15,6 +15,11 @@ export interface RollupHtmlTransformContext {
// files: Record<string, (OutputChunk | OutputAsset)[]>; // files: Record<string, (OutputChunk | OutputAsset)[]>;
} }
export interface RewriteUrlCallbackContext {
from: string;
rootPath: string;
}
export type RewriteUrlCallback = (relative: string, context: RewriteUrlCallbackContext) => string|Promise<string>;
export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>; export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>;
export interface RollupHtmlOptions { export interface RollupHtmlOptions {
@@ -23,6 +28,7 @@ export interface RollupHtmlOptions {
* Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames). * Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames).
*/ */
htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string); htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string);
/** /**
* Transform a source file passed into this plugin to HTML. For example: a handlebars transform * Transform a source file passed into this plugin to HTML. For example: a handlebars transform
* ``` * ```
@@ -33,6 +39,17 @@ export interface RollupHtmlOptions {
*/ */
transform?: TransformCallback; transform?: TransformCallback;
/**
* Optional callback to rewrite how resources are referenced in the output HTML.
* For example to rewrite urls to as paths from the root of your website:
* ```
* rewriteUrl(relative, {rootPath, from}){
* return `/${rootPath}`;
* }
* ```
*/
rewriteUrl?: RewriteUrlCallback;
/** /**
* Detect which references (<a href="...">, <img src="...">) to resolve from a HTML node. * 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. * This rarely needs to be overloaded, but can be used to support non-native attributes used by custom-elements.
@@ -49,7 +66,14 @@ export interface RollupHtmlOptions {
* Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id) * Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id)
*/ */
resolve?: ResolveCallback; resolve?: ResolveCallback;
/**
* [Pattern](https://github.com/micromatch/picomatch#globbing-features) to include
*/
include?: FilterPattern; include?: FilterPattern;
/**
* [Pattern](https://github.com/micromatch/picomatch#globbing-features) to exclude
*/
exclude?: FilterPattern exclude?: FilterPattern
} }