fix(cli): page closures survive keepNames transpilation; format motion-blur

Running the CLI from source (tsx dev script, as CI's smoke job does)
transpiles with keepNames, which rewrites named inner functions in
serialized page closures into __name(...) calls — a helper that exists
in the Node bundle but not in the browser realm. The unified seek
closure was the first validate-path page function with named inner
functions, so 'hyperframes validate' threw '__name is not defined' in
CI while the dist build worked. Every session opener now installs a
no-op __name shim via evaluateOnNewDocument before any page script
runs, immunizing all serialized closures regardless of build mode.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-10 14:30:54 -04:00
parent 4b03866c02
commit 2ceb0683a8
5 changed files with 48 additions and 4 deletions
@@ -339,3 +339,21 @@ describe("runFfmpegOnce", () => {
}
});
});
describe("installPageFunctionGuard", () => {
it("defines the keepNames __name shim in the page before any script runs", async () => {
const evaluateOnNewDocument = vi.fn(async () => undefined);
await installPageFunctionGuard({ evaluateOnNewDocument });
expect(evaluateOnNewDocument).toHaveBeenCalledOnce();
const source = evaluateOnNewDocument.mock.calls[0]?.[0] as string;
expect(source).toContain("self.__name");
// The shim must be a no-op passthrough so wrapped functions stay callable.
const shim = new Function(`const self = {}; ${source}; return self.__name;`)() as (
fn: unknown,
) => unknown;
const marker = () => 42;
expect(shim(marker)).toBe(marker);
});
});
@@ -131,6 +131,19 @@ async function waitForCompositionSettle(
return runtimeReady;
}
// tsx/esbuild-style dev transpilers run with keepNames, which rewrites named
// inner functions in serialized page closures into __name(...) calls — a
// helper that exists in the Node bundle but not in the browser realm, so any
// page.evaluate/waitForFunction whose closure defines a named function throws
// "__name is not defined" when the CLI runs from source. Defining a no-op in
// the page before any script runs immunizes every serialized closure, current
// and future, regardless of how the CLI was built.
export async function installPageFunctionGuard(page: {
evaluateOnNewDocument(source: string): Promise<unknown>;
}): Promise<void> {
await page.evaluateOnNewDocument("self.__name = self.__name || ((fn) => fn);");
}
export async function openSettledCompositionPage(
html: string,
url: string,
@@ -154,6 +167,7 @@ export async function openSettledCompositionPage(
});
const page = await chromeBrowser.newPage();
await installPageFunctionGuard(page);
await page.setViewport(viewport);
await options.beforeNavigate?.(page);
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 10000 });
+2
View File
@@ -28,6 +28,7 @@ import {
import { findMotionSpec, readMotionSpec, type MotionSpec } from "../utils/motionSpec.js";
import {
AUDIT_SEEK_OPTIONS,
installPageFunctionGuard,
seekCompositionTimeline,
waitForCompositionFonts,
type SeekCompositionTimelineOptions,
@@ -214,6 +215,7 @@ async function runLayoutAudit(
});
const page = await chromeBrowser.newPage();
await installPageFunctionGuard(page);
await page.setViewport({ width: 1920, height: 1080 });
await page.goto(server.url, { waitUntil: "domcontentloaded", timeout: 10000 });
await alignViewportToComposition(page, server.url);
+2
View File
@@ -15,6 +15,7 @@ import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport
import { c } from "../ui/colors.js";
import { printDeprecationNotice, withMeta } from "../utils/updateCheck.js";
import {
installPageFunctionGuard,
resolveCliChromeGpuMode,
seekCompositionTimeline,
} from "../capture/captureCompositionFrame.js";
@@ -421,6 +422,7 @@ async function validateInBrowser(
});
const page = await chromeBrowser.newPage();
await installPageFunctionGuard(page);
await page.setViewport(viewport);
page.on("console", (msg) => {