Skip to content

Commit 30a19b0

Browse files
fix: hmr reload with invalid link url (#402)
1 parent ee9df43 commit 30a19b0

File tree

3 files changed

+331
-13
lines changed

3 files changed

+331
-13
lines changed

src/hmr/hotModuleReplacement.js

+8-13
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ function updateCss(el, url) {
120120

121121
newEl.href = `${url}?${Date.now()}`;
122122

123-
el.parentNode.appendChild(newEl);
123+
if (el.nextSibling) {
124+
el.parentNode.insertBefore(newEl, el.nextSibling);
125+
} else {
126+
el.parentNode.appendChild(newEl);
127+
}
124128
}
125129

126130
function getReloadUrl(href, src) {
@@ -160,6 +164,7 @@ function reloadStyle(src) {
160164

161165
if (url) {
162166
updateCss(el, url);
167+
163168
loaded = true;
164169
}
165170
});
@@ -182,18 +187,8 @@ function reloadAll() {
182187
function isUrlRequest(url) {
183188
// An URL is not an request if
184189

185-
// 1. It's an absolute url
186-
if (/^[a-z][a-z0-9+.-]*:/i.test(url)) {
187-
return false;
188-
}
189-
190-
// 2. It's a protocol-relative
191-
if (/^\/\//.test(url)) {
192-
return false;
193-
}
194-
195-
// 3. Its a `#` link
196-
if (/^#/.test(url)) {
190+
// It is not http or https
191+
if (!/^https?:/i.test(url)) {
197192
return false;
198193
}
199194

test/HMR.test.js

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/* eslint-env browser */
2+
/* eslint-disable no-console */
3+
4+
import hotModuleReplacement from '../src/hmr/hotModuleReplacement';
5+
6+
function getLoadEvent() {
7+
const event = document.createEvent('Event');
8+
9+
event.initEvent('load', false, false);
10+
11+
return event;
12+
}
13+
14+
function getErrorEvent() {
15+
const event = document.createEvent('Event');
16+
17+
event.initEvent('error', false, false);
18+
19+
return event;
20+
}
21+
22+
describe('HMR', () => {
23+
let consoleMock = null;
24+
25+
beforeEach(() => {
26+
consoleMock = jest.spyOn(console, 'log').mockImplementation(() => () => {});
27+
28+
jest.spyOn(Date, 'now').mockImplementation(() => 1479427200000);
29+
30+
document.head.innerHTML = '<link rel="stylesheet" href="/dist/main.css" />';
31+
document.body.innerHTML = '<script src="/dist/main.js"></script>';
32+
});
33+
34+
afterEach(() => {
35+
consoleMock.mockClear();
36+
});
37+
38+
it('should works', (done) => {
39+
const update = hotModuleReplacement('./src/style.css', {});
40+
41+
update();
42+
43+
setTimeout(() => {
44+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
45+
46+
const links = Array.prototype.slice.call(
47+
document.querySelectorAll('link')
48+
);
49+
50+
expect(links[0].visited).toBe(true);
51+
expect(document.head.innerHTML).toMatchSnapshot();
52+
53+
links[1].dispatchEvent(getLoadEvent());
54+
55+
expect(links[1].isLoaded).toBe(true);
56+
57+
done();
58+
}, 100);
59+
});
60+
61+
it('should works with multiple updates', (done) => {
62+
const update = hotModuleReplacement('./src/style.css', {});
63+
64+
update();
65+
66+
setTimeout(() => {
67+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
68+
69+
const links = Array.prototype.slice.call(
70+
document.querySelectorAll('link')
71+
);
72+
73+
expect(links[0].visited).toBe(true);
74+
expect(document.head.innerHTML).toMatchSnapshot();
75+
76+
links[1].dispatchEvent(getLoadEvent());
77+
78+
expect(links[1].isLoaded).toBe(true);
79+
80+
jest.spyOn(Date, 'now').mockImplementation(() => 1479427200001);
81+
82+
const update2 = hotModuleReplacement('./src/style.css', {});
83+
84+
update2();
85+
86+
setTimeout(() => {
87+
const links2 = Array.prototype.slice.call(
88+
document.querySelectorAll('link')
89+
);
90+
91+
expect(links2[0].visited).toBe(true);
92+
expect(links2[0].isLoaded).toBe(true);
93+
expect(document.head.innerHTML).toMatchSnapshot();
94+
95+
links2[1].dispatchEvent(getLoadEvent());
96+
97+
expect(links2[1].isLoaded).toBe(true);
98+
99+
done();
100+
}, 100);
101+
}, 100);
102+
});
103+
104+
it('should reloads with locals', (done) => {
105+
const update = hotModuleReplacement('./src/style.css', {
106+
locals: { foo: 'bar' },
107+
});
108+
109+
update();
110+
111+
setTimeout(() => {
112+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
113+
114+
const links = Array.prototype.slice.call(
115+
document.querySelectorAll('link')
116+
);
117+
118+
expect(links[0].visited).toBe(true);
119+
expect(document.head.innerHTML).toMatchSnapshot();
120+
121+
links[1].dispatchEvent(getLoadEvent());
122+
123+
expect(links[1].isLoaded).toBe(true);
124+
125+
done();
126+
}, 100);
127+
});
128+
129+
it('should reloads with reloadAll option', (done) => {
130+
const update = hotModuleReplacement('./src/style.css', {
131+
reloadAll: true,
132+
});
133+
134+
update();
135+
136+
setTimeout(() => {
137+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
138+
139+
const links = Array.prototype.slice.call(
140+
document.querySelectorAll('link')
141+
);
142+
143+
expect(links[0].visited).toBe(true);
144+
expect(document.head.innerHTML).toMatchSnapshot();
145+
146+
links[1].dispatchEvent(getLoadEvent());
147+
148+
expect(links[1].isLoaded).toBe(true);
149+
150+
done();
151+
}, 100);
152+
});
153+
154+
it('should reloads with non http/https link href', (done) => {
155+
document.head.innerHTML =
156+
'<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" href="data:;base64,=" />';
157+
158+
const update = hotModuleReplacement('./src/style.css', {});
159+
160+
update();
161+
162+
setTimeout(() => {
163+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
164+
165+
const links = Array.prototype.slice.call(
166+
document.querySelectorAll('link')
167+
);
168+
169+
expect(links[0].visited).toBe(true);
170+
expect(document.head.innerHTML).toMatchSnapshot();
171+
172+
links[1].dispatchEvent(getLoadEvent());
173+
174+
expect(links[1].isLoaded).toBe(true);
175+
expect(links[2].visited).toBeUndefined();
176+
177+
done();
178+
}, 100);
179+
});
180+
181+
it('should reloads with # link href', (done) => {
182+
document.head.innerHTML =
183+
'<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" href="#href" />';
184+
185+
const update = hotModuleReplacement('./src/style.css', {});
186+
187+
update();
188+
189+
setTimeout(() => {
190+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
191+
192+
const links = Array.prototype.slice.call(
193+
document.querySelectorAll('link')
194+
);
195+
196+
expect(links[0].visited).toBe(true);
197+
expect(document.head.innerHTML).toMatchSnapshot();
198+
199+
links[1].dispatchEvent(getLoadEvent());
200+
201+
expect(links[1].isLoaded).toBe(true);
202+
expect(links[2].visited).toBeUndefined();
203+
204+
done();
205+
}, 100);
206+
});
207+
208+
it('should reloads with link without href', (done) => {
209+
document.head.innerHTML =
210+
'<link rel="stylesheet" href="/dist/main.css" /><link rel="shortcut icon" />';
211+
212+
const update = hotModuleReplacement('./src/style.css', {});
213+
214+
update();
215+
216+
setTimeout(() => {
217+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
218+
219+
const links = Array.prototype.slice.call(
220+
document.querySelectorAll('link')
221+
);
222+
223+
expect(links[0].visited).toBe(true);
224+
expect(document.head.innerHTML).toMatchSnapshot();
225+
226+
links[1].dispatchEvent(getLoadEvent());
227+
228+
expect(links[1].isLoaded).toBe(true);
229+
expect(links[2].visited).toBeUndefined();
230+
231+
done();
232+
}, 100);
233+
});
234+
235+
it('should reloads with absolute remove url', (done) => {
236+
document.head.innerHTML =
237+
'<link rel="stylesheet" href="/dist/main.css" /><link rel="stylesheet" href="http://dev.com/dist/main.css" />';
238+
239+
const update = hotModuleReplacement('./src/style.css', {});
240+
241+
update();
242+
243+
setTimeout(() => {
244+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
245+
246+
const links = Array.prototype.slice.call(
247+
document.querySelectorAll('link')
248+
);
249+
250+
expect(links[0].visited).toBe(true);
251+
expect(document.head.innerHTML).toMatchSnapshot();
252+
253+
links[1].dispatchEvent(getLoadEvent());
254+
255+
expect(links[1].isLoaded).toBe(true);
256+
expect(links[2].visited).toBeUndefined();
257+
258+
done();
259+
}, 100);
260+
});
261+
262+
it('should handle error event', (done) => {
263+
const update = hotModuleReplacement('./src/style.css', {});
264+
265+
update();
266+
267+
setTimeout(() => {
268+
expect(console.log.mock.calls[0][0]).toMatchSnapshot();
269+
270+
const links = Array.prototype.slice.call(
271+
document.querySelectorAll('link')
272+
);
273+
274+
expect(links[0].visited).toBe(true);
275+
expect(document.head.innerHTML).toMatchSnapshot();
276+
277+
links[1].dispatchEvent(getErrorEvent());
278+
279+
expect(links[1].isLoaded).toBe(true);
280+
281+
done();
282+
}, 100);
283+
});
284+
});

test/__snapshots__/HMR.test.js.snap

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`HMR should handle error event 1`] = `"[HMR] css reload %s"`;
4+
5+
exports[`HMR should handle error event 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
6+
7+
exports[`HMR should reloads with # link href 1`] = `"[HMR] css reload %s"`;
8+
9+
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\\">"`;
10+
11+
exports[`HMR should reloads with absolute remove url 1`] = `"[HMR] css reload %s"`;
12+
13+
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\\">"`;
14+
15+
exports[`HMR should reloads with link without href 1`] = `"[HMR] css reload %s"`;
16+
17+
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\\">"`;
18+
19+
exports[`HMR should reloads with locals 1`] = `"[HMR] Detected local css modules. Reload all css"`;
20+
21+
exports[`HMR should reloads with locals 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
22+
23+
exports[`HMR should reloads with non http/https link href 1`] = `"[HMR] css reload %s"`;
24+
25+
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,=\\">"`;
26+
27+
exports[`HMR should reloads with reloadAll option 1`] = `"[HMR] Reload all css"`;
28+
29+
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\\">"`;
30+
31+
exports[`HMR should works 1`] = `"[HMR] css reload %s"`;
32+
33+
exports[`HMR should works 2`] = `"<link rel=\\"stylesheet\\" href=\\"/dist/main.css\\"><link rel=\\"stylesheet\\" href=\\"http://localhost/dist/main.css?1479427200000\\">"`;
34+
35+
exports[`HMR should works with multiple updates 1`] = `"[HMR] css reload %s"`;
36+
37+
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\\">"`;
38+
39+
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\\">"`;

0 commit comments

Comments
 (0)