feat(mdx): add mermaid diagram support - #9107
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR adds Mermaid diagram rendering support to the site’s MDX pipeline by transforming ```mermaid fenced blocks into a renderable form during MDX compilation and then rendering them client-side at runtime.
Changes:
- Added
rehype-mermaidto the MDX rehype plugin chain (before Shiki) and addedmermaid/rehype-mermaiddependencies. - Introduced a new
MermaidMDX component that lazy-loads Mermaid on the client and re-renders on theme changes. - Updated the
MDXCodeBox(preoverride) to route Mermaid blocks to the new Mermaid renderer instead of the standard code box.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new dependencies pulled in by mermaid and rehype-mermaid. |
| apps/site/package.json | Adds mermaid and rehype-mermaid runtime dependencies. |
| apps/site/mdx/plugins.mjs | Inserts rehype-mermaid before Shiki to avoid Mermaid blocks being highlighted as plain code. |
| apps/site/components/MDX/Mermaid/index.tsx | New client component that loads Mermaid lazily and renders diagrams (with theme support). |
| apps/site/components/MDX/Mermaid/index.module.css | Basic layout constraints for Mermaid-rendered SVG output. |
| apps/site/components/MDX/CodeBox/index.tsx | Detects Mermaid <pre class="mermaid"> blocks and renders the Mermaid component instead of CodeBox. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const renderDiagram = async () => { | ||
| // Mermaid is heavy, so we only load it on the client when needed | ||
| const { default: mermaid } = await import('mermaid'); | ||
|
|
||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| theme: resolvedTheme === 'dark' ? 'dark' : 'default', | ||
| }); | ||
|
|
||
| try { | ||
| const { svg } = await mermaid.render( | ||
| `mermaid-${reactId}`, | ||
| String(children).trim() | ||
| ); | ||
|
|
||
| if (!cancelled && containerRef.current) { | ||
| containerRef.current.innerHTML = svg; | ||
| } | ||
| } catch { | ||
| // If the diagram source is invalid, fall back to showing the source | ||
| if (!cancelled && containerRef.current) { | ||
| containerRef.current.textContent = String(children); | ||
| } | ||
| } | ||
| }; |
4b6eca2 to
bc37755
Compare
|
Addressed the Copilot review on the Mermaid component (head
|
| import rehypeShikiji from '@node-core/rehype-shiki/plugin'; | ||
| import remarkHeadings from '@vcarl/remark-headings'; | ||
| import rehypeAutolinkHeadings from 'rehype-autolink-headings'; | ||
| import rehypeMermaid from 'rehype-mermaid'; |
There was a problem hiding this comment.
Doesn't this dramatically slow down builds since it requires initializing a whole browser instance?
There was a problem hiding this comment.
No — with strategy: 'pre-mermaid' no browser is ever started at build time. Verified in the dependency source:
createMermaidRenderer()only creates a lazybrowserPromise(browserPromise ||= getBrowser(...)inside the returned render function inmermaid-isomorphic) — nothing launches at plugin setup.rehype-mermaidnever calls that render function forpre-mermaid; it only rewrites the AST (<pre><code class="language-mermaid">→<pre class="mermaid">). The package comments this exact path as "No need to start a browser in this case."- Playwright (peer dep) is only exercised by the
inline-svg/img-*strategies.
So build cost is a single AST walk per document, and rendering happens client-side — with the mermaid library itself lazy-loaded via dynamic import(), so it stays out of the initial bundle too.
Happy to switch to inline-svg if the team prefers zero client-side JS — that's the trade-off (build-time Chromium vs. client rendering).
There was a problem hiding this comment.
Can you do some benchmarks and compare building site previously and after? + adding an example storybook with mermaid content so we can render/test?
There was a problem hiding this comment.
both done. stories are in packages/ui-components/src/MDX/Mermaid/index.stories.tsx (flowchart, sequence, dark, invalid-source). for the benchmark i compiled 25 real blog posts through the production chain, with vs without the plugin, 3 warmed runs — 5.62 vs 5.19 ms/doc, so no measurable build cost (pre-mermaid is ast-only, no browser involved).
There was a problem hiding this comment.
Could you please provide a table with the benchmark results?
| [rehypeAutolinkHeadings, { behavior: 'wrap' }], | ||
| // Transforms ```mermaid code blocks into renderable diagrams; | ||
| // must run before Shiki so they are not highlighted as plain code | ||
| [rehypeMermaid, { strategy: 'pre-mermaid' }], |
There was a problem hiding this comment.
If we're using rehype-mermaid, do we need to add mermaid support on code boxes? Or what exactly this rehype plugin does?
There was a problem hiding this comment.
it's pretty minimal — the plugin just rewrites ```mermaid fenced blocks into
at compile time. MDXCodeBox routes those to the mermaid component instead of a code box, and regular code blocks are untouched. so no, no extra codebox support needed.
There was a problem hiding this comment.
So why we need a mermaid UI component?
| const { resolvedTheme } = useTheme(); | ||
| const reactId = useId().replace(/:/g, '-'); | ||
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
So we're doing mermaid rendering solely on the client-side? Is this what we'd like to have, @avivkeller?
BTW, @ashrees could you extract this effect into a dedicated hook, so we can unit test the Mermaid specific API within an unit testable hook?
There was a problem hiding this comment.
I saw some people made browserless versions of Mermaid, e.g.:
https://github.com/1jehuang/mermaid-rs-renderer
There was a problem hiding this comment.
I can write bindings for it?
There was a problem hiding this comment.
Looks like they already exist at https://www.npmjs.com/package/@mermanjs/web
There was a problem hiding this comment.
yes, client-side only. the effect is extracted into useMermaid now (packages/ui-components/src/hooks/useMermaid.ts) with an injectable loader so tests can stub the wasm renderer — unit tests live in test/useMermaid.test.jsx, 2/2 passing locally. if you'd rather have build-time rendering (inline-svg) instead, happy to explore that.
There was a problem hiding this comment.
done — rendering now goes through @mermanjs/web (wasm), so the 500kb mermaid.js is out of the client bundle entirely.
There was a problem hiding this comment.
So, is the UI component piece still needed for @mermanjs/web? Why we need rehype mermaid and this at the same time?
bc37755 to
99db225
Compare
|
Updated the PR per the review (head @ovflowd's asks:
Renderer switch (@avivkeller): rendering now uses What the rehype plugin does (clarifying question): it only rewrites The component lives in |
Adds rehype-mermaid (pre-mermaid strategy) to the MDX rehype chain before Shiki. Diagrams render client-side through a dedicated useMermaid hook (@node-core/ui-components) backed by the @mermanjs/web WASM renderer — no browser at build time and no heavy mermaid.js bundle. - useMermaid hook with injectable loader + node:test unit tests (2/2 pass) - Mermaid component with light/dark themes and <pre> source fallback - Storybook stories (flowchart, sequence, dark, invalid source) - Benchmarked against 25 real blog docs: no measurable compile-time impact (within run variance) Fixes: nodejs#7540
99db225 to
4459c03
Compare
avivkeller
left a comment
There was a problem hiding this comment.
Can't we make a remark plugin to do the transform?
|
@ashrees Please familiarize yourself with Node.js's AI Policy, and review the code you write. Your comments do not match the actual code, which makes it seem as though you don't understand what you've written. If that's the case, it's okay, but be transparent. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9107 +/- ##
==========================================
+ Coverage 86.05% 86.21% +0.15%
==========================================
Files 86 87 +1
Lines 6046 6100 +54
Branches 358 361 +3
==========================================
+ Hits 5203 5259 +56
+ Misses 839 837 -2
Partials 4 4 ☔ View full report in Codecov by Harness. |
📦 Build Size ComparisonSummary
Changes🔄 Modified Routes (4)
|
|
i've updated the pr body so it matches the current implementation exactly. the mismatch was on me. the pr went through several fast iterations (copilot review, the mermaid.js to wasm switch, then the hook extraction) and i didn't keep the description and comments in sync with the branch. on the policy point: yes, i used ai assistance while drafting, and i've read the ai policy. i take full responsibility for every line. i reviewed and understand the code, verified it locally (unit tests 2/2, lint clean, benchmarks in the thread above), and i'm glad to walk through any part of it in review. |
|
@ashrees several CI steps are failing, please give them an eye and fix it. |
| // Mermaid diagrams arrive as `<pre class="mermaid">` (see rehype-mermaid), | ||
| // so we render them as diagrams instead of code boxes | ||
| if (className?.split(' ').includes('mermaid')) { | ||
| return <Mermaid>{String(code)}</Mermaid>; |
There was a problem hiding this comment.
I don't think our codebox component should have anything to do with Mermaid. If Mermaid renders a pre then it shouldn't. It is ambiguous that we have two transformations and I wonder if this is the right path. As asked on other comments could you describe why
- We need a rehype plugin that transforms the mermaid markdown code snippets into a pre and then another UI component that then grabs that
preelement and renders as mermaid? Wouldn't it be more efficient if only one pass is done? Is that possible? Or at the very least it'd be good to remove the dependency of the Mermaid component within the MDX CodeBox component. Point being, that the CodeBox component should have no logic that goes to Mermaid path. And I believe that can be easily fixed on our remark package within this monorepo, so that you early on catch that this is a Mermaid and easily point to the right component as we do for code boxes 🤔
Adds Mermaid diagram support to the MDX pipeline, as requested in #7540.
How it works
apps/site/mdx/plugins.mjs:rehype-mermaid(strategypre-mermaid) runs in the rehype chain before@node-core/rehype-shiki, so```mermaidfenced blocks become<pre class="mermaid">instead of being syntax-highlighted as plain code. This strategy is AST-only and never starts a browser at build time (verified against the dependency source and benchmarked, see below).apps/site/components/MDX/CodeBox(thepreMDX override) routes<pre class="mermaid">to the Mermaid component; all other code blocks render as before.@node-core/ui-components(packages/ui-components/src/MDX/Mermaid) and renders diagrams client-side through a dedicateduseMermaidhook (packages/ui-components/src/hooks/useMermaid.ts) backed by the@mermanjs/webWASM renderer. The hook lazy-loads the renderer only when a diagram is on the page, and accepts an injectableloaderso unit tests can stub the WASM module.apps/site/components/MDX/Mermaid) maps thenext-themesresolved theme onto the component (light/darkhost theme).<pre>.Testing
packages/ui-components/src/hooks/__test__/useMermaid.test.jsx: 2/2 pass (node:test + testing-library, stubbed loader).packages/ui-components/src/MDX/Mermaid/index.stories.tsx(flowchart, sequence, dark, invalid source).Fixes #7540