Skip to content

[docs-infra] Normalize hast className to the array shape - #1797

Merged
Janpot merged 6 commits into
masterfrom
worktree-docs-infra-classname-array
Aug 21, 2026
Merged

[docs-infra] Normalize hast className to the array shape#1797
Janpot merged 6 commits into
masterfrom
worktree-docs-infra-classname-array

Conversation

@Janpot

@Janpot Janpot commented Aug 19, 2026

Copy link
Copy Markdown
Member

The hast spec says class is a list: <div class="alpha bravo"> is ['alpha', 'bravo']. rehype-parse, fallbackToHast, starry-night and the compression dictionary all produce that already. createFrame, addLineGutters, diffHast and transformMarkdownCode wrote a plain string instead. Four separate workarounds had grown up to handle both, and they didn't agree with each other. This writes the list shape everywhere, drops the workarounds in favour of hasClassName, and checks with includes. The HTML we output doesn't change.

This was already causing a bug. In getInitialVisibleSourceLines the frame check handled both shapes but the line check only matched a plain string. Trees from fallbackToHast use lists, so no lines matched and the function returned nothing. Nothing showed when the block was collapsed. Master is green on this because the test fixtures were built with strings too, so they agreed with the bug. Switching those fixtures to lists breaks 5 of the 8 tests against the old implementation.

The build caches under .next/cache/docs-infra store hast and are keyed on a hash of their inputs, so this change is invisible to them, and Netlify restores that directory between builds. A warm cache would carry on handing the old string shape to the new code without erroring. CACHE_SCHEMA_VERSION invalidates the two caches that hold hast.

@types/hast goes to 3.0.5, which spells out className?: Array<string> instead of leaving it to the catch-all index signature. The new shape needs that to typecheck, and it unblocks #1754, whose CI is red on these errors.

`className` was written as a plain string by `createFrame`, `addLineGutters`
and `diffHast`, but every other producer — starry-night, `rehype-parse`,
`fallbackToHast` — emits the array shape, and the compression dictionary
encodes `"className":["frame"]`. Class checks were split across four ad-hoc
normalizations that disagreed with each other, and the bare `=== 'line'`
comparisons silently missed array-shaped trees.

`getInitialVisibleSourceLines` was already broken by this: it guards frames
with the shape-tolerant `isFrameSpan` but tested lines with `=== 'line'`, so
on any tree through `fallbackToHast` it returned an empty visible set.

Write the array shape everywhere, drop the four normalizations in favour of
`hasClassName`, and check membership with `includes`. Serialized HTML is
unchanged; the emitted hast now matches the compression dictionary entries.

`@types/hast` 3.0.5 declares `className?: Array<string>` rather than letting
it fall through the index signature, so it is bumped here to typecheck the
new shape.
@oliviertassinari
oliviertassinari temporarily deployed to worktree-docs-infra-classname-array - mui-tools-public PR #1797 August 19, 2026 14:27 — with Render Destroyed
@oliviertassinari
oliviertassinari temporarily deployed to worktree-docs-infra-classname-array - code-infra-dashboard PR #1797 August 19, 2026 14:27 — with Render Destroyed
@Janpot Janpot added scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). type: bug It doesn't behave as expected. labels Aug 19, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Total Size Change: ▼-1.01KB(-0.03%) - Total Gzip Change: ▼-109B(-0.01%)
Files: 69 total (0 added, 0 removed, 11 changed)

Show details for 69 more bundles

CodeHighlighterClientparsed: 🔺+5B(+0.01%) gzip: 🔺+3B(+0.01%)
createFrameparsed: 🔺+2B(+0.62%) gzip: 🔺+3B(+1.36%)
embedTransformsparsed: ▼-241B(-2.41%) gzip: ▼-33B(-0.91%)
loadIsomorphicCodeVariantparsed: ▼-235B(-0.91%) gzip: ▼-38B(-0.43%)
@mui/internal-docs-infra/CodeProviderparsed: ▼-175B(-0.11%) gzip: ▼-11B(-0.02%)
@mui/internal-docs-infra/abstractCreateTypesparsed: ▼-148B(-0.11%) gzip: ▼-20B(-0.05%)
enhanceCodeEmphasisparsed: ▼-140B(-0.66%) gzip: 0B(0.00%)
TransformEngineparsed: ▼-35B(-0.17%) gzip: 🔺+6B(+0.09%)
isFrameSpanparsed: ▼-28B(-18.92%) gzip: ▼-19B(-13.97%)
addLineGuttersparsed: ▼-12B(-0.88%) gzip: 🔺+1B(+0.15%)
@mui/internal-docs-infra/abstractCreateDemoparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/abstractCreateDemoClientparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/abstractCreateStreamparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/ChunkProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/cliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeControllerContextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeExternalsContextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeHighlighterparsed: 0B(0.00%) gzip: ▼-1B(-0.01%)
@mui/internal-docs-infra/CodeHighlighter/errorsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CodeHighlighter/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CoordinatedLazyparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/CoordinatedLazy/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createDemoDataparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createDemoData/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createSitemapparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/createSitemap/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCodeparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCodeWindowparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCoordinatedparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCopierparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useCrossTabStateparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useDemoparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useDemoControllerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useErrorsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useLocalStorageStateparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/usePreferenceparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useScrollAnchorparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useSearchparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useSearch/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useStreamparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useStream/typesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useTypeparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useTypesparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/useUrlHashStateparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/internal-docs-infra/withDocsInfraparsed: 0B(0.00%) gzip: 0B(0.00%)
BuildEngineparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeHighlighterChunkparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeInitialSourceLoaderparsed: 0B(0.00%) gzip: 0B(0.00%)
CodeSourceLoaderparsed: 0B(0.00%) gzip: 0B(0.00%)
compileCssWithPostcssparsed: 0B(0.00%) gzip: 0B(0.00%)
createParseSourceWorkerClientparsed: 0B(0.00%) gzip: 0B(0.00%)
EditingEngineparsed: 0B(0.00%) gzip: 0B(0.00%)
findExpandingRangesparsed: 0B(0.00%) gzip: 0B(0.00%)
getHastTextContentparsed: 0B(0.00%) gzip: 0B(0.00%)
grammarLoadersparsed: 0B(0.00%) gzip: 0B(0.00%)
grammarsparsed: 0B(0.00%) gzip: 0B(0.00%)
parseSourceparsed: 0B(0.00%) gzip: 0B(0.00%)
rolldown-runtimeparsed: 0B(0.00%) gzip: 0B(0.00%)
source.cssparsed: 0B(0.00%) gzip: 0B(0.00%)
source.jsparsed: 0B(0.00%) gzip: 0B(0.00%)
source.jsonparsed: 0B(0.00%) gzip: 0B(0.00%)
source.mdxparsed: 0B(0.00%) gzip: 0B(0.00%)
source.shellparsed: 0B(0.00%) gzip: 0B(0.00%)
source.tsparsed: 0B(0.00%) gzip: 0B(0.00%)
source.tsxparsed: 0B(0.00%) gzip: 0B(0.00%)
source.yamlparsed: 0B(0.00%) gzip: 0B(0.00%)
text.html.basicparsed: 0B(0.00%) gzip: 0B(0.00%)
text.mdparsed: 0B(0.00%) gzip: 0B(0.00%)
transpileSourceparsed: 0B(0.00%) gzip: 0B(0.00%)

