hotModuleReplacement.js 5.8 KB

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