Skip to content

Architecture

The lint pipeline

crackdown is built on the unified ecosystem. Every lint pass follows the same five-step pipeline:

Markdown source
┌─────────────────────────────────────────────────────────┐
│ 1. PARSE (remark-parse + micromark + remark-gfm) │
│ Markdown text → mdast (Markdown Abstract Syntax │
│ Tree). Position information is preserved on every │
│ node, enabling precise line:column reporting. │
└──────────────────────────┬──────────────────────────────┘
│ mdast (Root node)
┌─────────────────────────────────────────────────────────┐
│ 2. TRANSFORM (remark-lint + plugins) │
│ Each lint rule is a unified transformer. It │
│ traverses the mdast via unist-util-visit and calls │
│ file.message() to record violations. Rules run │
│ in the order they were registered. │
└──────────────────────────┬──────────────────────────────┘
│ VFile with messages[]
┌─────────────────────────────────────────────────────────┐
│ 3. COLLECT (lintString / lintFile) │
│ VFile.messages are mapped to LintViolation objects: │
│ { ruleId, message, line, column, severity }. │
│ config.rules severity overrides are applied here. │
└──────────────────────────┬──────────────────────────────┘
│ LintResult
┌─────────────────────────────────────────────────────────┐
│ 4. FIX (optional — lintStringFix / --fix) │
│ config.fixers are applied sequentially as pure │
│ string-to-string transformers. The pipeline re-runs │
│ steps 1–3 on the fixed content to surface any │
│ remaining violations. │
└──────────────────────────┬──────────────────────────────┘
│ FixResult
┌─────────────────────────────────────────────────────────┐
│ 5. REPORT (reporters or programmatic API) │
│ Pretty terminal reporter, JSON reporter, or raw │
│ LintResult / FixResult objects for library use. │
│ LSP server publishes textDocument/publishDiagnostics│
└─────────────────────────────────────────────────────────┘

Package layout

crackdown/
packages/
core/ @crackdown/core
│ ├─ lint.ts lintString, lintFile
│ ├─ fix.ts lintStringFix, lintFileFix
│ ├─ config.ts loadConfig (jiti-based crackdown.config.ts loader)
│ ├─ api.ts lint() — multi-file programmatic API
│ └─ rules/
│ ├─ md009.ts trailing spaces
│ └─ md010.ts hard tabs
cli/ @crackdown/cli
│ ├─ cli.ts crackdown lint / crackdown lsp / crackdown migrate
│ ├─ migrate.ts crackdown migrate (markdownlint → crackdown.config.ts)
│ └─ reporters/
│ ├─ pretty.ts terminal output
│ └─ json.ts JSON output
plugin-mermaid/ @crackdown/plugin-mermaid
│ └─ index.ts remarkLintMermaid (@mermaid-js/parser)
compat-markdownlint/ @crackdown/compat-markdownlint
│ ├─ compat.ts loadMarkdownlintConfig
│ └─ rules/
│ ├─ md001.ts heading-increment
│ ├─ md013.ts line-length
│ ├─ md022.ts blanks-around-headings
│ └─ md041.ts first-line-heading
lsp/ @crackdown/lsp
│ ├─ convert.ts LintViolation → LSP Diagnostic
│ ├─ validate.ts validateMarkdown
│ └─ server.ts createServer (TextDocuments, debounce, codeAction)
vscode/ @crackdown/vscode
└─ extension.ts VS Code extension (LanguageClient → @crackdown/lsp)

Config loading

crackdown.config.ts is loaded at runtime using jiti, which evaluates TypeScript without a compilation step. Config discovery walks up the directory tree from the linted file’s location until it finds a crackdown.config.ts or reaches the filesystem root.

Mermaid validation

The @crackdown/plugin-mermaid plugin takes a different approach from the text-based rules: it passes the raw content of each mermaid code block to @mermaid-js/parser, which uses a Langium-based grammar for accurate syntax validation.

Diagram type is detected from the first non-empty token (e.g. flowchart, pie, gitGraph). Types not supported by the v1 parser (e.g. sequenceDiagram, classDiagram) are skipped gracefully — no false positives.

LSP architecture

Editor (VS Code, Neovim, Zed…)
│ LSP protocol (JSON-RPC over stdio)
@crackdown/lsp server
├─ TextDocuments manager ← tracks open files
├─ Config cache ← loadConfig() per workspace root
├─ fs.watch watcher ← hot-reload crackdown.config.ts
├─ Debounce timer (300ms) ← onDidChange validation
└─ validateMarkdown() ← @crackdown/core lintString
│ Diagnostic[]
textDocument/publishDiagnostics → inline squiggles in editor