hotModuleReplacement.js 5.0 KB

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