fix: stabilize remote preview proxy

This commit is contained in:
Bohdan Triapitsyn
2026-04-30 12:22:49 +03:00
parent d7ddc7cf4d
commit 371eac9db3
2 changed files with 45 additions and 11 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ export async function detectDevServerCommand(
if (devScript) {
// Determine the package manager command
const pm = await detectPackageManager(directory);
const pmCommand = pm === 'npm' ? 'npm run' : pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm run';
const pmCommand = pm === 'npm' ? 'npm run' : pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun run --shell=bun' : 'npm run';
return {
command: `${pmCommand} ${devScript}`,
label: `Start (${devScript})`,
@@ -75,6 +75,7 @@ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
const isInternalDevToolResource = (element, value) => {
const tag = element && element.tagName && typeof element.tagName.toLowerCase === 'function' ? element.tagName.toLowerCase() : '';
if (tag !== 'script' && tag !== 'link') return false;
if (tag === 'script' && typeof element.hasAttribute === 'function' && element.hasAttribute('data-cf-beacon')) return true;
const path = upstreamPathForUrl(value);
const pathAndSearch = upstreamPathAndSearchForUrl(value);
return path === '/@vite/client'
@@ -128,6 +129,14 @@ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
|| isStyleRuntimeNoise;
};
const isInternalDevToolConsoleNoise = (level, args) => {
if (level !== 'error' || typeof args[0] !== 'string' || args[0].indexOf('[vite]') !== 0) return false;
const text = args.map((arg) => stringifyArg(arg)).join(' ');
return text.indexOf('failed to connect to websocket') >= 0
|| text.indexOf("Cannot read properties of undefined (reading 'send')") >= 0
|| text.indexOf('Cannot read properties of undefined (reading "send")') >= 0;
};
const installViteHmrProxyPatch = () => {
if (window.__openchamberViteHmrProxyPatched || typeof window.WebSocket !== 'function') return;
window.__openchamberViteHmrProxyPatched = true;
@@ -363,6 +372,9 @@ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
if (level === 'debug' && typeof args[0] === 'string' && args[0].indexOf('[vite]') === 0) {
return original.apply(console, args);
}
if (isInternalDevToolConsoleNoise(level, args)) {
return original.apply(console, args);
}
post({ type: 'console', level, args: args.map(stringifyArg), ts: Date.now() });
return original.apply(console, args);
};
@@ -676,25 +688,46 @@ export const createPreviewProxyRuntime = ({
}) => {
ensureSweeper();
const rewritePreviewBody = (bodyText, proxyBasePath) => {
const rewritePreviewBody = (bodyText, proxyBasePath, targetOrigin) => {
if (typeof bodyText !== 'string' || bodyText.length === 0) {
return bodyText;
}
const prefix = proxyBasePath.endsWith('/') ? proxyBasePath.slice(0, -1) : proxyBasePath;
const target = targetOrigin ? new URL(targetOrigin) : null;
const isSameLoopbackTarget = (url) => {
if (!target) return false;
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
const host = url.hostname;
if (host !== 'localhost' && host !== '127.0.0.1' && host !== '0.0.0.0' && host !== '::1' && host !== '[::1]') {
return false;
}
return url.port === target.port;
};
const rewriteRootPath = (value) => {
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) {
if (typeof value !== 'string' || value.length === 0) {
return value;
}
if (value.startsWith('/api/preview/proxy/')) {
if (value.startsWith('/') && !value.startsWith('//')) {
if (value.startsWith('/api/preview/proxy/')) {
return value;
}
return `${prefix}${value}`;
}
try {
const parsed = new URL(value);
if (isSameLoopbackTarget(parsed)) {
return `${prefix}${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
return value;
}
return `${prefix}${value}`;
return value;
};
return bodyText
.replace(/\b(src|href|action)=(["'])\/(?!\/)([^"']*)\2/gi, (_match, attr, quote, path) => {
return `${attr}=${quote}${rewriteRootPath(`/${path}`)}${quote}`;
.replace(/\b(src|href|action)=(["'])([^"']*)\2/gi, (_match, attr, quote, value) => {
return `${attr}=${quote}${rewriteRootPath(value)}${quote}`;
})
.replace(/\bsrcset=(["'])([^"']*)\1/gi, (_match, quote, value) => {
const rewritten = String(value).split(',').map((part) => {
@@ -707,9 +740,9 @@ export const createPreviewProxyRuntime = ({
}).join(', ');
return `srcset=${quote}${rewritten}${quote}`;
})
.replace(/url\((['"]?)\/(?!\/)([^)'"]*)\1\)/gi, (_match, quote, path) => {
.replace(/url\((['"]?)([^)'"]*)\1\)/gi, (_match, quote, value) => {
const q = quote || '';
return `url(${q}${rewriteRootPath(`/${path}`)}${q})`;
return `url(${q}${rewriteRootPath(value)}${q})`;
})
.replace(/@import\s+(["'])\/(?!\/)([^"']*)\1/gi, (_match, quote, path) => {
return `@import ${quote}${rewriteRootPath(`/${path}`)}${quote}`;
@@ -881,10 +914,10 @@ export const createPreviewProxyRuntime = ({
const parsed = new URL(req.originalUrl || req.url || '', 'http://localhost');
const upstreamPath = stripProxyPrefix(parsed.pathname, resolved.id);
if (isJavaScript && upstreamPath === '/@vite/client') {
return rewritePreviewBody(rewriteViteClientHmr(responseBuffer.toString('utf8'), proxyBasePath), proxyBasePath);
return rewritePreviewBody(rewriteViteClientHmr(responseBuffer.toString('utf8'), proxyBasePath), proxyBasePath, resolved.entry.origin);
}
const rewrittenBody = rewritePreviewBody(responseBuffer.toString('utf8'), proxyBasePath);
const rewrittenBody = rewritePreviewBody(responseBuffer.toString('utf8'), proxyBasePath, resolved.entry.origin);
return isHtml ? injectPreviewBridge(rewrittenBody) : rewrittenBody;
}),
error: (err, _req, res) => {
@@ -953,6 +986,7 @@ export const createPreviewProxyRuntime = ({
// Rewrite req.url to what the dev server expects.
const rawUrl = req.url || '';
req.originalUrl = rawUrl;
const parsed = new URL(rawUrl, 'http://localhost');
const nextPath = stripProxyPrefix(parsed.pathname, resolved.id);
const search = parsed.searchParams.toString();