mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
refactor(core): settle the four remaining preview-vs-render divergences (#3097)
## Why #3094 fixed one way the mount and render paths disagreed, and added the gate that catches disagreement. It deliberately left the rest. Four divergences are still live. Each one means a composition assembles differently depending on whether it is being previewed or rendered — the same class of defect that shipped three catalog components unstyled, just with smaller blast radii. ## How Both paths now derive root discovery, scope identity, asset sources and order, hoisted links, variable carriers and nested-host enumeration from the shared module #3094 introduced. Each keeps its own I/O, which is where they genuinely differ. The compiler's local depth cap and root lookup and the runtime's three pre-filtered head parameters are gone; the runtime hands over the head node and lets the module decide what comes out of it. Four behaviour changes, each stated by what actually differs rather than by the edit: **Inline `<head>` scripts.** The compiler looped head scripts with a `src` branch and no `else`, so an inline one was silently discarded on render while the runtime ran it. That is losing code, not holding a convention — the runtime's answer wins. Head and content scripts now share one loop, head first, order preserved. A non-templated sub-composition with an inline head script went from **0 collected scripts to 1**, wrapped, body intact. **`<link>` hoisting.** Conditional on render, unconditional on mount, so a templated sub-composition's webfont link was dropped in video and kept in preview. Hoisting is the superset and matches what the author declared. A templated composition with a stylesheet link went from **no external links to that link**. The parity fixture that previously recorded this shape as a known exclusion now gates it. **Anonymous hosts.** With a host naming no id, the compiler fell back to the first declared composition and scoped to it; the mount left the content unflattened and injected its stylesheet into the host `<head>` **unscoped**, so a composition's CSS leaked into whatever mounted it. The compiler's answer wins. The injected rule went from a bare `.label { … }` to `[data-composition-id="scoped-text"] .label { … }`. **Scope ids.** The compiler splits the CSS scope id from the script composition id; they differ only when a host names an id the content does not declare, and there the scripts follow the declared id so their self-referencing queries resolve. The runtime used one for both. The split wins: a host naming `captions-comp` over content declaring `captions` now emits scripts bound to `captions` while its CSS still scopes to `captions-comp`. ## Test plan - [x] Unit tests added/updated - [x] Manual testing performed - [ ] Documentation updated (if applicable) Core 1694 passing, producer 574 passing, the parity contract now gates the two divergences it can observe (the other two carry no contract field, so they are gated by unit tests naming the exact before/after). Lint 0, `typecheck:runtime` and the runtime preview guards clean, package cycles unchanged. Characterization-first: both suites were run and recorded green before any decision moved, so a behavioural drift would surface as a red test rather than a silent difference. **One assertion changed, deliberately.** A runtime test asserted that an anonymous host's composition is *not* flattened, and documented that as intentional. That premise is now false. What the test actually cared about — the root and its content present under the host — still holds and is still asserted; the "not flattened" claim flipped, and the test now also asserts the scoping that was missing. ## Not covered The variable-carrier divergence and its `TODO(template-var-carriers)` are untouched by design, as is recursion on the mount path — a sub-composition containing its own `data-composition-src` is still silently dropped in live preview. Both are behaviour changes with their own units, and both are now one-line-ish changes because the shared module already reports what they need. `runtimeScopeCompositionId` no longer falls back to the authored scope id. This is a functional change beyond the four above, surfaced in review: for an anonymous host with authored variable defaults, the runtime previously stashed them under the declared id, and now does not. It removes a runtime-vs-compiler divergence in the correct direction — the runtime was doing work the compiler never did, and the compiler is authoritative for a shipped composition — but a caller relying on runtime-only variable exposure loses it. The three copies each of the flattened-root helper and the id assignment are left alone: they look mergeable and are not cheaply, and they touch the instancing contract the pixel harness guards. ## Worth knowing The parity test's compiler arms import core's **built dist** while the mount arm imports source, so core must be rebuilt before that lane means anything after a compiler change. Skipping it produces a phantom divergence that looks exactly like a real one.
This commit is contained in:
@@ -151,6 +151,15 @@ describe("preview/render semantic compilation parity", () => {
|
||||
|
||||
const FONT_FACE = `@font-face { font-family: ParityBody; src: url(data:font/woff2;base64,d09GMgAB) format("woff2"); }`;
|
||||
|
||||
const anonymousCardHost = (body: string) => ({
|
||||
"index.html":
|
||||
shell(`<main data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="6">
|
||||
<section id="card-host" data-composition-src="compositions/card.html"
|
||||
data-start="1" data-duration="3"></section>
|
||||
</main>`),
|
||||
"compositions/card.html": body,
|
||||
});
|
||||
|
||||
const cardHost = (body: string) => ({
|
||||
"index.html":
|
||||
shell(`<main data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="6">
|
||||
@@ -186,6 +195,37 @@ const MOUNT_PARITY_FIXTURES: { name: string; files: Record<string, string> }[] =
|
||||
</article>
|
||||
</template>`),
|
||||
},
|
||||
{
|
||||
name: "a TEMPLATED composition hoisting a head stylesheet link",
|
||||
// The compiler used to hoist a <link> only for a non-templated
|
||||
// composition, so a templated one's webfont survived preview (the mount
|
||||
// path always hoisted) and vanished from the render.
|
||||
files: cardHost(`<!doctype html><html><head>
|
||||
<link rel="preconnect" href="https://fonts.example.com" />
|
||||
</head><body>
|
||||
<template id="card-template">
|
||||
<style>${FONT_FACE}
|
||||
.parity-card { --parity-contract: 6; font-family: ParityBody, sans-serif; }</style>
|
||||
<article id="card-root" data-composition-id="card" data-width="800" data-height="600">
|
||||
<h2 class="parity-card">Card</h2>
|
||||
</article>
|
||||
</template>
|
||||
</body></html>`),
|
||||
},
|
||||
{
|
||||
name: "an anonymous host scoping to the id its content declares",
|
||||
// A host naming no composition id. The mount path used to drop the
|
||||
// content in whole and unscoped, so this composition's CSS landed on the
|
||||
// host document at large; the compiler has always fallen back to the
|
||||
// first declared root and scoped to it.
|
||||
files: anonymousCardHost(`<template id="card-template">
|
||||
<style>${FONT_FACE}
|
||||
.parity-card { --parity-contract: 7; font-family: ParityBody, sans-serif; }</style>
|
||||
<article id="card-root" data-composition-id="card" data-width="800" data-height="600">
|
||||
<h2 class="parity-card">Card</h2>
|
||||
</article>
|
||||
</template>`),
|
||||
},
|
||||
{
|
||||
name: "a full-document composition hoisting a head stylesheet link",
|
||||
files: cardHost(`<!doctype html><html><head>
|
||||
@@ -223,12 +263,13 @@ const subCompositions = (files: Record<string, string>) =>
|
||||
* bootstrap script are injected by the player and the producer AROUND a mount,
|
||||
* never by `loadExternalCompositions`. Comparing them would compare harnesses.
|
||||
*
|
||||
* Excluded for now, and deliberately NOT worked around: a templated
|
||||
* sub-composition whose document `<head>` carries a `<link>` — the mount path
|
||||
* hoists it unconditionally, the compiler only for a non-templated composition.
|
||||
* That is one of the live divergences U1 catalogued; closing it is a behaviour
|
||||
* decision for a later unit, not something a gate should paper over. The
|
||||
* non-templated shape IS covered above, where both paths agree.
|
||||
* Nothing else is excluded. The templated-head-`<link>` divergence this file
|
||||
* used to carve out is closed and gated by a fixture above; so is the
|
||||
* anonymous-host one. Two divergences remain ungated HERE rather than
|
||||
* unfixed — an inline `<head>` script and the split between the CSS scope id
|
||||
* and the script scope id both live in script bodies, and this contract
|
||||
* carries no script-body field. Their gates are the unit suites in
|
||||
* `packages/core/src/{compiler,runtime}`.
|
||||
*/
|
||||
function assembledContract(contract: ParityContract) {
|
||||
const { runtimeBootstrap: _runtime, variableBootstrap: _variables, ...assembled } = contract;
|
||||
|
||||
Reference in New Issue
Block a user