@@ -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, '"' ) ;
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
5389function 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