Introducing mermaid-lint: Stop Shipping Broken Diagrams
/ 6 min read
Table of Contents
A while back a teammate sent me a screenshot of a flowchart on our docs site rendering as a red Syntax error box. I’d pushed a wrong arrow two weeks earlier and nothing had caught it — tests green, build green. Mermaid diagrams are plain text in fenced code blocks, so there’s nothing to type-check or lint. They only fail when something tries to render them, and by then they’ve already shipped.
That’s the gap mermaid-lint closes.
Links
- GitHub: jasonworden/mermaid-lint
- GitHub Action: jasonworden/mermaid-lint-action
- npm org: @mermaid-lint
- Packages:
@mermaid-lint/core·@mermaid-lint/vitest·@mermaid-lint/jest·@mermaid-lint/cli
Here’s the project arc at a glance:
timeline title mermaid-lint v0.1 : CLI + core extraction v0.2 : Globs, .mdx/.mmd, JSON output v0.3 : Semantic warnings (duplicate node IDs) v0.4 : Rust WASM fast path (3–4× speedup) v0.5 : CI-enforced parity harness v0.6 : GitHub Action
What it does
mermaid-lint validates every Mermaid diagram in your Markdown using a two-tier pipeline — a Rust WASM fast path for speed, with the official mermaid.parse() API as the authoritative fallback. If the diagram can’t be parsed, the check fails with the file path, line number, and the actual error message from the parser.
What you get as of v0.5.0:
- Finds every diagram. Scans
.md,.mdx,.markdown, and.mmdfiles with indentation-aware extraction — it catches diagrams in indented fences (inside list items, callouts, and other indented structures), 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. Pass globs if you want something specific:
mermaid-lint "docs/**/*.md". - 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. - 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 and pre-commit hooks@mermaid-lint/core—discoverFiles(),extractMermaidBlocks(), andvalidateBlock()for building custom pipelines
The drop-in
Here’s the entire diagram-checking test file in this blog’s repo:
import { defineMermaidTests } from '@mermaid-lint/vitest'
defineMermaidTests()Two lines. That discovers every Mermaid block in every git-tracked Markdown file, runs the real parser over each one, and gives each diagram its own named test case — src/content/post/introducing-mermaid-lint.md:84 is valid. When something breaks, the report points straight at the file and line. No registry to maintain: any new diagram in any tracked file is covered automatically.
Not using a test runner? The CLI works the same way:
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-readableGitHub Actions
If you’d rather skip the test runner entirely, there’s now a first-class Action:
- uses: jasonworden/mermaid-lint-action@v1 with: files: 'docs/**/*.md **/*.mmd' strict: trueDrop it into any workflow step and it validates diagrams with the same two-tier pipeline — no install, no config file needed. It also posts inline PR annotations so a broken diagram surfaces as a file-level comment pointing at the exact line, not just a red CI badge you have to dig into.
Rust WASM + JS/TS: fast and accurate
Here’s the engineering tradeoff that makes this interesting.
Pure JavaScript validation — running mermaid.parse() through jsdom on every diagram — is authoritative but slow. The jsdom + mermaid.js stack costs ~400 ms to initialize even for a single diagram. For a repo with hundreds of diagrams, that adds up quickly.
The fast alternative is a Rust-based parser compiled to WASM. @mermanjs/web is a Rust implementation of the Mermaid grammar, and it’s much faster: ~100 ms one-time init, then ~0.1 ms per diagram. That’s a 3–4× speedup on a valid corpus.
But Rust and JavaScript parsers can drift. A Rust parser might reject something mermaid.js accepts (false positive — your CI breaks on valid diagrams) or accept something mermaid.js rejects (false negative — broken diagrams slip through). Either way, you lose trust in the tool.
v0.5.0 solves this with a two-tier pipeline and a CI-enforced parity harness:
The pipeline:
- merman WASM first — validates each diagram in ~0.1 ms. If it says valid, we return immediately.
- mermaid.js as authoritative fallback — only loads when merman signals an error. If mermaid.js accepts a diagram merman rejected, we call it valid. No false positives from parser divergence. When mermaid.js also rejects, it supplies the precise line/col error location.
flowchart LR A["discover files"] --> B["extract blocks"] B --> C["merman WASM"] C -->|valid| PASS[pass] C -->|invalid| D["mermaid.parse()"] D -->|parses| PASS D -->|throws| FAIL["fail: file:line + message"]
The parity harness:
On every PR, a test suite runs a corpus of 24+ valid and 10+ invalid diagrams — spanning all 19 supported diagram types (flowchart, sequence, class, state, ER, pie, Gantt, git graph, and more) — against both parsers. If merman ever accepts a diagram that mermaid.js rejects, CI fails. This isn’t a one-time check; it’s enforced on every change.
The result: Rust WASM speed on the happy path (valid diagrams in CI) with a guarantee that the fast path never silently passes a broken diagram.
Benchmarks on Apple M4 Max:
| Diagrams | v0.3.0 | v0.5.0 | Speedup |
|---|---|---|---|
| 50 | 407 ms | 121 ms | 3.4× |
| 200 | 553 ms | 159 ms | 3.5× |
| 1,000 | 1018 ms | 260 ms | 3.9× |
| 10,000 | 6643 ms | 1699 ms | 3.9× |
| 100,000 | 62734 ms | 15590 ms | 4.0× |
Why this matters for agentic engineering
There’s a less obvious reason to keep diagrams valid that didn’t exist a few years ago: AI coding agents.
In agentic engineering workflows — where AI agents are reading your docs, parsing your architecture diagrams, and generating code alongside you — your Markdown files are live context. A broken diagram doesn’t just fail a human reader; it injects a parse error into a context window. An agent trying to understand a system from a flowchart that renders as Syntax error is flying blind.
mermaid-lint keeps your documentation clean for both audiences. The same one-liner that catches a fat-fingered arrow before it ships also keeps the signal quality high for every agent that reads your repo.
What it won’t do
mermaid-lint validates syntax and catches one semantic issue — nodes re-declared with conflicting labels in flowcharts and graphs — but it doesn’t know if your diagram is still accurate. A flowchart that perfectly parses but describes an architecture your team moved away from six months ago will pass, because no parser knows your codebase changed. That’s a code-review problem, not a linter one.
This post is a test case
There are two Mermaid diagrams in this post. The moment it was committed, mermaid-lint started running against it on every CI build. If that diagram were malformed, this post would not have shipped. The tool eats its own cooking.
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.