const loadedScripts = new Set(); /** * Check wether it's a real script tag or not * @param {HTMLScriptElement} script */ const isScriptTag = (script) => { return [null, undefined, "", "text/javascript"].includes(script.type); }; /** * Create a blob url from inline script * @param {String} scriptContent */ const createScriptLink = (scriptContent) => { const blob = new Blob([scriptContent], { type: "text/javascript" }); return URL.createObjectURL(blob).toString(); }; export const isScriptValid = (scriptTag, forceUpdate) => { // Check wether the tag is a real script or not if (!isScriptTag(scriptTag)) { return false; } // Skip loading already existing scripts if (scriptTag.dataset.alwaysReload === undefined) { if (forceUpdate !== true && loadedScripts.has(scriptTag.outerHTML)) { return false; } } // Skip if the script is not attached to DOM if (!scriptTag.parentNode) { return false; } return true; }; export const clearScriptsCache = () => { loadedScripts.clear(); }; /** * @param {HTMLScriptElement} targetScript * @param {HTMLScriptElement} sourceScript */ const swapScripts = (targetScript, sourceScript) => { return new Promise((resolve) => { /**@type {HTMLScriptElement} */ const newScript = document.createElement("script"); sourceScript = sourceScript ?? targetScript; // Use existing src or create url from script content // This is necessary for onload event to work properly const src = sourceScript.src || createScriptLink(sourceScript.text); // Remember the script to prevent loading it more than once loadedScripts.add(sourceScript.outerHTML); // We respect async attribute, so we only wait for script to load // when it's explicitly no async attribute if (!sourceScript.async) { const onScriptLoaded = ({ type }) => { newScript.removeEventListener("load", onScriptLoaded); newScript.removeEventListener("error", onScriptLoaded); resolve(type === "error" ? false : newScript); }; newScript.addEventListener("load", onScriptLoaded); newScript.addEventListener("error", onScriptLoaded); } else { resolve(); } if (sourceScript.dataset.alwaysReload !== undefined) { newScript.dataset.alwaysReload = ""; } if (sourceScript.id) newScript.id = sourceScript.id; if (sourceScript.className) newScript.className = sourceScript.className; newScript.dataset.replaced = "true"; newScript.async = sourceScript.async; newScript.defer = sourceScript.defer; newScript.type = "text/javascript"; newScript.src = src; targetScript.parentNode.insertBefore(newScript, targetScript); targetScript.remove(); }); }; /** * @param {HTMLScriptElement} scriptTag * @param {Function} onReplace */ export const replaceScript = async (scriptTag, { sourceScript, forceUpdate = false } = {}) => { sourceScript = sourceScript ?? scriptTag; if (!isScriptValid(scriptTag, forceUpdate)) return; if (sourceScript !== scriptTag && !isScriptValid(sourceScript, forceUpdate)) return; return swapScripts(scriptTag, sourceScript); }; const scriptIterator = function* (scripts) { while (scripts.length) { const nextScript = scripts.shift(); yield replaceScript(nextScript).then((result) => { return result; }); } }; /** * Re-inserts script tags inside a given element * Only scripts with src or with empty type, or with * type text/javascript will be processed * @param {HTMLElement} root */ export const reInsertScripts = async (root) => { const scripts = root.querySelectorAll("script"); if (!scripts.length) return []; const iterarot = scriptIterator(Array.from(scripts)); const result = []; for await (const script of iterarot) { result.push(script); } return result; };