cheat sheet
diff2html-cli
Package-level reference for the diff2html-cli renderer on npm — install, runtime support, peer-deps, and alternatives.
diff2html-cli
What it is
diff2html-cli is the command-line wrapper around the diff2html library — it consumes a unified diff (from git diff, svn diff, or stdin) and emits a styled HTML file with side-by-side or line-by-line views, optional dark theme, syntax highlighting, and a file-tree summary. It is most often used to attach a viewable diff to a code-review email or to render a static visual summary in CI.
Reach for diff2html-cli when reviewers want an HTML deliverable they can open in any browser. Reach for delta or riff instead if the goal is rich in-terminal diff viewing without leaving the shell.
Install
This is a global-CLI-style package, but local install + npx is the modern recommendation.
npm install -g diff2html-cli
Output: added diff2html-cli to global packages — exposes diff2html bin
npx diff2html-cli --help
Output: prints usage from the local cache
pnpm add -g diff2html-cli
Output: added 1 package globally
yarn global add diff2html-cli
Output: added diff2html-cli to global cache
bun add -g diff2html-cli
Output: installed diff2html-cli globally; diff2html on PATH
deno run --allow-all npm:diff2html-cli --help
Output: runs the CLI with explicit Deno permissions
Versioning & Node support
The current line is diff2html-cli@5.x. Versions are loosely paired with the underlying diff2html library (also 5.x at time of writing).
- Node 16+ required (the CLI uses optional chaining and ESM-only deps internally).
- CommonJS-friendly — the published bin loads via CJS and shells out to esbuild-bundled internals.
- Semver — major bumps usually change the default template (line-by-line vs side-by-side) or the asset bundling strategy.
Package metadata
- Maintainer: Rodrigo Fernandes (
rtfpessoaon GitHub) and contributors - Project home: github.com/rtfpessoa/diff2html-cli
- Library: github.com/rtfpessoa/diff2html — the renderer itself
- Docs: diff2html.xyz
- npm: npmjs.com/package/diff2html-cli
- License: MIT
- First released: 2015
- Downloads: a few hundred thousand weekly downloads — niche but stable in CI and code-review workflows.
Peer dependencies & extras
diff2html-cli pulls in diff2html (the renderer), highlight.js (for syntax colouring), yargs (for argument parsing), and a small set of utilities. No peer-deps — everything ships as direct dependencies.
There is no extras flag. Customisation happens through CLI options:
--style=side/--style=line— layout--highlightCode=true— syntax highlighting via highlight.js--summary=closed|open|hidden— file-tree summary state--format=html|json— rendering target--output=preview|stdout|file— destination--diffStyle=word|char— intra-line diff granularity
Alternatives
| Package | Trade-off |
|---|---|
delta (Rust) | In-terminal pager replacement for git diff. Much faster, never produces HTML. Install via cargo or brew install git-delta. |
riff (Rust) | Similar to delta. Smaller feature set, faster startup. |
diff-so-fancy | Older Perl-based formatter used by Atom and GitHub for years. Pipes through less. |
| GitHub / GitLab / Gitea diff viewers | The web UI for any forge already produces HTML diffs. Use diff2html-cli when you want a self-contained HTML file independent of a forge. |
git format-patch + email | The traditional patch-by-email workflow. Plain text only. |
prettydiff | Older, larger renderer. Largely superseded by diff2html. |
Common gotchas
- Input mode: stdin vs file vs git. With no positional arg, the CLI reads stdin. Pass a
.diff/.patchpath to read a file. Pass-i commandto run agit diffcommand internally. Mixing modes (e.g.git diff | diff2html some.diff) silently ignores stdin. --output=previewopens the browser only on the local machine. In CI, this hangs because there's no GUI. Use--output=stdout > out.htmlor--output=file --file ./out.htmlin headless environments.highlight.jslanguages must be auto-detected. It infers language from filename extension. Diffs against extension-less files (Dockerfile, Makefile) lose syntax colouring unless you preprocess.- Large diffs balloon HTML output. A 10 MB diff can produce a 30 MB HTML file because every changed line embeds inline styles + highlight markup. Split with
git diff --statfirst to identify oversized files. - Line endings normalise to LF. If the source repo uses CRLF, the rendered HTML displays LF and the line numbers may drift compared to the editor. Set
git config core.autocrlf falseon Windows or usegit diff --text. - CLI flags are case-sensitive.
--diffStyle(camelCase) and--style(lowercase) live side-by-side. Usediff2html --helprather than guessing. - No first-class GitHub-PR mode. To diff a PR, fetch the unified-diff URL (
https://github.com/<org>/<repo>/pull/<n>.diff) and pipe it via stdin.
Real-world recipes
Pre-PR diff review
Render the working-branch's diff against main into an HTML file the reviewer can open offline. Pair with a Slack / email attachment for async code review.
git diff main...HEAD | diff2html -s side -i stdin -F review.html --hwt none
open review.html
Output: review.html with side-by-side diff for every changed file.
The flags decompose as: -s side (side-by-side), -i stdin (read piped input), -F review.html (output filename), --hwt none (no embedded highlight.js theme — uses default).
CI artifact for pull requests
In GitHub Actions, render the PR diff into an HTML file and upload as an artifact. Reviewers download it from the workflow run page.
# .github/workflows/diff-html.yml
name: PR diff HTML
on:
pull_request:
branches: [main]
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: |
git fetch origin ${{ github.base_ref }}
git diff origin/${{ github.base_ref }}...HEAD > pr.diff
- run: npx diff2html-cli@latest -i file -F pr.html pr.diff
- uses: actions/upload-artifact@v4
with:
name: diff-html
path: pr.html
Output: every PR produces a downloadable diff-html artifact.
Render a GitHub PR's .diff URL directly
GitHub exposes every PR's diff via a stable URL pattern. Pipe it straight into diff2html without cloning.
curl -sL https://github.com/withastro/astro/pull/12345.diff \
| diff2html -i stdin -s side -F astro-12345.html
Output: astro-12345.html rendered from the live GitHub diff.
GitLab uses .diff similarly: https://gitlab.com/group/project/-/merge_requests/<n>.diff. Gitea, Forgejo, and Codeberg follow the same convention.
Embed in a static site
--format=json produces machine-readable output you can stitch into a custom page. Useful for review dashboards or doc sites that want diffs without diff2html's default chrome.
git diff HEAD~5 | diff2html -i stdin -f json > diff.json
Output: JSON array with one object per file (path, language, line counts, hunks).
Combined with the npm diff2html library at build time, this powers custom UIs that render diffs alongside your own design system.
Programmatic use via the library
The CLI is a thin wrapper around the diff2html library. For tighter integration (Astro page, React component, server route), call the library directly.
import { html as renderDiff } from "diff2html";
import { readFile } from "node:fs/promises";
const unifiedDiff = await readFile("change.diff", "utf8");
const htmlOut = renderDiff(unifiedDiff, {
drawFileList: true,
matching: "lines",
outputFormat: "side-by-side",
highlight: true,
});
console.log(htmlOut);
Output: HTML fragment ready to embed inside a layout's <body>.
The library and CLI follow the same minor-version line — install both at the same version to avoid template drift.
Performance tuning
A 10 MB unified diff against thousands of files takes 30-60 seconds and produces a 30+ MB HTML file. The tuning knobs:
--highlightCode=falsedisableshighlight.js. Cuts CPU by ~80% on large diffs. Use when diffs are huge or syntax is irrelevant.--summary=closedcollapses the file tree by default; the browser doesn't pre-render every section.--matching=nonedisables the intra-line matching algorithm (defaultlines). Cheaper but coarser diff.- Split per-file. Pipe
git diff --name-onlyand render one file at a time when the consolidated output is unwieldy. - Cache JSON, render late. Generate JSON in CI (cheap), render to HTML at view time (in a serverless function). Avoids storing huge HTML artifacts.
- Skip binary / generated files. Add a
.gitattributesentry*.snap binarysogit diffomits them, or post-filter withgit diff -- ':!*.lock'.
A quick benchmark on a 5 MB diff (single Astro repo upgrade):
| Config | Time | Output size |
|---|---|---|
| Default | 18 s | 21 MB |
--highlightCode=false | 4 s | 8 MB |
--matching=none | 14 s | 19 MB |
| Both | 3 s | 7 MB |
Production deployment
diff2html-cli is a developer tool — it usually runs in CI or on a workstation, not as a long-lived service. Two deployment patterns appear in practice.
Lockfile-pinned in CI
For reproducibility, install as a dev-dep instead of using npx diff2html-cli@latest (which picks up new majors silently).
{
"devDependencies": {
"diff2html-cli": "^5.2.16"
},
"scripts": {
"diff:render": "diff2html -i stdin -s side -F dist/pr.html"
}
}
git diff main...HEAD | npm run diff:render
Output: uses the locally installed binary, consistent across CI runs.
Bundled into a review-bot image
A custom GitHub bot that comments on PRs with a hosted diff URL can build a small Docker image that includes diff2html-cli and a static-file server.
FROM node:20-slim
RUN npm install -g diff2html-cli@5
WORKDIR /work
ENTRYPOINT ["diff2html"]
docker build -t diff-bot .
git diff | docker run --rm -i diff-bot -i stdin -o stdout > review.html
Output: containerised CLI without polluting the host's global node_modules.
Version migration guide
The CLI and library share a release line but ship at slightly different tempos. The CLI usually lags by a minor.
| From | To | Key change |
|---|---|---|
diff2html-cli@3 | diff2html-cli@4 | Default output changed from line-by-line to side-by-side. Reverse with --style line if you relied on the old default. |
diff2html-cli@4 | diff2html-cli@5 | Node 16+ floor. Internal bundler switched to esbuild. Some legacy templates removed. |
diff2html@3 | diff2html@4 | Output API changed — Diff2html.html(...) returns a fragment without surrounding <html> chrome. |
diff2html@4 | diff2html@5 | TypeScript types rewritten; some interfaces renamed. Affects programmatic users. |
For long-lived CI scripts, pin the version in package.json or in the npx call: npx diff2html-cli@5.2.16. The unpinned @latest form picks up majors without warning.
ESM/CJS interop & bundling
The CLI itself is a global binary — interop concerns only apply when using the underlying diff2html library programmatically.
| Setup | Pattern |
|---|---|
| ESM | import { html as renderDiff } from "diff2html"; |
| CJS | const { html: renderDiff } = require("diff2html"); |
| TypeScript | Types ship in-tree. Set "moduleResolution": "node16" for the "exports" map to work. |
| Webpack / Vite (front-end) | Works — diff2html is browser-compatible (it powers the diff2html.xyz playground). highlight.js is a large dep; tree-shake aggressively. |
| Cloudflare Workers | Library works on Workers. CLI does not (it shells out to git). |
| Bun | Library works. CLI works via bun x diff2html-cli. |
| Deno | Library works via npm:diff2html. CLI works via deno run --allow-all npm:diff2html-cli. |
The library bundle is ~150 KB minified before highlight.js. With highlight: true enabled and all languages, the total can exceed 1 MB — load on demand for browser use.
Plugin & ecosystem coverage
| Package | Role |
|---|---|
diff2html | The renderer library — CLI is a thin wrapper. Use directly for embedded diffs. |
parse-diff | Lower-level unified-diff parser. Useful for custom rendering when diff2html's templates don't fit. |
diff (jsdiff) | Produce diffs in JS (line, char, word). Pipe its output through diff2html for HTML. |
gitdiff-parser | Stricter parse of git diff extensions (file modes, renames). Pairs with custom renderers. |
highlight.js | Syntax highlighter. Transitive dep; configure via --hwt (highlight.js whole-theme) and --hl (highlight.js languages). |
prismjs | Alternative syntax highlighter. Some forks of diff2html swap in Prism for smaller bundles. |
delta (Rust, not npm) | In-terminal alternative. Sibling tool — use diff2html when HTML matters, delta when you stay in the shell. |
git-pull-request-formatter, custom bots | Compose around diff2html to post PR previews to chat. |
Testing & CI integration
The CLI's output is HTML, which is fiddly to assert against. Snapshot the output or assert against the JSON form.
Snapshot the JSON output
import { test, expect } from "vitest";
import { execSync } from "node:child_process";
test("renders a small diff", () => {
const diff = execSync("git diff HEAD~1 HEAD -- src/index.ts").toString();
const json = execSync("diff2html -i stdin -f json", { input: diff }).toString();
const parsed = JSON.parse(json);
expect(parsed[0].language).toBe("typescript");
expect(parsed[0].blocks.length).toBeGreaterThan(0);
});
Output: 1 passed.
CI rendering job
A common pattern is rendering the diff for every push to main and uploading to a static-site bucket so reviewers have a permalink.
- run: git diff HEAD~1 HEAD > daily.diff
- run: diff2html -i file -F /tmp/daily.html daily.diff
- run: aws s3 cp /tmp/daily.html s3://review/diffs/$(date -u +%F).html
Security considerations
- Untrusted diffs render arbitrary HTML? No — diff2html escapes user content. But the CLI runs
git diffvia-i commandmode using shell semantics. Never pass user input through that path:diff2html -i command "git diff; rm -rf /"would execute thermafter the diff. Use-i stdinor-i filefor any untrusted source. - highlight.js DoS. Very long lines or pathological inputs can stall the highlighter. Set a max line length in your wrapper, or run
diff2html --highlightCode=falseon diffs larger than a threshold. - CSP for embedded output. The CLI emits inline
<style>blocks. To embed inside a CSP-strict page, render with--format=jsonand use your own CSS, or add'unsafe-inline'tostyle-src(not recommended). - HTML in a code-review email. Mail clients (Outlook, Gmail) strip most styles. Render with
--style=lineand a minimalist template for cross-client rendering. - Disk usage on large repos. A monorepo's full history diff can produce gigabytes of HTML. Cap the input with
git diff --max-size=1mor--statfirst.
Troubleshooting common errors
Error: ENOENT reading the diff — the file path doesn't exist. With -i file, the path is relative to cwd; pass an absolute path or cd first.
Empty HTML output — no diff in the input (working tree clean, or HEAD~1..HEAD was empty). Check git diff | wc -l first.
Syntax highlighting missing for a recognised language — highlight.js infers from the filename. Files with no extension (Dockerfile, Makefile, COMMIT_EDITMSG) lose colouring. Preprocess to add an extension, or render with --highlightCode=false for consistency.
--output=preview hangs in headless CI — there's no browser to open. Use --output=stdout > out.html or --output=file --file=out.html.
Side-by-side layout overflows on narrow screens — render with --style=line (line-by-line / unified) for mobile-friendly output, or wrap the embed in a horizontally scrolling container.
Old browser shows blank page — diff2html emits modern HTML/CSS. For IE11 (rare these days), pre-render server-side and serve a static snapshot.
Diff doesn't match what the user sees in GitHub — GitHub shows merge-base diffs (base...head); plain git diff base head is the two-dot form (which excludes commits on base since the branch split). Use git diff base...head (three dots) for parity.
Bin not found after global install — npm's global prefix isn't on PATH. Run npm config get prefix and add <prefix>/bin to PATH, or prefer npx diff2html-cli.
When NOT to use this
Skip diff2html-cli when:
- In-terminal viewing is enough.
delta(Rust) ordiff-so-fancyproduce richer terminal output without HTML overhead.git config core.pager deltasets it globally. - The forge already renders diffs. GitHub / GitLab / Gitea / Bitbucket all have built-in diff viewers with permalinks, comments, and reactions. diff2html shines for off-platform sharing (email, Slack, archived snapshots), not as a replacement.
- You need IDE-quality diffs. VS Code's diff view, JetBrains' diff tool, and
git difftoolintegrate with the editor and offer in-place editing. diff2html is read-only. - The output is fed to another tool. Plain unified diff is the universal interchange format. Wrapping it in HTML adds friction for downstream parsers.
- Bandwidth is constrained (email-heavy review workflow). A 10 MB diff produces a 30 MB HTML attachment.
git format-patch+ plain text is still better for that case. - You target the browser at scale. For server-rendered diffs on a busy site, use the
diff2htmllibrary directly and cache the output — the CLI launch cost (~500 ms) is too high per request.
See also
- JavaScript: diff2html-cli — full flag list and recipes
- Concept: pipes — feeding
git diffinto the CLI