Diffs

Overview

Diffs is in early active development—APIs are subject to change.

Diffs is a library for rendering code and diffs on the web. This includes both high-level, easy-to-use components, as well as exposing many of the internals if you want to selectively use specific pieces. We've built syntax highlighting on top of Shiki which provides a lot of great theme and language support.

We have an opinionated stance in our architecture: browsers are rather efficient at rendering raw HTML. We lean into this by having all the lower level APIs purely rendering strings (the raw HTML) that are then consumed by higher-order components and utilities. This gives us great performance and flexibility to support popular libraries like React as well as provide great tools if you want to stick to vanilla JavaScript and HTML. The higher-order components render all this out into Shadow DOM and CSS grid layout.

Generally speaking, you're probably going to want to use the higher level components since they provide an easy-to-use API that you can get started with rather quickly. We currently only have components for vanilla JavaScript and React, but will add more if there's demand.

For this overview, we'll talk about the vanilla JavaScript components for now but there are React equivalents for all of these.

Rendering Diffs

Our goal with visualizing diffs was to provide some flexible and approachable APIs for how you may want to render diffs. For this, we provide a component called FileDiff.

There are two ways to render diffs with FileDiff:

  1. Provide two versions of a file or code snippet to compare
  2. Consume a patch file

You can see examples of these approaches below, in both JavaScript and React.

Installation

Diffs is published as an npm package. Install Diffs with the package manager of your choice:

Package Exports

The package provides several entry points for different use cases:

PackageDescription
@pierre/diffsVanilla JS components and utility functions for parsing and rendering diffs
@pierre/diffs/reactReact components for rendering diffs with full interactivity
@pierre/diffs/ssrServer-side rendering utilities for pre-rendering diffs with syntax highlighting
@pierre/diffs/workerWorker pool utilities for offloading syntax highlighting to background threads

Core Types

Before diving into the components, it's helpful to understand the two core data structures used throughout the library.

FileContents

FileContents represents a single file. Use it when rendering a file with the <File> component, or pass two of them as oldFile and newFile to diff components.

FileDiffMetadata

FileDiffMetadata represents the differences between two files. It contains the hunks (changed regions), line counts, and optionally the full file contents for expansion.

Tip: You can generate FileDiffMetadata using parseDiffFromFile (from two file versions) or parsePatchFiles (from a patch string).

Creating Diffs

There are two ways to create a FileDiffMetadata.

From Two Files

Use parseDiffFromFile when you have both file versions. This approach includes the full file contents, enabling the "expand unchanged" feature.

From a Patch String

Use parsePatchFiles when you have a unified diff or patch file. This is useful when working with git output or patch files from APIs.

Tip: If you need to change the language after creating a FileContents or FileDiffMetadata, use the setLanguageOverride utility function.

React API

Import React components from @pierre/diffs/react.

We offer a variety of components to render diffs and files. Many of them share similar types of props, which you can find documented in Shared Props.

Components

The React API exposes four main components:

  • MultiFileDiff compares two file versions
  • PatchDiff renders from a patch string
  • FileDiff renders a pre-parsed FileDiffMetadata
  • File renders a single code file without a diff

Shared Props

The three diff components (MultiFileDiff, PatchDiff, and FileDiff) share a common set of props for configuration, annotations, and styling. The File component has similar props, but uses LineAnnotation instead of DiffLineAnnotation (no side property).

Vanilla JS API

Import vanilla JavaScript classes, components, and methods from @pierre/diffs.

Components

The Vanilla JS API exposes two core components: FileDiff (compare two file versions or render a pre-parsed FileDiffMetadata) and File (render a single code file without diff). Typically you'll want to interface with these as they'll handle all the complicated aspects of syntax highlighting, theming, and full interactivity for you.

Props

Both FileDiff and File accept an options object in their constructor. The File component has similar options, but excludes diff-specific settings and uses LineAnnotation instead of DiffLineAnnotation (no side property).

Custom Hunk Separators

If you want to render custom hunk separators that won't scroll with the content, there are a few tricks you will need to employ. See the following code snippet:

Renderers

For most use cases, you should use the higher-level components like FileDiff and File (vanilla JS) or the React components (MultiFileDiff, FileDiff, PatchDiff, File). These renderers are low-level building blocks intended for advanced use cases.

These renderer classes handle the low-level work of parsing and rendering code with syntax highlighting. Useful when you need direct access to the rendered output as HAST nodes or HTML strings for custom rendering pipelines.

DiffHunksRenderer

