feat(sdk): variable declaration read APIs + browser-safe variables entry#2046
Conversation
5a63475 to
6844906
Compare
6844906 to
0e3d59c
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
LGTM. Clean extraction of the composition-variables parser into a browser-safe module, and the three read APIs (getVariableDeclarations, getVariableValues, validateVariableValues) form a coherent surface with clearly documented strict-vs-loose semantics. The DEFAULT_TYPEOF table is a net improvement over the original switch statement. Test coverage is thorough, especially the strict/loose divergence case — that test alone justifies the design decision to use readDeclaredDefaults instead of the strict parser for getVariableValues.
One observation (not blocking): the documentElement cast in getVariableValues is a bit defensive — if linkedom ever changes its Document shape, this would silently return {} instead of erroring. Acceptable given it matches the runtime's own document.documentElement access.
— Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 0e3d59ca. Stack context: SDK foundation (#2046-#2048) — bottom of the 12-PR template-variables stack.
Clean read-surface. The three read APIs delegate cleanly, the browser-safe entrypoint pulls only what it advertises, and session.variables.test.ts drives a real openComposition (not a mock fixture), so I trust the coverage. Grepped variables.ts transitively — no node:* imports leak in. The subpath conditions match the existing entries in packages/core/package.json, so bundlers already using this convention pick it up without config.
🟠 Hold
packages/sdk/src/session.tsL176-185 —getVariableValues()reads defaults only fromdocument.documentElement, butruntime/getVariables()(packages/core/src/runtime/getVariables.tsL32-45) walksdocumentElementplus every[data-composition-variables]element in the body. The PR body claimsgetVariableValuesmirrors render-time "byte-for-byte" — that's true for the common<html data-composition-variables=…>case, but a composition source carrying sub-comp declarers inside<body>will diverge: the SDK returns a strict subset. Two ways to resolve: (a) match the runtime and walkdocument.querySelectorAll("[data-composition-variables]")too, or (b) tighten the docstring on theCompositiontype + PR body to "declarations on the root<html>element only" (which is what Studio inspects anyway). Worth deciding now — if Studio starts calling this to predict render output for a composition-with-nested-declarers, silent divergence is hard to trace.
🟡 Nits / questions
packages/parsers/src/compositionVariables.tsL26-33 — the doc comment onisScalarVariableValuesays "font/image values are object-shaped." That's the object form only;validateVariables(packages/core/src/runtime/validateVariables.tsL88-133) accepts a bare string as a legal font/image value too. If a caller usesisScalarVariableValueto gate CSS writes on an image URL string, they'll write raw URLs to a CSS custom prop (seeinjectCompositionCssVariables— same behavior at runtime, so consistent, just not what a naïve reader of the comment expects). Consider rephrasing to "…font/image object values are not scalar; string fallbacks are."packages/sdk/src/engine/variableModel.ts:readVariableDeclarations— no caching._gsapLabelCacheinsession.tsestablishes the content-keyed-cache precedent for parse-per-call reads. Not urgent (attribute JSON is tiny), and Studio's expected call frequency is probably fine — but if this ends up in a hot memo dep, the JSON.parse + array filter runs on every call.- The strict parser drops a declaration whose
defaulttypeof mismatches itstype, but leaves an enum declaration with adefaultNOT in itsoptionsarray intact (validation catches it later at value-check time). Intentional? Studio's Variables panel may want to defense-in-depth here since a "declaration that will never validate" is a weirder UX than one dropped upfront. - Nice touch: the "loose runtime defaults filter, not strict parser" test at L102-113 pins the exact divergence the PR body flags — future readers see the contract in code, not just prose.
🟢 What's good
- Single canonical predicate (
isCompositionVariable) shared between the parser and the writer path prepped for #2047 — the "readers and writers can't disagree" invariant is enforced by construction, not by test discipline. - The move of
parseCompositionVariablesout ofhtmlParser.tsis genuinely tree-shakable — the fn used to sit next tolinkedomimports; now it's importable without them. Verified by walking the transitive import graph from@hyperframes/core/variables. session.variables.test.tsuses realopenComposition+serialize()round-trip semantics viasetVariableValueat the end — that's the write→read consistency check I look for on read-API PRs (perfeedback_regression_fixture_environment_bypass— schema-object equality alone would prove nothing).- CI: all required checks green, including the SDK unit + contract + smoke lane.
vanceingalls
left a comment
There was a problem hiding this comment.
R1 — hyperframes #2046 at 0e3d59ca
🟢 LGTM concurring with Miga; holding on Rames-D's SDK-vs-runtime divergence finding.
Verified independently:
- Read APIs delegate cleanly.
getVariableDeclarations()returns strictCompositionVariable[],getVariableValues(overrides?)mirrors runtime merge,getVariableUsage()deferred to #2048. Test atsession.variables.test.ts:412-422locks in the intentional loose-vs-strict divergence. - Browser-safe subpath is real.
@hyperframes/core/variablestransitively pulls nonode:*imports;parseCompositionVariablesextracted fromhtmlParser.tsinto a self-containedcompositionVariables.ts. Import-graph clean. - Single canonical predicate.
isCompositionVariableshared between parser and the writer path (readied for #2047) — reader-writer invariant enforced by construction, not by test discipline. - CI 39/39 pass at head.
Concurring with Rames-D on the 🟠 Hold:
getVariableValues()reads defaults only fromdocument.documentElement; runtime walks documentElement PLUS every[data-composition-variables]element in body. Rames named this cleanly. Cross-referencing forward: #2081'sdeclarationElement()+collectAllDeclaredVariableIds()refactor starts to address the multi-element pattern at write time (SDK now targets root-div for wrapped templates, not<html>), but the SDK read path here in #2046 is where the divergence originates and it isn't resolved by the later PRs' changes. Two paths land Rames flagged (walk-body vs. tighten-docstring) — the walk-body option would keep #2046 as the SSOT for "what render will actually see"; the docstring-tighten option accepts SDK-scoped-to-html, but then downstream #2050 Variables inspector's schema display can silently under-represent a real composition. Worth deciding before Studio starts using this as a prediction API.
Concurring with Miga's LGTM.
Unique observation:
O1 — isScalarVariableValue docstring vs validateVariables grammar. Rames also called this. Reinforcing: validateVariables.ts:88-133 accepts a bare string as a legal font/image value; isScalarVariableValue's docstring "font/image values are object-shaped" reads narrower than the truth. Consistent at runtime (both sides treat the string form the same way) but a naïve reader of the predicate could gate CSS writes on an image-URL string and miss why the raw string gets written to a custom prop. Rephrase to "font/image object-form values are not scalar; string fallbacks are."
R1 by Via
9b1d9e8 to
1569719
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
Stack re-review for the old hold on #2046. I am approving this in stack context: the later Studio integration opens SDK sessions per composition file (including selection.sourceFile for sub-composition selections), so getVariableValues() being scoped to the open composition's declaration element is the intended model, not a bundled-document prediction API. CI is green on this head.
One non-blocking cleanup remains and I called it out on #2101: the public interface comment still says the method mirrors runtime getVariables() exactly. Please tighten that wording in the stack so SDK consumers don't read it as a full bundled-document parity guarantee.
— Magi
Verdict: APPROVE
Reasoning: The previous concern is resolved by the stack's per-file SDK session model; #2046 is safe to approve, with the public doc-comment cleanup tracked in the top review.
1569719 to
30b52c4
Compare
|
Follow-up from the #2101 stack review (Miguel): rewrote the |
30b52c4 to
8b286e3
Compare
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R2 at 8b286e3.
Prior R1 🟠 (SDK getVariableValues() reads documentElement only vs runtime's getVariables() walking every [data-composition-variables]) resolved via docstring narrowing at session.ts:186-193 and public types.ts:461-468 JSDoc — per-file scope now explicitly intentional, divergence with runtime named. This is the "tighten-docstring" resolution (vs "walk-body") and it's a fine call: Studio's --variables payload prediction is a per-file question, so keeping the SDK single-file honors the actual caller shape.
No residuals from my side. CI green. Stack-tip is stackable-onwards from here.
vanceingalls
left a comment
There was a problem hiding this comment.
🟢 LGTM concurring with Rames-D — R2 at 8b286e3.
R1 🟠 hold (SDK getVariableValues() reads only documentElement; runtime walks documentElement + [data-composition-variables] in body — divergence) resolved via the tighten-docstring path rather than the walk-body path. Author picked option (b) of Rames-D's original two-path R1 ask; concurring on that call.
Verified at head:
packages/sdk/src/session.ts:164-178—getVariableValues()code unchanged, but the inline comment now explicitly narrows scope: "reads the composition's single declaration element only — NOT a union of every[data-composition-variables]in the document ... the SDK models one composition file, so per-file scope is intended."packages/sdk/src/types.ts:420-430— publicComposition.getVariableValuesJSDoc gained a "Scope:" paragraph naming the runtime divergence on the API surface.packages/core/src/runtime/getVariables.ts:37— runtime still walksdocument.querySelectorAll("[data-composition-variables]"). Divergence real, now a documented contract rather than a silent trap.
Rationale for accepting: the SDK models unbundled single-composition files while the runtime handles bundled multi-comp documents. Per-file scope is the intended contract for SDK consumers; Studio's --variables prediction is a per-file question, so narrower SDK scope is correct. Silent-trap → documented-contract is a real quality improvement even without a code change.
No residuals from my side.
R2 by Via
8b286e3 to
24c1982
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Reconciliation review (post-#2098 rebase): Clean.
The browser-safe @hyperframes/core/variables subpath correctly replaces the root @hyperframes/core imports that #2098 used, and the parser extraction from htmlParser.ts into compositionVariables.ts avoids any duplication with #2098's engine helpers. The #2098 convenience methods (getVariableValue, listVariables) coexist properly as thin wrappers alongside the new canonical read surface. No stale artifacts from #2098 detected.
LGTM.
— Miga
vanceingalls
left a comment
There was a problem hiding this comment.
🟢 LGTM at R3 — 24c1982.
R2 verdict stands. Rebase-onto-#2098 preserves the tighten-docstring resolution and lands the canonical API from this stack while demoting #2098's conveniences to aliases. Concurring with the reconciliation shape.
Reconciliation verified at head:
- Canonical write ops:
declareVariable({declaration})atpackages/sdk/src/session.ts:193-195;updateVariableDeclaration(id, declaration)at:197-199;removeVariableDeclaration(id)at:201-203. All dispatch via the object-wrapped op payload. Header comment at :191-192 explicitly notes #2098's{decl}form was unified onto{declaration}with identical method signature — #2098 callers unaffected. - #2098 conveniences as aliases:
getVariableValue(id)delegates viathis.getVariableValues()[id]at:172-181;listVariables()delegates viathis.getVariableDeclarations()at:182-184. Alias-block comment at :169-171 names the intent — "per-file declaration-element scope resolved in exactly one place." - Patch grammar single-path: grep for
/variable-decls/returns 0 hits acrosssession.ts,engine/mutate.ts,engine/patches.ts,engine/apply-patches.ts— #2098's hyphenated grammar dropped. Canonical/variableDeclarations/grammar preserved atpackages/sdk/src/engine/patches.ts:82,apply-patches.ts:12/:25/:82/:130-131. Undo/replay single-path invariant intact. - R2 hold — read-scope docstring:
getVariableValues()still reads onlydocumentElement; JSDoc "Scope:" paragraph preserved. Documented-contract stands.
Observation (non-blocking, worth James's follow-up eye):
O1 — removeVariable alias delegates one layer deeper than its siblings. getVariableValue and listVariables delegate at the SDK-method-body layer (one-line forwards to the canonical getter). removeVariable(id) at session.ts:186-188 instead dispatches its OWN op (type: "removeVariable"), and the engine handler at packages/sdk/src/engine/mutate.ts:310-313 routes case "removeVariable" into handleRemoveVariableDeclaration. Semantically equivalent — both op types funnel through the same handler — but stylistically inconsistent with the other two aliases. Load-bearing risk: if a future refactor decouples the case "removeVariable" dispatch from handleRemoveVariableDeclaration, the SDK method silently diverges. Consider inlining removeVariable(id) as this.removeVariableDeclaration(id) at the SDK method layer to match its siblings.
Housekeeping: Miguel's approval at pre-rebase SHA 1569719 was dismissed on this PR (unique to #2046 — the other 11 kept APPROVED through the rebase, likely a CODEOWNERS rule on SDK read APIs). @james — you'll need Miguel to re-stamp 24c1982 to unblock mergeStateStatus=BLOCKED.
R3 by Via

What
First PR of the template-variables Studio stack (#2046 → #2052): the SDK read surface for composition variables, plus a new browser-safe
@hyperframes/core/variablesentry point.comp.getVariableDeclarations()— the typed schema fromdata-composition-variables(same strict filter the render pipeline uses)comp.getVariableValues(overrides?)— resolves values exactly like the runtime'sgetVariables()(loose defaults merged under overrides, undeclared keys pass through), so callers can predict what a composition script will read for a given--variablespayloadcomp.validateVariableValues(values)— same checks as--strict-variables(undeclared / type-mismatch / enum-out-of-range)@hyperframes/core/variables: one browser-safe import for the whole variables surface (schema types,parseCompositionVariables,getVariables/readDeclaredDefaults,validateVariables)parseCompositionVariablesmoved out of the linkedom-heavyhtmlParser.tsinto a browser-safe module (re-exported from the root and./compositionentries)Why
The variables backend (runtime merge, CLI/Lambda injection, validation) is complete, but Studio and embedders had no API to inspect a composition's variable contract. This PR is the foundation the rest of the stack (edit ops → studio-server plumbing → Variables panel) builds on, SDK-first so agents and embedded hosts get the same contract as Studio.
How
The subpath exists because the core/parsers root barrels pull Node-only modules and break browser bundles — verified by building the studio against a root import (it fails on
node:pathinassetPaths.ts).getVariableValuesdeliberately uses the runtime's loose defaults extraction, not the strict declaration parser, so its output matches render behavior byte-for-byte even for malformed entries the strict parser drops (covered by a dedicated test).Test plan
session.variables.test.ts(declarations parsing, runtime-parity merge including the loose-vs-strict divergence case, validation issues)🤖 Generated with Claude Code