script-loader.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. 'use strict';
  2. // loadScript(script: Array<Promise|string>|string): Promise
  3. // eslint-disable-next-line no-var
  4. var loadScript = (() => {
  5. const cache = new Map();
  6. function inject(file) {
  7. if (!cache.has(file)) {
  8. cache.set(file, doInject(file));
  9. }
  10. return cache.get(file);
  11. }
  12. function doInject(file) {
  13. return new Promise((resolve, reject) => {
  14. let el;
  15. if (file.endsWith('.js')) {
  16. el = document.createElement('script');
  17. el.src = file;
  18. } else {
  19. el = document.createElement('link');
  20. el.rel = 'stylesheet';
  21. el.href = file;
  22. }
  23. el.onload = () => {
  24. el.onload = null;
  25. el.onerror = null;
  26. resolve();
  27. };
  28. el.onerror = () => {
  29. el.onload = null;
  30. el.onerror = null;
  31. reject(new Error(`Failed to load script: ${file}`));
  32. };
  33. document.head.appendChild(el);
  34. });
  35. }
  36. return files => {
  37. if (!Array.isArray(files)) {
  38. files = [files];
  39. }
  40. return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
  41. };
  42. })();
  43. (() => {
  44. let subscribers, observer;
  45. // natively declared <script> elements in html can't have onload= attribute
  46. // due to the default extension CSP that forbids inline code (and we don't want to relax it),
  47. // so we're using MutationObserver to add onload event listener to the script element to be loaded
  48. window.onDOMscriptReady = (srcSuffix, timeout = 1000) => {
  49. if (!subscribers) {
  50. subscribers = new Map();
  51. observer = new MutationObserver(observe);
  52. observer.observe(document.head, {childList: true});
  53. }
  54. return new Promise((resolve, reject) => {
  55. const listeners = subscribers.get(srcSuffix);
  56. if (listeners) {
  57. listeners.push(resolve);
  58. } else {
  59. subscribers.set(srcSuffix, [resolve]);
  60. }
  61. // a resolved Promise won't reject anymore
  62. setTimeout(() => emptyAfterCleanup(srcSuffix) + reject(), timeout);
  63. });
  64. };
  65. return;
  66. function observe(mutations) {
  67. for (const {addedNodes} of mutations) {
  68. for (const n of addedNodes) {
  69. if (n.src && getSubscribersForSrc(n.src)) {
  70. n.addEventListener('load', notifySubscribers);
  71. }
  72. }
  73. }
  74. }
  75. function getSubscribersForSrc(src) {
  76. for (const [suffix, listeners] of subscribers.entries()) {
  77. if (src.endsWith(suffix)) {
  78. return {suffix, listeners};
  79. }
  80. }
  81. }
  82. function notifySubscribers(event) {
  83. this.removeEventListener('load', notifySubscribers);
  84. for (let data; (data = getSubscribersForSrc(this.src));) {
  85. data.listeners.forEach(fn => fn(event));
  86. if (emptyAfterCleanup(data.suffix)) {
  87. return;
  88. }
  89. }
  90. }
  91. function emptyAfterCleanup(suffix) {
  92. if (!subscribers) {
  93. return true;
  94. }
  95. subscribers.delete(suffix);
  96. if (!subscribers.size) {
  97. observer.disconnect();
  98. observer = null;
  99. subscribers = null;
  100. return true;
  101. }
  102. }
  103. })();