Introducing mermaid-lint: Stop Shipping Broken Diagrams
Published: / 11 min read
Updated: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
- VS Code extension: Mermaid Lint (Marketplace) · Open VSX
- API reference: docs.mermaidlint.com
- npm org: @mermaid-lint
- Packages:
@mermaid-lint/core·@mermaid-lint/vitest·@mermaid-lint/jest·@mermaid-lint/cli·@mermaid-lint/remark·@mermaid-lint/markdownlint·@mermaid-lint/textlint
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.35.0:
- Finds every diagram. Scans
.md,.mdx,.markdown, and.mmdfiles (plus any extra extensions you configure) with indentation-aware extraction — it catches diagrams in indented fences (inside list items, callouts, and other indented structures), not just top-level fences. Handles both backtick and CommonMark tilde (~~~) fences. - Semantic rules, not just syntax. Beyond parsing, it runs a set of semantic checks for diagrams that parse but still mislead — the legacy
graphkeyword, a flowchart with no direction, duplicate or self-looping edges, empty labels, asequenceDiagramactivatewith no matchingdeactivate, duplicate class methods, and more. By v0.35.0, that coverage extends into newer and experimental Mermaid families too — journey, timeline,C4Context, and the newer beta-prefixed syntax forarchitecture-beta,packet-beta,sankey-beta, andxychart-betadiagrams — so the tool keeps paying off after you outgrow the usual flowchart-only docs. Each rule has a configurable severity (off/warn/error, à la Biome);duplicate-ids— a node re-declared with a conflicting label, which Mermaid silently drops — defaults to a hard error. Silence one inline with%% mermaid-lint-disable <rule>, or pass--no-semanticto skip them all. - Zero-config, configurable when you need it. Works with no setup, but reads a
mermaid-lint.config.js,.mermaidlintrc, or amermaidLintkey inpackage.jsonwhen present — to scan custom file extensions, restrict which fence styles count, tune per-rule severity, or treat every warning as an error. CLI flags always override config. - Git-aware by default. Only checks tracked files, so a diagram you’re still drafting doesn’t fail CI until you commit it. Pass
--include/--excludeglobs to narrow scope, or--no-gitignoreto check everything. - 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. A GitHub Action is available if you’d rather skip the install entirely — it posts inline PR annotations at the exact failing line. - stdin support. Pipe diagrams directly:
cat diagram.md | mermaid-lint -. Works with--format jsontoo. - Auto-fix mechanical errors.
--fixrewrites files in place: normalizes->to-->inside flowcharts, inserts a missing colon-space separator in sequence diagram messages, and closes unclosed fences. Multi-pass up to convergence, always idempotent. Pipe mode (mermaid-lint --fix -) writes fixed content to stdout. - remark / unified plugin.
@mermaid-lint/remarkis aunified-lint-ruleplugin for validating Mermaid blocks inside remark pipelines — an Astro site configured with@astrojs/markdown-remark, docusaurus, MDX, or anything else built on unified. Errors surface as VFile messages with the absolute document line number. - Rust speed, zero native install. The fast path is a real Rust parser (merman) compiled to WebAssembly and shipped inside the npm package — so you get Rust-grade throughput (~0.1 ms/diagram) with no Rust toolchain, no native binary to build, and no platform-specific prebuilds. It runs anywhere Node does.
- Pure Node runtime. No headless browser, no Puppeteer, no CDN dependency — the WASM module and the
mermaid.parse()fallback both run in-process. 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/remark/@mermaid-lint/markdownlint/@mermaid-lint/textlint— plugins that fold Mermaid validation into a lint pipeline you already run- Mermaid Lint for VS Code — live squiggles in the editor, for
.mdblocks and.mmdfiles @mermaid-lint/core—discoverFiles(),extractMermaidBlocks(),validateBlock(),checkSemantics(), andfixText()for building custom pipelines (API docs)
Pick your lint lane
If you already run markdownlint, use the markdownlint adapter and let one content-lint command own both Markdown style and Mermaid blocks:
import mermaid from "@mermaid-lint/markdownlint";
export default { globs: ["README.md", "docs/**/*.md", "src/content/**/*.md"], customRules: mermaid, config: { default: true },};That’s what this blog uses now. A broken ```mermaid fence fails the same pnpm lint:markdown gate as a malformed Markdown table, so content hygiene stays in one place.
If you already have a test runner and want each diagram reported as its own test case, use the Vitest drop-in instead:
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 with the source file and line number baked into the test name. 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.
remark / unified
If you’re already in a unified pipeline — or in Astro with @astrojs/markdown-remark configured as your Markdown processor — you can wire mermaid-lint in as a lint plugin instead of adding a separate CI step:
import { remark } from "remark";import remarkLint from "remark-lint";import remarkLintMermaid from "@mermaid-lint/remark";
const result = await remark().use(remarkLint).use(remarkLintMermaid).process(markdown);// result.messages contains mermaid validation errorsOr in .remarkrc.mjs:
export default { plugins: ["remark-lint", "@mermaid-lint/remark"],};Errors are reported as VFile messages with the absolute line number of the failing diagram in the source file. Pass { strict: true } to elevate semantic warnings (e.g. a flowchart with no direction) to errors.
markdownlint and textlint
remark isn’t the only lint pipeline. If your docs linting already runs through markdownlint or textlint, there’s a plugin for each:
@mermaid-lint/markdownlint— an async custom rule. Add it tomarkdownlint-cli2(or the markdownlint VS Code extension) and broken diagrams surface right alongside your existing Markdown lint failures.@mermaid-lint/textlint— a textlint rule. (textlint awaits async rules, so it runs the real merman +mermaid.parse()validator — something an ESLint rule can’t do, since ESLint rules must be synchronous.)
All of these — CLI, test drop-in, remark, markdownlint, textlint — delegate extraction, validation, and line mapping to a shared Markdown adapter in @mermaid-lint/core. Same diagnostics, same line numbers, wherever you wire it in.
In your editor
The shortest feedback loop is the editor itself. The Mermaid Lint VS Code extension (also on Open VSX for Cursor, VSCodium, and Windsurf) puts red squiggles under invalid Mermaid as you type — in ```mermaid blocks inside .md files and in standalone .mmd files — with Problems-panel entries, hover messages, and Cmd . quick-fixes that apply the same mechanical --fix corrections without leaving the editor.
It’s powered by @mermaid-lint/core, the same engine as the CLI, so what you see while editing is exactly what CI enforces — you catch the broken arrow before you commit it, not after the build goes red.
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 runs a set of semantic rules — legacy keywords, missing directions, duplicate or self-looping edges, dangling sequence activations, and more — but it still can’t tell whether your diagram is true. A flowchart that parses cleanly and trips no rule, yet describes an architecture your team moved away from six months ago, will pass — because no linter knows your codebase changed. That’s a code-review problem, not a linter one.
Project arc
The project has grown in layers: first a CLI and shared parser core, then faster validation, then integrations for the places documentation already gets checked, then a broader semantic layer that now reaches into the newer and more experimental corners of Mermaid while also adding docs-consistency guardrails.
timeline TD title mermaid-lint v0.1 : CLI + core Markdown extraction v0.5 : Rust WASM fast path + JS fallback parity harness v0.6 : GitHub Action v0.10 : markdownlint + remark support v0.11 : VS Code extension v0.17 : configurable semantic rules v0.35 : expanded semantics + docs consistency guards for newer Mermaid diagrams
For a more detailed release-by-release view, see the mermaid-lint release history.
This post is a test case
This post is a Markdown file in an Astro blog. The two Mermaid diagrams above — the flowchart and the timeline — live as fenced code blocks inside that .md file, same as they would in any documentation repo. The moment it was committed, mermaid-lint started running against it on every CI build. If either diagram were malformed, this post would not have shipped.
That’s the whole point: Markdown is where diagrams live in practice, and mermaid-lint treats every fenced ```mermaid block in every tracked .md file as something that must parse. One line of config, the real parser, every diagram in every post covered for free — including this one.
If you keep diagrams in your repo, give it a try.