Skip to content

Commit 2694690

Browse files
committed
fix: keep live-reload injection out of HTML comments
The live-server injector picked the first `</body>` in the raw file text without checking whether it appeared inside an HTML comment. When a page contained something like `<!-- </body> -->`, the injected code was inserted inside the comment, splitting it open and leaking a stray `-->` onto the rendered page. Instead of anchoring on the first literal match, skip tags that lie inside a comment (<!-- ... -->) and inject at the first occurrence outside one. The in-JS injection also replaces the global es.replace so a tag that only exists inside a comment is no longer rewritten.
1 parent 4153b4b commit 2694690

1 file changed

Lines changed: 52 additions & 12 deletions

File tree

lib/live-server/index.js

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ var fs = require('fs'),
1717
http = require('http'),
1818
send = require('send'),
1919
open = require('opn'),
20-
es = require("event-stream"),
2120
os = require('os'),
2221
chokidar = require('chokidar'),
2322
httpProxy = require('http-proxy');
@@ -49,6 +48,43 @@ function escape(html) {
4948
.replace(/"/g, '&quot;');
5049
}
5150

51+
/**
52+
* Find the first match of `regex` in `contents` that is NOT inside an HTML
53+
* comment (`<!-- ... -->`).
54+
*
55+
* This prevents the live-reload injection from anchoring on a tag that only
56+
* appears inside a comment, e.g. `<!-- </body> -->`. Previously the first
57+
* `</body>` in the raw text was picked regardless of whether it was inside a
58+
* comment, which split the comment open and leaked a stray `/>` / `-->` onto
59+
* the rendered page.
60+
*/
61+
function findInjectCandidate(contents, regex) {
62+
var commentRe = /<!--[\s\S]*?-->/g,
63+
comments = [],
64+
match,
65+
re = new RegExp(regex.source, "ig");
66+
67+
while ((match = commentRe.exec(contents)) !== null) {
68+
comments.push(match.index);
69+
comments.push(match.index + match[0].length);
70+
}
71+
while ((match = re.exec(contents)) !== null) {
72+
if (!isInsideAnyComment(comments, match.index)) {
73+
return match;
74+
}
75+
}
76+
return null;
77+
}
78+
79+
function isInsideAnyComment(commentBounds, index) {
80+
for (var i = 0; i < commentBounds.length; i += 2) {
81+
if (index >= commentBounds[i] && index < commentBounds[i + 1]) {
82+
return true;
83+
}
84+
}
85+
return false;
86+
}
87+
5288
// Based on connect.static(), but streamlined and with added code injecter
5389
function staticServer(root, onTagMissedCallback) {
5490
var isFile = false;
@@ -72,7 +108,9 @@ function staticServer(root, onTagMissedCallback) {
72108
// injectCandidates.push(new RegExp(`</${item}>`, "i"))
73109
// });
74110

75-
var injectTag = null;
111+
var injectTag = null,
112+
injectIndex = -1,
113+
injectedContents = null;
76114

77115
function directory() {
78116
var pathname = url.parse(req.originalUrl).pathname;
@@ -89,9 +127,11 @@ function staticServer(root, onTagMissedCallback) {
89127
// TODO: Sync file read here is not nice, but we need to determine if the html should be injected or not
90128
var contents = fs.readFileSync(filepath, "utf8");
91129
for (var i = 0; i < injectCandidates.length; ++i) {
92-
match = injectCandidates[i].exec(contents);
130+
match = findInjectCandidate(contents, injectCandidates[i]);
93131
if (match) {
94132
injectTag = match[0];
133+
injectIndex = match.index;
134+
injectedContents = contents.slice(0, injectIndex) + GET_INJECTED_CODE() + contents.slice(injectIndex);
95135
break;
96136
}
97137
}
@@ -113,15 +153,15 @@ function staticServer(root, onTagMissedCallback) {
113153
}
114154

115155
function inject(stream) {
116-
if (injectTag) {
117-
// We need to modify the length given to browser
118-
var len = GET_INJECTED_CODE().length + res.getHeader('Content-Length');
119-
res.setHeader('Content-Length', len);
120-
var originalPipe = stream.pipe;
121-
stream.pipe = function (resp) {
122-
originalPipe.call(stream, es.replace(new RegExp(injectTag, "i"), GET_INJECTED_CODE() + injectTag))
123-
.pipe(resp);
124-
};
156+
if (injectedContents) {
157+
// We already have the fully injected document in memory, so serve it
158+
// directly instead of the raw streamed bytes. This also avoids the
159+
// global regex replace that used to rewrite `</body>` even inside
160+
// HTML comments, which split the comment and leaked a stray `-->`.
161+
res.setHeader('Content-Length', Buffer.byteLength(injectedContents, 'utf8'));
162+
stream.unpipe(res);
163+
stream.resume();
164+
res.end(injectedContents);
125165
}
126166
}
127167

0 commit comments

Comments
 (0)