Skip to content

Commit fbc4da7

Browse files
Inject route-change script for Live Preview path sync @W-21574818@ (#34)
1 parent eedcb5f commit fbc4da7

1 file changed

Lines changed: 100 additions & 2 deletions

File tree

src/proxy/ProxyServer.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,104 @@ export class ProxyServer extends EventEmitter {
108108
}
109109
}
110110

111+
/**
112+
* Wraps the response to inject a route-change script into HTML pages.
113+
* When the app is embedded in Live Preview iframe, this script notifies the parent
114+
* when the route changes (e.g. "Go to Home" click) so the path input stays in sync.
115+
* Injection only occurs when Content-Type is text/html and body contains </body>.
116+
*/
117+
private static wrapResponseForRouteInjection(_req: IncomingMessage, res: ServerResponse): ServerResponse {
118+
let statusCode = 200;
119+
let headers: Record<string, string | string[] | number | undefined> = {};
120+
const chunks: Buffer[] = [];
121+
122+
const routeScript =
123+
'<script>(function(){if(window===window.top)return;function send(){try{var p=window.location.pathname||\'/\';window.parent.postMessage({command:\'routeChanged\',route:p,_source:\'webapps-proxy-injected-script\'},\'*\')}catch(e){}}send();window.addEventListener(\'popstate\',send);var _push=history.pushState;if(_push){history.pushState=function(){_push.apply(this,arguments);setTimeout(send,0)}}var _replace=history.replaceState;if(_replace){history.replaceState=function(){_replace.apply(this,arguments);setTimeout(send,0)}}if(typeof window.navigation!==\'undefined\'&&window.navigation.addEventListener){window.navigation.addEventListener(\'navigate\',function(){setTimeout(send,0)})}})();</script>';
124+
125+
const wrapped = Object.create(res, {
126+
writeHead: {
127+
value: (
128+
code: number,
129+
h?: Record<string, string | string[] | number | undefined>
130+
) => {
131+
statusCode = code;
132+
if (h) {
133+
const merged: Record<string, string | string[] | number | undefined> = {
134+
...headers,
135+
...h
136+
};
137+
headers = merged;
138+
}
139+
return true;
140+
}
141+
},
142+
write: {
143+
value: (
144+
chunk: Buffer | string,
145+
encoding?: BufferEncoding | ((err?: Error) => void),
146+
cb?: (err?: Error) => void
147+
) => {
148+
let actualEncoding: BufferEncoding | undefined;
149+
let actualCb: ((err?: Error) => void) | undefined;
150+
if (typeof encoding === 'function') {
151+
actualCb = encoding;
152+
actualEncoding = undefined;
153+
} else {
154+
actualEncoding = encoding;
155+
actualCb = cb;
156+
}
157+
chunks.push(
158+
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, actualEncoding as BufferEncoding)
159+
);
160+
if (actualCb) actualCb();
161+
return true;
162+
}
163+
},
164+
end: {
165+
value: (
166+
chunk?: Buffer | string | (() => void),
167+
encoding?: BufferEncoding | (() => void),
168+
cb?: () => void
169+
) => {
170+
let actualChunk: Buffer | string | undefined;
171+
let actualEncoding: BufferEncoding | undefined;
172+
let actualCb: (() => void) | undefined;
173+
if (typeof chunk === 'function') {
174+
actualCb = chunk;
175+
actualChunk = undefined;
176+
actualEncoding = undefined;
177+
} else if (typeof encoding === 'function') {
178+
actualCb = encoding;
179+
actualChunk = chunk;
180+
actualEncoding = undefined;
181+
} else {
182+
actualChunk = chunk;
183+
actualEncoding = encoding;
184+
actualCb = cb;
185+
}
186+
if (actualChunk)
187+
chunks.push(
188+
Buffer.isBuffer(actualChunk)
189+
? actualChunk
190+
: Buffer.from(actualChunk, actualEncoding as BufferEncoding)
191+
);
192+
const body = Buffer.concat(chunks);
193+
const contentType = (headers['content-type'] ?? headers['Content-Type'] ?? '') as string;
194+
if (contentType.includes('text/html') && body.includes('</body>')) {
195+
const injected = body.toString().replace('</body>', `${routeScript}</body>`);
196+
res.writeHead(statusCode, { ...headers, 'content-length': Buffer.byteLength(injected) });
197+
res.end(injected, actualCb);
198+
} else {
199+
res.writeHead(statusCode, headers);
200+
res.end(body, actualCb);
201+
}
202+
}
203+
}
204+
}) as ServerResponse;
205+
206+
return wrapped;
207+
}
208+
111209
// Public instance methods
112210
public clearActiveDevServerError(): void {
113211
if (this.activeDevServerError) {
@@ -359,8 +457,8 @@ export class ProxyServer extends EventEmitter {
359457
}
360458

361459
if (this.proxyHandler) {
362-
// Package handles all errors internally and returns proper HTTP responses
363-
await this.proxyHandler(req, res);
460+
const wrappedRes = ProxyServer.wrapResponseForRouteInjection(req, res);
461+
await this.proxyHandler(req, wrappedRes);
364462
} else {
365463
this.logger.error('Proxy handler not initialized');
366464
res.writeHead(500, { 'Content-Type': 'application/json' });

0 commit comments

Comments
 (0)