From 998494a9987a49a1ac4a9b33a412e45929978166 Mon Sep 17 00:00:00 2001
From: evilebottnawi <sheo13666q@gmail.com>
Date: Fri, 24 May 2019 20:51:24 +0300
Subject: [PATCH] fix: hmr reload with invalid link url

---
 src/hmr/hotModuleReplacement.js     |  21 +-
 test/HMR.test.js                    | 284 ++++++++++++++++++++++++++++
 test/__snapshots__/HMR.test.js.snap |  39 ++++
 3 files changed, 331 insertions(+), 13 deletions(-)
 create mode 100644 test/HMR.test.js
 create mode 100644 test/__snapshots__/HMR.test.js.snap

diff --git a/src/hmr/hotModuleReplacement.js b/src/hmr/hotModuleReplacement.js
index 6d6611b1..802c0a12 100644
--- a/src/hmr/hotModuleReplacement.js
+++ b/src/hmr/hotModuleReplacement.js
@@ -120,7 +120,11 @@ function updateCss(el, url) {
 
   newEl.href = `${url}?${Date.now()}`;
 
-  el.parentNode.appendChild(newEl);
+  if (el.nextSibling) {
+    el.parentNode.insertBefore(newEl, el.nextSibling);
+  } else {
+    el.parentNode.appendChild(newEl);
+  }
 }
 
 function getReloadUrl(href, src) {
@@ -160,6 +164,7 @@ function reloadStyle(src) {
 
     if (url) {
       updateCss(el, url);
+
       loaded = true;
     }
   });
