Skip to content

Fix bugs encountered when integrating with @fairgarden/design - #2

Closed
dav-is wants to merge 7 commits into
mainfrom
fix/design-port-issues
Closed

dav-is wants to merge 7 commits into
mainfrom
fix/design-port-issues

Conversation

@dav-is

@dav-is dav-is commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes bugs found while building @fairgarden/design's code block, demo and search components on @fairgarden/docs 0.13.3, plus related issues found in review. Several were already on main.

  • Per-variant compression dictionaries. Variant switches under the TS → JS transform no longer fail with HAST dictionary mismatch in production. Related decode crashes are fixed, and available transforms now follow the rendered variant in every mode.
  • Copy success state. useCode / useDemo expose whether a copy succeeded, and an empty copy no longer reports success.
  • Import, comment and fence parsing. The loader handles semicolon-free code and imports split across lines, and tracks MDX code fences by CommonMark rules. A shared statement scanner removes several fake-import and dropped-line bugs.

The public API changes are additive. There's one behavior change in useCopier; see below.

1. Per-variant dictionaries and transforms

The main bug. 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 variant switch, a same-named file was decoded with the wrong dictionary. The transform then 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. The unused variant-agnostic fallbacks field is removed from the internal context type.
  • useCode passes the rendered variant's dictionaries to its sub-hooks.
  • TransformEngine and SourceEditingEngine prefer a file's own fallback over the map.

Related fixes, most of them already on main:

  • Available transforms follow the rendered variant, whether variant is controlled, uncontrolled, or driven by a controller. The context records which variant its transform list was built for (availableTransformsVariant), and useTransformManagement reuses that list only for that variant; otherwise it derives one from the rendered variant's own code. Before, a variant without transforms could still show the JS toggle.
  • Pass-through files keep their dictionary. With a transform active, files it leaves untouched (for example styles.css) 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. When there's no compressed residual, prepareInitialSource puts the full fallbacks on the client code. Before, <Pre> hit a dictionary mismatch.

2. Copy success state

  • useCode (and so useDemo) returns copyRecentlySuccessful and copyMarkdownRecentlySuccessful, one per copy action. Each is true for copy.timeout (2000 ms by default) after a successful copy, for driving a toast or other notification.
  • Behavior change in useCopier: with nothing to copy, it no longer sets recentlySuccessful and no longer calls onCopied or onError; onClick still fires. For example, copyMarkdown() on a block without a file name used to report success without writing anything. The use-copier docs are corrected.

3. Parsing (parseImportsAndComments)

