diff --git a/src/html-module.ts b/src/html-module.ts
new file mode 100644
index 0000000..df884ea
--- /dev/null
+++ b/src/html-module.ts
@@ -0,0 +1,34 @@
+// The HTML-Module is an internal helper structure to track the processing of an HTML file
+// This is intended to be serialized into chunk-meta, so it can be cached. (thus keep any functions and circular references out of it)
+// TODO: Actually making this serialiable (check rollupResolved, node, as we might no longer need them)
+
+import type {
+ ModuleInfo,
+ ResolvedId,
+} from 'rollup';
+
+import type {
+ LoadedReference
+} from "../types/load.d.ts";
+import {DefaultTreeAdapterMap} from "parse5";
+
+// Internal type
+export type HtmlImport = LoadedReference & {
+ id: string;
+ resolved: ResolvedId|null;
+ // loaded: ModuleInfo|null;
+ node: DefaultTreeAdapterMap['element'];
+ referenceId: string|null;
+ placeholder: string,
+ index: number;
+}
+
+export type HtmlModule = {
+ // TODO might want to impose an own unique id, in case this changes after multiple builds
+ id: string;
+ name: string;
+ importers: Set,
+ imports: HtmlImport[];
+ assetId?: string|null;
+ document?: DefaultTreeAdapterMap['document'];
+}
diff --git a/src/index.ts b/src/index.ts
index 2be9aa1..19ded21 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -16,57 +16,20 @@ import type {
LoadResult,
RollupHtmlOptions,
LoadNodeCallback,
- LoadReference
+ LoadReference, BodyReference, AttributeReference, LoadFunction
} from '../types/index.d.ts';
+
import {createFilter} from '@rollup/pluginutils';
import {parse as parseHtml, serialize as serializeHtml, DefaultTreeAdapterMap} from "parse5";
import {readFile} from "node:fs/promises"
-const getFiles = (bundle: OutputBundle): Record => {
- const result = {} as ReturnType;
- for (const file of Object.values(bundle)) {
- const { fileName } = file;
- const extension = extname(fileName).substring(1);
+import {makeLoader, makeInlineId} from "./loader.js";
+import {HtmlImport, HtmlModule} from "./html-module.js";
- result[extension] = (result[extension] || []).concat(file);
- }
+import {dirname} from "node:path";
+import posix from "node:path/posix";
- return result;
-};
-
-type LoaderNodeMapping = {attr?: string};
-type LoaderMappings = {[tagName: string]: LoaderNodeMapping[]};
-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 ([]).concat(...mappingResults);
- }
- }
- return fn;
-}
+import crypto from "node:crypto";
const defaults: RollupHtmlOptions = {
transform: (source: string)=>source,// NO-OP
@@ -78,25 +41,6 @@ const defaults: RollupHtmlOptions = {
]
};
-// Internal type
-type HtmlImport = {
- id: string;
- rollupResolved: ResolvedId|null;
- node: DefaultTreeAdapterMap['element'];
- reference: LoadReference;
- referenceId: string|null;
- index: number;
-}
-type HtmlModule = {
- // TODO might want to impose an own unique id, in case this changes after multiple builds
- id: string;
- name: string;
- importers: Set,
- resolved: HtmlImport[];
- assetId?: string|null;
- document?: DefaultTreeAdapterMap['document'];
-}
-
const modulePrefix = `// `;
const moduleSuffix = `// `;
@@ -118,6 +62,7 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
let filter = createFilter(include, exclude, {});
let htmlModules = new Map();// todo clean this per new build?
+ let virtualSources = new Map();
return {
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
@@ -126,6 +71,7 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
async handler(specifier: string,
importer: string | undefined,
options: { assertions: Record }){
+ if(virtualSources.has(specifier)) return specifier;
if(!filter(specifier)) return;
// Let it be resolved like others (node_modules, project aliases, ..)
@@ -141,7 +87,7 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
const htmlModule : HtmlModule = htmlModules.get(moduleId) ?? {
id: resolved.id,
name: moduleName,
- resolved: [],
+ imports: [],
assetId: null,
importers: new Set(),
};
@@ -154,6 +100,7 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
},
load: {
async handler(id: string) {
+ if(virtualSources.has(id)) return virtualSources.get(id);
if(!filter(id)) return;
// Load
@@ -166,36 +113,64 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
}) : contents;
// Parse document and store it (TODO: check for watch mode, we should check if it needs reparsing or not)
- const document = htmlModule.document ?? parseHtml(htmlSrc);
- if(!htmlModule.document){
- htmlModule.document = document;
- }
+ const document = htmlModule.document = htmlModule.document ?? parseHtml(htmlSrc);
// Figure out which references to load from this HTML by iterating all nodes (looking for src or href attributes)
- let loadResults: { reference: LoadReference, node: DefaultTreeAdapterMap['element'] }[] = [];
+ 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 = (node);
- let toLoad: LoadResult | undefined = undefined;
- if (el.attrs) {
- toLoad = load ? await load({
+ const loadFunction: LoadFunction = async ({
+ id: sourceId,
+ source,
+ type
+ })=>{
+ if(!sourceId){
+ sourceId = makeInlineId(id, node, 'js');
+ }
+ if(source){
+ virtualSources.set(sourceId, source);// TODO actually loading in the virtual source (if any)
+ }
+
+ const resolved = await this.resolve(sourceId, id, {
+ isEntry: type==='entryChunk',
+ });
+ // if(!resolved){
+ // throw new Error(`Could not resolve ${sourceId} from ${id}`);
+ // }
+ // const loaded = await this.load({
+ // id: sourceId,
+ // resolveDependencies: true,
+ // moduleSideEffects: 'no-treeshake'
+ // });
+
+ const htmlImport: HtmlImport = {
+ id: sourceId,
+ resolved: resolved,
+ // loaded: loaded,
node: el,
- sourceId: id
- }) : [];
+ type,
+ source,
+ referenceId:
+ (resolved && (['chunk','entryChunk'].includes(type!))) ? this.emitFile({
+ type: 'chunk', // Might want to adapt, or make configurable (see LoadType)
+ id: resolved.id,
+ importer: id,
+ }) : null,
+ placeholder: `html-import-${crypto.randomBytes(32).toString('base64')}`,
+ index: htmlImports.length,
+ }
+ htmlImports.push(htmlImport);
+ return htmlImport.placeholder;
}
- if (toLoad) {
- const loadIds: LoadReference[] = (toLoad instanceof Array) ? toLoad : [toLoad];
- for (const loadId of loadIds) {
- loadResults.push({
- reference: loadId,
- node: el,
- })
- }
- }
+ let toLoad: LoadResult | undefined = load? await Promise.resolve(load({
+ node: el,
+ sourceId: id
+ }, loadFunction)) : undefined;
if (toLoad !== false) {
let asParent = (node);
@@ -208,63 +183,25 @@ export default function html(opts: RollupHtmlOptions = {}): Plugin {
} 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 : (selfResolvedId);
- if (resolvedId) {
- const isEntry = !!/.*\.(js|jsx|ts|tsx)$/i.exec(resolvedId); // TODO: for scripts (via src-tag, not those inlined) entry=true. But it should not check via the id (rather how it is imported from html)
- const rollupResolved = await this.resolve(resolvedId, id, {
- skipSelf: true,
- isEntry: isEntry,
- });
-
- // TODO: should we test/check if this is refused for resolving here. i.e. external?
- const htmlImport: HtmlImport = {
- id: resolvedId,
- rollupResolved,
- node,
- reference,
- referenceId:
- // This was triggering resources being marked as entry, and thus their injected loader modules to be outputed to their own files (ie icon.js to load icon.svg)
- // Should be able to resolve the final HTML from the exported module instead (which though would ideally mean interpreting it as a browser would... )
- // TODO: however, probably need to uncomment this for
+