diff --git a/packages/ui/src/lib/detectDevServer.ts b/packages/ui/src/lib/detectDevServer.ts index 29e1e6fc..a616c71c 100644 --- a/packages/ui/src/lib/detectDevServer.ts +++ b/packages/ui/src/lib/detectDevServer.ts @@ -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})`, diff --git a/packages/web/server/lib/preview/proxy-runtime.js b/packages/web/server/lib/preview/proxy-runtime.js index 56d99717..e5348059 100644 --- a/packages/web/server/lib/preview/proxy-runtime.js +++ b/packages/web/server/lib/preview/proxy-runtime.js @@ -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();