fix: rewrite inline preview modules
The preview proxy now rewrites imports inside inline module scripts, which covers the Vite React preamble emitted into HTML. This keeps /@react-refresh behind the same proxy path and preserves preview/url auth tokens for iframe subresources. Added coverage so future proxy rewrites do not regress Vite React preview startup.
This commit is contained in:
@@ -1041,21 +1041,6 @@ export const rewritePreviewBody = ({ bodyText, proxyBasePath, targetOrigin, kind
|
|||||||
}
|
}
|
||||||
return value;
|
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
|
const stripPreviewCspMeta = (text) => text
|
||||||
.replace(/<meta\b(?=[^>]*\bhttp-equiv\s*=\s*(['"])content-security-policy\1)[^>]*>/gi, '')
|
.replace(/<meta\b(?=[^>]*\bhttp-equiv\s*=\s*(['"])content-security-policy\1)[^>]*>/gi, '')
|
||||||
.replace(/<meta\b(?=[^>]*\bhttp-equiv\s*=\s*content-security-policy\b)[^>]*>/gi, '');
|
.replace(/<meta\b(?=[^>]*\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) => {
|
.replace(/\bimport\(\s*(['"])\/(?!\/)([^'"]*)\1\s*\)/gi, (_match, quote, path) => {
|
||||||
return `import(${quote}${rewriteResourceUrl(`/${path}`)}${quote})`;
|
return `import(${quote}${rewriteResourceUrl(`/${path}`)}${quote})`;
|
||||||
});
|
});
|
||||||
|
const rewriteInlineModuleScripts = (text) => text.replace(
|
||||||
|
/<script\b([^>]*)>([\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 `<script${attrs}>${rewrittenScriptBody}</script>`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
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 === 'html') return stripPreviewCspMeta(rewriteHtml(bodyText));
|
||||||
if (kind === 'css') return rewriteCss(bodyText);
|
if (kind === 'css') return rewriteCss(bodyText);
|
||||||
|
|||||||
@@ -97,6 +97,21 @@ describe('preview body URL rewriting', () => {
|
|||||||
expect(output).toContain('const url = "/api/data";');
|
expect(output).toContain('const url = "/api/data";');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rewrites inline module imports in HTML responses', () => {
|
||||||
|
const input = [
|
||||||
|
'<script type="module">',
|
||||||
|
'import RefreshRuntime from "/@react-refresh";',
|
||||||
|
'window.__vite_plugin_react_preamble_installed__ = true;',
|
||||||
|
'</script>',
|
||||||
|
'<script>const refreshUrl = "/@react-refresh";</script>',
|
||||||
|
].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', () => {
|
it('removes CSP meta tags that block the preview bridge', () => {
|
||||||
const input = '<meta http-equiv="Content-Security-Policy" content="script-src \'self\'"><div>Preview</div>';
|
const input = '<meta http-equiv="Content-Security-Policy" content="script-src \'self\'"><div>Preview</div>';
|
||||||
const output = rewrite(input, 'html');
|
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', () => {
|
it('adds preview and URL auth tokens to rewritten proxy resources when provided', () => {
|
||||||
const output = rewritePreviewBody({
|
const output = rewritePreviewBody({
|
||||||
bodyText: '<script src="/entry.js"></script><a href="http://localhost:3000/docs?x=1&oc_client_token=legacy">Docs</a>',
|
bodyText: '<script src="/entry.js"></script><script type="module">import RefreshRuntime from "/@react-refresh";</script><a href="http://localhost:3000/docs?x=1&oc_client_token=legacy">Docs</a>',
|
||||||
kind: 'html',
|
kind: 'html',
|
||||||
proxyBasePath: '/api/preview/proxy/abc123',
|
proxyBasePath: '/api/preview/proxy/abc123',
|
||||||
targetOrigin: 'http://127.0.0.1:3000',
|
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('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).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');
|
expect(output).not.toContain('oc_client_token');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user