One statement scanner. findModuleStatementEnd serves both imports and export … from, replacing two near-duplicate loops. It skips strings, template literals and comments whole, and follows the import grammar:

  • Before the module path: line breaks never end the statement, so import React,\n { useState } from 'react', import\n * as React … and export type\n { A } from … work. Only binding names, type / as, *, commas and one { … } may appear.
  • from counts only as the keyword followed by a quoted path.
  • Anything else before a path is ordinary code. That includes a reserved word, =, (, ; or the end of the file. So an incomplete import can't swallow the rest of the file, and TypeScript's import x = require() is left alone.
  • After the path: ;, end of line or a trailing comment ends the statement. A with { … } / assert { … } clause may continue onto the next line. import assert from '…' followed by assert(x) is not joined.
  • isExportFromStatement accepts only export [type] * [as x] from '…' and export [type] { … } from '…'.

Fixed along the way:

  • Fake imports:
    • From a from inside an exported body, JSX text, strings or comments. For example, <p>Rows loaded from './rows.json'</p> became a relative import that failed the build, and // Adapted from "React docs" became an uninstallable dependency in the StackBlitz / CodeSandbox package.json.
    • From escaped quotes: the escape check compared against a two-character string that never matched, so \' and \` ended strings early.
    • From import.meta, which was read as an import.
  • Two stripped comments on one line. For example /* @highlight */ // eslint-disable-line. The rollback removed the source line instead of the output line, garbling it and shifting emphasis.
  • Last line dropped. A file ending in a stripped comment with no trailing newline lost its last line. This affected MDX fences with filename= or variant=.
  • Comment positions and endings. Comments glued to the closing quote, or between the path and ;, are now stripped. CRLF endings are kept when an inline comment is stripped.
  • Apostrophes in comments. An apostrophe in a comment inside import braces (// don't wrap) no longer swallows later imports.

MDX:

  • Code fences follow CommonMark:
    • a fence opens only at the start of a line, with at least 3 ` or ~
    • a backtick info string can't contain a backtick
    • it closes only with the same marker, at least as long as the opener, on a line of its own; an unclosed fence runs to the end of the file
  • Inline code is not a fence. Anything else with backticks is inline code, including runs of three or more backticks mid-line, which used to flip fence tracking. ~~~ fences are now recognized.
  • import / export count only at the start of a line, as in micromark. Prose such as "each import's url" no longer swallows text.
  • Checked against the real MDX parser (remark-parse + remark-mdx): detected imports match its ESM imports on all 189 MDX pages across fg-docs and @fairgarden/design.
  • The rules are implemented directly because the parser also runs in the browser (via absolutizeImports), where micromark would add bundle weight.

Behavior check. I compared main and this branch over both repos with three option sets:

  • fg-docs (1,366 files): every difference is a fix. Fake imports are removed, and imports main missed are found, including the ./types import in transform-markdown-code/page.mdx. The fence fix removes 2 fake imports from transform-html-code-block/page.mdx.
  • @fairgarden/design demos (829 files): all 617 semicolon-style files are identical to main. The 203 semicolon-free files change as intended (for example, detected relative imports go from 1 to 209).

Test changes to review

  • Snapshots updated: three inline snapshots in enhanceCodeEmphasis.test.ts recorded the dropped-last-line and unstripped-comment bugs. The "wrap complete elements" assertion only passed because an unstripped comment contained <mark>; it now checks <mark class="pl-e fgd-ak">value</mark>.
  • Export-from test updated: "should handle export-from with newline instead of semicolon" had pinned the old output (second export dropped). It now expects both.
  • Inline-code fixture fixed: in "should handle inline code blocks (triple backticks on same line)", the fixture put a closing ``` alone on its own line. CommonMark (and remark-mdx) treat that as opening a fence, so the fixture now uses a real code span across two lines, and one offset is updated.
  • Test removed: the one asserting that /* @highlight */ before an import's ; was left in place, contrary to removeCommentsWithPrefix's docs.
  • Order-independent: the CodeHighlighter/user.spec.tsx transform test clears localStorage and asserts none before toggling.
  • Shared fixture: the duplicated compressedFile() fixture is replaced by createCompressedFile in pipeline/hastUtils/hastCompression.testUtils.ts (not exported).
  • New test files: useCopier/useCopier.test.ts, useCode/SourceEditingEngine.test.ts and codeToFallbackProps.test.ts. Regression tests are added across the parser, TransformEngine, useCode, useTransformManagement, useFileNavigation, useCodeUtils, prepareInitialSource and CodeHighlighter suites, including statements split across lines (with and without semicolons) and CommonMark fences. Each failed before its fix.
  • Unlinked fixture: docs/app/lib/components/code-highlighter/demos/demo-variants-transform/, with demo-plain, demo-init and demo-loading routes plus an e2e test.ts. All three reproduced the mismatch on a production build before the fix.

Known limitations

  • An import / export line that directly continues a prose paragraph (no blank line before it) is still read as ESM, while MDX reads it as paragraph text. Fixing it means tracking paragraphs and would break existing tests whose fixtures do exactly this. No real page does it; all 189 match the MDX parser.
  • Not reproduced: deferHighlight in CodeHighlighterClient is still based on the highlighter's own variant. If that variant has no transforms but the rendered one does, a block might not wait while client-side deltas are computed. Changing it affects when every multi-variant block defers, so it's left for a follow-up.

Verification

Check Result
pnpm test --run 222 files, 4,964 tests passing
pnpm typescript / pnpm eslint / prettier clean
pnpm docs:lib / pnpm docs:build ✅
pnpm docs:validate 0 files updated
e2e, code-highlighter demos ×5 (production) all pass except demo-variants; demo-variants-transform 15 / 15

demo-variants is flaky on main too (12 / 20 failures on a clean main worktree, 6 / 20 here). Same signature: the Tailwind option never appears after clicking the Select, which looks like a click before hydration. coordinated-lazy/lazy-content is also intermittently flaky on main.

Follow-up

Once released, @fairgarden/design will bump its peer range to pick up these fixes, use the copy flags for its toasts, and show its demos' CSS files as tabs.

@dav-is dav-is added the bug Something isn't working label Sep 24, 2026
@vercel

vercel Bot commented Sep 24, 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 5:59pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The rewritten scanner still drops valid multiline imports when a specifier begins on the following line.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes integration issues involving variant-specific compression, copy feedback, and import/comment parsing.

Changes:

  • Adds per-variant fallback dictionaries and pass-through handling.
  • Exposes copy-success state through useCode and useDemo.
  • Reworks import scanning and expands regression coverage.
File Description
packages/​docs/​src/​useCopier/​useCopier.test.ts Tests copy success, empty content, and failures.
packages/​docs/​src/​useCopier/​index.ts Prevents empty copies from reporting success.
packages/​docs/​src/​useCode/​useTransformManagement.ts Uses rendered-variant transforms and fallbacks.
packages/​docs/​src/​useCode/​useTransformManagement.test.ts Tests variant-specific transform discovery.
packages/​docs/​src/​useCode/​useSourceEditing.ts Passes variant fallbacks into editing.
packages/​docs/​src/​useCode/​user.spec.ts Adds integration coverage for compression and copying.
packages/​docs/​src/​useCode/​useFileNavigation.tsx Preserves pass-through file metadata and fallbacks.
packages/​docs/​src/​useCode/​useFileNavigation.test.ts Tests pass-through rendering behavior.
packages/​docs/​src/​useCode/​useCopyFunctionality.ts Exposes independent copy-success flags.
packages/​docs/​src/​useCode/​useCopyFunctionality.test.ts Updates fallback terminology.
packages/​docs/​src/​useCode/​useCodeUtils.ts Adds pass-through file detection.
packages/​docs/​src/​useCode/​useCodeUtils.test.ts Tests pass-through classification.
packages/​docs/​src/​useCode/​useCode.ts Wires variant fallbacks and copy state.
packages/​docs/​src/​useCode/​TransformEngine.ts Prefers source-owned compression dictionaries.
packages/​docs/​src/​useCode/​TransformEngine.test.ts Tests compressed transform decoding.
packages/​docs/​src/​useCode/​SourceEditingEngine.ts Corrects fallback precedence during editing.
packages/​docs/​src/​useCode/​SourceEditingEngine.test.ts Tests compressed source conversion.
packages/​docs/​src/​pipeline/​loaderUtils/​parseImportsAndComments.ts Reworks import and comment scanning.
packages/​docs/​src/​pipeline/​loaderUtils/​parseImportsAndComments.test.ts Adds parser regressions and semicolon-free cases.
packages/​docs/​src/​pipeline/​hastUtils/​hastCompression.testUtils.ts Adds shared compressed-file fixtures.
packages/​docs/​src/​pipeline/​enhanceCodeEmphasis/​enhanceCodeEmphasis.test.ts Corrects emphasis snapshots.
packages/​docs/​src/​CodeHighlighter/​user.spec.tsx Tests transform behavior across variants.
packages/​docs/​src/​CodeHighlighter/​prepareInitialSource.tsx Retains full collapsed fallbacks without blobs.
packages/​docs/​src/​CodeHighlighter/​prepareInitialSource.test.tsx Tests collapsed fallback transport.
packages/​docs/​src/​CodeHighlighter/​codeToFallbackProps.ts Resolves fallbacks per variant.
packages/​docs/​src/​CodeHighlighter/​codeToFallbackProps.test.ts Tests fallback resolution and precedence.
packages/​docs/​src/​CodeHighlighter/​CodeHighlighterContext.tsx Replaces active fallbacks with variant maps.
packages/​docs/​src/​CodeHighlighter/​CodeHighlighterClient.tsx Publishes resolved variant fallback maps.
docs/​app/​lib/​hooks/​use-demo/​types.md Documents copy-success fields.
docs/​app/​lib/​hooks/​use-copier/​page.mdx Documents empty-copy behavior.
docs/​app/​lib/​hooks/​use-code/​types.md Adds copy-success result types.
docs/​app/​lib/​hooks/​use-code/​page.mdx Explains copy feedback flags.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​test.ts Adds production variant-switch E2E coverage.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​output-count/​Counter.tsx Adds output-based demo variant.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​inline-count/​Counter.tsx Adds inline-count demo variant.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-plain/​page.tsx Adds plain demo route.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-plain/​index.ts Configures the plain demo.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-loading/​page.tsx Adds loading-fallback demo route.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-loading/​index.ts Configures the loading demo.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-init/​page.tsx Adds init-highlighting demo route.
docs/​app/​lib/​components/​code-highlighter/​demos/​demo-variants-transform/​demo-init/​index.ts Configures the init demo.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The MDX scanner misses valid mid-line dynamic imports, and repository guidance remains stale.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Overlapping copy operations can shorten feedback timing, and MDX prose can still be misclassified as an import.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Prevent overlapping clipboard timers from clearing current state

packages/​docs/​src/​useCopier/​index.ts:38

The timeout callback clears whichever handle is currently in copyTimeoutRef, not necessarily the timer that fired. If two clipboard writes overlap and resolve out of order, the older timer can fire first, cancel the newer timer, and reset recentlySuccessful before the documented timeout has elapsed from the latest success. Capture each timer handle and only clear the state when that handle is still current (and add a deferred-write regression case).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Repeated-copy feedback and MDX ESM/fence boundary handling still have correctness gaps.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
Resolved since last review (1)

Comment thread packages/docs/src/useCopier/index.ts Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The pending transformed-code equality check can misclassify unchanged variants and reintroduce an avoidable rendering reflow.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (3)

if (!current || !previous || typeof current !== 'object' || typeof previous !== 'object') {
return false;
}
return JSON.stringify(current) === JSON.stringify(previous);
@dav-is

dav-is commented Sep 25, 2026

Copy link
Copy Markdown
Member Author

@dav-is dav-is closed this Sep 25, 2026

This branch was successfully deployed

1 active deployment
Preview — bc69efd4 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.

2 participants