@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
text/typescript
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, {});
}
}