mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
This reverts commit 3b53bfd2f7.
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
filterVariablesIfAbsent,
|
||||
parseHostVariableValues,
|
||||
readDeclaredDefaults,
|
||||
warnUnknownEnumValues,
|
||||
readRenderOverrides,
|
||||
} from "./getVariables";
|
||||
|
||||
@@ -400,12 +399,6 @@ async function mountCompositionContent(params: {
|
||||
* separate document root so no declared defaults are passed.
|
||||
*/
|
||||
declaredVariableDefaults?: Record<string, unknown>;
|
||||
/**
|
||||
* The element `declaredVariableDefaults` was read from. Carries the full
|
||||
* declaration (option sets, not just defaults) so the out-of-set enum guard
|
||||
* can run on the same merge. Same population rule as the defaults above.
|
||||
*/
|
||||
variableDeclarer?: Element;
|
||||
onDiagnostic?: (payload: {
|
||||
code: string;
|
||||
details: Record<string, string | number | boolean | null | string[]>;
|
||||
@@ -727,7 +720,6 @@ export async function loadExternalCompositions(
|
||||
// dual-carrier contract from #2081) loses its defaults on this lazy
|
||||
// external-load path — see inlineSubCompositions for the fixed path.
|
||||
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
|
||||
variableDeclarer: doc.documentElement,
|
||||
onDiagnostic: params.onDiagnostic,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -760,11 +752,7 @@ export async function loadExternalCompositions(
|
||||
* custom properties from a previous mount are cleared before (re)applying.
|
||||
*/
|
||||
function stashInstanceVariables(
|
||||
params: {
|
||||
host: Element;
|
||||
declaredVariableDefaults?: Record<string, unknown>;
|
||||
variableDeclarer?: Element;
|
||||
},
|
||||
params: { host: Element; declaredVariableDefaults?: Record<string, unknown> },
|
||||
contentNode: Node,
|
||||
runtimeScopeCompositionId: string,
|
||||
): void {
|
||||
@@ -775,14 +763,6 @@ function stashInstanceVariables(
|
||||
...declaredDefaults,
|
||||
...parseHostVariableValues(params.host),
|
||||
};
|
||||
// The sub-comp path never reaches the top-level getVariables(), so the
|
||||
// out-of-set enum guard runs here too, against the same merged values the
|
||||
// instance reads back out of __hfVariablesByComp.
|
||||
warnUnknownEnumValues(
|
||||
params.variableDeclarer ?? (contentNode instanceof Element ? contentNode : null),
|
||||
merged,
|
||||
runtimeScopeCompositionId,
|
||||
);
|
||||
clearAppliedCssVariables(params.host);
|
||||
if (Object.keys(merged).length > 0) {
|
||||
if (!window.__hfVariablesByComp) window.__hfVariablesByComp = {};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { getVariables, readDeclaredDefaults, resetUnknownEnumWarnings } from "./getVariables";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { getVariables, readDeclaredDefaults } from "./getVariables";
|
||||
|
||||
const VARIABLES_ATTR = "data-composition-variables";
|
||||
|
||||
@@ -204,140 +204,6 @@ describe("readDeclaredDefaults", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("out-of-set enum values (observability only — never changes the returned map)", () => {
|
||||
// One enum plus a number and a string, so "only enums are inspected" is
|
||||
// pinned by the same declaration the warning reads.
|
||||
const DECLARED = JSON.stringify([
|
||||
{
|
||||
id: "accent",
|
||||
type: "enum",
|
||||
label: "Accent",
|
||||
default: "green",
|
||||
options: [
|
||||
{ value: "green", label: "Green" },
|
||||
{ value: "blue", label: "Blue" },
|
||||
{ value: "violet", label: "Violet" },
|
||||
],
|
||||
},
|
||||
{ id: "swap_at", type: "number", label: "Swap at", default: 0.5 },
|
||||
{ id: "title", type: "string", label: "Title", default: "Hello" },
|
||||
]);
|
||||
const DEFAULTS = { accent: "green", swap_at: 0.5, title: "Hello" };
|
||||
|
||||
let warnings: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
resetUnknownEnumWarnings();
|
||||
document.body.innerHTML = "";
|
||||
setDeclared(DECLARED);
|
||||
document.documentElement.setAttribute("data-composition-id", "morph-swap");
|
||||
setOverrides(undefined);
|
||||
warnings = [];
|
||||
vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
||||
warnings.push(args.map(String).join(" "));
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
document.documentElement.removeAttribute("data-composition-id");
|
||||
setDeclared(null);
|
||||
setOverrides(undefined);
|
||||
resetUnknownEnumWarnings();
|
||||
});
|
||||
|
||||
it("a declared option warns nothing", () => {
|
||||
setOverrides({ accent: "violet" });
|
||||
expect(getVariables()).toEqual({ ...DEFAULTS, accent: "violet" });
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("an absent value warns nothing — absent is the normal case", () => {
|
||||
expect(getVariables()).toEqual(DEFAULTS);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("an enum declared without a default and never set warns nothing", () => {
|
||||
setDeclared(
|
||||
JSON.stringify([
|
||||
{ id: "accent", type: "enum", label: "Accent", options: [{ value: "green" }] },
|
||||
]),
|
||||
);
|
||||
expect(getVariables()).toEqual({});
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("an out-of-set value warns once, naming composition, variable, value and fallback", () => {
|
||||
setOverrides({ accent: "orange" });
|
||||
expect(getVariables()).toEqual({ ...DEFAULTS, accent: "orange" });
|
||||
expect(warnings).toHaveLength(1);
|
||||
const message = warnings[0] ?? "";
|
||||
expect(message).toContain("runtime_unknown_enum_value");
|
||||
expect(message).toContain("morph-swap");
|
||||
expect(message).toContain('"accent"');
|
||||
expect(message).toContain('got "orange"');
|
||||
expect(message).toContain("green, blue, violet");
|
||||
expect(message).toContain('Rendering "green" instead');
|
||||
});
|
||||
|
||||
it("falls back to the root composition id when the declarer carries none", () => {
|
||||
// The real top-level shape: <html> declares, the root <div> has the id.
|
||||
document.documentElement.removeAttribute("data-composition-id");
|
||||
document.body.innerHTML = '<div data-composition-id="hero-scene"></div>';
|
||||
setOverrides({ accent: "orange" });
|
||||
getVariables();
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain("hero-scene");
|
||||
});
|
||||
|
||||
it("the same bad value twice warns once; a different bad value warns again", () => {
|
||||
setOverrides({ accent: "orange" });
|
||||
getVariables();
|
||||
getVariables();
|
||||
expect(warnings).toHaveLength(1);
|
||||
|
||||
setOverrides({ accent: "puce" });
|
||||
getVariables();
|
||||
expect(warnings).toHaveLength(2);
|
||||
expect(warnings[1]).toContain('got "puce"');
|
||||
});
|
||||
|
||||
it("non-enum variables are never inspected (any number or string is legal)", () => {
|
||||
setOverrides({ swap_at: 9.9, title: "anything at all" });
|
||||
expect(getVariables()).toEqual({ ...DEFAULTS, swap_at: 9.9, title: "anything at all" });
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not warn when the declared default is itself out of set — nothing fell back", () => {
|
||||
setDeclared(
|
||||
JSON.stringify([
|
||||
{
|
||||
id: "accent",
|
||||
type: "enum",
|
||||
label: "Accent",
|
||||
default: "orange",
|
||||
options: [{ value: "green" }, { value: "blue" }],
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(getVariables()).toEqual({ accent: "orange" });
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns byte-identical values whether the value is in set or not", () => {
|
||||
setOverrides({ accent: "violet" });
|
||||
const good = getVariables();
|
||||
setOverrides({ accent: "orange" });
|
||||
const bad = getVariables();
|
||||
expect(warnings).toHaveLength(1);
|
||||
// Same keys, same non-enum values, and the bad value passed through
|
||||
// untouched — the composition's own guard still owns the coercion.
|
||||
expect(Object.keys(bad)).toEqual(Object.keys(good));
|
||||
expect(bad).toEqual({ ...good, accent: "orange" });
|
||||
expect(bad.accent).toBe("orange");
|
||||
});
|
||||
});
|
||||
|
||||
describe("css variable injection (figma brand-token chain)", () => {
|
||||
afterEach(() => {
|
||||
document.documentElement.removeAttribute(VARIABLES_ATTR);
|
||||
|
||||
@@ -42,132 +42,7 @@ export function getVariables<
|
||||
}
|
||||
const overrides = readRenderOverrides();
|
||||
|
||||
const merged = { ...declaredDefaults, ...overrides };
|
||||
for (const el of declarers) warnUnknownEnumValues(el, merged);
|
||||
return merged as Partial<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum variable given a value outside its declared `options` is coerced to
|
||||
* the composition's default by the composition's own guard. That fallback is
|
||||
* deliberate (a bad value must never break a frame) but it was silent, so a
|
||||
* meaningless value could sit in a project indefinitely and look correct only
|
||||
* by coincidence. Warn; do not change what renders.
|
||||
*
|
||||
* Reads the option set straight off `data-composition-variables`, so it covers
|
||||
* every declared enum, not just the `accentColors` guard shape, and needs no
|
||||
* per-composition code. Deduped per composition+variable+value so a remount
|
||||
* (studio re-init, seek) cannot spam the console.
|
||||
*
|
||||
* Silent for a valid value, for an absent one (an absent variable resolves to
|
||||
* its own declared default), and for a value that already equals that default
|
||||
* (nothing fell back).
|
||||
*/
|
||||
const warnedUnknownEnumValues = new Set<string>();
|
||||
|
||||
/**
|
||||
* The declaration array on an element, or empty when there isn't a usable one.
|
||||
*
|
||||
* One owner for `getAttribute` -> `JSON.parse` -> "is it an array", because
|
||||
* every reader of this attribute needs exactly that and each copy was a place
|
||||
* the three answers could drift apart.
|
||||
*/
|
||||
function readDeclarations(el: Element | null | undefined): Record<string, unknown>[] {
|
||||
const raw = el?.getAttribute("data-composition-variables");
|
||||
if (!raw) return [];
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter(
|
||||
(entry): entry is Record<string, unknown> => !!entry && typeof entry === "object",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* What to call the composition in a warning.
|
||||
*
|
||||
* The canonical top-level shape declares the variables on `<html>`, which
|
||||
* carries no id: the composition id sits on the root element below it. Without
|
||||
* the descendant lookup the message reads "composition variable ...", naming
|
||||
* nothing in a project that has more than one.
|
||||
*/
|
||||
function compositionLabel(declarer: Element, compositionId?: string): string {
|
||||
return (
|
||||
compositionId?.trim() ||
|
||||
declarer.getAttribute("data-composition-id")?.trim() ||
|
||||
declarer.querySelector("[data-composition-id]")?.getAttribute("data-composition-id")?.trim() ||
|
||||
"composition"
|
||||
);
|
||||
}
|
||||
|
||||
/** The declared option values of an enum entry, as comparable primitives. */
|
||||
function declaredOptions(entry: Record<string, unknown>): (string | number)[] {
|
||||
if (!Array.isArray(entry.options)) return [];
|
||||
return entry.options
|
||||
.map((option) =>
|
||||
option && typeof option === "object" ? (option as Record<string, unknown>).value : option,
|
||||
)
|
||||
.filter((v): v is string | number => typeof v === "string" || typeof v === "number");
|
||||
}
|
||||
|
||||
/**
|
||||
* Did `resolved` hand this entry a value outside its own option set?
|
||||
*
|
||||
* Silent for a valid value, for an absent one (an absent variable resolves to
|
||||
* its own declared default), and for a value that already equals that default,
|
||||
* because then nothing fell back. A default outside its own option set is a
|
||||
* declaration defect for the linter to catch; warning here would print the
|
||||
* self-contradictory "got X ... rendering X".
|
||||
*/
|
||||
function unknownEnumValue(
|
||||
entry: Record<string, unknown>,
|
||||
resolved: Record<string, unknown>,
|
||||
): { id: string; value: unknown; allowed: (string | number)[] } | null {
|
||||
if (typeof entry.id !== "string") return null;
|
||||
const value = resolved[entry.id];
|
||||
if (value === undefined || value === null) return null;
|
||||
if ("default" in entry && String(value) === String(entry.default)) return null;
|
||||
const allowed = declaredOptions(entry);
|
||||
// Stringified compare: `--variables` / `data-variable-values` can deliver a
|
||||
// declared numeric option as a string, and that is not the defect here.
|
||||
if (allowed.length === 0 || allowed.some((v) => String(v) === String(value))) return null;
|
||||
return { id: entry.id, value, allowed };
|
||||
}
|
||||
|
||||
export function warnUnknownEnumValues(
|
||||
declarer: Element | null | undefined,
|
||||
resolved: Record<string, unknown>,
|
||||
compositionId?: string,
|
||||
): void {
|
||||
if (!declarer) return;
|
||||
const entries = readDeclarations(declarer);
|
||||
if (entries.length === 0) return;
|
||||
const move = compositionLabel(declarer, compositionId);
|
||||
|
||||
for (const entry of entries) {
|
||||
const found = unknownEnumValue(entry, resolved);
|
||||
if (!found) continue;
|
||||
|
||||
// Deduped per composition+variable+value so a remount (studio re-init,
|
||||
// seek) cannot spam the console.
|
||||
const key = `${move}|${found.id}|${String(found.value)}`;
|
||||
if (warnedUnknownEnumValues.has(key)) continue;
|
||||
warnedUnknownEnumValues.add(key);
|
||||
|
||||
const fallback = "default" in entry ? JSON.stringify(entry.default) : "the composition default";
|
||||
console.warn(
|
||||
`[hyperframes] runtime_unknown_enum_value: ${move} variable "${found.id}" got ${JSON.stringify(found.value)}, which is not a declared option (${found.allowed.join(", ")}). Rendering ${fallback} instead.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Test-only: clear the per-page dedupe set. */
|
||||
export function resetUnknownEnumWarnings(): void {
|
||||
warnedUnknownEnumValues.clear();
|
||||
return { ...declaredDefaults, ...overrides } as Partial<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,10 +52,24 @@ export function resetUnknownEnumWarnings(): void {
|
||||
* compositionLoader can compute the same defaults map for sub-comp instances.
|
||||
*/
|
||||
export function readDeclaredDefaults(root: Element | null): Record<string, unknown> {
|
||||
if (!root) return {};
|
||||
const raw = root.getAttribute("data-composition-variables");
|
||||
if (!raw) return {};
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
if (!Array.isArray(parsed)) return {};
|
||||
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const entry of readDeclarations(root)) {
|
||||
if (typeof entry.id !== "string" || !("default" in entry)) continue;
|
||||
out[entry.id] = entry.default;
|
||||
for (const entry of parsed) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
const e = entry as Record<string, unknown>;
|
||||
if (typeof e.id !== "string" || !("default" in e)) continue;
|
||||
out[e.id] = e.default;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user