UNPKG

@hyperse/html-webpack-plugin-loader

Version:

A custom template loader that parses HTML templates for the `html-webpack-plugin` package

583 lines (575 loc) 18.2 kB
'use strict'; var parse5 = require('parse5'); var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var parseDocument = (htmlSource) => { const document = parse5.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 sortDocument = (document) => { const sortedDocument = { ...document }; const html = document.childNodes.find( (node) => node.nodeName === "html" ); if (!html) { return sortedDocument; } const head = html.childNodes.find( (node) => node.nodeName === "head" ); const body = html.childNodes.find( (node) => node.nodeName === "body" ); if (head) { sortNodesByPosition(head, ["link", "style"], (node) => { const elementNode = node; const hasId = elementNode.attrs?.find((attr) => attr.name === "id"); const hasOrder = elementNode.attrs?.find( (attr) => attr.name === "data-order" ); const hasPosition = elementNode.attrs?.find( (attr) => attr.name === "data-position" ); if (!hasId || !hasOrder || !hasPosition) { return false; } if (node.nodeName === "link") { const isStylesheet = elementNode.attrs?.find( (attr) => attr.name === "rel" && attr.value === "stylesheet" ); return !!isStylesheet; } return true; }); sortNodesByPosition(head, ["script"], (node) => { const elementNode = node; const hasId = elementNode.attrs?.find((attr) => attr.name === "id"); const hasOrder = elementNode.attrs?.find( (attr) => attr.name === "data-order" ); const hasPosition = elementNode.attrs?.find( (attr) => attr.name === "data-position" ); return !!(hasId && hasOrder && hasPosition); }); cleanupSortingAttributes(head); } if (body) { sortNodesByPosition(body, ["script"], (node) => { const elementNode = node; const hasId = elementNode.attrs?.find((attr) => attr.name === "id"); const hasOrder = elementNode.attrs?.find( (attr) => attr.name === "data-order" ); const hasPosition = elementNode.attrs?.find( (attr) => attr.name === "data-position" ); return !!(hasId && hasOrder && hasPosition); }); cleanupSortingAttributes(body); } return sortedDocument; }; var sortNodesByPosition = (element, nodeNames, filterFn) => { const beginningNodes = []; const endNodes = []; element.childNodes.forEach((node) => { if (nodeNames.includes(node.nodeName) && filterFn(node)) { const elementNode = node; const orderAttr = elementNode.attrs?.find( (attr) => attr.name === "data-order" ); const positionAttr = elementNode.attrs?.find( (attr) => attr.name === "data-position" ); if (orderAttr && positionAttr) { const order = parseInt(orderAttr.value, 10) || 0; const nodeInfo = { node: elementNode, order }; if (positionAttr.value === "beginning") { beginningNodes.push(nodeInfo); } else if (positionAttr.value === "end") { endNodes.push(nodeInfo); } } } }); [...beginningNodes, ...endNodes].forEach(({ node }) => { const index = element.childNodes.indexOf(node); if (index > -1) { element.childNodes.splice(index, 1); } }); beginningNodes.sort((a, b) => a.order - b.order); endNodes.sort((a, b) => a.order - b.order); beginningNodes.reverse().forEach(({ node }) => { element.childNodes.unshift(node); }); endNodes.forEach(({ node }) => { element.childNodes.push(node); }); }; var cleanupSortingAttributes = (element) => { if (element.attrs) { element.attrs = element.attrs.filter( (attr) => attr.name !== "data-order" && attr.name !== "data-position" ); } element.childNodes.forEach((node) => { if (node.nodeName && node.nodeName !== "#text" && node.nodeName !== "#comment") { const childElement = node; cleanupSortingAttributes(childElement); } }); }; 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 = parse5.parseFragment( `<script id="${script.id}" src="${script.src}" data-order="${script.order}" data-position="${script.position}"></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); }); }; // src/parser/upsertBodySctipts.ts var upsertBodySctipts = (body, scripts) => { return upsertScripts(body, scripts); }; 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 = parse5.parseFragment( `<link rel="${rel}" href="${href}" ${attributesString}>` ).childNodes[0]; let insertIndex = 0; let lastMetaIndex = -1; for (let i = 0; i < head.childNodes.length; i++) { if (head.childNodes[i].nodeName === "meta") { lastMetaIndex = i; } } if (lastMetaIndex >= 0) { insertIndex = lastMetaIndex + 1; } else { insertIndex = 0; } head.childNodes.splice(insertIndex, 0, 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 = parse5.parseFragment( `<script id="${script.id}" data-order="${script.order}" data-position="${script.position}">${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 = parse5.parseFragment( `<style id="${style.id}" data-order="${style.order}" data-position="${style.position}">${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 = parse5.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) => parse5.parseFragment(tag).childNodes[0] ); parsedTags.reverse().forEach((tag) => { head.childNodes.unshift(tag); }); }; 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 = parse5.parseFragment( `<link rel="stylesheet" href="${style.href}" id="${style.id}" data-order="${style.order}" data-position="${style.position}"></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 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 = parse5.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, html } = parseDocument(htmlSource); this.document = document; this.html = html; this.head = head; this.body = body; } /** * Upsert the title tag - inserts at beginning of <head> * @param title - The title to upsert * @returns The TemplateParser instance */ upsertTitleTag(title) { upsertTitle(this.head, title); return this; } /** * Upsert the favicon tag - inserts at beginning of <head> after title 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 meta tags in <head> - inserts at beginning for SEO priority * @param tags - The meta tags to upsert * @returns The TemplateParser instance */ upsertHeadMetaTags(tags) { upsertHeadMetaTags(this.head, tags); return this; } /** * Upsert external stylesheets in <head> - supports position-based insertion * @param styles - The external styles to upsert * @returns The TemplateParser instance */ upsertHeadStyles(styles) { upsertHeadStyles(this.head, styles); return this; } /** * Upsert inline styles in <head> - supports position-based insertion * @param styles - The inline styles to upsert * @returns The TemplateParser instance */ upsertHeadInlineStyles(styles) { upsertHeadInlineStyles(this.head, styles); return this; } /** * Upsert external scripts in <head> - supports position-based insertion * @param scripts - The external scripts to upsert * @returns The TemplateParser instance */ upsertHeadScripts(scripts) { upsertScripts(this.head, scripts); return this; } /** * Upsert inline scripts in <head> - supports position-based insertion * @param scripts - The inline scripts to upsert * @returns The TemplateParser instance */ upsertHeadInlineScripts(scripts) { upsertHeadInlineScripts(this.head, scripts); return this; } /** * Upsert scripts in <body> - supports position-based insertion, typically at end for performance * @param scripts - The scripts to upsert * @returns The TemplateParser instance */ upsertBodyScripts(scripts) { upsertBodySctipts(this.body, scripts); return this; } /** * Serialize the document to html string * @returns The serialized html string */ serialize() { const sortedDocument = sortDocument(this.document); return parse5.serialize(sortedDocument); } /** * Parse a fragment of html * @param html - The html fragment to parse * @returns The parsed document fragment */ parseFragment(html) { return parse5.parseFragment(html); } }; // src/parser/parseTemplate.ts var parseTemplate = (htmlSource, options = {}) => { const parser = new TemplateParser(htmlSource); if (options.headMetaTags?.length) { parser.upsertHeadMetaTags(options.headMetaTags); } if (options.favicon) { parser.upsertFaviconTag( options.favicon.href, options.favicon.rel, options.favicon.attributes ); } if (options.title) { parser.upsertTitleTag(options.title); } 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; }; // src/loader/htmlLoader.ts function htmlLoader(source) { const options = this.getOptions(); const force = options.force || false; const allLoadersButThisOne = this.loaders.filter( (loader) => loader.normal !== module.exports ); if (allLoadersButThisOne.length > 0 && !force) { return source; } const htmlWebpackPluginLoaders = this.loaders.filter( (loader) => loader.normal === module.exports ); const lastHtmlWebpackPluginLoader = htmlWebpackPluginLoaders[htmlWebpackPluginLoaders.length - 1]; if (this.loaders[this.loaderIndex] !== lastHtmlWebpackPluginLoader) { return source; } if (!/\.html$/.test(this.resourcePath)) { return source; } return [ 'const { TemplateParser } = eval("require")(' + JSON.stringify(__require.resolve("../index.cjs")) + ");", "const parseTemplate = " + parseTemplate.toString() + ";", "const source = " + JSON.stringify(source) + ";", "module.exports = (function(templateParams) { ", "return parseTemplate(source, templateParams || {}).serialize();", "});" ].join(""); } module.exports = htmlLoader;