Details of bundle changes

Performance

Total duration: 18.95 ms +2.24 ms(+13.4%) | Renders: 5 (+0) | Paint: 63.41 ms +1.44 ms(+2.3%)

Test Duration Renders
DataGrid mount with paint timing 3.01 ms 🔺+0.67 ms(+28.6%) 1 (+0)

6 tests within noise — details


Check out the code infra dashboard for more information about this PR.

Janpot added 2 commits August 19, 2026 16:45
`transformMarkdownCode` set `hProperties.className` to a plain string in
three of its five write sites. That reaches hast through `remark-rehype`, so
markdown code blocks carried the string shape too — invisible to the compiler
because mdast `Data` is untyped. Write the array shape there as well.

The build caches under `.next/cache/docs-infra` are validated by hashing their
*inputs*, so a pipeline change that alters the output for unchanged input is
invisible to that hash, and `@mui/internal-netlify-cache` restores that
directory between Netlify builds. A warm cache would therefore keep serving
string-shaped `className` to the new `includes()` readers — silently, since
nothing throws. Add CACHE_SCHEMA_VERSION and fold it into the two caches whose
value embeds hast (`types-enhanced`, `types-text`); `pages-index` stores plain
metadata and is left alone.

Also drop an orphaned `loadServerTypesMeta` snapshot (no matching test since
#1269) and correct the hast example in the prop-compression doc, both of which
still showed the string shape.
@Janpot
Janpot marked this pull request as ready for review August 19, 2026 15:12
@Janpot
Janpot requested review from brijeshb42 and a lite review from Copilot August 19, 2026 15:12
@Janpot

Janpot commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

@claude review

@github-actions

This comment was marked as resolved.

This comment was marked as resolved.

Comment thread packages/docs-infra/src/pipeline/loadIsomorphicCodeVariant/diffHast.ts Outdated
`objectHash` guarded its class check with `Array.isArray`, which read as if
the string shape could still occur. It could not: the guard was only there to
narrow `unknown`, because the value was cast to an ad-hoc local type. Cast to
`Element` instead and reuse `hasClassName`, so this reads like the other call
sites.

Also reword a stale mention of the string shape further down the
`deltaContainsCollapse` doc block.
Janpot added 2 commits August 21, 2026 11:57
`deltaContainsCollapse` still had the string branch the rest of the PR
removed, so `embedTransforms` and `diffHast` disagreed about whether the
string shape was possible. It now casts to `Element` and uses `hasClassName`,
matching `objectHash`.

Nothing pinned CACHE_SCHEMA_VERSION into either cache-content hash: the
existing tests only compare two calls to each other, so deleting the version
kept them green. That version is the only thing keeping a Netlify-restored
`.next/cache` from handing old-shape hast to the new readers. Extract
`buildTypesEnhancedCacheContent` so the types-enhanced content is reachable
from a test, and assert the version in both. Both new tests were confirmed to
fail with the version removed.

Two docs examples still taught the string shape, one of them in the same file
whose other example this PR already converted. `getInitialVisibleSourceLines`
kept `as Element` assertions on nodes `type === 'element'` had already
narrowed.
It had one caller in its own file and was extracted only to give a test
something to import. AGENTS.md 5.4 asks for the opposite: extract when callers
benefit, not to reach internals. Inline it again and drop the test that needed
it. `buildTypesTextCacheContent` keeps its test because it was already exported
for `syncTypes`, whose hash must match the reader's.
@Janpot
Janpot merged commit adffaff into master Aug 21, 2026
16 checks passed
@Janpot
Janpot deleted the worktree-docs-infra-classname-array branch August 21, 2026 10:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants