import cheerio from "cheerio"; import fs from "fs"; import semver from "semver"; import type { NodeCG } from "../../types/nodecg"; import { filteredConfig, sentryEnabled } from "../config"; import { noop } from "./noop"; interface Options { standalone?: boolean; createApiInstance?: NodeCG.Bundle; sound?: boolean; fullbleed?: boolean; } /** * Injects the appropriate assets into a panel, dialog, or graphic. */ export function injectScripts( pathOrHtml: string, resourceType: "panel" | "dialog" | "graphic", { standalone = false, createApiInstance, sound = false, }: Options = {} as Options, cb: (html: string) => void = noop, ): void { // Graphics only pass the path to the html file. // Panels and dialogs pass a cached HTML string. if (resourceType === "graphic") { fs.readFile(pathOrHtml, { encoding: "utf8" }, (error, data) => { inject(error ?? undefined, data); }); } else { inject(undefined, pathOrHtml); } function inject(err: NodeJS.ErrnoException | undefined, html: string): void { if (err) { throw err; } const $ = cheerio.load(html); const scripts = []; const styles = []; // Everything needs the config scripts.push( ``, ); if (resourceType === "panel" || resourceType === "dialog") { // If this bundle has sounds, inject SoundJS if (standalone && sound) { scripts.push( '', ); } if (standalone) { // Load the API scripts.push(''); } else { // Panels and dialogs can grab the API from the dashboard scripts.push(""); } // Both panels and dialogs need the main default styles scripts.push( '', ); if (standalone) { // Load the socket scripts.push(''); } else { // They both also need to reference the dashboard window's socket, rather than make their own scripts.push(""); } // Panels need the default panel styles and the dialog_opener. if (resourceType === "panel") { // In v1.1.0, we changed the Dashboard to have a dark theme. // This also meant that we wanted to update the default panel styles. // However, this technically would have been a breaking change... // To minimize breakage, we only inject the new styles if // the bundle specifically lists support for v1.0.0. // If it only supports v1.1.0 and on, we assume it wants the dark theme styles. if ( createApiInstance?.compatibleRange && semver.satisfies("1.0.0", createApiInstance.compatibleRange) ) { styles.push( '', ); } else { styles.push( '', ); } scripts.push(''); } else if (resourceType === "dialog") { styles.push( '', ); } } else if (resourceType === "graphic") { if (sentryEnabled) { scripts.unshift( '', '', ); } // Graphics need to create their own socket scripts.push(''); scripts.push(''); // If this bundle has sounds, inject SoundJS if (sound) { scripts.push( '', ); } // Graphics must include the API script themselves before attempting to make an instance of it scripts.push(''); } // Inject a small script to create a NodeCG API instance, if requested. if (createApiInstance) { const partialBundle = { name: createApiInstance.name, config: createApiInstance.config, version: createApiInstance.version, git: createApiInstance.git, _hasSounds: sound, }; scripts.push( ``, ); } // Inject the scripts required for singleInstance behavior, if requested. if ( resourceType === "graphic" && !(pathOrHtml.endsWith("busy.html") || pathOrHtml.endsWith("killed.html")) ) { scripts.push(''); } const concattedScripts = scripts.join("\n"); // Put our scripts before their first script or HTML import. // If they have no scripts or imports, put our scripts at the end of . const theirScriptsAndImports = $('script, link[rel="import"]'); if (theirScriptsAndImports.length > 0) { theirScriptsAndImports.first().before(concattedScripts); } else { $("body").append(concattedScripts); } // Prepend our styles before the first one. // If there are no styles, put our styles at the end of . if (styles.length > 0) { const concattedStyles = styles.join("\n"); const headStyles = $("head").find('style, link[rel="stylesheet"]'); if (headStyles.length > 0) { headStyles.first().before(concattedStyles); } else { $("head").append(concattedStyles); } } cb($.html()); } }