From 906c8d04f85b909252753b24102cdbad1780378d Mon Sep 17 00:00:00 2001
From: Xuanru Li <157947275+xuanruli@users.noreply.github.com>
Date: Mon, 6 Jul 2026 17:20:18 -0700
Subject: [PATCH] fix(cli): localize remote assets before validate so it
matches render (#2001)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
validate served the composition over a loopback origin and let headless
Chrome fetch remote /@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
---
packages/cli/src/commands/validate.test.ts | 19 ++++++++
packages/cli/src/commands/validate.ts | 47 ++++++++++++++++++-
.../cli/src/utils/staticProjectServer.test.ts | 45 +++++++++++++++++-
packages/cli/src/utils/staticProjectServer.ts | 23 +++++----
packages/producer/src/index.ts | 9 ++++
5 files changed, 130 insertions(+), 13 deletions(-)
diff --git a/packages/cli/src/commands/validate.test.ts b/packages/cli/src/commands/validate.test.ts
index 1cc7ce36d..7353806c4 100644
--- a/packages/cli/src/commands/validate.test.ts
+++ b/packages/cli/src/commands/validate.test.ts
@@ -9,6 +9,25 @@ import {
} from "./validate.js";
import type { ProjectLintResult } from "../utils/lintProject.js";
+// validateInBrowser lazy-loads the producer localize helpers via loadProducer;
+// mock it so these unit tests never resolve @hyperframes/producer's built dist.
+vi.mock("../utils/producer.js", () => ({
+ loadProducer: vi.fn(async () => ({
+ localizeRemoteMediaSources: vi.fn(async (html: string) => ({
+ html,
+ remoteMediaAssets: new Map(),
+ })),
+ localizeRemoteImageSources: vi.fn(async (html: string) => ({
+ html,
+ remoteMediaAssets: new Map(),
+ })),
+ localizeRemoteFontFaces: vi.fn(async (html: string) => ({
+ html,
+ remoteMediaAssets: new Map(),
+ })),
+ })),
+}));
+
// Regression for the validate audio-duration-probe timeout: a slow-loading
// media element's duration was snapshotted once, at a fixed point in time,
// and any element still mid-load was permanently misreported as unreadable.
diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts
index 9b20f9b97..07b5334ba 100644
--- a/packages/cli/src/commands/validate.ts
+++ b/packages/cli/src/commands/validate.ts
@@ -1,5 +1,6 @@
import { defineCommand } from "citty";
-import { existsSync, readFileSync } from "node:fs";
+import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveProject, type ProjectDir } from "../utils/project.js";
@@ -334,6 +335,37 @@ export function extractCompositionErrorsFromLint(
.map((f) => ({ level: "error" as const, text: f.message }));
}
+// Match the render pipeline: localize remote /