Bin
2025-12-17 05a69820e0c402b0b33c063d3b922f0a0571cbbb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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;
};