From 95137a12ea002991713d36db00925f3b8ae15dd5 Mon Sep 17 00:00:00 2001 From: Srikar Sunchu Date: Mon, 21 Sep 2026 02:37:51 -0700 Subject: [PATCH] fix(player): detect the runtime by __player and honour runtime-src for src embeds `` treated the existence of `window.__hf` in the composition frame as "the core runtime is present". `__hf` is a shared namespace, not a bridge: `@hyperframes/shader-transitions` creates it too (`window.__hf = window.__hf || {}`) to publish `shaderTransitionsReady`. An authored composition that used shader transitions and registered its own `__timelines` therefore never had the runtime injected and had its direct-timeline adapter refused, and the embed failed 8 s later with "Composition timeline not found". The only global the player ever drives is `__player`, which the runtime installs synchronously in the same task as `__hf`, so that is now the bridge check on both the probe tick and the adapter path. The probe's injection also always loaded the runtime from jsDelivr: the `runtime-src` attribute was resolved for srcdoc only. The resolver is now shared, and the player passes it to the probe, so an `src` embed can serve its own pinned copy offline or under a `script-src 'self'` CSP. A runtime that fails to load reports its URL on the script's own error event instead of being indistinguishable from a slow one until the timeout. Closes #4002 Closes #4003 Co-Authored-By: Claude Fable 5.1 --- packages/player/src/composition-probe.test.ts | 190 +++++++++++++++++- packages/player/src/composition-probe.ts | 42 +++- .../player/src/hyperframes-player.test.ts | 33 +++ packages/player/src/hyperframes-player.ts | 4 + packages/player/src/shader-options.ts | 14 +- packages/player/src/shouldInjectRuntime.ts | 4 +- 6 files changed, 276 insertions(+), 11 deletions(-) diff --git a/packages/player/src/composition-probe.test.ts b/packages/player/src/composition-probe.test.ts index 2b4a749ac4..1c6cab8e78 100644 --- a/packages/player/src/composition-probe.test.ts +++ b/packages/player/src/composition-probe.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, it } from "vitest"; -import { readCompositionSizeFromDocument, runtimeCdnUrlForVersion } from "./composition-probe.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + CompositionProbe, + readCompositionSizeFromDocument, + runtimeCdnUrlForVersion, +} from "./composition-probe.js"; +import { RUNTIME_CDN_URL } from "./runtime-url.js"; describe("readCompositionSizeFromDocument", () => { it("reads dimensions from the composition root", () => { @@ -36,3 +41,184 @@ describe("runtimeCdnUrlForVersion", () => { expect(() => runtimeCdnUrlForVersion("latest")).toThrow("Invalid HyperFrames runtime version"); }); }); + +// ── Runtime detection and injection ── +// +// `window.__hf` is a namespace, not a bridge. The core runtime creates it, but +// so does `@hyperframes/shader-transitions` (to publish `shaderTransitionsReady`), +// so an authored composition that uses shader transitions and registers its +// own `__timelines` carries `__hf` with no runtime behind it. The probe used +// to read that as "runtime present": it never injected the runtime, refused +// the direct-timeline adapter, and the embed timed out after 8 s. The only +// global the player ever drives is `__player`, so that is the bridge check. +describe("CompositionProbe runtime detection", () => { + type FakeTimeline = { + duration: () => number; + time: () => number; + seek: () => void; + play: () => void; + pause: () => void; + }; + type FakeWindow = { + __hf?: unknown; + __player?: unknown; + __timelines?: Record; + }; + type FakeScript = { src: string; onerror: (() => void) | null }; + + function fakeTimeline(duration = 10): FakeTimeline { + return { duration: () => duration, time: () => 0, seek() {}, play() {}, pause() {} }; + } + + // A minimal contentDocument so injection does not go through happy-dom's + // real `