Skip to content

udamir/api-diff-viewer

Repository files navigation

api-diff-viewer

npm npm npm type definitions GitHub

CodeMirror-based diff viewer for JSON-based API documents. Supports OpenAPI 3.x, AsyncAPI 2.x, and JsonSchema specifications.

Storybook

Features

  • Side-by-side & inline views — dual-editor or unified diff display
  • YAML & JSON output — render diffs in either format
  • Word-level diff highlighting — word or character granularity, or disable entirely
  • Change classification — breaking, non-breaking, annotation, unclassified with color-coded gutter indicators
  • Change filtering — show only specific classification types
  • Code folding — collapse/expand API structure blocks with change count badges
  • Array-level diffs — per-item change tracking for parameters, enum values, tags, etc.
  • Dark mode — built-in light and dark themes with CSS variable overrides
  • WebWorker support — non-blocking merge computation for large specs
  • Event system — lifecycle events for loading, ready, errors, and state changes
  • Zero framework dependency — pure DOM + CodeMirror, works with any framework or vanilla JS

Installation

npm install api-diff-viewer

Quick Start

import { createDiffViewer } from 'api-diff-viewer'

const viewer = createDiffViewer(
  document.getElementById('diff')!,
  beforeSpec, // object or JSON/YAML string
  afterSpec,
  { format: 'yaml', mode: 'side-by-side' }
)

viewer.on('ready', ({ summary }) => {
  console.log(`${summary.total} changes (${summary.breaking} breaking)`)
})

// Cleanup when done
viewer.destroy()

API Reference

createDiffViewer(container, before, after, options?)

Creates and returns a DiffViewer instance.

Parameter Type Description
container HTMLElement DOM element to mount into
before object | string The "before" API spec (object or JSON/YAML string)
after object | string The "after" API spec (object or JSON/YAML string)
options DiffViewerOptions Configuration (see below)

DiffViewerOptions

Option Type Default Description
mode 'side-by-side' | 'inline' 'side-by-side' Display mode
format 'json' | 'yaml' 'yaml' Output format
filters DiffType[] [] Active diff type filters (empty = show all)
dark boolean false Enable dark theme
theme Extension Base CodeMirror theme extension
colors Partial<DiffThemeColors> {} Diff-specific color overrides via CSS variables
enableFolding boolean false Enable code folding for API blocks
showClassification boolean false Show classification gutter bars and fold badges
wordDiffMode 'word' | 'char' | 'none' 'word' Word-level diff granularity
wordWrap boolean true Enable word wrapping (false adds synced horizontal scroll)
extensions Extension[] [] Additional CodeMirror extensions (search, active line, etc.)
useWorker boolean true Use WebWorker for merging (non-blocking)
workerUrl string '' Custom worker URL (default: inline blob)
mergeOptions CompareOptions {} Override options passed to api-smart-diff

DiffViewer Instance Methods

Display Controls

viewer.setMode('inline')           // Switch between 'side-by-side' and 'inline'
viewer.getMode()                   // Returns current mode

viewer.setFormat('json')           // Switch between 'json' and 'yaml'
viewer.getFormat()                 // Returns current format

viewer.setFilters(['breaking'])    // Filter visible changes by classification
viewer.getFilters()                // Returns active filters

viewer.setTheme({ dark: true })    // Toggle dark mode
viewer.setTheme({ colors: { ... }}) // Override diff colors
viewer.isDark()                    // Returns dark mode state

viewer.setWordDiffMode('char')     // Change word diff granularity
viewer.setWordWrap(false)          // Disable word wrap (enables synced horizontal scroll)
viewer.getWordWrap()               // Returns word wrap state
viewer.setFoldingEnabled(true)     // Toggle code folding
viewer.setClassificationEnabled(true) // Toggle classification indicators

Data Updates

viewer.update(newBefore, newAfter) // Replace specs and re-render

Fold Control

viewer.expandAll()                 // Expand all folded blocks
viewer.collapseAll()               // Collapse all foldable blocks
viewer.togglePath('info')          // Toggle a specific path
viewer.togglePath(['paths', '/pets', 'get']) // Array form also works

Events

viewer.on('loading', () => { /* merge started */ })

viewer.on('ready', ({ summary }) => {
  console.log(`${summary.total} changes`)
  console.log(`${summary.breaking} breaking`)
  console.log(`${summary.nonBreaking} non-breaking`)
  console.log(`${summary.annotation} annotation`)
  console.log(`${summary.unclassified} unclassified`)
})

viewer.on('error', ({ message, cause }) => {
  console.error('Merge failed:', message)
})

viewer.on('modeChange', ({ mode }) => { /* 'side-by-side' | 'inline' */ })
viewer.on('formatChange', ({ format }) => { /* 'json' | 'yaml' */ })
viewer.on('themeChange', ({ dark }) => { /* boolean */ })
viewer.on('wordWrapChange', ({ wordWrap }) => { /* boolean */ })

Advanced

// Access underlying CodeMirror editors
const { before, after } = viewer.getEditorViews()     // side-by-side
const { unified } = viewer.getEditorViews()            // inline

// Get change summary without navigation
viewer.getChangeSummary()

// Cleanup
viewer.destroy()

DiffThemeColors

CSS variable overrides for diff colors:

