hotModuleReplacement.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. "use strict";
  2. /* global document */
  3. /*
  4. eslint-disable
  5. no-console,
  6. func-names
  7. */
  8. var normalizeUrl = require("./normalize-url");
  9. var srcByModuleId = Object.create(null);
  10. var noDocument = typeof document === "undefined";
  11. var forEach = Array.prototype.forEach;
  12. // eslint-disable-next-line jsdoc/no-restricted-syntax
  13. /**
  14. * @param {Function} fn any function
  15. * @param {number} time time
  16. * @returns {() => void} wrapped function
  17. */
  18. function debounce(fn, time) {
  19. var timeout = 0;
  20. return function () {
  21. // @ts-expect-error
  22. var self = this;
  23. // eslint-disable-next-line prefer-rest-params
  24. var args = arguments;
  25. // eslint-disable-next-line func-style
  26. var functionCall = function functionCall() {
  27. return fn.apply(self, args);
  28. };
  29. clearTimeout(timeout);
  30. // @ts-expect-error
  31. timeout = setTimeout(functionCall, time);
  32. };
  33. }
  34. /**
  35. * @returns {void}
  36. */
  37. function noop() {}
  38. /** @typedef {(filename?: string) => string[]} GetScriptSrc */
  39. /**
  40. * @param {string | number} moduleId a module id
  41. * @returns {GetScriptSrc} current script url
  42. */
  43. function getCurrentScriptUrl(moduleId) {
  44. var src = srcByModuleId[moduleId];
  45. if (!src) {
  46. if (document.currentScript) {
  47. src = (/** @type {HTMLScriptElement} */document.currentScript).src;
  48. } else {
  49. var scripts = document.getElementsByTagName("script");
  50. var lastScriptTag = scripts[scripts.length - 1];
  51. if (lastScriptTag) {
  52. src = lastScriptTag.src;
  53. }
  54. }
  55. srcByModuleId[moduleId] = src;
  56. }
  57. /** @type {GetScriptSrc} */
  58. return function (fileMap) {
  59. if (!src) {
  60. return [];
  61. }
  62. var splitResult = src.split(/([^\\/]+)\.js$/);
  63. var filename = splitResult && splitResult[1];
  64. if (!filename) {
  65. return [src.replace(".js", ".css")];
  66. }
  67. if (!fileMap) {
  68. return [src.replace(".js", ".css")];
  69. }
  70. return fileMap.split(",").map(function (mapRule) {
  71. var reg = new RegExp("".concat(filename, "\\.js$"), "g");
  72. return normalizeUrl(src.replace(reg, "".concat(mapRule.replace(/{fileName}/g, filename), ".css")));
  73. });
  74. };
  75. }
  76. /**
  77. * @param {string} url URL
  78. * @returns {boolean} true when URL can be request, otherwise false
  79. */
  80. function isUrlRequest(url) {
  81. // An URL is not an request if
  82. // It is not http or https
  83. if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
  84. return false;
  85. }
  86. return true;
  87. }
  88. /** @typedef {HTMLLinkElement & { isLoaded: boolean, visited: boolean }} HotHTMLLinkElement */
  89. /**
  90. * @param {HotHTMLLinkElement} el html link element
  91. * @param {string=} url a URL
  92. */
  93. function updateCss(el, url) {
  94. if (!url) {
  95. if (!el.href) {
  96. return;
  97. }
  98. // eslint-disable-next-line
  99. url = el.href.split("?")[0];
  100. }
  101. if (!isUrlRequest(/** @type {string} */url)) {
  102. return;
  103. }
  104. if (el.isLoaded === false) {
  105. // We seem to be about to replace a css link that hasn't loaded yet.
  106. // We're probably changing the same file more than once.
  107. return;
  108. }
  109. // eslint-disable-next-line unicorn/prefer-includes
  110. if (!url || !(url.indexOf(".css") > -1)) {
  111. return;
  112. }
  113. el.visited = true;
  114. var newEl = /** @type {HotHTMLLinkElement} */
  115. el.cloneNode();
  116. newEl.isLoaded = false;
  117. newEl.addEventListener("load", function () {
  118. if (newEl.isLoaded) {
  119. return;
  120. }
  121. newEl.isLoaded = true;
  122. if (el.parentNode) {
  123. el.parentNode.removeChild(el);
  124. }
  125. });
  126. newEl.addEventListener("error", function () {
  127. if (newEl.isLoaded) {
  128. return;
  129. }
  130. newEl.isLoaded = true;
  131. if (el.parentNode) {
  132. el.parentNode.removeChild(el);
  133. }
  134. });
  135. newEl.href = "".concat(url, "?").concat(Date.now());
  136. if (el.parentNode) {
  137. if (el.nextSibling) {
  138. el.parentNode.insertBefore(newEl, el.nextSibling);
  139. } else {
  140. el.parentNode.appendChild(newEl);
  141. }
  142. }
  143. }
  144. /**
  145. * @param {string} href href
  146. * @param {string[]} src src
  147. * @returns {undefined | string} a reload url
  148. */
  149. function getReloadUrl(href, src) {
  150. var ret;
  151. href = normalizeUrl(href);
  152. src.some(
  153. /**
  154. * @param {string} url url
  155. */
  156. // eslint-disable-next-line array-callback-return
  157. function (url) {
  158. // @ts-expect-error fix me in the next major release
  159. // eslint-disable-next-line unicorn/prefer-includes
  160. if (href.indexOf(src) > -1) {
  161. ret = url;
  162. }
  163. });
  164. return ret;
  165. }
  166. /**
  167. * @param {string[]} src source
  168. * @returns {boolean} true when loaded, otherwise false
  169. */
  170. function reloadStyle(src) {
  171. var elements = document.querySelectorAll("link");
  172. var loaded = false;
  173. forEach.call(elements, function (el) {
  174. if (!el.href) {
  175. return;
  176. }
  177. var url = getReloadUrl(el.href, src);
  178. if (url && !isUrlRequest(url)) {
  179. return;
  180. }
  181. if (el.visited === true) {
  182. return;
  183. }
  184. if (url) {
  185. updateCss(el, url);
  186. loaded = true;
  187. }
  188. });
  189. return loaded;
  190. }
  191. /**
  192. * @returns {void}
  193. */
  194. function reloadAll() {
  195. var elements = document.querySelectorAll("link");
  196. forEach.call(elements, function (el) {
  197. if (el.visited === true) {
  198. return;
  199. }
  200. updateCss(el);
  201. });
  202. }
  203. /**
  204. * @param {number | string} moduleId a module id
  205. * @param {{ filename?: string, locals?: boolean }} options options
  206. * @returns {() => void} wrapper function
  207. */
  208. module.exports = function (moduleId, options) {
  209. if (noDocument) {
  210. console.log("no window.document found, will not HMR CSS");
  211. return noop;
  212. }
  213. var getScriptSrc = getCurrentScriptUrl(moduleId);
  214. /**
  215. * @returns {void}
  216. */
  217. function update() {
  218. var src = getScriptSrc(options.filename);
  219. var reloaded = reloadStyle(src);
  220. if (options.locals) {
  221. console.log("[HMR] Detected local css modules. Reload all css");
  222. reloadAll();
  223. return;
  224. }
  225. if (reloaded) {
  226. console.log("[HMR] css reload %s", src.join(" "));
  227. } else {
  228. console.log("[HMR] Reload all css");
  229. reloadAll();
  230. }
  231. }
  232. return debounce(update, 50);
  233. };