Merge pull request #2499 from heygen-com/task1-base-variable-defaults

feat(sdk): getVariableValue({ base: true }) reads pre-override declared defaults
This commit is contained in:
Vance Ingalls
2026-07-15 18:10:53 -07:00
committed by GitHub
4 changed files with 85 additions and 13 deletions
+7 -2
View File
@@ -117,7 +117,7 @@ comp.setVariableValue("heroImage", { url: "/assets/hero.jpg", fit: "cover" });
### `getVariableValue`
```typescript
getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined
getVariableValue(id: string, opts?: { base?: boolean }): string | number | boolean | FontValue | ImageValue | undefined
```
Return a declared variable's current `default` value, or `undefined` if the id is undeclared or has no value set.
@@ -126,6 +126,12 @@ Return a declared variable's current `default` value, or `undefined` if the id i
const color = comp.getVariableValue("brandColor"); // "#6C5CE7"
```
Pass `{ base: true }` to read the default **as authored at open** — before the
T3 override-set or any `setVariableValue` folded into the declaration. This is
the value to restore when a host replays an undo that removes a `var.<id>`
override. For a variable declared mid-session, `{ base: true }` returns its
live default (its declaration is its base).
### `listVariables`
```typescript
@@ -753,4 +759,3 @@ comp.dispose();
Template-driven products with host-owned history.
</Card>
</CardGroup>
+36 -10
View File
@@ -88,6 +88,13 @@ class CompositionImpl implements Composition {
/** Accumulated override-set — T3 embedded mode fold contract. */
private overrides: OverrideSet;
/**
* Declared variable defaults as authored at open — captured BEFORE the
* T3 override-set (and any later setVariableValue) folded into the
* declarations. Serves getVariableValue({ base: true }).
*/
private readonly baseVariableDefaults: Record<string, unknown>;
/** Lazily-built element snapshot, invalidated on every mutation. */
private elementsCache: ElementSnapshot[] | null = null;
/** Lazily-built root snapshot (getRootElements), invalidated alongside elementsCache. */
@@ -114,11 +121,16 @@ class CompositionImpl implements Composition {
/** Override-set state at outermost batch entry — restored if the batch throws. */
private batchOverridesSnapshot: OverrideSet = {};
constructor(parsed: ParsedDocument, opts: OpenCompositionOptions) {
constructor(
parsed: ParsedDocument,
opts: OpenCompositionOptions,
baseVariableDefaults: Record<string, unknown> = {},
) {
this.parsed = parsed;
this.persist = opts.persist;
this.preview = opts.preview;
this.overrides = { ...(opts.overrides ?? {}) };
this.baseVariableDefaults = baseVariableDefaults;
this.previewSelectionUnsubscribe =
this.preview?.on("selection", (ids) => this.updateSelection(ids)) ?? null;
}
@@ -169,14 +181,20 @@ class CompositionImpl implements Composition {
// ── #2098 CRUD conveniences — thin aliases over the canonical surface below.
// They delegate so the per-file declaration-element scope (template/fragment
// sub-comps included) is resolved in exactly one place.
getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined {
return this.getVariableValues()[id] as
| string
| number
| boolean
| FontValue
| ImageValue
| undefined;
getVariableValue(
id: string,
opts?: { base?: boolean },
): string | number | boolean | FontValue | ImageValue | undefined {
// { base: true } reads the declaration default as authored at open —
// BEFORE the override-set / any setVariableValue folded into it. This
// is the value an undo-to-base must restore; the live default IS the
// override after a fold. Ids unseen at open (declared mid-session) fall
// through to the live default — their declaration is their base.
const source =
opts?.base && id in this.baseVariableDefaults
? this.baseVariableDefaults
: this.getVariableValues();
return source[id] as string | number | boolean | FontValue | ImageValue | undefined;
}
listVariables(): CompositionVariable[] {
@@ -877,11 +895,19 @@ export async function openComposition(
// the query API derives element snapshots from it lazily.
const parsed = parseMutable(html);
// Pre-override declared defaults — applyOverrideSet below folds `var.<id>`
// overrides destructively into the declarations, so this is the last moment
// the authored base values are readable. getVariableValue({ base: true })
// serves them for the rest of the session (undo-to-base restores).
const baseVariableDefaults = readDeclaredDefaults(
declarationElement(parsed.document, parsed.wrapped),
);
// T3 embedded: replay the stored override-set onto the base in one pass,
// so the session exposes the user's exact edited state — not the template.
if (opts?.overrides) applyOverrideSet(parsed, opts.overrides);
const session = new CompositionImpl(parsed, opts ?? {});
const session = new CompositionImpl(parsed, opts ?? {}, baseVariableDefaults);
const isEmbedded = opts?.overrides !== undefined;
@@ -138,3 +138,36 @@ describe("validateVariableValues", () => {
expect(comp.validateVariableValues({ title: "still-a-string" })).toEqual([]);
});
});
describe("getVariableValue base defaults", () => {
it("returns the pre-override declared default with { base: true }", async () => {
// openComposition({overrides}) folds var.<id> into the declaration —
// the live default becomes the override. base:true must still see
// the authored value.
const comp = await openComposition(BASE_HTML, {
overrides: { "var.accent": "#FF0000" },
});
expect(comp.getVariableValue("accent")).toBe("#FF0000");
expect(comp.getVariableValue("accent", { base: true })).toBe("#00C3FF");
});
it("keeps the base default stable across live setVariableValue dispatches", async () => {
const comp = await openComposition(BASE_HTML);
comp.setVariableValue("accent", "#123456");
expect(comp.getVariableValue("accent")).toBe("#123456");
expect(comp.getVariableValue("accent", { base: true })).toBe("#00C3FF");
});
it("returns undefined for an undeclared id with { base: true }", async () => {
const comp = await openComposition(BASE_HTML);
expect(comp.getVariableValue("never-declared", { base: true })).toBeUndefined();
});
it("falls back to the live default for a variable declared mid-session", async () => {
// A variable that did not exist at open has no pre-override snapshot —
// its current declaration IS its base.
const comp = await openComposition(BASE_HTML);
comp.declareVariable({ id: "brand-title", type: "string", label: "Title", default: "Hi" });
expect(comp.getVariableValue("brand-title", { base: true })).toBe("Hi");
});
});
+9 -1
View File
@@ -434,8 +434,16 @@ export interface Composition {
/**
* Current `default` value for a declared variable, or undefined if
* undeclared/unset. Convenience over getVariableValues() (kept from #2098).
*
* `{ base: true }` returns the default as authored at open — BEFORE the
* T3 override-set or any setVariableValue folded into the declaration.
* Hosts replaying an undo that removes a `var.<id>` override should restore
* this value. Ids declared mid-session fall through to the live default.
*/
getVariableValue(id: string): string | number | boolean | FontValue | ImageValue | undefined;
getVariableValue(
id: string,
opts?: { base?: boolean },
): string | number | boolean | FontValue | ImageValue | undefined;
/**
* Every declared variable's full schema (id/type/label/default/…), or [] when
* none. Alias of getVariableDeclarations() (kept from #2098's surface).