viewer.setTheme({
  colors: {
    addedBg: 'rgba(46, 160, 67, 0.15)',
    removedBg: 'rgba(248, 81, 73, 0.15)',
    modifiedBg: 'rgba(227, 179, 65, 0.15)',
    breakingColor: '#cf222e',
    nonBreakingColor: '#1a7f37',
    annotationColor: '#8250df',
    unclassifiedColor: '#656d76',
    addedTextBg: 'rgba(46, 160, 67, 0.4)',
    removedTextBg: 'rgba(248, 81, 73, 0.4)',
    spacerBg: '#f6f8fa',
    spacerStripe: '#e1e4e8',
  }
})

ChangeSummary

Returned by ready event and getChangeSummary():

interface ChangeSummary {
  total: 
  breaking: number
  nonBreaking: number
  annotation: number
  unclassified: number
  byPath: Map<string, { type: DiffType; count: number }[]>
}

Diff Engine

The diff logic is powered by api-smart-diff, which performs structural merging and semantic comparison of API specifications. It understands the semantics of OpenAPI, AsyncAPI, and JsonSchema documents — so changes are classified not just as text edits but by their impact on API consumers.

The merge pipeline:

  1. Structural mergeapi-smart-diff walks both specs in parallel, producing a single merged document annotated with $diff metadata on every changed node. Each $diff entry carries an action (add, remove, replace, rename) and a type (breaking, non-breaking, annotation, unclassified).
  2. Diff tree construction — the merged document is converted into a DiffBlockData tree by the diff builder, which generates format-specific tokens (JSON or YAML syntax) for rendering.
  3. Content alignment — for side-by-side mode, spacer lines are inserted so corresponding sections align visually across both editors.

You can pass options through to api-smart-diff via the mergeOptions field:

createDiffViewer(container, before, after, {
  mergeOptions: {
    // Custom comparison rules
    rules: { ... },
    // Custom annotation hook
    annotateHook: (before, after, ctx) => { ... },
    // Resolved external $ref sources
    externalSources: {
      before: { 'common.yaml': { ... } },
      after: { 'common.yaml': { ... } },
    },
  },
})

The mergeOptions field accepts api-smart-diff options. Available parameters:

Option Type Description
rules CompareRules Custom comparison rules
annotateHook AnnotateHook Custom hook for change annotations
externalSources { before?: Record<string, unknown>; after?: Record<string, unknown> } Resolved external $ref sources

Note: metaKey and arrayMeta are managed internally and cannot be overridden.

Change Classification Types

Type Description Examples
breaking Changes that break existing clients Removing endpoints/parameters, changing types, adding required fields
non-breaking Backwards-compatible changes Adding optional parameters/properties, relaxing constraints, adding enum values
annotation Documentation-only changes Description, summary, example changes
unclassified Changes without clear classification Extension field (x-*) changes

Usage Examples

Basic Side-by-Side

import { createDiffViewer } from 'api-diff-viewer'

const viewer = createDiffViewer(
  document.getElementById('diff')!,
  openApiV1,
  openApiV2,
)

Inline Mode with Folding

const viewer = createDiffViewer(container, before, after, {
  mode: 'inline',
  enableFolding: true,
  showClassification: true,
})

Dark Mode with Custom Colors

const viewer = createDiffViewer(container, before, after, {
  dark: true,
  colors: {
    breakingColor: '#ff6b6b',
    nonBreakingColor: '#51cf66',
  },
})

Filtering Breaking Changes

const viewer = createDiffViewer(container, before, after, {
  filters: ['breaking'],
  enableFolding: true,
  showClassification: true,
})

CodeMirror Extensions

import { createDiffViewer } from 'api-diff-viewer'
import { search, searchKeymap, highlightSelectionMatches } from '@codemirror/search'
import { highlightActiveLine, keymap } from '@codemirror/view'

const viewer = createDiffViewer(container, before, after, {
  extensions: [
    search(),
    keymap.of(searchKeymap),
    highlightActiveLine(),
    highlightSelectionMatches(),
  ],
})
// Ctrl+F / Cmd+F opens search in each panel

Note: The library does not bundle @codemirror/search — install it separately: npm install @codemirror/search

Synchronous Mode (No Worker)

const viewer = createDiffViewer(container, before, after, {
  useWorker: false,
})
// Ready immediately after construction (no 'loading' event)

Runtime Option Changes

const viewer = createDiffViewer(container, before, after)

// Toggle mode
document.getElementById('toggle-mode')!.onclick = () => {
  viewer.setMode(viewer.getMode() === 'side-by-side' ? 'inline' : 'side-by-side')
}

// Toggle format
document.getElementById('toggle-format')!.onclick = () => {
  viewer.setFormat(viewer.getFormat() === 'yaml' ? 'json' : 'yaml')
}

// Toggle dark mode
document.getElementById('toggle-dark')!.onclick = () => {
  viewer.setTheme({ dark: !viewer.isDark() })
}

Updating Specs

const viewer = createDiffViewer(container, specV1, specV2)

// Later, compare different versions
viewer.update(specV2, specV3)

Development

pnpm install          # Install dependencies
pnpm run build        # Type-check + build library
pnpm run storybook    # Run Storybook on port 6006
pnpm run test         # Run tests
pnpm run lint         # Lint with Biome

License

MIT

About

React component to view difference between two Json based API documents

Topics

Resources

License

Stars

Watchers

Forks

Contributors