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:
Miguel Ángel
2026-08-03 21:13:35 -07:00
committed by GitHub
parent 91d14744a0
commit 4e7fcf7f2a
13 changed files with 299 additions and 80 deletions
@@ -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",
);
});
});