mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
Addresses the still-outstanding review concerns from merged PRs #1569 / #1570 / #1572 not already hoisted into #1573. WS-B (#1569): - validateVariables requires discriminant fields for object-valued font/image ({name,source} / {url}); a {name:42} font or {foo:42} image previously passed runtime validation and surfaced as a bogus font-family / missing image. - Dropped ImageValue's [key:string]:unknown index signature (let any {url}-shaped object through, swallowed typos); explicit alt?/fit? instead. - Documented the OverrideSet widening for SDK consumers. WS-C (#1570): - getElementTimings caches parsed GSAP labels by exact script text (avoids a full acorn re-parse per read; content-key invalidates on edit). - Documented end-inclusive label window + best-effort extractGsapLabels catch. WS-3.C (#1572): - Added typed Composition.addWithKeyframes / replaceWithKeyframes (was asymmetric with addGsapTween; Studio had to use raw dispatch). - Extracted shared KeyframeSpec type; documented position as seconds/number-only. Gates: build + core 18/18 + sdk 19/19 + oxlint + oxfmt + fallow + typecheck all green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
import type { CompositionVariable } from "../core.types";
|
|
|
|
export type VariableValidationIssue =
|
|
| { kind: "undeclared"; variableId: string }
|
|
| { kind: "type-mismatch"; variableId: string; expected: string; actual: string }
|
|
| { kind: "enum-out-of-range"; variableId: string; allowed: string[]; actual: string };
|
|
|
|
/**
|
|
* Compare a flat values map (from `--variables` / `data-variable-values`) to
|
|
* the declared schema (`data-composition-variables`). Returns issues for keys
|
|
* that aren't declared, plus per-key type mismatches against the declared
|
|
* type. Pure / sync — caller decides how to surface them (warning vs render
|
|
* failure under `--strict-variables`).
|
|
*/
|
|
export function validateVariables(
|
|
values: Record<string, unknown>,
|
|
declarations: readonly CompositionVariable[],
|
|
): VariableValidationIssue[] {
|
|
const decls = new Map<string, CompositionVariable>();
|
|
for (const decl of declarations) decls.set(decl.id, decl);
|
|
|
|
const issues: VariableValidationIssue[] = [];
|
|
for (const [id, value] of Object.entries(values)) {
|
|
const decl = decls.get(id);
|
|
if (!decl) {
|
|
issues.push({ kind: "undeclared", variableId: id });
|
|
continue;
|
|
}
|
|
const mismatch = checkType(value, decl);
|
|
if (mismatch) issues.push(mismatch);
|
|
}
|
|
return issues;
|
|
}
|
|
|
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
// fallow-ignore-next-line complexity
|
|
function checkType(value: unknown, decl: CompositionVariable): VariableValidationIssue | null {
|
|
switch (decl.type) {
|
|
case "string":
|
|
case "color":
|
|
if (typeof value !== "string") {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: decl.type,
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
return null;
|
|
case "number":
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "number",
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
return null;
|
|
case "boolean":
|
|
if (typeof value !== "boolean") {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "boolean",
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
return null;
|
|
case "enum": {
|
|
if (typeof value !== "string") {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "enum (string)",
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
const allowed = decl.options.map((o) => o.value);
|
|
if (!allowed.includes(value)) {
|
|
return { kind: "enum-out-of-range", variableId: decl.id, allowed, actual: value };
|
|
}
|
|
return null;
|
|
}
|
|
case "font": {
|
|
// Font value is an object {name: string, source: string} OR a fallback string.
|
|
if (typeof value === "string") return null;
|
|
if (!isPlainObject(value)) {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "font (object {name, source} or string)",
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
// Object form: require the discriminant fields so a malformed brand-kit
|
|
// value ({name: 42} / {}) is caught here rather than surfacing as a bogus
|
|
// font-family at render time.
|
|
if (typeof value.name !== "string" || typeof value.source !== "string") {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "font object {name: string, source: string}",
|
|
actual: "object missing string name/source",
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
case "image": {
|
|
// Image value is an object {url: string} OR a fallback string.
|
|
if (typeof value === "string") return null;
|
|
if (!isPlainObject(value)) {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "image (object {url} or string)",
|
|
actual: jsTypeOf(value),
|
|
};
|
|
}
|
|
// Object form: require the discriminant field.
|
|
if (typeof value.url !== "string") {
|
|
return {
|
|
kind: "type-mismatch",
|
|
variableId: decl.id,
|
|
expected: "image object {url: string}",
|
|
actual: "object missing string url",
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function jsTypeOf(value: unknown): string {
|
|
if (value === null) return "null";
|
|
if (Array.isArray(value)) return "array";
|
|
return typeof value;
|
|
}
|
|
|
|
export function formatVariableValidationIssue(issue: VariableValidationIssue): string {
|
|
switch (issue.kind) {
|
|
case "undeclared":
|
|
return `Variable "${issue.variableId}" is not declared in data-composition-variables.`;
|
|
case "type-mismatch":
|
|
return `Variable "${issue.variableId}" expected ${issue.expected}, got ${issue.actual}.`;
|
|
case "enum-out-of-range":
|
|
return `Variable "${issue.variableId}" must be one of ${issue.allowed.map((v) => `"${v}"`).join(", ")} (got "${issue.actual}").`;
|
|
}
|
|
}
|