UNPKG

@jarred/htmlbuild

Version:

Configure & run esbuild on <script> and <link> used in an HTML file, then output an updated HTML file with the results.

131 lines (113 loc) 3.58 kB
import * as serializer from "dom-serializer"; import { DomHandler, DomUtils, parseDocument, ElementType, Parser, } from "htmlparser2"; import { BuildOptions, BuildResult, Metafile } from "esbuild"; import * as path from "path"; export class HTML2ESBuild { dom: ReturnType<typeof parseDocument>; generate( source: string, resolve: (...relativePath: string[]) => string ): BuildOptions { const dom = parseDocument(source); this.dom = dom; const config: BuildOptions = { bundle: true, metafile: true, entryPoints: [], }; let src = ""; for (let script of DomUtils.getElementsByTagName("script", dom)) { src = script.attribs["src"]; if (src && !src.includes("://")) { src = resolve(src); this.scripts.set(src, script); config.entryPoints.push(src); } } for (let link of DomUtils.getElementsByTagName("link", dom)) { if ( (!link.attribs["rel"] || link.attribs["rel"] === "stylesheet") && link.attribs["href"] && !link.attribs["href"].includes("://") ) { src = resolve(link.attribs["href"]); this.links.set(src, link); config.entryPoints.push(src); } } this.config = config; return config; } scripts: Map<string, ReturnType<typeof DomUtils.getElementById>> = new Map(); links: Map<string, ReturnType<typeof DomUtils.getElementById>> = new Map(); config: BuildOptions; renderToString( build: BuildResult, config: BuildOptions = this.config, resolveFrom: (...relativePath: string[]) => string, resolveTo: ( outpath: string, node?: ReturnType<typeof DomUtils.getElementById> ) => string ) { if (!build.metafile) throw "Build is missing metafile."; const { links, scripts } = this; let meta: Metafile = build.metafile; const cssOutputs = new Map(); let file; for (let output in meta.outputs) { file = meta.outputs[output]; if (path.extname(output) === ".css") { cssOutputs.set(output, file); } } const stylesheetsToInsert = new Map< string, ReturnType<typeof DomUtils.getElementById> >(); const prefix = config.publicPath ? config.publicPath : ""; for (let output in meta.outputs) { file = meta.outputs[output]; if (!file.entryPoint) continue; const entryPoint = resolveFrom(file.entryPoint); if (scripts.has(entryPoint)) { // CSS imports from JS const ext = path.extname(output); const basename = output.substring(0, output.length - ext.length); const cssName = basename + ".css"; const script = scripts.get(entryPoint); if ( cssOutputs.has(cssName) && (!cssOutputs.get(cssName).entryPoint || !links.has(cssOutputs.get(cssName).entryPoint)) ) { stylesheetsToInsert.set(cssName, script); } const _output = resolveTo(output, script); if (_output) { script.attribs["src"] = _output; } } else if (links.has(entryPoint)) { links.get(entryPoint).attribs["href"] = resolveTo(output); } } for (let [stylesheetName, above] of stylesheetsToInsert.entries()) { var parser = new Parser( new DomHandler((err, elems) => { DomUtils.prepend(above, elems[0]); }) ); parser.write( `<link rel="stylesheet" href="${resolveTo(stylesheetName)}" />` ); parser.end(); } return serializer.default(this.dom, {}); } }