Takes a FileDiffMetadata data structure and renders out the raw HAST (Hypertext Abstract Syntax Tree) elements for diff hunks. You can generate FileDiffMetadata via parseDiffFromFile or parsePatchFiles utility functions.

FileRenderer

Takes a FileContents object (just a filename and contents string) and renders syntax-highlighted code as HAST elements. Useful for rendering single files without any diff context.

Utilities

Import utility functions from @pierre/diffs. These can be used with any framework or rendering approach.

diffAcceptRejectHunk

Programmatically accept or reject individual hunks in a diff. This is useful for building interactive code review interfaces, AI-assisted coding tools, or any workflow where users need to selectively apply changes.

When you accept a hunk, the new (additions) version is kept and the hunk is converted to context lines. When you reject a hunk, the old (deletions) version is restored. The function returns a new FileDiffMetadata object with all line numbers properly adjusted for subsequent hunks.

disposeHighlighter

Dispose the shared Shiki highlighter instance to free memory. Useful when cleaning up resources in single-page applications.

getSharedHighlighter

Get direct access to the shared Shiki highlighter instance used internally by all components. Useful for custom highlighting operations.

parseDiffFromFile

Compare two versions of a file and generate a FileDiffMetadata structure. Use this when you have the full contents of both file versions rather than a patch string.

If both oldFile and newFile have a cacheKey, the resulting FileDiffMetadata will automatically receive a combined cache key (format: oldKey:newKey). See Render Cache for more information.

parsePatchFiles

Parse unified diff / patch file content into structured data. Handles both single patches and multi-commit patch files (like those from GitHub pull request .patch URLs). An optional second parameter cacheKeyPrefix can be provided to generate cache keys for each file in the patch (format: prefix-patchIndex-fileIndex), enabling caching of rendered diff results in the worker pool.

preloadHighlighter

Preload specific themes and languages before rendering to ensure instant highlighting with no async loading delay.

registerCustomTheme

Register a custom Shiki theme for use with any component. The theme name you register must match the name field inside your theme JSON file.

setLanguageOverride

Override the syntax highlighting language for a FileContents or FileDiffMetadata object. This is useful when the filename doesn't have an extension or doesn't match the actual language.

Styling

Diff and code components are rendered using shadow DOM APIs, allowing styles to be well-isolated from your page's existing CSS. However, it also means you may have to utilize some custom CSS variables to override default styles. These can be done in your global CSS, as style props on parent components, or on the FileDiff component directly.

Advanced: Unsafe CSS

For advanced customization, you can inject arbitrary CSS into the shadow DOM using the unsafeCSS option. This CSS will be wrapped in an @layer unsafe block, giving it the highest priority in the cascade. Use this sparingly and with caution, as it bypasses the normal style isolation.

We also recommend that any CSS you apply uses simple, direct selectors targeting the existing data attributes. Avoid structural selectors like :first-child, :last-child, :nth-child(), sibling combinators (+ or ~), deeply nested descendant selectors, or bare tag selectors—these are susceptible to breaking in future versions or in edge cases that may be difficult to anticipate.

We cannot currently guarantee backwards compatibility for this feature across any future changes to the library, even in patch versions. Please reach out so that we can discuss a more permanent solution for modifying styles.

Worker Pool

This feature is experimental and undergoing active development. There may be bugs and the API is subject to change.

Import worker utilities from @pierre/diffs/worker.

By default, syntax highlighting runs on the main thread using Shiki. If you're rendering large files or many diffs, this can cause a bottleneck on your JavaScript thread resulting in jank or unresponsiveness. To work around this, we've provided some APIs to run all syntax highlighting in worker threads. The main thread will still attempt to render plain text synchronously and then apply the syntax highlighting when we get a response from the worker threads.

Basic usage differs a bit depending on if you're using React or Vanilla JS APIs, so continue reading for more details.

Setup

One unfortunate side effect of using Web Workers is that different bundlers and environments require slightly different approaches to create a Web Worker. You'll need to create a function that spawns a worker that's appropriate for your environment and bundler and then pass that function to our provided APIs.

Lets begin with the workerFactory function. We've provided some examples for common use cases below.

Only the Vite and NextJS examples have been tested by us. Additional examples were generated by AI. If any of them are incorrect, please let us know.

Vite

You may need to explicitly set the worker.format option in your Vite Config to 'es'.

NextJS

Workers only work in client components. Ensure your function has the 'use client' directive if using App Router.

VS Code Webview Extension

VS Code webviews have special security restrictions that require a different approach. You'll need to configure both the extension side (to expose the worker file) and the webview side (to load it via blob URL).

Extension side: Add the worker directory to localResourceRoots in your getWebviewOptions():

