mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-09 20:46:13 +00:00
fix(core): never merge head styles across a stylesheet link, in preview or render
This commit is contained in:
@@ -1481,6 +1481,18 @@ describe("bundleToSingleHtml", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not merge styles across a stylesheet link", async () => {
|
||||
expect(
|
||||
await bundledHeadStyles(
|
||||
`<style>p{color:red}</style><link rel="stylesheet" href="https://example.com/x.css">` +
|
||||
`<link rel="preconnect" href="https://example.com"><style>p{color:blue}</style>`,
|
||||
),
|
||||
).toEqual([
|
||||
{ media: null, type: null, css: "p{color:red}" },
|
||||
{ media: null, type: null, css: "p{color:blue}" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps rule order across a conditional style", async () => {
|
||||
expect(
|
||||
await bundledHeadStyles(
|
||||
|
||||
@@ -720,7 +720,8 @@ function pushRun<T>(runs: PartRun<T>[], scene: string | undefined, chunk: T): vo
|
||||
function coalesceHeadStylesAndBodyScripts(document: Document): void {
|
||||
const allHeadStyles = [...document.querySelectorAll("head style")];
|
||||
const isScenePart = (el: Element) => el.hasAttribute(SCENE_PART_ATTR);
|
||||
for (const run of allHeadStyles.length > 1 ? headStyleRuns(allHeadStyles, isScenePart) : []) {
|
||||
const headSheets = [...document.querySelectorAll("head style, head link")];
|
||||
for (const run of allHeadStyles.length > 1 ? headStyleRuns(headSheets, isScenePart) : []) {
|
||||
const merged = joinCssHoistingImports(run.map((el) => el.textContent || ""));
|
||||
if (!merged) continue;
|
||||
run[0]!.textContent = merged;
|
||||
|
||||
@@ -46,15 +46,17 @@ export function cssStyleMergeKey(el: Element): string | undefined {
|
||||
return JSON.stringify([media === "all" ? "" : media, el.getAttribute("title") ?? ""]);
|
||||
}
|
||||
|
||||
/** Groups head styles into runs of adjacent styles with one merge key, so merging a run never reorders rules. */
|
||||
/** Groups head styles into runs of adjacent same-key styles; a stylesheet link or pinned style ends a run. */
|
||||
export function headStyleRuns(
|
||||
styles: readonly Element[],
|
||||
elements: readonly Element[],
|
||||
isPinned: (el: Element) => boolean = () => false,
|
||||
): Element[][] {
|
||||
const runs: Element[][] = [];
|
||||
let previousKey: string | undefined;
|
||||
for (const el of styles) {
|
||||
const key = isPinned(el) ? undefined : cssStyleMergeKey(el);
|
||||
for (const el of elements) {
|
||||
if (el.tagName === "LINK" && !/(^|\s)stylesheet(\s|$)/i.test(el.getAttribute("rel") ?? ""))
|
||||
continue;
|
||||
const key = el.tagName === "STYLE" && !isPinned(el) ? cssStyleMergeKey(el) : undefined;
|
||||
if (key !== undefined && key === previousKey) runs.at(-1)!.push(el);
|
||||
else if (key !== undefined) runs.push([el]);
|
||||
previousKey = key;
|
||||
|
||||
@@ -234,6 +234,27 @@ describe("preview/render semantic compilation parity", () => {
|
||||
expect(render).toEqual(preview);
|
||||
});
|
||||
|
||||
it("keeps a stylesheet link between the head styles it separates in render", async () => {
|
||||
const dir = project({
|
||||
"index.html": shell(
|
||||
`<main data-composition-id="main" data-start="0" data-width="320" data-height="180" data-duration="1"></main>`,
|
||||
`<style>p{color:red}</style><link rel="stylesheet" href="x.css"><style>p{color:blue}</style>`,
|
||||
),
|
||||
"x.css": "p{color:green}",
|
||||
});
|
||||
const sheets = (html: string) =>
|
||||
[...new DOMParser().parseFromString(html, "text/html").head.querySelectorAll("style, link")]
|
||||
.filter((el) => !el.hasAttribute("data-hyperframes-text-rendering"))
|
||||
.map((el) => el.getAttribute("href") ?? (el.textContent ?? "").trim());
|
||||
expect(sheets(await bundleToSingleHtml(dir))).toEqual([
|
||||
"p{color:red}\n\np{color:green}\n\np{color:blue}",
|
||||
]);
|
||||
const render = await compileForRender(dir, join(dir, "index.html"), join(dir, ".downloads"), {
|
||||
allowSystemFontCapture: false,
|
||||
});
|
||||
expect(sheets(render.html)).toEqual(["p{color:red}", "x.css", "p{color:blue}"]);
|
||||
});
|
||||
|
||||
it("keeps legacy end/layer timing semantically identical", async () => {
|
||||
const result = await contracts({
|
||||
"index.html":
|
||||
|
||||
@@ -873,7 +873,8 @@ function coalesceHeadStylesAndBodyScripts(html: string): string {
|
||||
|
||||
const styleEls = Array.from(head.querySelectorAll("style"));
|
||||
const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
|
||||
for (const run of styleEls.length > 1 ? headStyleRuns(styleEls) : []) {
|
||||
const headSheets = Array.from(head.querySelectorAll("style, link"));
|
||||
for (const run of styleEls.length > 1 ? headStyleRuns(headSheets) : []) {
|
||||
const imports: string[] = [];
|
||||
const cssParts: string[] = [];
|
||||
const seenImports = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user