mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 02:36:10 +00:00
fix(core): resolve sub-composition sibling asset paths everywhere (#2994)
Extends the studio-preview fix to the render path and the asset-discovery utilities, which share the same resolver and had the same defect. `rewriteAssetPath` takes an optional `assetExists` probe. A plain relative ref authored in a sub-composition (`_shared.css`, `clip.mp4`) is re-pointed at the composition's own directory when that sibling exists on disk; project-root refs with no sibling (the registry's `assets/logo.png` convention) stay as authored. Callers that can see the filesystem supply the probe, so the module stays free of node:fs. Also fixes a second defect in the inliner: `<head>` <link> hrefs and external script srcs are hoisted into the root document but never went through the rewrite at all, so even the documented `../` form escaped the project and 404'd at render time. Wired into the preview bundler, the producer compiler, the studio preview builder, the HEVC preview lint, the project lint's asset scans, publish proxy baking, and media-treatment source resolution.
This commit is contained in:
@@ -898,6 +898,13 @@ export async function bundleToSingleHtml(
|
||||
parseHtml: parseHTMLContent,
|
||||
hostIdentityMap: hostIdentityByElement,
|
||||
rewriteInlineStyles: true,
|
||||
// A sub-composition's SIBLING assets (`_shared.css` next to it) must be
|
||||
// re-pointed at its own directory when its content moves to the root
|
||||
// document; project-root refs with no such sibling stay as authored.
|
||||
assetExists: (path: string) => {
|
||||
const resolved = resolveEntryPath(path);
|
||||
return resolved !== null && existsSync(resolved);
|
||||
},
|
||||
flattenInnerRoot: prepareFlattenedInnerRoot,
|
||||
readVariableDefaults: readDeclaredDefaults,
|
||||
parseHostVariables: parseHostVariableValues,
|
||||
|
||||
@@ -553,3 +553,71 @@ describe("inlineSubCompositions – recursive host discovery", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inlineSubCompositions – sub-composition asset paths", () => {
|
||||
// Every asset ref a sub-composition in a subdirectory carries has to be
|
||||
// re-pointed when its content moves into the project-root document. Hoisted
|
||||
// <head> <link>/<script src> used to bypass the rewrite entirely (so even the
|
||||
// documented `../` form escaped the project), and sibling refs (`_shared.css`)
|
||||
// silently 404'd. Both render the frame unstyled.
|
||||
const SUB_COMP = `<!doctype html>
|
||||
<html><head>
|
||||
<link rel="stylesheet" href="_shared.css">
|
||||
<link rel="stylesheet" href="../shared/theme.css">
|
||||
<script src="helper.js"></script>
|
||||
<style>.badge { background-image: url("frame.png"); }</style>
|
||||
</head><body>
|
||||
<div data-composition-id="frame" data-width="1920" data-height="1080">
|
||||
<img src="frame.png" alt="">
|
||||
<div style="background-image: url('frame.png')"></div>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
const PROJECT_FILES = [
|
||||
"design/styleframes/_shared.css",
|
||||
"design/styleframes/frame.png",
|
||||
"design/styleframes/helper.js",
|
||||
"design/shared/theme.css",
|
||||
];
|
||||
|
||||
function inlineFrame() {
|
||||
const { document } = parseHTML(`<!DOCTYPE html>
|
||||
<html><body>
|
||||
<div data-composition-id="main">
|
||||
<div data-composition-id="frame" data-composition-src="design/styleframes/frame-01.html"
|
||||
data-start="0" data-duration="4" data-track-index="0"></div>
|
||||
</div>
|
||||
</body></html>`);
|
||||
const host = document.querySelector("[data-composition-src]")!;
|
||||
const result = inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
rewriteInlineStyles: true,
|
||||
assetExists: (path: string) => PROJECT_FILES.includes(path),
|
||||
});
|
||||
return { document, result };
|
||||
}
|
||||
|
||||
it("rewrites hoisted <link> hrefs against the sub-composition dir", () => {
|
||||
const { result } = inlineFrame();
|
||||
const hrefs = result.externalLinks.map((l) => l.href);
|
||||
expect(hrefs).toContain("design/styleframes/_shared.css");
|
||||
expect(hrefs).toContain("design/shared/theme.css");
|
||||
expect(hrefs).not.toContain("_shared.css");
|
||||
expect(hrefs).not.toContain("../shared/theme.css");
|
||||
});
|
||||
|
||||
it("rewrites hoisted external script srcs", () => {
|
||||
const { result } = inlineFrame();
|
||||
expect(result.externalScriptSrcs).toContain("design/styleframes/helper.js");
|
||||
});
|
||||
|
||||
it("rewrites sibling refs in markup, hoisted CSS, and inline styles", () => {
|
||||
const { document, result } = inlineFrame();
|
||||
expect(document.querySelector("img")?.getAttribute("src")).toBe("design/styleframes/frame.png");
|
||||
expect(result.styles.join("\n")).toContain("design/styleframes/frame.png");
|
||||
expect(document.querySelector("[style]")?.getAttribute("style")).toContain(
|
||||
"design/styleframes/frame.png",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,9 +9,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
rewriteAssetPath,
|
||||
rewriteAssetPaths,
|
||||
rewriteCssAssetUrls,
|
||||
rewriteInlineStyleAssetUrls,
|
||||
type AssetExists,
|
||||
} from "./rewriteSubCompPaths";
|
||||
import { queryByAttr } from "../utils/cssSelector";
|
||||
import {
|
||||
@@ -101,6 +103,15 @@ export interface InlineSubCompositionsOptions {
|
||||
*/
|
||||
scriptErrorLabel?: string;
|
||||
|
||||
/**
|
||||
* Probe for "does this project-root-relative path exist?". Supplied by
|
||||
* callers that can see the filesystem so a sub-composition's SIBLING asset
|
||||
* refs (`<link href="_shared.css">` next to the composition) resolve against
|
||||
* its own directory instead of 404ing at the project root. Omit it and plain
|
||||
* relative paths pass through unchanged. See `AssetExists`.
|
||||
*/
|
||||
assetExists?: AssetExists;
|
||||
|
||||
/**
|
||||
* Log a warning when a composition file cannot be resolved. `reason` is a
|
||||
* short, human-readable explanation (e.g. "the file is empty (0 bytes or
|
||||
@@ -169,6 +180,7 @@ export function inlineSubCompositions(
|
||||
buildScopeSelector = defaultBuildScopeSelector,
|
||||
scriptErrorLabel = "[HyperFrames] composition script error:",
|
||||
onMissingComposition,
|
||||
assetExists,
|
||||
} = options;
|
||||
|
||||
const styles: string[] = [];
|
||||
@@ -264,11 +276,17 @@ export function inlineSubCompositions(
|
||||
}
|
||||
}
|
||||
|
||||
// `<head>` <link>/<script src> are hoisted into the ROOT document, so they
|
||||
// need the same directory rewrite the body's [src]/[href] pass applies —
|
||||
// without it even the documented `../` form escapes the project and 404s.
|
||||
const resolveSubAssetPath = (raw: string | null): string =>
|
||||
rewriteAssetPath(src, (raw || "").trim(), assetExists);
|
||||
|
||||
// Scope one sub-composition <style> body. scopeRootSelectors keeps the
|
||||
// sub-comp's html/body/:root rules from clobbering the host document (they
|
||||
// are remapped to the composition box); see compositionScoping.
|
||||
const scopeSubStyle = (raw: string): string => {
|
||||
const css = rewriteCssAssetUrls(raw, src);
|
||||
const css = rewriteCssAssetUrls(raw, src, assetExists);
|
||||
return scopeCompId
|
||||
? scopeCssToComposition(css, scopeCompId, runtimeScope || undefined, authoredRootId, {
|
||||
compoundAuthoredRoot: compoundAuthoredRoot === true,
|
||||
@@ -286,7 +304,7 @@ export function inlineSubCompositions(
|
||||
styles.push(scopeSubStyle(s.textContent || ""));
|
||||
}
|
||||
for (const s of [...compDoc.head.querySelectorAll("script")]) {
|
||||
const externalSrc = (s.getAttribute("src") || "").trim();
|
||||
const externalSrc = resolveSubAssetPath(s.getAttribute("src"));
|
||||
if (externalSrc) {
|
||||
if (!externalScriptSrcs.includes(externalSrc)) {
|
||||
externalScriptSrcs.push(externalSrc);
|
||||
@@ -297,7 +315,7 @@ export function inlineSubCompositions(
|
||||
for (const link of [
|
||||
...compDoc.head.querySelectorAll('link[rel="stylesheet"], link[rel="preconnect"]'),
|
||||
]) {
|
||||
const href = (link.getAttribute("href") || "").trim();
|
||||
const href = resolveSubAssetPath(link.getAttribute("href"));
|
||||
if (href && !seenLinkHrefs.has(href)) {
|
||||
seenLinkHrefs.add(href);
|
||||
const rel = (link.getAttribute("rel") || "").trim();
|
||||
@@ -317,7 +335,7 @@ export function inlineSubCompositions(
|
||||
|
||||
// Extract scripts from content
|
||||
for (const s of [...contentDoc.querySelectorAll("script")]) {
|
||||
const externalSrc = (s.getAttribute("src") || "").trim();
|
||||
const externalSrc = resolveSubAssetPath(s.getAttribute("src"));
|
||||
if (externalSrc) {
|
||||
if (!externalScriptSrcs.includes(externalSrc)) {
|
||||
externalScriptSrcs.push(externalSrc);
|
||||
@@ -352,6 +370,7 @@ export function inlineSubCompositions(
|
||||
(el: Element, attr: string, val: string) => {
|
||||
el.setAttribute(attr, val);
|
||||
},
|
||||
assetExists,
|
||||
);
|
||||
|
||||
if (rewriteInlineStyles) {
|
||||
@@ -365,6 +384,7 @@ export function inlineSubCompositions(
|
||||
(el: Element, val: string) => {
|
||||
el.setAttribute("style", val);
|
||||
},
|
||||
assetExists,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,5 @@ export {
|
||||
rewriteAssetPaths,
|
||||
rewriteInlineStyleAssetUrls,
|
||||
rewriteCssAssetUrls,
|
||||
type AssetExists,
|
||||
} from "@hyperframes/parsers/asset-paths";
|
||||
|
||||
Reference in New Issue
Block a user