Skip to content

Fix variant transforms and highlight gating - #4

Merged
dav-is merged 2 commits into
mainfrom
fix/variant-transforms-and-highlight
Sep 25, 2026
Merged

dav-is merged 2 commits into
mainfrom
fix/variant-transforms-and-highlight

Conversation

@dav-is

@dav-is dav-is commented Sep 25, 2026

Copy link
Copy Markdown
Member

Summary

Fixes decoding, transforms and highlighting when a block has several variants. Found while building @fairgarden/design's code block on @fairgarden/docs.

  • Variant switches no longer fail with HAST dictionary mismatch in production under the TS → JS transform.
  • Available transforms follow the rendered variant, and a re-parse no longer drops an applied transform.
  • Related decode crashes are fixed, some of them already on main.
  • deferHighlight waits whenever any variant has transforms. Rendering gets its own narrower gate, so code stays highlighted, and current, while deltas compute.

Per-variant dictionaries

Cause. useCode chooses the variant it renders, but the context's fallbacks belonged to the highlighter's initial variant, looked up by file name alone. After a switch, a same-named file was decoded with the wrong dictionary, the transform threw, and the TS source stayed on screen. Only production payloads are compressed, which is why this never showed in dev. highlightAfter: 'init' without a ContentLoading failed the same way.

Fix:

  • The context publishes variantFallbacks (variant → file name → dictionary), built by resolveVariantFallbacks in codeToFallbackProps.ts. useCode passes the rendered variant's dictionaries to its sub-hooks.
  • TransformEngine and SourceEditingEngine prefer a file's own fallback over the map.
  • Pass-through files keep their dictionary. Files a transform leaves untouched (for example styles.css while TS → JS is active) keep their fallback, line counts and language (isPassThroughFile). Before, a block that mounted with the transform already on crashed with "Failed to decode the source HAST payload".
  • fallbackCollapsed renders on the client. With no compressed residual, prepareInitialSource puts the full fallbacks on the client code. Before, <Pre> hit a dictionary mismatch.

Transforms follow the rendered variant

  • The transform list: the context records which variant its list was built for (availableTransformsVariant). useTransformManagement reuses that list only for that variant, and otherwise derives one from the rendered variant's code. This holds whether variant is controlled, uncontrolled, or driven by a controller. Before, a variant without transforms could show the JS toggle.
  • The saved preference: the stored transform preference is hidden only until the highlighter first settles (the same one-way latch useVariantSelection uses), so a re-parse no longer drops an applied transform.

Highlight gating

  • deferHighlight waits for deltas whenever any variant has transforms (hasAnyVariantTransforms, next to getAvailableTransforms). Before, it checked only the highlighter's own variant. So in a block whose first variant has no transforms, switching to a TypeScript variant and toggling JS could commit before the deltas landed, then re-flow a frame later. Blocks with no transforms anywhere still skip the wait, and all-transform blocks are unchanged. holdGate, the variant bootstrap, transform toggles and swapInFlight all use this gate.
  • Rendering waits only for the parse (deferHighlightRender). Before, useCode also used deferHighlight for rendering. So on a re-parse (a new code prop, refresh(), a lazily loaded variant) a block dropped to plain text, and showed the previous code, stale if it had changed.
  • What shows while deltas compute is chosen by getPendingTransformedCode:
    • variants with unchanged content keep their finished highlighted output, transforms included
    • changed variants show their new highlighted parse
    • a transform on a changed variant shows its new text as plain text until its deltas land, and is never replaced by old or untransformed code
  • Line deltas on highlighted sources: applyCodeTransformWithComments now applies a text-level delta to an already-highlighted source. Before, it threw "Transform failed" and showed TS under a .jsx name.

Tests

  • Highlight gating: CodeHighlighterClient.deferHighlight.test.tsx holds the deltas computation by hand. It covers the mixed-variant reproduction and guard cases for commit gating (4), plus rendering during a re-parse (5; the first-load markup is pinned unchanged).
  • Unit tests: resolveVariantFallbacks, getPendingTransformedCode, hasAnyVariantTransforms, isPassThroughFile, applyCodeTransform line deltas, prepareInitialSource without a residual, and compressed sources in TransformEngine, SourceEditingEngine (new file), toControlledCode, useFileNavigation, useTransformManagement and useCode.
  • CodeHighlighter/user.spec.tsx: transforms per rendered variant, and keeping a transform across same-named files. The transform test now clears localStorage, so it doesn't depend on test order.
  • Shared fixture: createCompressedFile in pipeline/hastUtils/hastCompression.testUtils.ts replaces three copies of the same fixture (not exported).
  • Unlinked e2e fixtures:
    • demo-variants-transform/ (variants sharing a file name) reproduced the mismatch on a production build before the fix.
    • demo-variants-mixed-transform/ (one variant without transforms, one TypeScript) checks for re-flow.
    • Precomputed production demos never run the deltas step, so the jsdom tests are the reproduction for the gating changes.

Verification

Check Result
pnpm test --run 4,921 passing (+48)
pnpm typescript / pnpm eslint / prettier clean
pnpm docs:lib / pnpm docs:build ✅
pnpm docs:validate 0 files updated
e2e lib/components/code-highlighter (production) 16 / 16; both fixtures 20 / 20 over 5 repeats
  • Render counts are unchanged for mount, variant switch and JS toggle, and a re-parse takes one render fewer.
  • Known flake: demo-variants is flaky on main too, failing 30–60% of the time. The Tailwind option never appears after clicking the Select, which looks like a click before hydration.

- Keep fallbacks per variant (`variantFallbacks`) so a file decodes with
  its own dictionary when variants share file names; the transform and
  source-editing engines prefer a file's own fallback
- Scope the context's `availableTransforms` to the variant it was
  computed for, and latch the stored transform once highlighting settles
- Gate `deferHighlight` on transforms in any variant, and render with
  `deferHighlightRender` so code stays highlighted while the deltas of a
  re-parse are computed (`getPendingTransformedCode`, line deltas on
  highlighted sources)
- Render files a transform passes through with their original dictionary
  (`isPassThroughFile`)
- Keep full fallbacks inline for `fallbackCollapsed` blocks that have no
  residual blob
- Add e2e fixtures for switching variants with a transform applied

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dav-is dav-is added the bug Something isn't working label Sep 25, 2026
@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
fairgarden-docs Ready Ready Preview Sep 25, 2026 9:52pm UTC

Resolve the useCode/user.spec.ts conflict with the copy feedback tests
by keeping both: the variant blocks first, then 'copy feedback'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dav-is
dav-is marked this pull request as ready for review September 25, 2026 21:59
@dav-is
dav-is merged commit 46e85b9 into main Sep 25, 2026
9 checks passed
@dav-is
dav-is deleted the fix/variant-transforms-and-highlight branch September 25, 2026 21:59

This branch was successfully deployed

1 active deployment
Preview — a794c19d Deployed Sep 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant