fix(cli): localize remote assets before validate so it matches render (#2001)

validate served the composition over a loopback origin and let headless
Chrome fetch remote <img crossorigin>/@font-face assets cross-origin, while
the render pipeline downloads them to disk first. Buckets whose CORS
allowlist omits the loopback origin then failed the CORS-mode request with a
false net::ERR_FAILED that never occurs in the real render, pushing authors
(and agent pipelines) to delete crossorigin — which disables WebGL
color-grading/shaders for that asset.

Reuse producer's localizeRemote{Media,Image,FontFace}Sources in validate,
downloading into a temp dir served as an extra static-server asset root
(project dir untouched, cleaned up after). validate now matches render.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Xuanru Li
2026-07-06 17:20:18 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 89e36da13d
commit 906c8d04f8
5 changed files with 130 additions and 13 deletions
+13 -10
View File
@@ -71,7 +71,11 @@ export async function serveStaticProjectHtml(
projectDir: string,
html: string,
bindErrorMessage = "Failed to bind local HTTP server",
// Extra dirs to resolve non-index requests against, after projectDir (e.g. a
// temp dir of localized remote assets).
assetRoots: readonly string[] = [],
): Promise<StaticProjectServer> {
const roots = [projectDir, ...assetRoots];
// fallow-ignore-next-line complexity
const server = createServer((req, res) => {
const url = req.url ?? "/";
@@ -81,16 +85,15 @@ export async function serveStaticProjectHtml(
return;
}
const filePath = resolve(projectDir, decodeURIComponent(url).replace(/^\//, ""));
const rel = relative(projectDir, filePath);
if (rel.startsWith("..") || isAbsolute(rel)) {
res.writeHead(403);
res.end();
return;
}
if (existsSync(filePath)) {
serveFileWithRange(filePath, req.headers.range, res);
return;
const requestPath = decodeURIComponent(url).replace(/^\//, "");
for (const root of roots) {
const filePath = resolve(root, requestPath);
const rel = relative(root, filePath);
if (rel.startsWith("..") || isAbsolute(rel)) continue; // traversal guard; try next root
if (existsSync(filePath)) {
serveFileWithRange(filePath, req.headers.range, res);
return;
}
}
res.writeHead(404);
res.end();