From 662a6c45e5f871416d847ef79b59f7e46bdb8d2e Mon Sep 17 00:00:00 2001 From: Div627 Date: Mon, 24 Aug 2026 20:45:06 +0800 Subject: [PATCH] fix(Mermaid): stop leaking the error diagram into document.body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `renderDiagram` calls `mermaid.render(id, children)` without a container element, so mermaid mounts its temporary `div#d{id}` on `document.body`. mermaid defaults to `suppressErrorRendering: false`, and in that mode both failure branches of `render()` draw the "Syntax error in text" bomb diagram into that temporary element and *then* throw: try { await diag.renderer.draw(text, id, version, diag) } catch (e) { if (config.suppressErrorRendering) { removeTempElements() } else { errorRenderer.draw(text, id, version) } throw e } `removeTempElements()` is only reached on the fully successful path, so every failed render leaves a visible bomb SVG behind in `document.body` — outside the component, outside any layout, and accumulating one per failure. Callers cannot clean this up: the node is injected directly into the DOM, so an ErrorBoundary never sees it. `mermaid.parse(..., { suppressErrors: true })` guards syntax errors but not failures inside `renderer.draw`, which is easy to hit while a diagram is being streamed in chunk by chunk. Default `suppressErrorRendering` to true (still overridable through `config`) so both failure branches clean up after themselves. Follow-ups worth considering, left out to keep this focused: - pass `containerRef.current` as `mermaid.render`'s third argument so the temporary element never touches `document.body` at all; - `renderDiagram` re-creates its `throttle(..., 100)` wrapper on every render, so the throttle never actually applies. --- packages/x/components/mermaid/Mermaid.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/x/components/mermaid/Mermaid.tsx b/packages/x/components/mermaid/Mermaid.tsx index 1cc3437bc5..eb475913fb 100644 --- a/packages/x/components/mermaid/Mermaid.tsx +++ b/packages/x/components/mermaid/Mermaid.tsx @@ -98,6 +98,14 @@ const Mermaid: React.FC = React.memo((props) => { securityLevel: 'strict', theme: 'default', fontFamily: 'monospace', + // `mermaid.render(id, text)` below is called without a container element, so mermaid + // mounts its temporary `div#d{id}` on `document.body`. With error rendering enabled + // (mermaid's default) a failed render draws the "Syntax error in text" bomb diagram + // into that element and *then* throws, so mermaid's own `removeTempElements()` never + // runs — leaving the error SVG behind in `document.body`, outside this component and + // outside any layout, once per failure. Suppressing it makes both failure branches + // clean up after themselves instead. + suppressErrorRendering: true, ...(config || {}), }); }, [config]);