mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix: harden CDN script inlining with linkedom (#352)
* fix: harden CDN script inlining * test: add spanish empire regression fixture
This commit is contained in:
@@ -152,6 +152,26 @@ describe("inlineExternalScripts", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves non-src script attributes when inlining", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock(
|
||||
async () => new Response('console.log("module");', { status: 200 }),
|
||||
) as any;
|
||||
|
||||
try {
|
||||
const html =
|
||||
'<html><body><script type="module" data-role="boot" src="https://cdn.example.com/module.js"></script></body></html>';
|
||||
const result = await inlineExternalScripts(html);
|
||||
|
||||
expect(result).toMatch(/<script\b[^>]*\btype="module"/);
|
||||
expect(result).toMatch(/<script\b[^>]*\bdata-role="boot"/);
|
||||
expect(result).toContain('console.log("module");');
|
||||
expect(result).not.toContain('src="https://cdn.example.com/module.js"');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("escapes </script in downloaded content", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock(
|
||||
@@ -169,6 +189,45 @@ describe("inlineExternalScripts", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves literal replacement tokens in downloaded script content", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock(
|
||||
async () =>
|
||||
new Response('const before = "$`"; const after = "$\'"; const both = "$&";', {
|
||||
status: 200,
|
||||
}),
|
||||
) as any;
|
||||
|
||||
try {
|
||||
const html = `<html><body><script src="https://cdn.example.com/d3.min.js"></script><div>tail</div></body></html>`;
|
||||
const result = await inlineExternalScripts(html);
|
||||
|
||||
expect(result).toContain('const before = "$`";');
|
||||
expect(result).toContain('const after = "$\'";');
|
||||
expect(result).toContain('const both = "$&";');
|
||||
expect(result.match(/<script>/g)?.length).toBe(1);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a fragment when the input has no html/body wrapper", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock(async () => new Response("var d3 = {};", { status: 200 })) as any;
|
||||
|
||||
try {
|
||||
const html = '<script src="https://cdn.example.com/d3.min.js"></script><div>tail</div>';
|
||||
const result = await inlineExternalScripts(html);
|
||||
|
||||
expect(result).not.toMatch(/<!DOCTYPE|<html|<head|<body/i);
|
||||
expect(result).toContain("var d3 = {};");
|
||||
expect(result).toContain("<div>tail</div>");
|
||||
expect(result).not.toContain('src="https://cdn.example.com/d3.min.js"');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
it("warns but keeps original tag when fetch fails", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = mock(async () => {
|
||||
@@ -223,10 +282,11 @@ describe("inlineExternalScripts", () => {
|
||||
<script src="https://cdn.example.com/gsap.min.js"></script>
|
||||
</body></html>`;
|
||||
const result = await inlineExternalScripts(html);
|
||||
// Both should be found, both fetched
|
||||
// Both identical script tags should be fetched and replaced independently.
|
||||
expect(fetchCount).toBe(2);
|
||||
// At least one should be inlined (regex replaces first occurrence)
|
||||
expect(result).toContain("var gsap = {};");
|
||||
expect(
|
||||
result.match(/\/\* inlined: https:\/\/cdn\.example\.com\/gsap\.min\.js \*\//g)?.length,
|
||||
).toBe(2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
@@ -777,7 +777,9 @@ function ensureFullDocument(html: string): string {
|
||||
* works without network access (Docker, CI, restricted environments).
|
||||
*/
|
||||
export async function inlineExternalScripts(html: string): Promise<string> {
|
||||
const { document } = parseHTML(html);
|
||||
const fullHtml = ensureFullDocument(html);
|
||||
const wrappedFragment = fullHtml !== html;
|
||||
const { document } = parseHTML(fullHtml);
|
||||
const scripts = document.querySelectorAll("script[src]");
|
||||
const externalScripts: { el: Element; src: string }[] = [];
|
||||
|
||||
@@ -800,21 +802,21 @@ export async function inlineExternalScripts(html: string): Promise<string> {
|
||||
}),
|
||||
);
|
||||
|
||||
let result = html;
|
||||
for (let i = 0; i < downloads.length; i++) {
|
||||
const download = downloads[i]!;
|
||||
const { src } = externalScripts[i]!;
|
||||
const { el, src } = externalScripts[i]!;
|
||||
if (download.status === "fulfilled") {
|
||||
const escapedSrc = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const scriptTagRe = new RegExp(
|
||||
`<script\\b[^>]*\\bsrc=["']${escapedSrc}["'][^>]*>\\s*</script>`,
|
||||
"is",
|
||||
);
|
||||
// Escape </script in downloaded content to prevent premature tag closure.
|
||||
// <\/script is safe: the HTML parser doesn't recognize it as a close tag,
|
||||
// but JS treats \/ as / so the code executes identically.
|
||||
const safeText = download.value.text.replace(/<\/script/gi, "<\\/script");
|
||||
result = result.replace(scriptTagRe, `<script>/* inlined: ${src} */\n${safeText}\n</script>`);
|
||||
const inlineScript = document.createElement("script");
|
||||
for (const attr of Array.from(el.attributes)) {
|
||||
if (attr.name.toLowerCase() === "src") continue;
|
||||
inlineScript.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
inlineScript.textContent = `/* inlined: ${src} */\n${safeText}\n`;
|
||||
el.replaceWith(inlineScript);
|
||||
console.log(`[Compiler] Inlined CDN script: ${src}`);
|
||||
} else {
|
||||
console.warn(
|
||||
@@ -825,7 +827,7 @@ export async function inlineExternalScripts(html: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return wrappedFragment ? document.body.innerHTML || "" : document.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "spanish-empire-cdn-inline",
|
||||
"description": "Regression guard for the Spanish Empire composition, trimmed to the first 8 seconds so the suite stays practical while still exercising D3, TopoJSON, and GSAP loaded from CDNs. The fixture vendors world-atlas locally so the render remains deterministic while covering the CDN script inlining path that previously corrupted third-party bundles.",
|
||||
"tags": ["regression", "cdn-inline", "prod-style", "slow"],
|
||||
"minPsnr": 30,
|
||||
"maxFrameFailures": 0,
|
||||
"minAudioCorrelation": 0,
|
||||
"maxAudioLagWindows": 1,
|
||||
"renderConfig": {
|
||||
"fps": 30,
|
||||
"workers": 1
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72ceaede15c4270c8351455eb20aacd79458a95ed76b4bf7907f0e336b7567b3
|
||||
size 6910717
|
||||
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user