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 // Programmatically control which lines are selected. This51 // allows two-way binding with state. Selections should be 52 // stable across 'split' and 'unified' diff styles.53 // 'start' and 'end' map to the visual line numbers you see in the54 // number column. 'side' and 'endSide' are considered optional.55 // 'side' will default to the 'additions' side. 'endSide' will56 // default to whatever 'side' is unless you specify otherwise.57 selectedLines={{ start: 3, side: 'additions', end: 4, endSide: 'deletions' }}58
59 // Here's every property you can pass to options, with their60 // default values if not specified.61 options={{62 // You can provide a 'theme' prop that maps to any63 // built in shiki theme or you can register a custom64 // theme. We also include 2 custom themes65 //66 // 'pierre-dark' and 'pierre-light67 //68 // You can also pass an object with 'dark' and 'light' keys69 // to theme based on OS or 'themeType' setting below.70 //71 // By default we initialize with our custom pierre themes72 // for dark and light theme73 //74 // For the rest of the available shiki themes, either check75 // typescript autocomplete or visit:76 // https://shiki.style/themes77 theme: { dark: 'pierre-dark', light: 'pierre-light' },78
79 // When using the 'theme' prop that specifies dark and light80 // themes, 'themeType' allows you to force 'dark' or 'light'81 // theme, or inherit from the OS ('system') theme.82 themeType: 'system',83
84 // Disable the line numbers for your diffs, generally85 // not recommended86 disableLineNumbers: false,87
88 // Whether code should 'wrap' with long lines or 'scroll'.89 overflow: 'scroll',90
91 // Normally you shouldn't need this prop, but if you don't92 // provide a valid filename or your file doesn't have an93 // extension you may want to override the automatic detection94 // You can specify that language here:95 // https://shiki.style/languages96 // lang?: SupportedLanguages;97
98 // 'diffStyle' controls whether the diff is presented side by99 // side or in a unified (single column) view100 diffStyle: 'split',101
102 // Unchanged context regions are collapsed by default, set this103 // to true to force them to always render. This depends on using104 // the oldFile/newFile API or FileDiffMetadata including newLines.105 expandUnchanged: false,106
107 // Line decorators to help highlight changes.108 // 'bars' (default):109 // Shows some red-ish or green-ish (theme dependent) bars on the110 // left edge of relevant lines111 //112 // 'classic':113 // shows '+' characters on additions and '-' characters114 // on deletions115 //116 // 'none':117 // No special diff indicators are shown118 diffIndicators: 'bars',119
120 // By default green-ish or red-ish background are shown on added121 // and deleted lines respectively. Disable that feature here122 disableBackground: false,123
124 // Diffs are split up into hunks, this setting customizes what125 // to show between each hunk.126 //127 // 'line-info' (default):128 // Shows a bar that tells you how many lines are collapsed. If129 // you are using the oldFile/newFile API then you can click those130 // bars to expand the content between them131 //132 // 'metadata':133 // Shows the content you'd see in a normal patch file, usually in134 // some format like '@@ -60,6 +60,22 @@'. You cannot use these to135 // expand hidden content136 //137 // 'simple':138 // Just a subtle bar separator between each hunk139 hunkSeparators: 'line-info',140
141 // On lines that have both additions and deletions, we can run a142 // separate diff check to mark parts of the lines that change.143 // 'none':144 // Do not show these secondary highlights145 //146 // 'char':147 // Show changes at a per character granularity148 //149 // 'word':150 // Show changes but rounded up to word boundaries151 //152 // 'word-alt' (default):153 // Similar to 'word', however we attempt to minimize single154 // character gaps between highlighted changes155 lineDiffType: 'word-alt',156
157 // If lines exceed these character lengths then we won't perform158 // the line lineDiffType check159 maxLineDiffLength: 1000,160
161 // If any line in the diff exceeds this value then we won't162 // attempt to syntax highlight the diff163 maxLineLengthForHighlighting: 1000,164
165 // Enabling this property will hide the file header with file166 // name and diff stats.167 disableFileHeader: false,168
169 // For the collapsed code between diff hunks, this controls the170 // maximum code revealed per click171 expansionLineCount: 100,172
173 // Enable interactive line selection - users can click line174 // numbers to select lines. Click to select a single line,175 // click and drag to select a range, or hold Shift and click176 // to extend the selection.177 enableLineSelection: false,178
179 // Callback fired when the selection changes (continuously180 // during drag operations).181 onLineSelected(range: SelectedLineRange | null) {182 console.log('Selection changed:', range);183 },184
185 // Callback fired when user begins a selection interaction186 // (mouse down on a line number).187 onLineSelectionStart(range: SelectedLineRange | null) {188 console.log('Selection started:', range);189 },190
191 // Callback fired when user completes a selection interaction192 // (mouse up). This is useful for triggering actions like193 // adding comment annotations or saving the selection.194 onLineSelectionEnd(range: SelectedLineRange | null) {195 console.log('Selection completed:', range);196 },197 }}198 />199 );200}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
191// If you ever want to update the options for an instance, simple call192// 'setOptions' with the new options. Bear in mind, this does NOT merge193// existing properties, it's a full replace194instance.setOptions({195 ...instance.options,196 theme: 'pierre-dark',197});198
199// When ready to render, simply call .render with old/new file, optional200// annotations and a container element to hold the diff201await instance.render({202 oldFile,203 newFile,204 lineAnnotations,205 containerWrapper: document.body,206});207
208// Programmatically control which lines are selected. Selections should209// be stable across 'split' and 'unified' diff styles.210// 'start' and 'end' map to the visual line numbers you see in the211// number column. 'side' and 'endSide' are considered optional.212// 'side' will default to the 'additions' side. 'endSide' will213// default to whatever 'side' is unless you specify otherwise.214instance.setSelectedLines({215 start: 12,216 side: 'additions',217 end: 22,218 endSide: 'deletions'219});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)