Skip to content

fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint#2053

Open
vanceingalls wants to merge 3 commits into
mainfrom
vi/figma-loop-fixes
Open

fix(core,cli,lint): close the figma brand-token loop — runtime CSS variables, --name, snippet lint#2053
vanceingalls wants to merge 3 commits into
mainfrom
vi/figma-loop-fixes

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

What

The brand-loop live test (SDS community duplicate; full log in the figma test-plan doc) ran the whole lifecycle — pull tokens → import frames → render → rebrand in Figma → re-sync — and proved the var() chain end-to-end, surfacing three gaps. All fixed here:

1. Runtime CSS custom properties (the missing half of the recolor loop)

Imported components reference brand tokens as var(--slug, literal), but nothing ever defined those custom properties — the frozen literal fallback always won, so changing a composition variable could never recolor anything. The runtime now defines every declared composition variable as a CSS custom property:

  • at init on the document root (from data-composition-variables on <html> or any composition element — both authoring conventions exist)
  • on scoped sub-composition hosts in the loader (per-instance values beat the document root via the cascade)
  • render --variables '<json>' overrides win over declared defaults

The slug is kept byte-compatible with the figma importer's (slugify parity test), since a drifted slug silently breaks the chain.

Validated live: the brand-loop comp renders purple from the attribute alone — no hand-written :root block — after flipping one variable default. Green-probe comp confirms injection in the real render engine.

2. figma component --name

SDS-style variant frames are all named Platform=Desktop — three imports produced three silent slug-collision overwrites. --name overrides the component name.

3. data-hf-snippet lint exemption

Imported fragments under compositions/ tripped root_missing_composition_id/root_missing_dimensions — they're mountable fragments, not standalone compositions. The importer marks the fragment root and the project linter skips composition-root rules for marked files (asset-reference checks still apply).

4. Skill: non-Enterprise tokens path

Documents the field-tested MCP-assisted flow: get_variable_defs (name→value, not Enterprise-gated) joined with REST boundVariables ids per node/property → binding rows + composition variables, everything downstream shipped machinery.

