diff --git a/packages/web/server/lib/preview/proxy-runtime.js b/packages/web/server/lib/preview/proxy-runtime.js index 36a21266..7aefcc5d 100644 --- a/packages/web/server/lib/preview/proxy-runtime.js +++ b/packages/web/server/lib/preview/proxy-runtime.js @@ -1041,21 +1041,6 @@ export const rewritePreviewBody = ({ bodyText, proxyBasePath, targetOrigin, kind } return value; }; - const rewriteHtml = (text) => text - .replace(/\b(src|href|action)=(['"])([^'"]*)\2/gi, (_match, attr, quote, value) => { - return `${attr}=${quote}${rewriteResourceUrl(value)}${quote}`; - }) - .replace(/\bsrcset=(['"])([^'"]*)\1/gi, (_match, quote, value) => { - const rewritten = String(value).split(',').map((part) => { - const trimmed = part.trim(); - if (!trimmed) return trimmed; - const segments = trimmed.split(/\s+/); - const url = segments[0] || ''; - segments[0] = rewriteResourceUrl(url); - return segments.join(' '); - }).join(', '); - return `srcset=${quote}${rewritten}${quote}`; - }); const stripPreviewCspMeta = (text) => text .replace(/]*\bhttp-equiv\s*=\s*(['"])content-security-policy\1)[^>]*>/gi, '') .replace(/]*\bhttp-equiv\s*=\s*content-security-policy\b)[^>]*>/gi, ''); @@ -1077,6 +1062,35 @@ export const rewritePreviewBody = ({ bodyText, proxyBasePath, targetOrigin, kind .replace(/\bimport\(\s*(['"])\/(?!\/)([^'"]*)\1\s*\)/gi, (_match, quote, path) => { return `import(${quote}${rewriteResourceUrl(`/${path}`)}${quote})`; }); + const rewriteInlineModuleScripts = (text) => text.replace( + /]*)>([\s\S]*?)<\/script>/gi, + (match, attrs, scriptBody) => { + if (/\bsrc\s*=/i.test(attrs)) return match; + + const typeMatch = String(attrs || '').match(/\btype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); + const type = String(typeMatch?.[1] ?? typeMatch?.[2] ?? typeMatch?.[3] ?? '').trim().toLowerCase(); + if (type !== 'module') return match; + + const rewrittenScriptBody = rewriteJavaScript(scriptBody); + if (rewrittenScriptBody === scriptBody) return match; + return `${rewrittenScriptBody}`; + }, + ); + const rewriteHtml = (text) => rewriteInlineModuleScripts(text + .replace(/\b(src|href|action)=(['"])([^'"]*)\2/gi, (_match, attr, quote, value) => { + return `${attr}=${quote}${rewriteResourceUrl(value)}${quote}`; + }) + .replace(/\bsrcset=(['"])([^'"]*)\1/gi, (_match, quote, value) => { + const rewritten = String(value).split(',').map((part) => { + const trimmed = part.trim(); + if (!trimmed) return trimmed; + const segments = trimmed.split(/\s+/); + const url = segments[0] || ''; + segments[0] = rewriteResourceUrl(url); + return segments.join(' '); + }).join(', '); + return `srcset=${quote}${rewritten}${quote}`; + })); if (kind === 'html') return stripPreviewCspMeta(rewriteHtml(bodyText)); if (kind === 'css') return rewriteCss(bodyText); diff --git a/packages/web/server/lib/preview/proxy-runtime.test.js b/packages/web/server/lib/preview/proxy-runtime.test.js index 06770a2c..e810a173 100644 --- a/packages/web/server/lib/preview/proxy-runtime.test.js +++ b/packages/web/server/lib/preview/proxy-runtime.test.js @@ -97,6 +97,21 @@ describe('preview body URL rewriting', () => { expect(output).toContain('const url = "/api/data";'); }); + it('rewrites inline module imports in HTML responses', () => { + const input = [ + '', + '', + ].join(''); + const output = rewrite(input, 'html'); + + expect(output).toContain('from "/api/preview/proxy/abc123/@react-refresh"'); + expect(output).toContain('window.__vite_plugin_react_preamble_installed__ = true;'); + expect(output).toContain('const refreshUrl = "/@react-refresh";'); + }); + it('removes CSP meta tags that block the preview bridge', () => { const input = '
Preview
'; const output = rewrite(input, 'html'); @@ -107,7 +122,7 @@ describe('preview body URL rewriting', () => { it('adds preview and URL auth tokens to rewritten proxy resources when provided', () => { const output = rewritePreviewBody({ - bodyText: 'Docs', + bodyText: 'Docs', kind: 'html', proxyBasePath: '/api/preview/proxy/abc123', targetOrigin: 'http://127.0.0.1:3000', @@ -116,6 +131,7 @@ describe('preview body URL rewriting', () => { }); expect(output).toContain('src="/api/preview/proxy/abc123/entry.js?oc_preview_token=preview-secret&oc_url_token=url-secret"'); + expect(output).toContain('from "/api/preview/proxy/abc123/@react-refresh?oc_preview_token=preview-secret&oc_url_token=url-secret"'); expect(output).toContain('href="/api/preview/proxy/abc123/docs?x=1&oc_preview_token=preview-secret&oc_url_token=url-secret"'); expect(output).not.toContain('oc_client_token'); });