WIP: transforming through handlebars, parsing the html and resolving the imports
This commit is contained in:
parent
da9dc3bdc1
commit
240d5cfe9a
@ -1,3 +1,6 @@
|
|||||||
|
# Remove me:
|
||||||
|
TODO: This started as a fork of, but is now something different entirely. Changelog is no longer relevant (neither is the [README.md](README.md))
|
||||||
|
|
||||||
# @rollup/plugin-html ChangeLog
|
# @rollup/plugin-html ChangeLog
|
||||||
|
|
||||||
## v1.0.2
|
## v1.0.2
|
||||||
|
|||||||
@ -30,7 +30,8 @@
|
|||||||
"test": "ava",
|
"test": "ava",
|
||||||
"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",
|
||||||
|
"dev-test": "ava --match='handlebars*'"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@ -53,6 +54,10 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
|
"parse5": "^7.1.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@rollup/plugin-typescript": "^11.1.0",
|
"@rollup/plugin-typescript": "^11.1.0",
|
||||||
|
|||||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@ -1,5 +1,13 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
'@rollup/pluginutils':
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.2(rollup@3.20.3)
|
||||||
|
parse5:
|
||||||
|
specifier: ^7.1.2
|
||||||
|
version: 7.1.2
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@babel/core':
|
'@babel/core':
|
||||||
specifier: ^7.21.4
|
specifier: ^7.21.4
|
||||||
@ -565,7 +573,6 @@ packages:
|
|||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
rollup: 3.20.3
|
rollup: 3.20.3
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@trysound/sax@0.2.0:
|
/@trysound/sax@0.2.0:
|
||||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
@ -590,7 +597,6 @@ packages:
|
|||||||
|
|
||||||
/@types/estree@1.0.0:
|
/@types/estree@1.0.0:
|
||||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/glob@7.2.0:
|
/@types/glob@7.2.0:
|
||||||
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
|
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
|
||||||
@ -1370,6 +1376,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/entities@4.5.0:
|
||||||
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
|
engines: {node: '>=0.12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/error-ex@1.3.2:
|
/error-ex@1.3.2:
|
||||||
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1412,7 +1423,6 @@ packages:
|
|||||||
|
|
||||||
/estree-walker@2.0.2:
|
/estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/esutils@2.0.3:
|
/esutils@2.0.3:
|
||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
@ -1528,7 +1538,6 @@ packages:
|
|||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/function-bind@1.1.1:
|
/function-bind@1.1.1:
|
||||||
@ -2468,6 +2477,12 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/parse5@7.1.2:
|
||||||
|
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||||
|
dependencies:
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/path-exists@4.0.0:
|
/path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -2509,7 +2524,6 @@ packages:
|
|||||||
/picomatch@2.3.1:
|
/picomatch@2.3.1:
|
||||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/pidtree@0.6.0:
|
/pidtree@0.6.0:
|
||||||
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
|
||||||
@ -3088,7 +3102,6 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/run-parallel@1.2.0:
|
/run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|||||||
341
src/index.ts
341
src/index.ts
@ -1,10 +1,25 @@
|
|||||||
import { extname } from "node:path";
|
import { extname } from "node:path";
|
||||||
|
|
||||||
import type { Plugin, NormalizedOutputOptions, OutputBundle, EmittedAsset } from 'rollup';
|
import type {
|
||||||
|
Plugin,
|
||||||
|
OutputBundle,
|
||||||
|
OutputChunk,
|
||||||
|
OutputAsset,
|
||||||
|
NormalizedOutputOptions,
|
||||||
|
// ModuleInfo,
|
||||||
|
ResolvedId, PreRenderedChunk
|
||||||
|
} from 'rollup';
|
||||||
|
|
||||||
import type { RollupHtmlOptions, RollupHtmlTemplateOptions } from '../types/index.d.ts';
|
import type {
|
||||||
|
LoadResult,
|
||||||
|
RollupHtmlOptions,
|
||||||
|
LoadNodeCallback,
|
||||||
|
LoadReference
|
||||||
|
} from '../types/index.d.ts';
|
||||||
|
import {createFilter} from '@rollup/pluginutils';
|
||||||
|
import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";
|
||||||
|
|
||||||
const getFiles = (bundle: OutputBundle): RollupHtmlTemplateOptions['files'] => {
|
const getFiles = (bundle: OutputBundle): Record<string, (OutputChunk | OutputAsset)[]> => {
|
||||||
const result = {} as ReturnType<typeof getFiles>;
|
const result = {} as ReturnType<typeof getFiles>;
|
||||||
for (const file of Object.values(bundle)) {
|
for (const file of Object.values(bundle)) {
|
||||||
const { fileName } = file;
|
const { fileName } = file;
|
||||||
@ -16,118 +31,244 @@ const getFiles = (bundle: OutputBundle): RollupHtmlTemplateOptions['files'] => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeHtmlAttributes = (attributes: Record<string, any>): string => {
|
type LoaderNodeMapping = {attr?: string};
|
||||||
if (!attributes) {
|
type LoaderMappings = {[tagName: string]: LoaderNodeMapping[]};
|
||||||
return '';
|
const defaultLoaderMappings: LoaderMappings = {
|
||||||
|
'script': [{attr: 'src'}], // Javascript
|
||||||
|
'link': [{attr: 'href'}], // Style
|
||||||
|
// 'style': [{body: true}] // Body of a style tag may have links that we want to resolve (images, other css, ..),
|
||||||
|
'img': [{attr: 'src'}], // Images, svgs
|
||||||
|
// 'a': [{attr: 'href'}], // Links
|
||||||
|
//'iframe': [{attr: 'src'}], // Very unlikely to become a default, but who knows if someone has a valid use for this
|
||||||
|
'source': [{attr: 'src'}], // video source
|
||||||
|
'track': [{attr: 'src'}], // subtitle
|
||||||
|
'audio': [{attr: 'src'}], // audio
|
||||||
|
//'portal': [{attr: 'src'}], // An experimantal feature to replace valid use cases for iframes? Might want to [look into it...](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/portal)
|
||||||
|
//'object': [{attr: 'data'}], // Not sure what to do with this, is this still commonly used? Any valid use-case for this? [See MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function makeLoader(mappings: LoaderMappings = defaultLoaderMappings){
|
||||||
|
const fn : LoadNodeCallback = function ({node}){
|
||||||
|
const tagMapping = mappings[node.tagName];
|
||||||
|
if(tagMapping){
|
||||||
|
const mappingResults = tagMapping.map(mapping=>{
|
||||||
|
let ids : LoadReference[] = [];
|
||||||
|
if(mapping.attr){
|
||||||
|
ids.push(...node.attrs.filter(({name})=>name===mapping.attr).map(attr=>({get: ()=>attr.value, set: (id: string)=>attr.value=id})));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
});
|
||||||
|
return (<LoadReference[]>[]).concat(...mappingResults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return fn;
|
||||||
|
}
|
||||||
|
|
||||||
const keys = Object.keys(attributes);
|
const defaults: RollupHtmlOptions = {
|
||||||
// eslint-disable-next-line no-param-reassign
|
transform: (source: string)=>source,// NO-OP
|
||||||
return keys.reduce((result, key) => (result += ` ${key}="${attributes[key]}"`), '');
|
load: makeLoader(),
|
||||||
|
resolve: ()=>true,
|
||||||
|
htmlFileNames: "[name].html",
|
||||||
|
include: [
|
||||||
|
'**/*.(html|hbs)',// html or handlebars
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTemplate = async ({
|
// Internal type
|
||||||
attributes,
|
type HtmlImport = {
|
||||||
files,
|
id: string,
|
||||||
meta,
|
rollupResolved: ResolvedId|null,
|
||||||
publicPath,
|
node: DefaultTreeAdapterMap['element'],
|
||||||
title
|
reference: LoadReference,
|
||||||
}: RollupHtmlTemplateOptions) => {
|
referenceId: string|null,
|
||||||
const scripts = (files.js || [])
|
index: number,
|
||||||
.map(({ fileName }) => {
|
}
|
||||||
const attrs = makeHtmlAttributes(attributes.script);
|
type HtmlModule = {
|
||||||
return `<script src="${publicPath}${fileName}"${attrs}></script>`;
|
// TODO might want to impose an own unique id, in case this changes after multiple builds
|
||||||
})
|
id: string,
|
||||||
.join('\n');
|
resolved: HtmlImport[];
|
||||||
|
}
|
||||||
const links = (files.css || [])
|
|
||||||
.map(({ fileName }) => {
|
|
||||||
const attrs = makeHtmlAttributes(attributes.link);
|
|
||||||
return `<link href="${publicPath}${fileName}" rel="stylesheet"${attrs}>`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
const metas = meta
|
|
||||||
.map((input) => {
|
|
||||||
const attrs = makeHtmlAttributes(input);
|
|
||||||
return `<meta${attrs}>`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!doctype html>
|
|
||||||
<html${makeHtmlAttributes(attributes.html)}>
|
|
||||||
<head>
|
|
||||||
${metas}
|
|
||||||
<title>${title}</title>
|
|
||||||
${links}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${scripts}
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const supportedFormats = ['es', 'esm', 'iife', 'umd'];
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
attributes: {
|
|
||||||
link: null,
|
|
||||||
html: { lang: 'en' },
|
|
||||||
script: null
|
|
||||||
},
|
|
||||||
fileName: 'index.html',
|
|
||||||
meta: [{ charset: 'utf-8' }],
|
|
||||||
publicPath: '',
|
|
||||||
template: defaultTemplate,
|
|
||||||
title: 'Rollup Bundle'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
export default function html(opts: RollupHtmlOptions = {}): Plugin {
|
||||||
const { attributes, fileName, meta, publicPath, template, title } = Object.assign(
|
const {
|
||||||
|
publicPath,
|
||||||
|
transform,
|
||||||
|
load,
|
||||||
|
htmlFileNames,
|
||||||
|
resolve,
|
||||||
|
include,
|
||||||
|
exclude,
|
||||||
|
} = Object.assign(
|
||||||
{},
|
{},
|
||||||
defaults,
|
defaults,
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
if(publicPath){ throw new Error("TODO, do something with the public path or throw it out of the options. this is just to stop typescript complaining")}
|
||||||
|
|
||||||
|
let filter = createFilter(include, exclude, {});
|
||||||
|
let handledHtmls = new Map<string, HtmlModule>();// todo clean this per new build?
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'html',
|
name: 'html2',// TODO: Need a better name, original plugin was just named `html` and might still make sense to use in conjunction with this one
|
||||||
|
|
||||||
|
load: {
|
||||||
|
async handler(id: string) {
|
||||||
|
if(!filter(id)) return;
|
||||||
|
// We'll be transforming this, but it appears there is no need for us to load it. Rollup will do this
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outputOptions(options){
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
entryFileNames: (chunkInfo)=>{
|
||||||
|
const htmlModule = chunkInfo.facadeModuleId ? handledHtmls.get(chunkInfo.facadeModuleId!) : null;
|
||||||
|
const defaultOption = options.entryFileNames ?? "[name]-[hash].js";// This default is copied from the docs. TODO: don't like overwrite it this way, can we remove the need for this or fetch the true default?
|
||||||
|
if(htmlModule){
|
||||||
|
let fileName = typeof (htmlFileNames) === 'string' ? htmlFileNames : (<(chunkInfo:PreRenderedChunk)=>string>htmlFileNames)(chunkInfo);
|
||||||
|
if(fileName) {
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return typeof (defaultOption) === 'string' ? defaultOption : (<(chunkInfo:PreRenderedChunk)=>string>defaultOption)(chunkInfo);
|
||||||
|
},
|
||||||
|
// TODO do we need to do the same for chunks?? (what if they're dynamically imported?)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
async handler(code: string, id: string){
|
||||||
|
if(!filter(id)) return;
|
||||||
|
|
||||||
|
const handled : HtmlModule = {
|
||||||
|
id,
|
||||||
|
resolved: [],
|
||||||
|
};
|
||||||
|
handledHtmls.set(id, handled);
|
||||||
|
|
||||||
|
const htmlSrc = transform? await transform(code, {
|
||||||
|
id,
|
||||||
|
}) : code;
|
||||||
|
|
||||||
|
const document = parseHtml(htmlSrc);
|
||||||
|
|
||||||
|
// Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes)
|
||||||
|
let loadResults : { reference: LoadReference, node: DefaultTreeAdapterMap['element'] }[] = [];
|
||||||
|
if(document.childNodes){
|
||||||
|
let nodeQueue = document.childNodes;
|
||||||
|
do{
|
||||||
|
const nextQueue: DefaultTreeAdapterMap['childNode'][][] = [];
|
||||||
|
await Promise.all(nodeQueue.map(async (node)=>{
|
||||||
|
const el = (<DefaultTreeAdapterMap['element']>node);
|
||||||
|
let toLoad: LoadResult|undefined = undefined;
|
||||||
|
if(el.attrs) {
|
||||||
|
toLoad = load ? await load({
|
||||||
|
node: el,
|
||||||
|
sourceId: id
|
||||||
|
}): [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(toLoad){
|
||||||
|
const loadIds: LoadReference[] = (toLoad instanceof Array)? toLoad: [toLoad];
|
||||||
|
for(const loadId of loadIds){
|
||||||
|
loadResults.push({
|
||||||
|
reference: loadId,
|
||||||
|
node: el,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(toLoad !== false) {
|
||||||
|
let asParent = (<DefaultTreeAdapterMap['parentNode']>node);
|
||||||
|
if (asParent.childNodes) {
|
||||||
|
nextQueue.push(asParent.childNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
nodeQueue = nextQueue.flat();
|
||||||
|
}while(nodeQueue.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out what to resolve (todo, an id can actually be loaded in multiple times, something we might want to keep in mind)
|
||||||
|
await Promise.all(loadResults.map(async ({reference, node}, index)=>{
|
||||||
|
const refId = reference.get();
|
||||||
|
const selfResolvedId = resolve? resolve(refId, {
|
||||||
|
sourceId: id,
|
||||||
|
node,
|
||||||
|
}) : refId;
|
||||||
|
const resolvedId : string = selfResolvedId===true?refId:(<string>selfResolvedId);
|
||||||
|
if(resolvedId){
|
||||||
|
const rollupResolved = await this.resolve(resolvedId, id, {
|
||||||
|
skipSelf: true,
|
||||||
|
isEntry: true, // TODO: for href/src tags, this is probably the right option. For anything that is to be inlined into the HTML... probably no
|
||||||
|
});
|
||||||
|
// TODO: should we check if this is refused for resolving here. i.e. external?
|
||||||
|
const htmlImport: HtmlImport = {
|
||||||
|
id: resolvedId,
|
||||||
|
rollupResolved,//rollupResolved,
|
||||||
|
node,
|
||||||
|
reference,
|
||||||
|
referenceId: rollupResolved? this.emitFile({
|
||||||
|
type: 'chunk', // Might want to adapt, or make configurable,
|
||||||
|
id: rollupResolved.id,
|
||||||
|
importer: id,
|
||||||
|
implicitlyLoadedAfterOneOf: [id],
|
||||||
|
}) : null,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
if(htmlImport.referenceId) {
|
||||||
|
reference.set(`\${import.meta.ROLLUP_FILE_URL_${htmlImport.referenceId}\}`);
|
||||||
|
}
|
||||||
|
handled.resolved.push(htmlImport);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// console.log(`TODO, add the following for further transformations:\n${resolveResults.map(x=>` ${x.id}`).join('\n')}`); //and figure out how other libraries later replace the import...
|
||||||
|
|
||||||
|
// Transform to JS
|
||||||
|
const serialized = serializeHtml(document);
|
||||||
|
const jsModule = [
|
||||||
|
//...resolveResults.map(x=>`import * as dep${x.index} from "${x.id}";`),
|
||||||
|
// ...handled.resolved.map(x=>`import("${x.id}");`),// Inject as a dynamic import. We need to remove these before outputting // todo better solution to mark the ids as dependencies of this bundle...
|
||||||
|
`export const html = \`${serialized.replaceAll(/`/g,'\\\`')}\`;`,
|
||||||
|
`export default html;`
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return {code: jsModule};
|
||||||
|
}
|
||||||
|
},
|
||||||
async generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
|
async generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
|
||||||
if (!supportedFormats.includes(output.format) && !opts.template) {
|
|
||||||
this.warn(
|
|
||||||
`plugin-html: The output format '${
|
|
||||||
output.format
|
|
||||||
}' is not directly supported. A custom \`template\` is probably required. Supported formats include: ${supportedFormats.join(
|
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.format === 'es') {
|
|
||||||
attributes.script = Object.assign({}, attributes.script, {
|
|
||||||
type: 'module'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = getFiles(bundle);
|
const files = getFiles(bundle);
|
||||||
const source = await template({
|
console.log("must output?!", output, bundle, files);
|
||||||
attributes,
|
},
|
||||||
bundle,
|
|
||||||
files,
|
|
||||||
meta,
|
|
||||||
publicPath,
|
|
||||||
title
|
|
||||||
});
|
|
||||||
|
|
||||||
const htmlFile: EmittedAsset = {
|
// async generateBundle(output: NormalizedOutputOptions, bundle: OutputBundle) {
|
||||||
type: 'asset',
|
// if (!supportedFormats.includes(output.format) && !opts.transform) {
|
||||||
source,
|
// this.warn(
|
||||||
name: 'Rollup HTML Asset',
|
// `plugin-html: The output format '${
|
||||||
fileName
|
// output.format
|
||||||
};
|
// }' is not directly supported. A custom \`template\` is probably required. Supported formats include: ${supportedFormats.join(
|
||||||
|
// ', '
|
||||||
this.emitFile(htmlFile);
|
// )}`
|
||||||
}
|
// );
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (output.format === 'es') {
|
||||||
|
// attributes.script = Object.assign({}, attributes.script, {
|
||||||
|
// type: 'module'
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const files = getFiles(bundle);
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// const htmlFile: EmittedAsset = {
|
||||||
|
// type: 'asset',
|
||||||
|
// source,
|
||||||
|
// name: 'Rollup HTML Asset',
|
||||||
|
// fileName
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// this.emitFile(htmlFile);
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ test.serial('template', async (t) => {
|
|||||||
input: 'batman.js',
|
input: 'batman.js',
|
||||||
plugins: [
|
plugins: [
|
||||||
html({
|
html({
|
||||||
template: () => '<html><body><main></main></body></html>'
|
transform: () => '<html><body><main></main></body></html>'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export const notSoIifi = ()=>{
|
||||||
|
return `I'm "annoying" ${"in case we need to test \`string\` escaping.''"}`;
|
||||||
|
}
|
||||||
|
console.log(notSoIifi());
|
||||||
@ -8,7 +8,7 @@ import { getCode } from "../util/test.js";
|
|||||||
import html from "../../src/index.ts";
|
import html from "../../src/index.ts";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
|
|
||||||
const output = { dir: 'output', format: 'umd' };
|
const output = { dir: 'output', format: 'es' };
|
||||||
|
|
||||||
import {readFile} from "node:fs/promises";
|
import {readFile} from "node:fs/promises";
|
||||||
import {fileURLToPath} from "node:url";
|
import {fileURLToPath} from "node:url";
|
||||||
@ -18,11 +18,11 @@ process.chdir(join(__dirname, 'fixtures'));
|
|||||||
test.serial('handlebars', async (t) => {
|
test.serial('handlebars', async (t) => {
|
||||||
const template = await readFile('index.hbs', {encoding: "utf-8"});
|
const template = await readFile('index.hbs', {encoding: "utf-8"});
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: 'batman.js',
|
input: 'index.hbs',
|
||||||
plugins: [
|
plugins: [
|
||||||
html({
|
html({
|
||||||
fileName: 'index.html',
|
// Should we define an output template here?!
|
||||||
template(ctx){
|
transform(ctx){
|
||||||
return handlebars.compile(template)({a:'a'})
|
return handlebars.compile(template)({a:'a'})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
71
types/index.d.ts
vendored
71
types/index.d.ts
vendored
@ -1,24 +1,67 @@
|
|||||||
import type { Plugin, OutputChunk, OutputAsset, OutputBundle } from 'rollup';
|
import type {Plugin, OutputChunk, OutputAsset, OutputBundle, TransformModuleJSON, } from 'rollup';
|
||||||
|
import {FilterPattern} from "@rollup/pluginutils";
|
||||||
|
import type {DefaultTreeAdapterMap} from "parse5";
|
||||||
|
import {PreRenderedChunk} from "rollup";
|
||||||
|
|
||||||
export interface RollupHtmlTemplateOptions {
|
export interface RollupHtmlTransformContext {
|
||||||
title: string;
|
id?: string;
|
||||||
attributes: Record<string, any>;
|
// bundle: OutputBundle;
|
||||||
publicPath: string;
|
// files: Record<string, (OutputChunk | OutputAsset)[]>;
|
||||||
meta: Record<string, any>[];
|
|
||||||
bundle: OutputBundle;
|
|
||||||
files: Record<string, (OutputChunk | OutputAsset)[]>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RollupHtmlLoadContext {
|
||||||
|
node: DefaultTreeAdapterMap['element'];
|
||||||
|
sourceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RollupHtmlResolveContext {
|
||||||
|
node: DefaultTreeAdapterMap['element'];
|
||||||
|
sourceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransformCallback = (source: string, transformContext: RollupHtmlTransformContext) => string|Promise<string>;
|
||||||
|
export type LoadReference = {get: ()=>string, set: (id: string)=>void};
|
||||||
|
export type LoadResult = LoadReference|LoadReference[]|undefined|void|false;
|
||||||
|
export type LoadNodeCallback = (loadContext: RollupHtmlLoadContext) => LoadResult|Promise<LoadResult>;
|
||||||
|
export type ResolveResult = string|true|undefined|void|false;
|
||||||
|
export type ResolveCallback = (id: string, resolveContext: RollupHtmlResolveContext) => ResolveResult|Promise<ResolveResult>;
|
||||||
|
|
||||||
export interface RollupHtmlOptions {
|
export interface RollupHtmlOptions {
|
||||||
title?: string;
|
|
||||||
attributes?: Record<string, any>;
|
|
||||||
fileName?: string;
|
|
||||||
meta?: Record<string, any>[];
|
|
||||||
publicPath?: string;
|
publicPath?: string;
|
||||||
template?: (templateoptions?: RollupHtmlTemplateOptions) => string;
|
/**
|
||||||
|
* Follows the same logic as rollup's [entryFileNames](https://rollupjs.org/configuration-options/#output-entryfilenames).
|
||||||
|
*/
|
||||||
|
htmlFileNames?: string|((chunkInfo: PreRenderedChunk) => string);
|
||||||
|
/**
|
||||||
|
* Transform a source file passed into this plugin to HTML. For example: a handlebars transform
|
||||||
|
* ```
|
||||||
|
* transform(source){
|
||||||
|
* return handlebars.compile(source)({myVar:'example'})
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
transform?: TransformCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect which references (<a href="...">, <img src="...">) to resolve from a HTML node.
|
||||||
|
* This rarely needs to be overloaded, but can be used to support non-native attributes used by custom-elements.
|
||||||
|
*
|
||||||
|
* Return false to skip any further processing on this node. Else return the id's the resolve based on this node
|
||||||
|
*/
|
||||||
|
load?: LoadNodeCallback;
|
||||||
|
/**
|
||||||
|
* Callback to filter which references actually need to be resolved. Here you can filter out:
|
||||||
|
* - Links to extensions that don't need to be handled through rollup
|
||||||
|
* - Resources that are external to the app (for example non-relative paths)
|
||||||
|
* - Page navigation within the app
|
||||||
|
*
|
||||||
|
* Return a falsey value to skip this reference. Return true to resolve as is. (or string to transform the id)
|
||||||
|
*/
|
||||||
|
resolve?: ResolveCallback;
|
||||||
|
include?: FilterPattern;
|
||||||
|
exclude?: FilterPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeHtmlAttributes(attributes: Record<string, string>): string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Rollup plugin which creates HTML files to serve Rollup bundles.
|
* A Rollup plugin which creates HTML files to serve Rollup bundles.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user