Create the worker URI in _getHtmlForWebview(). Note: use worker-portable.js instead of worker.js — the portable version is designed for environments where ES modules aren't supported in web workers.

Pass the URI to the webview via an inline script in your HTML:

Your Content Security Policy must include worker-src and connect-src:

Webview side: Declare the global type for the URI:

Fetch the worker code and create a blob URL:

Create the workerFactory function:

Webpack 5

esbuild

Rollup / Static Files

If your bundler doesn't have special worker support, build and serve the worker file statically:

Vanilla JS (No Bundler)

For projects without a bundler, host the worker file on your server and reference it directly:

Usage

With your workerFactory function created, you can integrate it with our provided APIs. In React, you'll want to pass this workerFactory to a <WorkerPoolContextProvider> so all components can inherit the pool automatically. If you're using the Vanilla JS APIs, we provide a getOrCreateWorkerPoolSingleton helper that ensures a single pool instance that you can then manually pass to all your File/FileDiff instances.

When using the worker pool, the theme, lineDiffType, and tokenizeMaxLineLength render options are controlled by WorkerPoolManager, not individual components. Passing these options into component instances will be ignored. To change render options after WorkerPoolManager instantiates, use the setRenderOptions() method on the WorkerPoolManager. Note: Changing render options will force all mounted components to re-render and will clear the render cache.

React

Wrap your component tree with WorkerPoolContextProvider from @pierre/diffs/react. All FileDiff and File components nested within will automatically use the worker pool for syntax highlighting.

The WorkerPoolContextProvider will automatically spin up or shut down the worker pool based on its react lifecycle. If you have multiple context providers, they will all share the same pool, and termination won't occur until all contexts are unmounted.

Workers only work in client components. Ensure your function has the 'use client' directive if using App Router.

To change themes or other render options dynamically, use the useWorkerPool() hook to access the pool manager and call setRenderOptions().

Vanilla JS

Use getOrCreateWorkerPoolSingleton to spin up a singleton worker pool. Then pass that as the second argument to File and/or FileDiff. When you are done with the worker pool, you can use terminateWorkerPoolSingleton to free up resources.

To change themes or other render options dynamically, call setRenderOptions(options) on the pool instance.

Render Cache

This is an experimental feature being validated in production use cases. The API is subject to change.

The worker pool can cache rendered AST results to avoid redundant highlighting work. When a file or diff has a cacheKey, subsequent requests with the same key will return cached results immediately instead of reprocessing through a worker. This works automatically for both React and Vanilla JS APIs.

Caching is enabled per-file/diff by setting a cacheKey property. Files and diffs without a cacheKey will not be cached. The cache also validates against render options — if options like theme or line diff type change, the cached result is skipped and re-rendered.

API Reference

These methods are exposed for advanced use cases. In most scenarios, you should use the WorkerPoolContextProvider for React or pass the pool instance via the workerPool option for Vanilla JS rather than calling these methods directly.

Architecture

The worker pool manages a configurable number of worker threads that each initialize their own Shiki highlighter instance. Tasks are distributed across available workers, with queuing when all workers are busy.

SSR

Import SSR utilities from @pierre/diffs/ssr.

The SSR API allows you to pre-render file diffs on the server with syntax highlighting, then hydrate them on the client for full interactivity.

Usage

Each preload function returns an object containing the original inputs plus a prerenderedHTML string. This object can be spread directly into the corresponding React component for automatic hydration.

Inputs used for pre-rendering must exactly match what's rendered in the client component. We recommend spreading the entire result object into your File or Diff component to ensure the client receives the same inputs that were used to generate the pre-rendered HTML.

Server Component

Client Component

Preloaders

We provide several preload functions to handle different input formats. Choose the one that matches your data source.

preloadFile

Preloads a single file with syntax highlighting (no diff). Use this when you want to render a file without any diff context. Spread into the File component.

preloadFileDiff

Preloads a diff from a FileDiffMetadata object. Use this when you already have parsed diff metadata (e.g., from parseDiffFromFile or parsePatchFiles). Spread into the FileDiff component.

preloadMultiFileDiff

Preloads a diff directly from old and new file contents. This is the simplest option when you have the raw file contents and want to generate a diff. Spread into the MultiFileDiff component.

preloadPatchDiff

Preloads a diff from a unified patch string for a single file. Use this when you have a patch in unified diff format. Spread into the PatchDiff component.

preloadPatchFile

Preloads multiple diffs from a multi-file patch string. Returns an array of results, one for each file in the patch. Each result can be spread into a FileDiff component.