Shared-helper extractions (injectScopedStyles, flattenedRoot, parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the dedup/complexity audit the runtime changes tripped — no behavior change.

Testing

  • New: cssVariableName↔slugify parity, root injection, override precedence, --name, snippet lint skip
  • 118 figma + 662 runtime/compiler + 331 lint tests green; tsc clean on core/cli/lint
  • Live: brand-loop v4 render purple via runtime injection only; minimal green-probe comp

🤖 Generated with Claude Code

…riables, --name, snippet lint

Brand-loop live test (SDS duplicate, plans/figma/brand-loop-test-plan.md)
proved the recolor chain end-to-end and surfaced three gaps:

- runtime now defines every declared composition variable as a CSS
  custom property (document root at init + scoped sub-comp hosts in the
  loader), so imported var(--slug, literal) fills resolve live — without
  this the frozen literal always won and variable-driven rebranding
  could not propagate. Slug kept byte-compatible with the figma
  importer (parity test). render --variables overrides win.
- figma component --name: variant frames are often all named
  'Platform=Desktop' and slug-collided across imports.
- imported fragments carry data-hf-snippet and the project linter skips
  composition-root rules for them.
- /figma skill documents the field-tested non-Enterprise tokens path
  (MCP get_variable_defs joined with REST boundVariables ids).

Shared-helper extractions (injectScopedStyles, flattenedRoot module,
parseHostVariableValues, rasterizeFallback, shapeCss) satisfy the
dedup/complexity audit the runtime changes tripped.

Validated live: brand-loop renders purple from the attribute alone (no
manual :root); 118 figma + 662 runtime/compiler + 331 lint tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls vanceingalls force-pushed the vi/figma-loop-fixes branch from 6b746e7 to def2765 Compare July 8, 2026 07:47
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Max-effort review found 15 distinct defects (13 CONFIRMED) — the documentElement-global injection design was wrong. Redesigned + pushed:

Injection redesign (fixes the systemic cluster):

  • Composition-root scoping — variables now apply to the DECLARING element, not <html>. Two comps on one page keep their own same-id values; a flattened sub-comp root styles only its own subtree (cross-composition clobber gone).
  • Define-if-absent — declared defaults no longer clobber authored :root/inline custom properties; render-time --variables overrides still always win (explicit intent).
  • Compile-time emission — the bundler now emits scoped <style data-hf-composition-variables> rules (declared defaults + per-host data-variable-values), so compiled render/publish output is branded AND eval-time reads (GSAP .from immediateRender, canvas tinting) see correct values during body parse. Runtime DCL injection stays for preview and won't double-apply.
  • Sub-comp host completeness — per-instance merge now includes inline-template declared defaults and render-time overrides; applied properties are tracked (data-hf-css-vars) and cleared on remount so stale values can't leak.
  • getVariables() unified with the injection's collection (element-declared honored); empty-string values skipped (CSSOM setProperty("") removes); applyCssVariables runtime type-guarded (kills the hidden TS2345).

Other confirmed fixes:

  • Slug: one shared tokenSlug module (importer + runtime import the same function — the 'byte-compatible' comment is now a compiler guarantee), with collision detection warning when distinct ids collapse to one CSS property.
  • --name reaches the DOMnodeToHtml({rootName}) so the emitted root id is the override, not the colliding variant name. Test asserts the id.
  • Lint snippet skip anchored to the ROOT element — a composition merely containing snippet markup is linted normally; tests for both directions in a new snippetFragment.test.ts.
  • Vacuous precedence test rewritten (distinct colors, full cleanup, plus scoping/define-if-absent/clearing coverage — 25 tests in the suite now).

Re-validated live: the brand-loop comp still renders purple under the scoped design; 118 figma + 756 runtime/compiler + 333 lint tests green.

🤖 Generated with Claude Code

Comment thread packages/core/src/tokenSlug.ts Fixed

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: LGTM with nits ✅

Solid PR that correctly closes the brand-token loop with well-factored shared plumbing. The SSOT extractions are the right call.

SSOT audit — clean

  • tokenSlug.tsslugify and cssVariableName unified in one place. No duplication between bundler and runtime. ✓
  • flattenedRoot.tsFLATTENED_INNER_ROOT_STRIP_ATTRS and markFlattenedInnerRoot correctly factored out. ✓
  • parseHostVariableValues — shared from getVariables.ts, used by both runtime init and composition loader. ✓
  • CSS variable injection chain — runtime init → element-scoped properties, bundler → compile-time stylesheet rules, loader → per-instance host properties. Define-if-absent with override-always-wins correctly implemented in both paths. ✓

No SSOT violations found.

Verified

  • Runtime variable injection: injectCompositionCssVariables correctly walks [data-composition-variables] elements, reads declared defaults, and sets them as CSS custom properties on scoped elements. Override precedence (render overrides > host attributes > declared defaults) is correct.
  • Bundler emission: emitRootCompositionVariableStyles emits :root rules for documentElement defaults and [data-composition-id] rules for scoped compositions. Correct.
  • --name flag: Solves the SDS variant overwrite problem (multiple "Platform=Desktop" frames).
  • Snippet lint exemption: isSnippetFragment correctly detects sub-composition fragments under compositions/ and skips root-level composition rules. Regex correctly skips comments and doctypes.
  • Test coverage: Slug parity, scoped injection, override precedence, clear/apply lifecycle, snippet lint exemption all covered.

Nits (non-blocking)

  1. readRenderOverrides wrapper (getVariables.ts:183-185) — Trivial re-export of readOverrides with no added information. Making the private function public would be cleaner than the one-line wrapper.

  2. Double-read of documentElement (getVariables.ts:30-34) — readDeclaredDefaults(document.documentElement) is called first, then the querySelectorAll loop matches <html> again. Functionally idempotent but inconsistent with injectCompositionCssVariables which uses a Set to deduplicate.

  3. Mid-file import (getVariables.ts:68) — import { cssVariableName, detectSlugCollisions } sits after readDeclaredDefaults rather than at the top with other imports. ESM hoists it fine, but breaks the top-of-file convention.

Ship it.

— Miga

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One blocker before this can be stamped: CodeQL is red on the current head.

blocker: packages/core/src/tokenSlug.ts:14 — the required CodeQL check reports a high-severity js/polynomial-redos alert on the slug trim regex (/^-+|-+$/g). Per the CodeQL recommendation for polynomial ReDoS, this should be rewritten to remove the ambiguous repeated-regex path rather than suppressing the alert. A small non-regex trim loop for leading/trailing - would preserve behavior and avoid the security finding.

I also spot-checked the main behavior Miga called out: the CSS-variable injection path is properly centralized through tokenSlug.ts, readDeclaredDefaults/parseHostVariableValues, injectCompositionCssVariables, and the compile-time bundler emission; the --name plumbing reaches nodeToHtml({ rootName }); and the snippet lint exemption is root-anchored. No additional blocking code issue from that pass.

I cannot approve while a required security check is failing.

Verdict: REQUEST CHANGES
Reasoning: The implementation shape looks sound, but the current head has a required CodeQL failure in changed code, so it needs the slug regex fix and green CI before stamp.

  • Magi

…t eval time

Live testing of the compile-time variable emission surfaced four gaps:

- The producer render path never emitted the compile-time stylesheet (only
  the preview bundler did), so eval-time reads — GSAP .from immediateRender,
  top-level getComputedStyle — saw undefined vars in rendered output. The
  producer's inlineSubCompositions now calls the shared
  emitRootCompositionVariableStyles and passes the variable hooks.
- --variables overrides weren't visible at eval time. They now thread from
  the orchestrator / distributed plan through compileStage into the emitted
  rules (window.__hfVariables still covers script reads).
- Per-declarer rules anchored on data-composition-id, which two inlined
  instances of one sub-composition share — instance A's rule restyled
  instance B, and a rule directly on the declarer defeated the host's
  inherited data-variable-values. Rules now anchor on per-instance
  data-hf-var-scope markers and layer nearest-host values over declared
  defaults, mirroring the runtime loader.
- Emission ignored authored CSS; a declared default now yields to a var
  already defined in an authored <style> block (define-if-absent, matching
  the runtime injection).

Also: the figma importer emits background-color (longhand) for solid fills.
GSAP backgroundColor tweens cannot read a var() through the background
shorthand — its pending-substitution longhands serialize empty, so .from
captured nothing and settled on transparent (pre-existing GSAP interaction,
reproduced with no composition variables involved).

Validated live: eval-time default + override, .from + override, two-instance
host branding, authored :root precedence, SDS brand-loop pixel parity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

Live end-to-end validation of the redesign surfaced four render-path defects, all fixed in e2c88ef:

  1. The producer render path never emitted the compile-time variable stylesheet — only the preview bundler did. Eval-time reads (GSAP .from immediateRender, top-level getComputedStyle) saw undefined vars in actual renders while previews looked fine. The producer's inlineSubCompositions now calls the shared emitRootCompositionVariableStyles and passes the variable hooks.
  2. --variables overrides were invisible at eval time — now threaded from the orchestrator / distributed plan through compileStage into the emitted rules.
  3. Per-declarer rules anchored on data-composition-id, which two inlined instances of one sub-composition share: instance A's rule restyled instance B, and a rule directly on the declarer defeated the host's inherited data-variable-values. Rules now anchor on per-instance data-hf-var-scope markers and layer nearest-host values over declared defaults, mirroring the runtime loader.
  4. Emission ignored authored CSS — a declared default now yields to a var already defined in an authored <style> block (define-if-absent, matching the runtime injection).

Also found a pre-existing GSAP interaction (reproduces with zero composition variables): backgroundColor tweens can't read a var() through the background shorthand — its pending-substitution longhands serialize empty, so .from settles on transparent. Worked around at the source: the figma importer now emits background-color (longhand) for solid fills.

Validation matrix (all real CLI renders, pixel-probed): eval-time declared default ✅, eval-time --variables override ✅, .from(backgroundColor) + override (the finding-1 scenario) ✅, two-instance per-host branding ✅, authored :root precedence ✅, SDS brand-loop pixel parity vs pre-change render ✅. Suites: core 1126 / cli 1341 / lint 333 green; producer chunk-boundary byte-equality 7/7.

…leanups

- slugify: replace the anchored alternated trim regex (/^-+|-+$/g) with a
  character-scan trim — CodeQL js/polynomial-redos blocker.
- readRenderOverrides: fold the readOverrides wrapper into the exported
  function (one name, no pass-through).
- getVariables: deduplicate declarers with a Set, matching
  injectCompositionCssVariables.
- Move the tokenSlug import to the top of the file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vanceingalls

Copy link
Copy Markdown
Collaborator Author

All feedback addressed in d9368ec:

@miguel-heygen blockerslugify trim rewritten as a character-scan loop (no regex on the trim path), removing the js/polynomial-redos alert at its source rather than suppressing it. CodeQL should go green on this head.

@miga-heygen nits — all three taken:

  1. readRenderOverrides wrapper folded into the exported function (the private readOverrides is gone).
  2. getVariables now deduplicates declarers with a Set, matching injectCompositionCssVariables.
  3. tokenSlug import moved to the top of the file.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at d9368ec: LGTM, blocker cleared.

Verified delta since my def27652 block:

  • packages/core/src/tokenSlug.ts:10 rewrites the slug trim as a character scan; CodeQL is green now.
  • packages/core/src/runtime/getVariables.ts:22 has the import at the top, uses a Set for declarer dedupe, and exports readRenderOverrides directly.
  • packages/core/src/compiler/htmlBundler.ts:1025 now exports the shared compile-time variable stylesheet emitter and anchors declarer rules on per-instance data-hf-var-scope.
  • Producer render path threads variables through renderOrchestrator.ts:1285compileStage.ts:128htmlCompiler.ts:1647, and distributed planning passes config.variables at plan.ts:796.

I did not run local tests because this host is out of disk, but GitHub CI is green: CodeQL, Windows, and all 8 regression shards pass.

Verdict: APPROVE
Reasoning: The CodeQL blocker that caused my CHANGES_REQUESTED review is fixed, the new producer/render variable plumbing is covered by focused tests and CI, and I found no remaining blocker.

— Magi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants