mermaid-lint: Stop Shipping Broken Diagrams
/ 5 min read
Table of Contents
You tweak a flowchart, fat-finger an edge, and push. The build is green. The diagram renders as a red Syntax error box — live, on the published page — and you don’t find out until someone screenshots it back to you.
Mermaid diagrams break quietly. They’re plain text in your Markdown, so nothing type-checks them. They only fail when something tries to render them, and by then they’ve already shipped. A broken unit test stops the line; a broken diagram sails straight through, because nothing in CI is looking at it.
That’s a shame, because diagrams in the repo are some of the best documentation you have — they render on GitHub, they sit right next to the code they describe, and they’re plain text that humans and coding agents can both read. Which is exactly why a broken one stings: it looks authoritative right up until it doesn’t render.
So I built mermaid-lint to make broken diagrams fail CI like any other test.
What it is
mermaid-lint validates Mermaid diagrams using the official mermaid.parse() API — the same parser your renderer uses, not a reimplemented approximation. If Mermaid can’t parse it, the check fails, with the file, line, and the actual error message.
What you get in the box:
- Finds every diagram. Scans
.md,.mdx,.markdown, and.mmdfiles, and extraction is indentation-aware — it catches diagrams nested inside blockquotes, list items, and indented sections, not just top-level fences. - Git-aware by default. Only checks tracked files (so a diagram you’re still drafting doesn’t fail CI until you commit it);
--allscans the whole filesystem. Pass glob patterns likemermaid-lint "docs/**/*.md" "src/**/*.md"and the tool expands them itself — no shell-globbing surprises. - Built for CI and tooling.
--format jsongives machine-readable output for CI annotations, editor integrations, or custom pipelines. Colored human output respectsNO_COLORand non-TTY. You also get a diagram-type distribution summary — how many flowcharts, sequence diagrams, and so on live in your docs. - Pure Node. No headless browser, no Puppeteer, no CDN dependency. ESM-native, Node ≥ 20.
It ships as a small family of packages so you adopt exactly the layer you need:
@mermaid-lint/vitest/@mermaid-lint/jest— a one-line drop-in that turns every diagram into a test case in your existing suite.@mermaid-lint/cli— amermaid-lintbinary for CI steps, pre-commit hooks, or any non-test setup.@mermaid-lint/core—discoverFiles(),extractMermaidBlocks(), and a typedvalidateBlock()for building your own pipeline.
The drop-in
Here’s the entire diagram-checking test file in this blog’s repo:
import { defineMermaidTests } from '@mermaid-lint/vitest'
defineMermaidTests()That discovers every Mermaid block in every tracked Markdown file, runs the real parser over each one, and gives each diagram its own named test case — src/content/post/06-mermaid-lint.md:47 is valid. When something breaks, the report points straight at it. There’s no registry of diagrams to maintain: any new diagram in any tracked file is covered automatically.
Prefer Jest? Same call, from @mermaid-lint/jest. Not using a test runner at all?
npx @mermaid-lint/cli # check git-tracked filesnpx @mermaid-lint/cli "docs/**/*.md" # or a globnpx @mermaid-lint/cli --all --format json # whole tree, machine-readableflowchart LR A["discover: git-tracked or glob"] --> B["extract (indentation-aware)"] B --> C["mermaid.parse(body)"] C -->|throws| FAIL["fail: file:line + message"] C -->|ok| PASS[pass]
How it compares
There are good tools in this space already. The two closest are suwa-sh/md-mermaid-lint and sammcj/mermaid-check. Here’s an honest read on where each fits.
| mermaid-lint | md-mermaid-lint | mermaid-check | |
|---|---|---|---|
| Validation engine | official mermaid.parse() | official mermaid.parse() | Go reimplementation (own AST) |
| In lockstep with what Mermaid renders | ✅ | ✅ | ❌ — separate parser, can drift |
| Deeper semantic checks (undefined refs, dup IDs) | ❌ | ❌ | ✅ |
| Vitest / Jest adapters | ✅ | ❌ | ❌ |
| Programmatic library API | ✅ (@mermaid-lint/core) | ❌ | ✅ (Go) |
| CLI | ✅ | ✅ | ✅ |
| File discovery | git-tracked by default, or glob | glob you pass | glob / extension |
| Indentation-aware extraction | ✅ | — | — |
| JSON output | ✅ | — | ✅ (grouped) |
| Runtime | Node, pure JS | Node | Go binary |
mermaid-check is the most thorough validator of the three — and I’ll say so plainly. It builds a full AST in Go and does semantic checks mermaid-lint doesn’t: undefined references, duplicate identifiers, type mismatches. It’s also extremely fast. The trade-off is that it’s a from-scratch reimplementation of the Mermaid grammar, so it can diverge from what Mermaid.js actually renders; it needs a Go toolchain; and it has no JavaScript test-runner integration. If you want the deepest possible validation and a Go binary fits your stack, it’s an excellent choice.
md-mermaid-lint uses the same engine mermaid-lint does — the real mermaid.parse() with jsdom. The difference is shape: it’s a CLI you hand globs to, and that’s it.
mermaid-lint’s niche is running the real parser while meeting a JavaScript project where it already lives:
- Each diagram becomes a case in the Vitest or Jest run you already have — same reporter, same watch mode, same green check. A broken diagram fails for the same reason a broken unit test does, and shows up in the same place.
- The core is a library with a typed
validateBlock(), so unusual setups build on the primitives instead of fighting a CLI. - Git-aware discovery means there’s usually no glob config to maintain at all.
If your repo is JS/TS and your diagrams live in Markdown, that combination is the point.
What it won’t do
One limit, stated plainly: mermaid-lint validates syntax — that the diagram parses. It doesn’t validate meaning. A diagram that parses perfectly but describes a flow your code moved away from will pass, because no parser knows your architecture changed; that’s a code-review problem, not a linter one. And it stops at parsing, so it won’t flag the deeper semantic issues (undefined references, duplicate IDs) that mermaid-check will — if you need those, reach for that.
The post is a test case
This post has a Mermaid diagram in it. The moment it was committed, the check this post describes started running against it on every CI build — using mermaid-lint, against this blog’s own repo. If that diagram were malformed, this post would not have shipped.
That’s the whole point: the same discipline you apply to code starts applying to documentation. The real parser, one line of config, every tracked diagram covered for free — including this one.
If you keep diagrams in your repo, give it a try.
Links
- GitHub: jasonworden/mermaid-lint
- npm org: @mermaid-lint
- Packages:
@mermaid-lint/core·@mermaid-lint/vitest·@mermaid-lint/jest·@mermaid-lint/cli