1import { css } from '@emotion/react'; 2import { spacing, theme, typography } from '@expo/styleguide'; 3import React, { useEffect, useState, PropsWithChildren } from 'react'; 4import { parseDiff, Diff, Hunk } from 'react-diff-view'; 5 6import { Snippet } from '../Snippet'; 7import { SnippetContent } from '../SnippetContent'; 8import { SnippetHeader } from '../SnippetHeader'; 9 10const randomCommitHash = () => Math.random().toString(36).slice(2, 9); 11 12// These types come from `react-diff-view` library 13type RenderLine = { 14 oldRevision: string; 15 newRevision: string; 16 type: 'unified' | 'split'; 17 hunks: object[]; 18 newPath: string; 19 oldPath: string; 20}; 21 22type Props = PropsWithChildren<{ 23 source?: string; 24 raw?: string; 25}>; 26 27export const DiffBlock = ({ source, raw }: Props) => { 28 const [diff, setDiff] = useState<RenderLine[] | null>(raw ? parseDiff(raw) : null); 29 useEffect(() => { 30 if (source) { 31 const fetchDiffAsync = async () => { 32 const response = await fetch(source); 33 const result = await response.text(); 34 setDiff(parseDiff(result)); 35 }; 36 37 fetchDiffAsync(); 38 } 39 }, [source]); 40 41 if (!diff) { 42 return null; 43 } 44 45 const renderFile = ({ 46 oldRevision = randomCommitHash(), 47 newRevision = randomCommitHash(), 48 type, 49 hunks, 50 newPath, 51 }: RenderLine) => ( 52 <Snippet css={diffContainerStyles} key={oldRevision + '-' + newRevision}> 53 <SnippetHeader title={newPath} /> 54 <SnippetContent skipPadding hideOverflow> 55 <Diff viewType="unified" diffType={type} hunks={hunks}> 56 {(hunks: any[]) => hunks.map(hunk => <Hunk key={hunk.content} hunk={hunk} />)} 57 </Diff> 58 </SnippetContent> 59 </Snippet> 60 ); 61 62 return <>{diff.map(renderFile)}</>; 63}; 64 65const diffContainerStyles = css` 66 table { 67 ${typography.fontSizes[14]} 68 } 69 70 td, 71 th { 72 border-bottom: none; 73 } 74 75 .diff-line:first-of-type { 76 height: 29px; 77 78 td { 79 padding-top: ${spacing[2]}px; 80 } 81 } 82 83 .diff-line:last-of-type { 84 height: 29px; 85 } 86 87 .diff-gutter-col { 88 width: ${spacing[10]}px; 89 background: ${theme.background.tertiary}; 90 } 91 92 .diff-gutter-normal { 93 color: ${theme.icon.secondary}; 94 } 95 96 .diff-code { 97 word-break: break-word; 98 padding-left: ${spacing[4]}px; 99 } 100 101 .diff-gutter-insert, 102 .diff-code-insert { 103 background: ${theme.palette.green['000']}; 104 color: ${theme.text.success}; 105 } 106 107 .diff-gutter-insert { 108 background: ${theme.background.success}; 109 } 110 111 .diff-gutter-delete, 112 .diff-code-delete { 113 background: ${theme.palette.red['000']}; 114 color: ${theme.text.error}; 115 } 116 117 .diff-gutter-delete { 118 background: ${theme.background.error}; 119 } 120 121 [data-expo-theme='dark'] & { 122 .diff-gutter-insert { 123 background: ${theme.palette.green['100']}; 124 } 125 126 .diff-gutter-delete { 127 background: ${theme.palette.red['100']}; 128 } 129 } 130`; 131