@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
183 lines (160 loc) • 5.59 kB
text/typescript
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(
`<script>globalThis.ncgConfig = ${JSON.stringify(filteredConfig)};</script>`,
);
if (resourceType === "panel" || resourceType === "dialog") {
// If this bundle has sounds, inject SoundJS
if (standalone && sound) {
scripts.push(
'<script src="/node_modules/soundjs/lib/soundjs.min.js"></script>',
);
}
if (standalone) {
// Load the API
scripts.push('<script src="/api.js"></script>');
} else {
// Panels and dialogs can grab the API from the dashboard
scripts.push("<script>window.NodeCG = window.top.NodeCG</script>");
}
// Both panels and dialogs need the main default styles
scripts.push(
'<link rel="stylesheet" href="/dashboard/css/panel-and-dialog-defaults.css">',
);
if (standalone) {
// Load the socket
scripts.push('<script src="/socket.js"></script>');
} else {
// They both also need to reference the dashboard window's socket, rather than make their own
scripts.push("<script>window.socket = window.top.socket;</script>");
}
// 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(
'<link rel="stylesheet" href="/dashboard/css/old-panel-defaults.css">',
);
} else {
styles.push(
'<link rel="stylesheet" href="/dashboard/css/panel-defaults.css">',
);
}
scripts.push('<script async src="/dialog_opener.js"></script>');
} else if (resourceType === "dialog") {
styles.push(
'<link rel="stylesheet" href="/dashboard/css/dialog-defaults.css">',
);
}
} else if (resourceType === "graphic") {
if (sentryEnabled) {
scripts.unshift(
'<script src="/node_modules/@sentry/browser/build/bundle.es6.min.js"></script>',
'<script src="/sentry.js"></script>',
);
}
// Graphics need to create their own socket
scripts.push('<script src="/socket.io/socket.io.js"></script>');
scripts.push('<script src="/socket.js"></script>');
// If this bundle has sounds, inject SoundJS
if (sound) {
scripts.push(
'<script src="/node_modules/soundjs/lib/soundjs.min.js"></script>',
);
}
// Graphics must include the API script themselves before attempting to make an instance of it
scripts.push('<script src="/api.js"></script>');
}
// 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(
`<script>globalThis.nodecg = new globalThis.NodeCG(${JSON.stringify(
partialBundle,
)}, globalThis.socket)</script>`,
);
}
// Inject the scripts required for singleInstance behavior, if requested.
if (
resourceType === "graphic" &&
!(pathOrHtml.endsWith("busy.html") || pathOrHtml.endsWith("killed.html"))
) {
scripts.push('<script src="/client_registration.js"></script>');
}
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 <body>.
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 <head>.
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());
}
}