test: added a react application test (in the browser)
This commit is contained in:
parent
e96c2248ee
commit
3e46055845
@ -63,12 +63,18 @@
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@rollup/plugin-babel": "^6.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-url": "^8.0.2",
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@types/node": "^18.18.12",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"ava": "^5.3.1",
|
||||
"chalk": "^5.3.0",
|
||||
"del-cli": "^5.1.0",
|
||||
|
||||
1226
pnpm-lock.yaml
generated
1226
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
src/index.ts
20
src/index.ts
@ -77,25 +77,25 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
||||
let filter = createFilter(include, exclude, {});
|
||||
|
||||
// TODO, we need to clear all these properly at sme point to avoid odd bugs in watch mode
|
||||
// let htmlModules = new Map<string, HtmlModule>();// todo clean this per new build?
|
||||
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
|
||||
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).
|
||||
* To satisfy rollup a JS version of the html file is returned, optionally including a few imports left for rollup to resolve
|
||||
* 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 the html file.
|
||||
* and replaced with a html file.
|
||||
*
|
||||
* Caveats:
|
||||
* - to get the resulting html content file we're parsing the evaluating the result JS module and take its default export
|
||||
* - 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.
|
||||
* - sourcemaps won't work yet (to add support we'd need to rewrite using magic-string?)
|
||||
* - 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?
|
||||
@ -182,6 +182,10 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
||||
// 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) {
|
||||
@ -261,8 +265,9 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
||||
} while (nodeQueue.length > 0);
|
||||
}
|
||||
|
||||
let htmlJS = new MagicString(serializeHtml(htmlModule.document));// TODO this is still a leak of AST, we're taking parse5 edited result and then transforming sourcemaps, this will only work when no edits were made
|
||||
let htmlJS = new MagicString(serializeHtml(htmlModule.document));
|
||||
htmlJS.replaceAll(/`/g,'\\\`').replaceAll(/\$\{/g,'\\${');
|
||||
|
||||
const moduleImports = [];
|
||||
for(const htmlImport of htmlImports){
|
||||
if(htmlImport.type === 'default') {
|
||||
@ -275,6 +280,7 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = \``
|
||||
|
||||
13
test/evaluated-web-bundle/fixtures/app.mjs
Normal file
13
test/evaluated-web-bundle/fixtures/app.mjs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export const batman = 'bum badum badum baaaaa dum!';
|
||||
@ -13,6 +13,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">Here the app should load!</div>
|
||||
<script src="./index.js" type="module"></script>
|
||||
<script src="./index.mjs" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
const [
|
||||
appModule,
|
||||
] = await Promise.all([
|
||||
import("./batman.js"),
|
||||
import("./app.mjs"),
|
||||
]);
|
||||
|
||||
console.log("Bootstrapped, ready to go!");
|
||||
@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
// Start the app!
|
||||
root.innerHTML = `<div style="align-self: center"><b>${appModule.batman}</b></div>`;
|
||||
await appModule.app({root});
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
}
|
||||
@ -11,6 +11,9 @@ Generated by [AVA](https://avajs.dev).
|
||||
{
|
||||
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>␊
|
||||
@ -25,7 +28,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<title>I'm cool!</title>␊
|
||||
</head>␊
|
||||
<body>␊
|
||||
<div id="root"><div style="align-self: center"><b>bum badum badum baaaaa dum!</b></div></div>␊
|
||||
<div id="root"><div style="align-self: center"><b>App ended</b></div></div>␊
|
||||
<script src="index.js" type="module"></script>␊
|
||||
␊
|
||||
␊
|
||||
@ -34,40 +37,6 @@ Generated by [AVA](https://avajs.dev).
|
||||
responses: [
|
||||
'200 http://localhost/index.html',
|
||||
'200 http://localhost/index.js',
|
||||
'200 http://localhost/batman.js',
|
||||
],
|
||||
}
|
||||
|
||||
## copied-assets
|
||||
|
||||
> Snapshot 1
|
||||
|
||||
{
|
||||
console: [
|
||||
'[log] Bootstrapped, ready to go!',
|
||||
],
|
||||
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>bum badum badum baaaaa dum!</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/batman.js',
|
||||
'200 http://localhost/app.js',
|
||||
],
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,9 +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>
|
||||
|
||||
@ -14,9 +14,11 @@ Generated by [AVA](https://avajs.dev).
|
||||
␊
|
||||
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>-->␊
|
||||
␊
|
||||
␊
|
||||
@ -27,26 +29,28 @@ Generated by [AVA](https://avajs.dev).
|
||||
}␊
|
||||
␊
|
||||
export { render };␊
|
||||
//# sourceMappingURL=index-f75fa1e5.js.map␊
|
||||
//# sourceMappingURL=index-3d1ca61b.js.map␊
|
||||
`,
|
||||
fileName: 'index-f75fa1e5.js',
|
||||
fileName: 'index-3d1ca61b.js',
|
||||
map: SourceMap {
|
||||
file: 'index-f75fa1e5.js',
|
||||
mappings: 'AAAA,aAAe;;ACAf,MAAA,IAAA,GAAA,CAAA;AACA,+BAA+B,EAAwD,MAAA,CAAA;AACvF;AACA;AACA;AACA;AACA;AACA;AACA,cAAa,CAAA;;ACNN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;',
|
||||
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.ts',
|
||||
'../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>␊
|
||||
@ -64,8 +68,8 @@ Generated by [AVA](https://avajs.dev).
|
||||
},
|
||||
{
|
||||
code: undefined,
|
||||
fileName: 'index-f75fa1e5.js.map',
|
||||
fileName: 'index-3d1ca61b.js.map',
|
||||
map: undefined,
|
||||
source: '{"version":3,"file":"index-f75fa1e5.js","sources":["../icon.svg","../index.html","../index.ts"],"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<!-- <link rel=\\"stylesheet\\" href=\\"./joker.css\\">-->\\n </head>\\n <body>\\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,cAAa,CAAA;;ACNN,SAAS,MAAM,EAAE;AACxB,IAAI,OAAO,IAAI,CAAC;AAChB;;;;"}',
|
||||
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.
36
test/jsx-web-app/fixtures/app.tsx
Normal file
36
test/jsx-web-app/fixtures/app.tsx
Normal 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>);
|
||||
}
|
||||
8
test/jsx-web-app/fixtures/assets/logo-sq.svg
Normal file
8
test/jsx-web-app/fixtures/assets/logo-sq.svg
Normal 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 |
14
test/jsx-web-app/fixtures/babel.config.js
Normal file
14
test/jsx-web-app/fixtures/babel.config.js
Normal 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"
|
||||
}]
|
||||
],
|
||||
}
|
||||
18
test/jsx-web-app/fixtures/index.hbs
Normal file
18
test/jsx-web-app/fixtures/index.hbs
Normal 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>
|
||||
27
test/jsx-web-app/fixtures/index.mjs
Normal file
27
test/jsx-web-app/fixtures/index.mjs
Normal 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);
|
||||
}
|
||||
})()
|
||||
16
test/jsx-web-app/fixtures/tsconfig.json
Normal file
16
test/jsx-web-app/fixtures/tsconfig.json
Normal 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
|
||||
},
|
||||
}
|
||||
90
test/jsx-web-app/test.js
Normal file
90
test/jsx-web-app/test.js
Normal file
@ -0,0 +1,90 @@
|
||||
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 serveTest from "../util/test-server.ts";
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @type {OutputOptions}
|
||||
*/
|
||||
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
|
||||
chunkFileNames: '[name].js',
|
||||
entryFileNames: '[name].[extname]',
|
||||
assetFileNames: '[name].[extname]',
|
||||
};
|
||||
|
||||
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 bundle = await rollup({
|
||||
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,
|
||||
}),
|
||||
|
||||
serveTest({
|
||||
path: 'index.html',
|
||||
t,
|
||||
})
|
||||
],
|
||||
});
|
||||
const generated = await bundle.generate(output);
|
||||
});
|
||||
|
||||
5
test/util/debug-mode.ts
Normal file
5
test/util/debug-mode.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import inspector from 'node:inspector';
|
||||
|
||||
export function isInDebugMode() {
|
||||
return (inspector.url() !== undefined) || process.env.DEBUG;
|
||||
}
|
||||
155
test/util/run-browser-test.ts
Normal file
155
test/util/run-browser-test.ts
Normal file
@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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 TestFilterOptions{
|
||||
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 TestOptions {
|
||||
page: string
|
||||
cb: PageTestCallback
|
||||
filterOutput: TestFilterOptions
|
||||
replaceHost: boolean
|
||||
replaceHostWith?: string
|
||||
}
|
||||
const defaultOptions: Partial<TestOptions> = {
|
||||
page: 'index.html',
|
||||
cb: async (page: Page)=>{
|
||||
await page.waitForNetworkIdle({});
|
||||
},
|
||||
replaceHost: true,
|
||||
replaceHostWith: `http://localhost`,
|
||||
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
|
||||
}
|
||||
}
|
||||
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 runTest(opts: Partial<TestOptions>, hostUrl: string){
|
||||
const options : TestOptions = (<TestOptions>{
|
||||
...defaultOptions,
|
||||
...opts,
|
||||
filterOutput: {
|
||||
...defaultOptions.filterOutput,
|
||||
...(opts?.filterOutput),
|
||||
},
|
||||
});
|
||||
const {
|
||||
page: path,
|
||||
cb,
|
||||
replaceHost,
|
||||
replaceHostWith,
|
||||
filterOutput
|
||||
} = options;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: isInDebugMode()? false : 'new',
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
let output : TestOutput = {
|
||||
console: [],
|
||||
errors: [],
|
||||
responses: [],
|
||||
requestsFailed: []
|
||||
};
|
||||
|
||||
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}`)
|
||||
}
|
||||
});
|
||||
|
||||
const url = new URL(`${hostUrl}/${path??''}`);
|
||||
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;
|
||||
}finally{
|
||||
if(isInDebugMode()){
|
||||
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();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -4,21 +4,20 @@
|
||||
*/
|
||||
|
||||
|
||||
import puppeteer, {Page} from "puppeteer";
|
||||
import http from 'http';
|
||||
import {runTest, TestFilterOptions, PageTestCallback} from "./run-browser-test.ts";
|
||||
import {isInDebugMode} from "./debug-mode.ts";
|
||||
|
||||
import {resolve, posix} from "node:path";
|
||||
import {URL} from "node:url";
|
||||
|
||||
|
||||
import { readFile } from '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 {NormalizedOutputOptions, OutputAsset, OutputBundle, OutputChunk, Plugin} from 'rollup';
|
||||
|
||||
import type {
|
||||
IncomingHttpHeaders, OutgoingHttpHeaders,
|
||||
@ -26,21 +25,34 @@ import type {
|
||||
Server
|
||||
} from 'http'
|
||||
import type { ServerOptions } from 'https'
|
||||
|
||||
|
||||
type TypeMap = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
type ErrorCodeException = Error & {code: string};
|
||||
|
||||
export interface RollupServeOptions {
|
||||
export interface RollupServeTestOptions {
|
||||
/**
|
||||
* Change the path to be opened when the test is started
|
||||
* Remember to start with a slash, e.g. `'/different/page'`
|
||||
*/
|
||||
path?: string
|
||||
|
||||
/**
|
||||
* Optionally specify what to filter from the output
|
||||
*/
|
||||
filterOutput?: TestFilterOptions;
|
||||
|
||||
/**
|
||||
* A callback to manually take control of the page and simulate user interactions
|
||||
*/
|
||||
cb?: PageTestCallback
|
||||
t?: any
|
||||
/**
|
||||
* The AVA context used to test (ie t.snapshot(..) )
|
||||
*/
|
||||
t: any
|
||||
|
||||
/**
|
||||
* Set to `true` to return index.html (200) instead of error page (404)
|
||||
@ -89,7 +101,7 @@ export interface RollupServeOptions {
|
||||
* Serve your rolled up bundle like webpack-dev-server
|
||||
* @param {import('..').RollupServeOptions} options
|
||||
*/
|
||||
export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
export default function serveTest (options: RollupServeTestOptions ): Plugin {
|
||||
const mime = new Mime(standardTypes, otherTypes)
|
||||
const testOptions = {
|
||||
port: 0,
|
||||
@ -135,36 +147,6 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
const content = (<OutputChunk>file).code || (<OutputAsset>file).source; // Todo might need to read a source file;
|
||||
return found(response, mime.getType(filePath!), content);
|
||||
}
|
||||
//
|
||||
// if(bundle[urlPath]){
|
||||
// const fallbackPath = typeof testOptions.historyApiFallback === 'string' ? testOptions.historyApiFallback : '/index.html'
|
||||
// }
|
||||
//
|
||||
// readFileFromContentBase(contentBase, urlPath, function (error, content, filePath) {
|
||||
// if (!error) {
|
||||
// return found(response, mime.getType(filePath!), content)
|
||||
// }
|
||||
// if ((<ErrorCodeException>error).code !== 'ENOENT') {
|
||||
// response.writeHead(500)
|
||||
// response.end('500 Internal Server Error' +
|
||||
// '\n\n' + filePath +
|
||||
// '\n\n' + Object.values(error).join('\n') +
|
||||
// '\n\n(rollup-plugin-serve)', 'utf-8')
|
||||
// return
|
||||
// }
|
||||
// if (testOptions.historyApiFallback) {
|
||||
// const fallbackPath = typeof testOptions.historyApiFallback === 'string' ? testOptions.historyApiFallback : '/index.html'
|
||||
// readFileFromContentBase(contentBase, fallbackPath, function (error, content, filePath) {
|
||||
// if (error) {
|
||||
// notFound(response, filePath)
|
||||
// } else {
|
||||
// found(response, mime.getType(filePath), content)
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
// notFound(response, filePath)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
|
||||
@ -180,7 +162,6 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// release previous server instance if rollup is reloading configuration in watch mode
|
||||
// @ts-ignore
|
||||
if (server) {
|
||||
@ -229,6 +210,7 @@ export default function serveTest (options: RollupServeOptions ): Plugin {
|
||||
const testOutput = await runTest({
|
||||
page: testOptions.path!,
|
||||
cb: testOptions.cb,
|
||||
filterOutput: testOptions.filterOutput,
|
||||
}, url)
|
||||
testOptions.t?.snapshot?.(testOutput);
|
||||
}
|
||||
@ -257,128 +239,3 @@ function found (response: ServerResponse, mimeType: string|null, content: any) {
|
||||
function green (text: string) {
|
||||
return '\u001b[1m\u001b[32m' + text + '\u001b[39m\u001b[22m'
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type PageTestCallback = (page: Page)=>Promise<void>;
|
||||
export interface TestFilterOptions{
|
||||
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 TestOptions {
|
||||
page: string
|
||||
cb: PageTestCallback
|
||||
filterOutput: TestFilterOptions
|
||||
replaceHost: boolean
|
||||
replaceHostWith?: string
|
||||
}
|
||||
const defaultOptions: Partial<TestOptions> = {
|
||||
page: 'index.html',
|
||||
cb: async (page: Page)=>{
|
||||
await page.waitForNetworkIdle({});
|
||||
},
|
||||
replaceHost: true,
|
||||
replaceHostWith: `http://localhost`,
|
||||
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
|
||||
}
|
||||
}
|
||||
export interface TestOutput{
|
||||
html?: string,
|
||||
console?: string[],
|
||||
errors?: string[],
|
||||
responses?: string[],
|
||||
requestsFailed?: string[],
|
||||
}
|
||||
export async function runTest(opts: Partial<TestOptions>, hostUrl: string){
|
||||
const options : TestOptions = (<TestOptions>{
|
||||
...defaultOptions,
|
||||
...opts,
|
||||
filterOutput: {
|
||||
...defaultOptions.filterOutput,
|
||||
...(opts?.filterOutput),
|
||||
},
|
||||
});
|
||||
const {
|
||||
page: path,
|
||||
cb,
|
||||
replaceHost,
|
||||
replaceHostWith,
|
||||
filterOutput
|
||||
} = options;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
|
||||
let output : TestOutput = {
|
||||
console: [],
|
||||
errors: [],
|
||||
responses: [],
|
||||
requestsFailed: []
|
||||
};
|
||||
|
||||
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}`)
|
||||
}
|
||||
});
|
||||
|
||||
const url = new URL(`${hostUrl}/${path??''}`);
|
||||
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;
|
||||
}finally{
|
||||
await page.close();
|
||||
await browser.close();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user