Install the Precision Diffs package using bun, pnpm, npm, or yarn:
1bun add @pierre/precision-diffsPrecision 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.
1const std = @import("std");2
3pub fn main() !void {4 const stdout = std.io.getStdOut().writer();5 try stdout.print("Hi you, {s}!\n", .{"world"});5 try stdout.print("Hello there, {s}!\n", .{"zig"});6}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.
It‘s in the name, it‘s probably why you‘re here. 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 (available in both JavaScript and React versions).
There are two ways to render diffs with FileDiff:
You can see examples of these approaches below, in both JavaScript and React.
1import {2 type FileContents,3 FileDiff,4} from '@pierre/precision-diffs';5
6// Comparing two files7const oldFile: FileContents = {8 name: 'main.zig',9 contents: `const std = @import("std");10
11pub fn main() !void {12 const stdout = std.io.getStdOut().writer();13 try stdout.print("Hi you, {s}!\\\\n", .{"world"});14}15`,16};17
18const newFile: FileContents = {19 name: 'main.zig',20 contents: `const std = @import("std");21
22pub fn main() !void {23 const stdout = std.io.getStdOut().writer();24 try stdout.print("Hello there, {s}!\\\\n", .{"zig"});25}26`,27};28
29// We automatically detect the language based on the filename30// You can also provide a lang property when instantiating FileDiff.31const fileDiffInstance = new FileDiff({ theme: 'pierre-dark' });32
33// Render is awaitable if you need that34await fileDiffInstance.render({35 oldFile,36 newFile,37 // where to render the diff into38 containerWrapper: document.body,39});Right now, the React API exposes two main components, FileDiff (for rendering diffs for a specific file) and File for rendering just a single code file. We plan to add more components like a file picker and tools for virtualization of longer diffs in the future.
You can import the React components from @pierre/precision-diffs/react
1import {2 type FileContents,3 type DiffLineAnnotation,4 MultiFileDiff,5} from '@pierre/precision-diffs/react';6
7const oldFile: FileContents = {8 name: 'filename.ts',9 contents: 'console.log("Hello world")',10};11
12const newFile: FileContents = {13 name: 'filename.ts',14 contents: 'console.warn("Uh oh")',15};16
17interface ThreadMetadata {18 threadId: string;19}20
21// Annotation metadata can be typed any way you'd like22const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [23 {24 side: 'additions',25 // The line number specified for an annotation is the visual line26 // number you see in the number column of a diff27 lineNumber: 16,28 metadata: { threadId: '68b329da9893e34099c7d8ad5cb9c940' },29 },30];31
32// Comparing two files33export function SingleDiff() {34 return (35 <MultiFileDiff<ThreadMetadata>36 // We automatically detect the language based on filename37 // You can also provide 'lang' property in 'options' when38 // rendering MultiFileDiff.39 oldFile={oldFile}40 newFile={newFile}41 lineAnnotations={lineAnnotations}42 renderLineAnnotation={(annotation: DiffLineAnnotation) => {43 // Despite the diff itself being rendered in the shadow dom,44 // annotations are inserted via the web components 'slots'45 // api and you can use all your normal normal css and styling46 // for them47 return <CommentThread threadId={annotation.metadata.threadId} />;48 }}49
50 // You must pass `enableHoverUtility: true` to the `options`51 // object below. This allows you to render some UI in the number52 // column when the user is hovered over the line. This is not a53 // reactive API, in other words, render is not called every time54 // you mouse over a new line (by design). You can call55 // `getHoveredLine()` in a click handler to know what56 // line is hovered.57 renderHoverUtility={(getHoveredLine): ReactNode => {58 return (59 <button60 onClick={() => {61 console.log(62 'you clicked on line:',63 getHoveredLine().lineNumber,64 'on side:',65 getHoveredLine().side // 'additions' | 'deletions'66 );67 }}68 >69 +70 </button>71 );72 }}73
74 // Programmatically control which lines are selected. This75 // allows two-way binding with state. Selections should be76 // stable across 'split' and 'unified' diff styles.77 // 'start' and 'end' map to the visual line numbers you see in the78 // number column. 'side' and 'endSide' are considered optional.79 // 'side' will default to the 'additions' side. 'endSide' will80 // default to whatever 'side' is unless you specify otherwise.81 selectedLines={{ start: 3, side: 'additions', end: 4, endSide: 'deletions' }}82
83 // Here's every property you can pass to options, with their84 // default values if not specified.85 options={{86 // You can provide a 'theme' prop that maps to any87 // built in shiki theme or you can register a custom88 // theme. We also include 2 custom themes89 //90 // 'pierre-dark' and 'pierre-light91 //92 // You can also pass an object with 'dark' and 'light' keys93 // to theme based on OS or 'themeType' setting below.94 //95 // By default we initialize with our custom pierre themes96 // for dark and light theme97 //98 // For the rest of the available shiki themes, either check99 // typescript autocomplete or visit:100 // https://shiki.style/themes101 theme: { dark: 'pierre-dark', light: 'pierre-light' },102
103 // When using the 'theme' prop that specifies dark and light104 // themes, 'themeType' allows you to force 'dark' or 'light'105 // theme, or inherit from the OS ('system') theme.106 themeType: 'system',107
108 // Disable the line numbers for your diffs, generally109 // not recommended110 disableLineNumbers: false,111
112 // Whether code should 'wrap' with long lines or 'scroll'.113 overflow: 'scroll',114
115 // Normally you shouldn't need this prop, but if you don't116 // provide a valid filename or your file doesn't have an117 // extension you may want to override the automatic detection118 // You can specify that language here:119 // https://shiki.style/languages120 // lang?: SupportedLanguages;121
122 // 'diffStyle' controls whether the diff is presented side by123 // side or in a unified (single column) view124 diffStyle: 'split',125
126 // Unchanged context regions are collapsed by default, set this127 // to true to force them to always render. This depends on using128 // the oldFile/newFile API or FileDiffMetadata including newLines.129 expandUnchanged: false,130
131 // Line decorators to help highlight changes.132 // 'bars' (default):133 // Shows some red-ish or green-ish (theme dependent) bars on the134 // left edge of relevant lines135 //136 // 'classic':137 // shows '+' characters on additions and '-' characters138 // on deletions139 //140 // 'none':141 // No special diff indicators are shown142 diffIndicators: 'bars',143
144 // By default green-ish or red-ish background are shown on added145 // and deleted lines respectively. Disable that feature here146 disableBackground: false,147
148 // Diffs are split up into hunks, this setting customizes what149 // to show between each hunk.150 //151 // 'line-info' (default):152 // Shows a bar that tells you how many lines are collapsed. If153 // you are using the oldFile/newFile API then you can click those154 // bars to expand the content between them155 //156 // 'metadata':157 // Shows the content you'd see in a normal patch file, usually in158 // some format like '@@ -60,6 +60,22 @@'. You cannot use these to159 // expand hidden content160 //161 // 'simple':162 // Just a subtle bar separator between each hunk163 hunkSeparators: 'line-info',164
165 // On lines that have both additions and deletions, we can run a166 // separate diff check to mark parts of the lines that change.167 // 'none':168 // Do not show these secondary highlights169 //170 // 'char':171 // Show changes at a per character granularity172 //173 // 'word':174 // Show changes but rounded up to word boundaries175 //176 // 'word-alt' (default):177 // Similar to 'word', however we attempt to minimize single178 // character gaps between highlighted changes179 lineDiffType: 'word-alt',180
181 // If lines exceed these character lengths then we won't perform182 // the line lineDiffType check183 maxLineDiffLength: 1000,184
185 // If any line in the diff exceeds this value then we won't186 // attempt to syntax highlight the diff187 maxLineLengthForHighlighting: 1000,188
189 // Enabling this property will hide the file header with file190 // name and diff stats.191 disableFileHeader: false,192
193 // For the collapsed code between diff hunks, this controls the194 // maximum code revealed per click195 expansionLineCount: 100,196
197 // Enable interactive line selection - users can click line198 // numbers to select lines. Click to select a single line,199 // click and drag to select a range, or hold Shift and click200 // to extend the selection.201 enableLineSelection: false,202
203 // Callback fired when the selection changes (continuously204 // during drag operations).205 onLineSelected(range: SelectedLineRange | null) {206 console.log('Selection changed:', range);207 },208
209 // Callback fired when user begins a selection interaction210 // (mouse down on a line number).211 onLineSelectionStart(range: SelectedLineRange | null) {212 console.log('Selection started:', range);213 },214
215 // Callback fired when user completes a selection interaction216 // (mouse up). This is useful for triggering actions like217 // adding comment annotations or saving the selection.218 onLineSelectionEnd(range: SelectedLineRange | null) {219 console.log('Selection completed:', range);220 },221
222 // If you pass a `renderHoverUtility` method as a top223 // level prop, the ensures it will will display on hover224 enableHoverUtility: false,225 }}226 />227 );228}The vanilla JavaScript API for Precision Diffs exposes a mix of components and raw classes. The components and the React API are built on many of these foundation classes. The goal has been to abstract away a lot of the heavy lifting when working with Shiki directly and provide a set of standardized APIs that can be used with any framework and even server rendered if necessary.
You can import all of this via the core package @pierre/precision-diffs
There are two core components in the vanilla JavaScript API, FileDiff and File
1import {2 type FileContents,3 FileDiff,4 type DiffLineAnnotation,5} from '@pierre/precision-diffs';6
7const oldFile: FileContents = {8 name: 'filename.ts',9 contents: 'console.log("Hello world")',10};11
12const newFile: FileContents = {13 name: 'filename.ts',14 contents: 'console.warn("Uh oh")',15};16
17interface ThreadMetadata {18 threadId: string;19}20
21// Annotation metadata can be typed any way you'd like22const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [23 {24 side: 'additions',25 // The line number specified for an annotation is the visual line26 // number you see in the number column of a diff27 lineNumber: 16,28 metadata: { threadId: '68b329da9893e34099c7d8ad5cb9c940' },29 },30];31
32const instance = new FileDiff<ThreadMetadata>({33 // You can provide a 'theme' prop that maps to any34 // built in shiki theme or you can register a custom35 // theme. We also include 2 custom themes36 //37 // 'pierre-dark' and 'pierre-light38 //39 // You can also pass an object with 'dark' and 'light' keys40 // to theme based on OS or 'themeType' setting below.41 //42 // By default we initialize with our custom pierre themes43 // for dark and light theme44 //45 // For the rest of the available shiki themes, either check46 // typescript autocomplete or visit:47 // https://shiki.style/themes48 theme: { dark: 'pierre-dark', light: 'pierre-light' },49
50 // When using the 'theme' prop that specifies dark and light51 // themes, 'themeType' allows you to force 'dark' or 'light'52 // theme, or inherit from the OS ('system') theme.53 themeType: 'system',54
55 // Disable the line numbers for your diffs, generally not recommended56 disableLineNumbers: false,57
58 // Whether code should 'wrap' with long lines or 'scroll'.59 overflow: 'scroll',60
61 // Normally you shouldn't need this prop, but if you don't provide a62 // valid filename or your file doesn't have an extension you may want63 // to override the automatic detection. You can specify that64 // language here:65 // https://shiki.style/languages66 // lang?: SupportedLanguages;67
68 // 'diffStyle' controls whether the diff is presented side by side or69 // in a unified (single column) view70 diffStyle: 'split',71
72 // Unchanged context regions are collapsed by default, set this73 // to true to force them to always render. This depends on using74 // the oldFile/newFile API or FileDiffMetadata including newLines.75 expandUnchanged: false,76
77 // Line decorators to help highlight changes.78 // 'bars' (default):79 // Shows some red-ish or green-ish (theme dependent) bars on the left80 // edge of relevant lines81 //82 // 'classic':83 // shows '+' characters on additions and '-' characters on deletions84 //85 // 'none':86 // No special diff indicators are shown87 diffIndicators: 'bars',88
89 // By default green-ish or red-ish background are shown on added and90 // deleted lines respectively. Disable that feature here91 disableBackground: false,92
93 // Diffs are split up into hunks, this setting customizes what to94 // show between each hunk.95 //96 // 'line-info' (default):97 // Shows a bar that tells you how many lines are collapsed. If you98 // are using the oldFile/newFile API then you can click those bars99 // to expand the content between them100 //101 // (hunk: HunkData) => HTMLElement | DocumentFragment:102 // If you want to fully customize what gets displayed for hunks you103 // can pass a custom function to generate dom nodes to render.104 // 'hunkData' will include the number of lines collapsed as well as105 // the 'type' of column you are rendering into. Bear in the elements106 // you return will be subject to the css grid of the document, and107 // if you want to prevent the elements from scrolling with content108 // you will need to use a few tricks. See a code example below this109 // file example. Click to expand will happen automatically.110 //111 // 'metadata':112 // Shows the content you'd see in a normal patch file, usually in113 // some format like '@@ -60,6 +60,22 @@'. You cannot use these to114 // expand hidden content115 //116 // 'simple':117 // Just a subtle bar separator between each hunk118 hunkSeparators: 'line-info',119
120 // On lines that have both additions and deletions, we can run a121 // separate diff check to mark parts of the lines that change.122 // 'none':123 // Do not show these secondary highlights124 //125 // 'char':126 // Show changes at a per character granularity127 //128 // 'word':129 // Show changes but rounded up to word boundaries130 //131 // 'word-alt' (default):132 // Similar to 'word', however we attempt to minimize single character133 // gaps between highlighted changes134 lineDiffType: 'word-alt',135
136 // If lines exceed these character lengths then we won't perform the137 // line lineDiffType check138 maxLineDiffLength: 1000,139
140 // If any line in the diff exceeds this value then we won't attempt to141 // syntax highlight the diff142 maxLineLengthForHighlighting: 1000,143
144 // Enabling this property will hide the file header with file name and145 // diff stats.146 disableFileHeader: false,147
148 // For the collapsed code between diff hunks, this controls the 149 // maximum code revealed per click150 expansionLineCount: 100,151
152 // You can optionally pass a render function for rendering out line153 // annotations. Just return the dom node to render154 renderAnnotation(155 annotation: DiffLineAnnotation<ThreadMetadata>156 ): HTMLElement {157 // Despite the diff itself being rendered in the shadow dom,158 // annotations are inserted via the web components 'slots' api and you159 // can use all your normal normal css and styling for them160 const element = document.createElement('div');161 element.innerText = annotation.metadata.threadId;162 return element;163 },164
165 // Enable interactive line selection - users can click line166 // numbers to select lines. Click to select a single line,167 // click and drag to select a range, or hold Shift and click168 // to extend the selection.169 enableLineSelection: false,170
171 // Callback fired when the selection changes (continuously172 // during drag operations).173 onLineSelected(range: SelectedLineRange | null) {174 console.log('Selection changed:', range);175 },176
177 // Callback fired when user begins a selection interaction178 // (mouse down on a line number).179 onLineSelectionStart(range: SelectedLineRange | null) {180 console.log('Selection started:', range);181 },182
183 // Callback fired when user completes a selection interaction184 // (mouse up). This is useful for triggering actions like185 // adding comment annotations or saving the selection.186 onLineSelectionEnd(range: SelectedLineRange | null) {187 console.log('Selection completed:', range);188 },189
190 // If you pass a `renderHoverUtility` method as an option,191 // this ensures it will will display on hover192 enableHoverUtility: true,193
194 // You must pass `enableHoverUtility: true` as well to enable 195 // or disable this functionality. This API allows you to render196 // some UI in the number column when the user is hovered over197 // the line. This is not a reactive API, in other words,198 // render is not called every time you mouse over a new line199 // (by design). You can call `getHoveredLine()` in a click200 // handler to know what line is hovered.201 renderHoverUtility(getHoveredLine): HTMLElement {202 const button = document.createElement('button');203 button.innerText = '+';204 button.addEventListener('click', () => {205 console.log(206 'you clicked on line:',207 getHoveredLine().lineNumber,208 'on side:',209 getHoveredLine().side // 'additions' | 'deletions'210 );211 })212 return button;213 }214
215});216
217// If you ever want to update the options for an instance, simple call218// 'setOptions' with the new options. Bear in mind, this does NOT merge219// existing properties, it's a full replace220instance.setOptions({221 ...instance.options,222 theme: 'pierre-dark',223});224
225// When ready to render, simply call .render with old/new file, optional226// annotations and a container element to hold the diff227await instance.render({228 oldFile,229 newFile,230 lineAnnotations,231 containerWrapper: document.body,232});233
234// Programmatically control which lines are selected. Selections should235// be stable across 'split' and 'unified' diff styles.236// 'start' and 'end' map to the visual line numbers you see in the237// number column. 'side' and 'endSide' are considered optional.238// 'side' will default to the 'additions' side. 'endSide' will239// default to whatever 'side' is unless you specify otherwise.240instance.setSelectedLines({241 start: 12,242 side: 'additions',243 end: 22,244 endSide: 'deletions'245});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:
1import { FileDiff } from '@pierre/precision-diffs';2
3// A hunk separator that utilizes the existing grid to have4// a number column and a content column where neither will5// scroll with the code6const instance = new FileDiff({7 hunkSeparators(hunkData: HunkData) {8 const fragment = document.createDocumentFragment();9 const numCol = document.createElement('div');10 numCol.textContent = `${hunkData.lines}`;11 numCol.style.position = 'sticky';12 numCol.style.left = '0';13 numCol.style.backgroundColor = 'var(--pjs-bg)';14 numCol.style.zIndex = '2';15 fragment.appendChild(numCol);16 const contentCol = document.createElement('div');17 contentCol.textContent = 'unmodified lines';18 contentCol.style.position = 'sticky';19 contentCol.style.width = 'var(--pjs-column-content-width)';20 contentCol.style.left = 'var(--pjs-column-number-width)';21 fragment.appendChild(contentCol);22 return fragment;23 },24})25
26// If you want to create a single column that spans both colums27// and doesn't scroll, you can do something like this:28const instance2 = new FileDiff({29 hunkSeparators(hunkData: HunkData) {30 const wrapper = document.createElement('div');31 wrapper.style.gridColumn = 'span 2';32 const contentCol = document.createElement('div');33 contentCol.textContent = `${hunkData.lines} unmodified lines`;34 contentCol.style.position = 'sticky';35 contentCol.style.width = 'var(--pjs-column-width)';36 contentCol.style.left = '0';37 wrapper.appendChild(contentCol);38 return wrapper;39 },40})41
42// If you want to create a single column that's aligned with the content43// column and doesn't scroll, you can do something like this:44const instance2 = new FileDiff({45 hunkSeparators(hunkData: HunkData) {46 const wrapper = document.createElement('div');47 wrapper.style.gridColumn = '2 / 3';48 wrapper.textContent = `${hunkData.lines} unmodified lines`;49 wrapper.style.position = 'sticky';50 wrapper.style.width = 'var(--pjs-column-content-width)';51 wrapper.style.left = 'var(--pjs-column-number-width)';52 return wrapper;53 },54})These core classes can be thought of as the building blocks for the different components and APIs in Precision Diffs. Most of them should be usable in a variety of environments (server and browser).
Essentially a class that takes FileDiffMetadata data structure and can render out the raw hast elements of the code which can be subsequently rendered as HTML strings or transformed further. You can generate FileDiffMetadata via parseDiffFromFile or parsePatchFiles utility functions.
1import {2 DiffHunksRenderer,3 type FileDiffMetadata,4 type HunksRenderResult,5 parseDiffFromFile,6} from '@pierre/precision-diffs';7
8const instance = new DiffHunksRenderer();9
10// this API is a full replacement of any existing options, it will11// not merge in existing options already set12instance.setOptions({ theme: 'github-dark', diffStyle: 'split' });13
14// Parse diff content from 2 versions of a file15const fileDiff: FileDiffMetadata = parseDiffFromFile(16 { name: 'file.ts', contents: 'const greeting = "Hello";' },17 { name: 'file.ts', contents: 'const greeting = "Hello, World!";' }18);19
20// Render hunks21const result: HunksRenderResult | undefined =22 await instance.render(fileDiff);23
24// Depending on your diffStyle settings and depending the type of25// changes, you'll get raw hast nodes for each line for each column26// type based on your settings. If your diffStyle is 'unified',27// then additionsAST and deletionsAST will be undefined and 'split'28// will be the inverse29console.log(result?.additionsAST);30console.log(result?.deletionsAST);31console.log(result?.unifiedAST);32
33// There are 2 utility methods on the instance to render these hast34// nodes to html, '.renderFullHTML' and '.renderPartialHTML'Because it‘s important to re-use your highlighter instance when using Shiki, we‘ve ensured that all the classes and components you use with Precision Diffs will automatically use a shared highlighter instance and also automatically load languages and themes on demand as necessary.
We provide APIs to preload the highlighter, themes, and languages if you want to have that ready before rendering. Also there are some cleanup utilities if you want to be memory conscious.
Shiki comes with a lot of built-in themes, but if you would like to use your own custom or modified theme, you simply have to register it and then it‘ll just work as any other built-in theme.
1import {2 getSharedHighlighter,3 preloadHighlighter,4 registerCustomTheme,5 disposeHighlighter6} from '@pierre/precision-diffs';7
8// Preload themes and languages9await preloadHighlighter({10 themes: ['pierre-dark', 'github-light'],11 langs: ['typescript', 'python', 'rust']12});13
14// Register custom themes (make sure the name you pass15// for your theme and the name in your shiki json theme16// are identical)17registerCustomTheme('my-custom-theme', () => import('./theme.json'));18
19// Get the shared highlighter instance20const highlighter = await getSharedHighlighter();21
22// Cleanup when shutting down. Just note that if you call this,23// all themes and languages will have to be reloaded24disposeHighlighter();Diff and code are rendered using shadow DOM APIs. This means that the styles applied to the diffs will be well isolated from your page's existing CSS. However, it also means if you want to customize the built-in styles, you'll have to utilize some custom CSS variables. These can be done either in your global CSS, as style props on parent components, or on the FileDiff component directly.
1:root {2 /* Available Custom CSS Variables. Most should be self explanatory */3 /* Sets code font, very important */4 --pjs-font-family: 'Berkeley Mono', monospace;5 --pjs-font-size: 14px;6 --pjs-line-height: 1.5;7 /* Controls tab character size */8 --pjs-tab-size: 2;9 /* Font used in header and separator components,10 * typically not a monospace font, but it's your call */11 --pjs-header-font-family: Helvetica;12 /* Override or customize any 'font-feature-settings'13 * for your code font */14 --pjs-font-features: normal;15 /* Override the minimum width for the number column. Be default16 * it should accomodate 4 numbers with padding at a value 17 * of 7ch (the default) */18 --pjs-min-number-column-width: 7ch;19
20 /* By default we try to inherit the deletion/addition/modified21 * colors from the existing Shiki theme, however if you'd like22 * to override them, you can do so via these css variables: */23 --pjs-deletion-color-override: orange;24 --pjs-addition-color-override: yellow;25 --pjs-modified-color-override: purple;26
27 /* Line selection colors - customize the highlighting when users28 * select lines via enableLineSelection. These support light-dark()29 * for automatic theme adaptation. */30 --pjs-selection-color-override: rgb(37, 99, 235);31 --pjs-bg-selection-override: rgba(147, 197, 253, 0.28);32 --pjs-bg-selection-number-override: rgba(96, 165, 250, 0.55);33 --pjs-bg-selection-background-override: rgba(96, 165, 250, 0.2);34 --pjs-bg-selection-number-background-override: rgba(59, 130, 246, 0.4);35
36 /* Some basic variables for tweaking the layouts of some of the built in37 * components */38 --pjs-gap-inline: 8px;39 --pjs-gap-block: 8px;40}1<FileDiff2 style={{3 '--pjs-font-family': 'JetBrains Mono, monospace',4 '--pjs-font-size': '13px'5 } as React.CSSProperties}6 // ... other props7/>Precision Diffs supports Server-Side Rendering (SSR) for improved performance and SEO. 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.
The SSR functionality is available from the @pierre/precision-diffs/ssr module:
1import {2 // There are matching preload functions for each react component3 preloadMultiFileDiff,4 preloadFileDiff,5 preloadPatchDiff,6 preloadFile,7} from '@pierre/precision-diffs/ssr';Create a server component that pre-renders the diff using preloadFileDiff:
1// app/diff/page.tsx (Server Component)2import { preloadMultiFileDiff } from '@pierre/precision-diffs/ssr';3import { DiffPage } from './DiffPage';4
5const OLD_FILE = {6 name: 'main.ts',7 contents: `function greet(name: string) {8 console.log("Hello, " + name);9}`,10};11
12const NEW_FILE = {13 name: 'main.ts',14 contents: `function greet(name: string) {15 console.log(\`Hello, \${name}!\`);16}`,17};18
19export default async function DiffRoute() {20 const preloadedFileDiff = await preloadMultiFileDiff({21 oldFile: OLD_FILE,22 newFile: NEW_FILE,23 options: {24 theme: 'pierre-dark',25 diffStyle: 'split',26 diffIndicators: 'bars',27 },28 });29
30 return <DiffPage preloadedFileDiff={preloadedFileDiff} />;31}The preloadFileDiff function returns a PreloadedFileDiffResult object containing the original oldFile, newFile, options, and annotations you passed in, plus a prerenderedHTML string with the fully syntax-highlighted diff. This object can be spread directly into the React or raw JS component's for automatic hydration.
Create a client component that hydrates and displays the pre-rendered diff:
1// app/diff/DiffPage.tsx (Client Component)2'use client';3
4import { MultiFileDiff } from '@pierre/precision-diffs/react';5import type { PreloadMultiFileDiffResult } from '@pierre/precision-diffs/ssr';6
7interface DiffPageProps {8 preloadedFileDiff: PreloadMultiFileDiffResult;9}10
11export function DiffPage({ preloadedFileDiff }: DiffPageProps) {12 return (13 <MultiFileDiff14 {...preloadedFileDiff}15 className="overflow-hidden rounded-lg border"16 />17 );18}prerenderedHTML includes inline styles for the theme, eliminating FOUC (Flash of Unstyled Content)