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