@@ -182,18 +187,8 @@ function reloadAll() {
 function isUrlRequest(url) {
   // An URL is not an request if
 
-  // 1. It's an absolute url
-  if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {
-    return false;
-  }
-
-  // 2. It's a protocol-relative
-  if (/^\/\//.test(url)) {
-    return false;
-  }
-
-  // 3. Its a `#` link
-  if (/^#/.test(url)) {
+  // It is not http or https
+  if (!/^https?:/i.test(url)) {
     return false;
   }
 
diff --git a/test/HMR.test.js b/test/HMR.test.js
new file mode 100644
index 00000000..be56e01d
--- /dev/null
+++ b/test/HMR.test.js
@@ -0,0 +1,284 @@
+/* eslint-env browser */
+/* eslint-disable no-console */
+
+import hotModuleReplacement from '../src/hmr/hotModuleReplacement';
+
+function getLoadEvent() {
+  const event = document.createEvent('Event');
+
+  event.initEvent('load', false, false);
+
+  return event;
+}
+
+function getErrorEvent() {
+  const event = document.createEvent('Event');
+
+  event.initEvent('error', false, false);
+
+  return event;
+}
+
+describe('HMR', () => {
+  let consoleMock = null;
+
+  beforeEach(() => {
+    consoleMock = jest.spyOn(console, 'log').mockImplementation(() => () => {});
+
+    jest.spyOn(Date, 'now').mockImplementation(() => 1479427200000);
+
+    document.head.innerHTML = '<link rel="stylesheet" href="/dist/main.css" />';
+    document.body.innerHTML = '<script src="/dist/main.js"></script>';
+  });
+
+  afterEach(() => {
+    consoleMock.mockClear();
+  });
+
+  it('should works', (done) => {
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+
+      done();
+    }, 100);
+  });
+
+  it('should works with multiple updates', (done) => {
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+
+      jest.spyOn(Date, 'now').mockImplementation(() => 1479427200001);
+
+      const update2 = hotModuleReplacement('./src/style.css', {});
+
+      update2();
+
+      setTimeout(() => {
+        const links2 = Array.prototype.slice.call(
+          document.querySelectorAll('link')
+        );
+
+        expect(links2[0].visited).toBe(true);
+        expect(links2[0].isLoaded).toBe(true);
+        expect(document.head.innerHTML).toMatchSnapshot();
+
+        links2[1].dispatchEvent(getLoadEvent());
+
+        expect(links2[1].isLoaded).toBe(true);
+
+        done();
+      }, 100);
+    }, 100);
+  });
+
+  it('should reloads with locals', (done) => {
+    const update = hotModuleReplacement('./src/style.css', {
+      locals: { foo: 'bar' },
+    });
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+
+      done();
+    }, 100);
+  });
+
+  it('should reloads with reloadAll option', (done) => {
+    const update = hotModuleReplacement('./src/style.css', {
+      reloadAll: true,
+    });
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+
+      done();
+    }, 100);
+  });
+
+  it('should reloads with non http/https link href', (done) => {
+    document.head.innerHTML =
+      '<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" href="data:;base64,=" />';
+
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+      expect(links[2].visited).toBeUndefined();
+
+      done();
+    }, 100);
+  });
+
+  it('should reloads with # link href', (done) => {
+    document.head.innerHTML =
+      '<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" href="#href" />';
+
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+      expect(links[2].visited).toBeUndefined();
+
+      done();
+    }, 100);
+  });
+
+  it('should reloads with link without href', (done) => {
+    document.head.innerHTML =
+      '<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" />';
+
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+      expect(links[2].visited).toBeUndefined();
+
+      done();
+    }, 100);
+  });
+
+  it('should reloads with absolute remove url', (done) => {
+    document.head.innerHTML =
+      '<link rel="stylesheet" href="/dist/main.css" /><link rel="stylesheet" href="http://dev.com/dist/main.css" />';
+
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getLoadEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+      expect(links[2].visited).toBeUndefined();
+
+      done();
+    }, 100);
+  });
+
+  it('should handle error event', (done) => {
+    const update = hotModuleReplacement('./src/style.css', {});
+
+    update();
+
+    setTimeout(() => {
+      expect(console.log.mock.calls[0][0]).toMatchSnapshot();
+
+      const links = Array.prototype.slice.call(
+        document.querySelectorAll('link')
+      );
+
+      expect(links[0].visited).toBe(true);
+      expect(document.head.innerHTML).toMatchSnapshot();
+
+      links[1].dispatchEvent(getErrorEvent());
+
+      expect(links[1].isLoaded).toBe(true);
+
+      done();
+    }, 100);
+  });
+});
diff --git a/test/__snapshots__/HMR.test.js.snap b/test/__snapshots__/HMR.test.js.snap
new file mode 100644
index 00000000..269260d0
--- /dev/null
+++ b/test/__snapshots__/HMR.test.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HMR should handle error event 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should handle error event 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
+
+exports[`HMR should reloads with # link href 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should reloads with # link href 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\"><link rel=\\"shortcut icon\\" href=\\"#href\\">"`;
+
+exports[`HMR should reloads with absolute remove url 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should reloads with absolute remove url 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\"><link rel=\\"stylesheet\\" href=\\"http://dev.com/dist/main.css\\">"`;
+
+exports[`HMR should reloads with link without href 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should reloads with link without href 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\"><link rel=\\"shortcut icon\\">"`;
+
+exports[`HMR should reloads with locals 1`] = `"[HMR] Detected local css modules. Reload all css"`;
+
+exports[`HMR should reloads with locals 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
+
+exports[`HMR should reloads with non http/https link href 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should reloads with non http/https link href 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\"><link rel=\\"shortcut icon\\" href=\\"data:;base64,=\\">"`;
+
+exports[`HMR should reloads with reloadAll option 1`] = `"[HMR] Reload all css"`;
+
+exports[`HMR should reloads with reloadAll option 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
+
+exports[`HMR should works 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should works 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
+
+exports[`HMR should works with multiple updates 1`] = `"[HMR] css reload %s"`;
+
+exports[`HMR should works with multiple updates 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
+
+exports[`HMR should works with multiple updates 3`] = `"<link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200001\\">"`;