fix: stabilize remote preview proxy
This commit is contained in:
@@ -51,7 +51,7 @@ export async function detectDevServerCommand(
|
|||||||
if (devScript) {
|
if (devScript) {
|
||||||
// Determine the package manager command
|
// Determine the package manager command
|
||||||
const pm = await detectPackageManager(directory);
|
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 {
|
return {
|
||||||
command: `${pmCommand} ${devScript}`,
|
command: `${pmCommand} ${devScript}`,
|
||||||
label: `Start (${devScript})`,
|
label: `Start (${devScript})`,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
|
|||||||
const isInternalDevToolResource = (element, value) => {
|
const isInternalDevToolResource = (element, value) => {
|
||||||
const tag = element && element.tagName && typeof element.tagName.toLowerCase === 'function' ? element.tagName.toLowerCase() : '';
|
const tag = element && element.tagName && typeof element.tagName.toLowerCase === 'function' ? element.tagName.toLowerCase() : '';
|
||||||
if (tag !== 'script' && tag !== 'link') return false;
|
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 path = upstreamPathForUrl(value);
|
||||||
const pathAndSearch = upstreamPathAndSearchForUrl(value);
|
const pathAndSearch = upstreamPathAndSearchForUrl(value);
|
||||||
return path === '/@vite/client'
|
return path === '/@vite/client'
|
||||||
@@ -128,6 +129,14 @@ const PREVIEW_BRIDGE_SCRIPT = String.raw`(() => {
|
|||||||
|| isStyleRuntimeNoise;
|
|| 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 = () => {
|
const installViteHmrProxyPatch = () => {
|
||||||
if (window.__openchamberViteHmrProxyPatched || typeof window.WebSocket !== 'function') return;
|
if (window.__openchamberViteHmrProxyPatched || typeof window.WebSocket !== 'function') return;
|
||||||
window.__openchamberViteHmrProxyPatched = true;
|
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) {
|
if (level === 'debug' && typeof args[0] === 'string' && args[0].indexOf('[vite]') === 0) {
|
||||||
return original.apply(console, args);
|
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() });
|
post({ type: 'console', level, args: args.map(stringifyArg), ts: Date.now() });
|
||||||
return original.apply(console, args);
|
return original.apply(console, args);
|
||||||
};
|
};
|
||||||
@@ -676,25 +688,46 @@ export const createPreviewProxyRuntime = ({
|
|||||||
}) => {
|
}) => {
|
||||||
ensureSweeper();
|
ensureSweeper();
|
||||||
|
|
||||||
const rewritePreviewBody = (bodyText, proxyBasePath) => {
|
const rewritePreviewBody = (bodyText, proxyBasePath, targetOrigin) => {
|
||||||
if (typeof bodyText !== 'string' || bodyText.length === 0) {
|
if (typeof bodyText !== 'string' || bodyText.length === 0) {
|
||||||
return bodyText;
|
return bodyText;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefix = proxyBasePath.endsWith('/') ? proxyBasePath.slice(0, -1) : proxyBasePath;
|
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) => {
|
const rewriteRootPath = (value) => {
|
||||||
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) {
|
if (typeof value !== 'string' || value.length === 0) {
|
||||||
return value;
|
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 value;
|
||||||
}
|
}
|
||||||
return `${prefix}${value}`;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
return bodyText
|
return bodyText
|
||||||
.replace(/\b(src|href|action)=(["'])\/(?!\/)([^"']*)\2/gi, (_match, attr, quote, path) => {
|
.replace(/\b(src|href|action)=(["'])([^"']*)\2/gi, (_match, attr, quote, value) => {
|
||||||
return `${attr}=${quote}${rewriteRootPath(`/${path}`)}${quote}`;
|
return `${attr}=${quote}${rewriteRootPath(value)}${quote}`;
|
||||||
})
|
})
|
||||||
.replace(/\bsrcset=(["'])([^"']*)\1/gi, (_match, quote, value) => {
|
.replace(/\bsrcset=(["'])([^"']*)\1/gi, (_match, quote, value) => {
|
||||||
const rewritten = String(value).split(',').map((part) => {
|
const rewritten = String(value).split(',').map((part) => {
|
||||||
@@ -707,9 +740,9 @@ export const createPreviewProxyRuntime = ({
|
|||||||
}).join(', ');
|
}).join(', ');
|
||||||
return `srcset=${quote}${rewritten}${quote}`;
|
return `srcset=${quote}${rewritten}${quote}`;
|
||||||
})
|
})
|
||||||
.replace(/url\((['"]?)\/(?!\/)([^)'"]*)\1\)/gi, (_match, quote, path) => {
|
.replace(/url\((['"]?)([^)'"]*)\1\)/gi, (_match, quote, value) => {
|
||||||
const q = quote || '';
|
const q = quote || '';
|
||||||
return `url(${q}${rewriteRootPath(`/${path}`)}${q})`;
|
return `url(${q}${rewriteRootPath(value)}${q})`;
|
||||||
})
|
})
|
||||||
.replace(/@import\s+(["'])\/(?!\/)([^"']*)\1/gi, (_match, quote, path) => {
|
.replace(/@import\s+(["'])\/(?!\/)([^"']*)\1/gi, (_match, quote, path) => {
|
||||||
return `@import ${quote}${rewriteRootPath(`/${path}`)}${quote}`;
|
return `@import ${quote}${rewriteRootPath(`/${path}`)}${quote}`;
|
||||||
@@ -881,10 +914,10 @@ export const createPreviewProxyRuntime = ({
|
|||||||
const parsed = new URL(req.originalUrl || req.url || '', 'http://localhost');
|
const parsed = new URL(req.originalUrl || req.url || '', 'http://localhost');
|
||||||
const upstreamPath = stripProxyPrefix(parsed.pathname, resolved.id);
|
const upstreamPath = stripProxyPrefix(parsed.pathname, resolved.id);
|
||||||
if (isJavaScript && upstreamPath === '/@vite/client') {
|
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;
|
return isHtml ? injectPreviewBridge(rewrittenBody) : rewrittenBody;
|
||||||
}),
|
}),
|
||||||
error: (err, _req, res) => {
|
error: (err, _req, res) => {
|
||||||
@@ -953,6 +986,7 @@ export const createPreviewProxyRuntime = ({
|
|||||||
|
|
||||||
// Rewrite req.url to what the dev server expects.
|
// Rewrite req.url to what the dev server expects.
|
||||||
const rawUrl = req.url || '';
|
const rawUrl = req.url || '';
|
||||||
|
req.originalUrl = rawUrl;
|
||||||
const parsed = new URL(rawUrl, 'http://localhost');
|
const parsed = new URL(rawUrl, 'http://localhost');
|
||||||
const nextPath = stripProxyPrefix(parsed.pathname, resolved.id);
|
const nextPath = stripProxyPrefix(parsed.pathname, resolved.id);
|
||||||
const search = parsed.searchParams.toString();
|
const search = parsed.searchParams.toString();
|
||||||
|
|||||||
Reference in New Issue
Block a user