You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Fix bugs encountered when integrating with @fairgarden/design - #2
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.
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes bugs found while building
@fairgarden/design's code block, demo and search components on@fairgarden/docs0.13.3, plus related issues found in review. Several were already onmain.HAST dictionary mismatchin production. Related decode crashes are fixed, and available transforms now follow the rendered variant in every mode.useCode/useDemoexpose whether a copy succeeded, and an empty copy no longer reports success.The public API changes are additive. There's one behavior change in
useCopier; see below.1. Per-variant dictionaries and transforms
The main bug.
useCodechooses 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 aContentLoadingfailed the same way.Fix:
variantFallbacks(variant → file name → dictionary), built byresolveVariantFallbacksincodeToFallbackProps.ts. The unused variant-agnosticfallbacksfield is removed from the internal context type.useCodepasses the rendered variant's dictionaries to its sub-hooks.TransformEngineandSourceEditingEngineprefer a file's ownfallbackover the map.Related fixes, most of them already on
main:variantis controlled, uncontrolled, or driven by a controller. The context records which variant its transform list was built for (availableTransformsVariant), anduseTransformManagementreuses 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.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".fallbackCollapsedrenders on the client. When there's no compressed residual,prepareInitialSourceputs the full fallbacks on the client code. Before,<Pre>hit a dictionary mismatch.2. Copy success state
useCode(and souseDemo) returnscopyRecentlySuccessfulandcopyMarkdownRecentlySuccessful, one per copy action. Each istrueforcopy.timeout(2000 ms by default) after a successful copy, for driving a toast or other notification.useCopier: with nothing to copy, it no longer setsrecentlySuccessfuland no longer callsonCopiedoronError;onClickstill fires. For example,copyMarkdown()on a block without a file name used to report success without writing anything. Theuse-copierdocs are corrected.3. Parsing (
parseImportsAndComments)One statement scanner.
findModuleStatementEndserves both imports andexport … from, replacing two near-duplicate loops. It skips strings, template literals and comments whole, and follows the import grammar:import React,\n { useState } from 'react',import\n * as React …andexport type\n { A } from …work. Only binding names,type/as,*, commas and one{ … }may appear.fromcounts only as the keyword followed by a quoted path.=,(,;or the end of the file. So an incomplete import can't swallow the rest of the file, and TypeScript'simport x = require()is left alone.;, end of line or a trailing comment ends the statement. Awith { … }/assert { … }clause may continue onto the next line.import assert from '…'followed byassert(x)is not joined.isExportFromStatementaccepts onlyexport [type] * [as x] from '…'andexport [type] { … } from '…'.Fixed along the way:
frominside 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 / CodeSandboxpackage.json.\'and\`ended strings early.import.meta, which was read as an import./* @highlight */ // eslint-disable-line. The rollback removed the source line instead of the output line, garbling it and shifting emphasis.filename=orvariant=.;, are now stripped. CRLF endings are kept when an inline comment is stripped.// don't wrap) no longer swallows later imports.MDX:
`or~~~~fences are now recognized.import/exportcount only at the start of a line, as in micromark. Prose such as "each import'surl" no longer swallows text.@fairgarden/design.absolutizeImports), where micromark would add bundle weight.Behavior check. I compared
mainand this branch over both repos with three option sets:mainmissed are found, including the./typesimport intransform-markdown-code/page.mdx. The fence fix removes 2 fake imports fromtransform-html-code-block/page.mdx.@fairgarden/designdemos (829 files): all 617 semicolon-style files are identical tomain. The 203 semicolon-free files change as intended (for example, detected relative imports go from 1 to 209).Test changes to review
enhanceCodeEmphasis.test.tsrecorded 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>./* @highlight */before an import's;was left in place, contrary toremoveCommentsWithPrefix's docs.CodeHighlighter/user.spec.tsxtransform test clears localStorage and assertsnonebefore toggling.compressedFile()fixture is replaced bycreateCompressedFileinpipeline/hastUtils/hastCompression.testUtils.ts(not exported).useCopier/useCopier.test.ts,useCode/SourceEditingEngine.test.tsandcodeToFallbackProps.test.ts. Regression tests are added across the parser,TransformEngine,useCode,useTransformManagement,useFileNavigation,useCodeUtils,prepareInitialSourceandCodeHighlightersuites, including statements split across lines (with and without semicolons) and CommonMark fences. Each failed before its fix.docs/app/lib/components/code-highlighter/demos/demo-variants-transform/, withdemo-plain,demo-initanddemo-loadingroutes plus an e2etest.ts. All three reproduced the mismatch on a production build before the fix.Known limitations
import/exportline 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.deferHighlightinCodeHighlighterClientis 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
pnpm test --runpnpm typescript/pnpm eslint/ prettierpnpm docs:lib/pnpm docs:buildpnpm docs:validatedemo-variants;demo-variants-transform15 / 15demo-variantsis flaky onmaintoo (12 / 20 failures on a cleanmainworktree, 6 / 20 here). Same signature: the Tailwind option never appears after clicking the Select, which looks like a click before hydration.coordinated-lazy/lazy-contentis also intermittently flaky onmain.Follow-up
Once released,
@fairgarden/designwill bump its peer range to pick up these fixes, use the copy flags for its toasts, and show its demos' CSS files as tabs.