@hyperse/html-webpack-plugin-loader
Version:
A custom template loader that parses HTML templates for the `html-webpack-plugin` package
410 lines (405 loc) • 11.7 kB
JavaScript
import { serialize, parseFragment, parse } from 'parse5';
// src/parser/TemplateParser.ts
var parseDocument = (htmlSource) => {
const document = parse(htmlSource);
const html = document.childNodes.find(
(node) => node.nodeName === "html"
);
const head = html?.childNodes.find(
(node) => node.nodeName === "head"
);
const body = html?.childNodes.find(
(node) => node.nodeName === "body"
);
if (!head || !body) {
throw new Error("Invalid HTML template: missing <head> or <body> tags");
}
return { document, html, head, body };
};
var upsertFavicon = (head, href, rel = "icon", attributes = {}) => {
const existingLinkIndex = head.childNodes.findIndex(
(node) => node.nodeName === "link" && node.attrs?.find(
(attr) => attr.name === "rel" && attr.value === rel
)
);
if (existingLinkIndex > -1) {
head.childNodes.splice(existingLinkIndex, 1);
}
const attributesString = Object.entries(attributes).map(([key, value]) => `${key}="${value}"`).join(" ");
const linkNode = parseFragment(
`<link rel="${rel}" href="${href}" ${attributesString}>`
).childNodes[0];
head.childNodes.push(linkNode);
};
var upsertHeadInlineScripts = (head, scripts) => {
scripts.forEach((script) => {
const existingScriptIndex = head.childNodes.findIndex(
(node) => node.nodeName === "script" && node.attrs?.find(
(attr) => attr.name === "id" && attr.value === script.id
)
);
if (existingScriptIndex > -1) {
head.childNodes.splice(existingScriptIndex, 1);
}
});
const sortedScripts = [...scripts].sort(
(a, b) => (a.order ?? 0) - (b.order ?? 0)
);
const scriptTags = sortedScripts.map((script) => {
const scriptNode = parseFragment(
`<script id="${script.id}">${script.content}</script>`
).childNodes[0];
return scriptNode;
});
const beginningScripts = sortedScripts.reduce((acc, script, index) => {
if (script.position === "beginning") {
acc.push(scriptTags[index]);
}
return acc;
}, []);
const endScripts = sortedScripts.reduce(
(acc, script, index) => {
if (script.position === "end") {
acc.push(scriptTags[index]);
}
return acc;
},
[]
);
beginningScripts.reverse().forEach((scriptNode) => {
head.childNodes.unshift(scriptNode);
});
endScripts.forEach((scriptNode) => {
head.childNodes.push(scriptNode);
});
};
var upsertHeadInlineStyles = (head, styles) => {
const sortedStyles = [...styles].sort(
(a, b) => (a.order ?? 0) - (b.order ?? 0)
);
sortedStyles.forEach((style) => {
const existingStyleIndex = head.childNodes.findIndex(
(node) => node.nodeName === "style" && node.attrs?.find(
(attr) => attr.name === "id" && attr.value === style.id
)
);
if (existingStyleIndex > -1) {
head.childNodes.splice(existingStyleIndex, 1);
}
});
const styleTags = sortedStyles.map((style) => {
const styleNode = parseFragment(
`<style id="${style.id}">${style.content}</style>`
).childNodes[0];
return styleNode;
});
const beginningStyles = sortedStyles.reduce((acc, style, index) => {
if (style.position === "beginning") {
acc.push(styleTags[index]);
}
return acc;
}, []);
const endStyles = sortedStyles.reduce(
(acc, style, index) => {
if (style.position === "end") {
acc.push(styleTags[index]);
}
return acc;
},
[]
);
beginningStyles.reverse().forEach((styleNode) => {
head.childNodes.unshift(styleNode);
});
endStyles.forEach((styleNode) => {
head.childNodes.push(styleNode);
});
};
var upsertHeadMetaTags = (head, tags) => {
tags.forEach((tag) => {
const parsedTag = parseFragment(tag).childNodes[0];
if (parsedTag.nodeName !== "meta") return;
const parsedNameAttr = parsedTag.attrs?.find(
(attr) => attr.name === "name"
);
if (!parsedNameAttr) return;
const existingTagIndex = head.childNodes.findIndex((node) => {
if (node.nodeName !== "meta") return false;
const nodeElement = node;
const nodeNameAttr = nodeElement.attrs?.find(
(attr) => attr.name === "name"
);
return nodeNameAttr?.value === parsedNameAttr.value;
});
if (existingTagIndex > -1) {
head.childNodes.splice(existingTagIndex, 1);
}
});
const parsedTags = tags.map(
(tag) => parseFragment(tag).childNodes[0]
);
head.childNodes.unshift(...parsedTags);
};
var upsertHeadStyles = (head, styles) => {
const sortedStyles = [...styles].sort(
(a, b) => (a.order ?? 0) - (b.order ?? 0)
);
sortedStyles.forEach((style) => {
const existingStyleIndex = head.childNodes.findIndex(
(node) => node.nodeName === "link" && node.attrs?.find(
(attr) => attr.name === "rel" && attr.value === "stylesheet"
) && node.attrs?.find(
(attr) => attr.name === "id" && attr.value === style.id
)
);
if (existingStyleIndex > -1) {
head.childNodes.splice(existingStyleIndex, 1);
}
});
const styleTags = sortedStyles.map((style) => {
const styleNode = parseFragment(
`<link rel="stylesheet" href="${style.href}" id="${style.id}"></link>`
).childNodes[0];
return styleNode;
});
const beginningStyles = sortedStyles.reduce((acc, style, index) => {
if (style.position === "beginning") {
acc.push(styleTags[index]);
}
return acc;
}, []);
const endStyles = sortedStyles.reduce(
(acc, style, index) => {
if (style.position === "end") {
acc.push(styleTags[index]);
}
return acc;
},
[]
);
beginningStyles.reverse().forEach((styleNode) => {
head.childNodes.unshift(styleNode);
});
endStyles.forEach((styleNode) => {
head.childNodes.push(styleNode);
});
};
var upsertScripts = (element, scripts) => {
const sortedScripts = [...scripts].sort(
(a, b) => (a.order ?? 0) - (b.order ?? 0)
);
sortedScripts.forEach((script) => {
const existingScriptIndex = element.childNodes.findIndex(
(node) => node.nodeName === "script" && node.attrs?.find(
(attr) => attr.name === "id" && attr.value === script.id
)
);
if (existingScriptIndex > -1) {
element.childNodes.splice(existingScriptIndex, 1);
}
});
const scriptTags = sortedScripts.map((script) => {
const scriptNode = parseFragment(
`<script id="${script.id}" src="${script.src}"></script>`
).childNodes[0];
if (script.type) {
scriptNode.attrs?.push({ name: "type", value: script.type });
}
if (script.async) {
scriptNode.attrs?.push({ name: "async", value: "true" });
}
if (script.defer) {
scriptNode.attrs?.push({ name: "defer", value: "true" });
}
if (script.crossOrigin) {
scriptNode.attrs?.push({
name: "crossorigin",
value: script.crossOrigin
});
}
if (script.integrity) {
scriptNode.attrs?.push({ name: "integrity", value: script.integrity });
}
if (script.nonce) {
scriptNode.attrs?.push({ name: "nonce", value: script.nonce });
}
return scriptNode;
});
const beginningScripts = sortedScripts.reduce((acc, script, index) => {
if (script.position === "beginning") {
acc.push(scriptTags[index]);
}
return acc;
}, []);
const endScripts = sortedScripts.reduce(
(acc, script, index) => {
if (script.position === "end") {
acc.push(scriptTags[index]);
}
return acc;
},
[]
);
beginningScripts.reverse().forEach((scriptNode) => {
element.childNodes.unshift(scriptNode);
});
endScripts.forEach((scriptNode) => {
element.childNodes.push(scriptNode);
});
};
var upsertTitle = (head, title) => {
const titleNode = head.childNodes?.find(
(node) => node.nodeName === "title"
);
if (titleNode) {
titleNode.childNodes = [
{
nodeName: "#text",
value: title,
parentNode: titleNode
}
];
} else {
const newTitle = parseFragment("<title></title>").childNodes[0];
newTitle.childNodes = [
{
nodeName: "#text",
value: title,
parentNode: newTitle
}
];
head.childNodes.unshift(newTitle);
}
};
// src/parser/TemplateParser.ts
var TemplateParser = class {
constructor(htmlSource) {
const { document, head, body } = parseDocument(htmlSource);
this.document = document;
this.head = head;
this.body = body;
}
/**
* Upsert the title tag
* @param title - The title to upsert
* @returns The TemplateParser instance
*/
upsertTitleTag(title) {
upsertTitle(this.head, title);
return this;
}
/**
* Upsert the favicon tag
* @param href - The favicon to upsert
* @param rel - The rel attribute of the favicon tag
* @param attributes - The attributes of the favicon tag
* @returns The TemplateParser instance
*/
upsertFaviconTag(href, rel = "icon", attributes = {}) {
upsertFavicon(this.head, href, rel, attributes);
return this;
}
/**
* Upsert the head before html tags
* @param tags - The tags to upsert
* @returns The TemplateParser instance
*/
upsertHeadMetaTags(tags) {
upsertHeadMetaTags(this.head, tags);
return this;
}
/**
* Upsert the head before styles
* @param styles - The styles to upsert
* @returns The TemplateParser instance
*/
upsertHeadStyles(styles) {
upsertHeadStyles(this.head, styles);
return this;
}
/**
* Upsert the head inline styles
* @param styles - The styles to upsert
* @returns The TemplateParser instance
*/
upsertHeadInlineStyles(styles) {
upsertHeadInlineStyles(this.head, styles);
return this;
}
/**
* Upsert the head before scripts
* @param scripts - The scripts to upsert
* @returns The TemplateParser instance
*/
upsertHeadScripts(scripts) {
upsertScripts(this.head, scripts);
return this;
}
/**
* Upsert the inline scripts
* @param scripts - The scripts to upsert
* @returns The TemplateParser instance
*/
upsertHeadInlineScripts(scripts) {
upsertHeadInlineScripts(this.head, scripts);
return this;
}
/**
* Upsert the body after scripts
* @param scripts - The scripts to upsert
* @returns The TemplateParser instance
*/
upsertBodyScripts(scripts) {
upsertScripts(this.body, scripts);
return this;
}
/**
* Serialize the document to html string
* @returns The serialized html string
*/
serialize() {
return serialize(this.document);
}
/**
* Parse a fragment of html
* @param html - The html fragment to parse
* @returns The parsed document fragment
*/
parseFragment(html) {
return parseFragment(html);
}
};
// src/parser/parseTemplate.ts
var parseTemplate = (htmlSource, options = {}) => {
const parser = new TemplateParser(htmlSource);
if (options.title) {
parser.upsertTitleTag(options.title);
}
if (options.favicon) {
parser.upsertFaviconTag(
options.favicon.href,
options.favicon.rel,
options.favicon.attributes
);
}
if (options.headMetaTags?.length) {
parser.upsertHeadMetaTags(options.headMetaTags);
}
if (options.headStyles?.length) {
parser.upsertHeadStyles(options.headStyles);
}
if (options.headInlineStyles?.length) {
parser.upsertHeadInlineStyles(options.headInlineStyles);
}
if (options.headScripts?.length) {
parser.upsertHeadScripts(options.headScripts);
}
if (options.headInlineScripts?.length) {
parser.upsertHeadInlineScripts(options.headInlineScripts);
}
if (options.bodyScripts?.length) {
parser.upsertBodyScripts(options.bodyScripts);
}
return parser;
};
export { TemplateParser, parseTemplate };