Skip to content

Commit 97c6279

Browse files
authored
[code-infra] Add package diff tool (#672)
Signed-off-by: Jan Potoms <[email protected]>
1 parent b4c5644 commit 97c6279

File tree

15 files changed

+1299
-70
lines changed

15 files changed

+1299
-70
lines changed

apps/code-infra-dashboard/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111
"@netlify/functions": "^4.2.5",
1212
"@octokit/rest": "^22.0.0",
1313
"@tanstack/react-query": "^5.87.4",
14+
"diff": "^8.0.2",
1415
"etag": "^1.8.1",
16+
"pako": "^2.1.0",
1517
"react": "^19.1.1",
1618
"react-dom": "^19.1.1",
1719
"react-router": "^7.9.1",
1820
"semver": "^7.7.2"
1921
},
2022
"devDependencies": {
23+
"@types/diff": "^8.0.0",
2124
"@types/etag": "^1.8.4",
2225
"@types/node": "^22.18.3",
26+
"@types/pako": "^2.0.4",
2327
"@types/react": "^19.1.13",
2428
"@types/react-dom": "^19.1.9",
2529
"@types/semver": "^7.7.1",

apps/code-infra-dashboard/src/App.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ import './index.css';
1818

1919
const Landing = React.lazy(() => import('./pages/Landing'));
2020
const SizeComparison = React.lazy(() => import('./pages/SizeComparison'));
21+
const RepositoryLayout = React.lazy(() => import('./pages/RepositoryLayout'));
2122
const RepositoryPRs = React.lazy(() => import('./pages/RepositoryPRs'));
23+
const RepositoryPR = React.lazy(() => import('./pages/RepositoryPR'));
24+
const RepositoryCharts = React.lazy(() => import('./pages/RepositoryCharts'));
2225
const NpmVersions = React.lazy(() => import('./pages/NpmVersions'));
26+
const DiffPackage = React.lazy(() => import('./pages/DiffPackage'));
2327

2428
// Redirect component for size comparison with query params
2529
function SizeComparisonRedirect() {
@@ -206,13 +210,39 @@ function App() {
206210
}
207211
/>
208212
<Route
209-
path="/size-comparison/:owner/:repo"
213+
path="/repository/:owner/:repo"
210214
element={
211215
<React.Suspense fallback={<div>Loading...</div>}>
212-
<RepositoryPRs />
216+
<RepositoryLayout />
213217
</React.Suspense>
214218
}
215-
/>
219+
>
220+
<Route
221+
path="prs"
222+
element={
223+
<React.Suspense fallback={<div>Loading...</div>}>
224+
<RepositoryPRs />
225+
</React.Suspense>
226+
}
227+
/>
228+
<Route
229+
path="prs/:prNumber"
230+
element={
231+
<React.Suspense fallback={<div>Loading...</div>}>
232+
<RepositoryPR />
233+
</React.Suspense>
234+
}
235+
/>
236+
<Route
237+
path="bundle-size"
238+
element={
239+
<React.Suspense fallback={<div>Loading...</div>}>
240+
<RepositoryCharts />
241+
</React.Suspense>
242+
}
243+
/>
244+
<Route index element={<Navigate replace to="prs" />} />
245+
</Route>
216246
<Route
217247
path="/npm-versions"
218248
element={
@@ -221,6 +251,14 @@ function App() {
221251
</React.Suspense>
222252
}
223253
/>
254+
<Route
255+
path="/diff-package"
256+
element={
257+
<React.Suspense fallback={<div>Loading...</div>}>
258+
<DiffPackage />
259+
</React.Suspense>
260+
}
261+
/>
224262
</Routes>
225263
</Container>
226264
</BrowserRouter>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import * as React from 'react';
2+
import Paper from '@mui/material/Paper';
3+
import Box from '@mui/material/Box';
4+
import Typography from '@mui/material/Typography';
5+
import { useTheme } from '@mui/material/styles';
6+
import * as diff from 'diff';
7+
8+
interface FileDiffProps {
9+
oldValue: string;
10+
newValue: string;
11+
filePath: string;
12+
oldHeader: string;
13+
newHeader: string;
14+
ignoreWhitespace: boolean;
15+
wrapLines?: boolean;
16+
}
17+
18+
function getLineClass(line: string, index: number): string | null {
19+
if (index < 2) {
20+
return 'diff-preamble';
21+
}
22+
23+
if (line.startsWith('+')) {
24+
return 'diff-added';
25+
}
26+
27+
if (line.startsWith('-')) {
28+
return 'diff-removed';
29+
}
30+
31+
if (line.startsWith('@@')) {
32+
return 'diff-hunk';
33+
}
34+
35+
return null;
36+
}
37+
38+
export default function FileDiff({
39+
oldValue,
40+
newValue,
41+
filePath,
42+
oldHeader,
43+
newHeader,
44+
ignoreWhitespace,
45+
wrapLines = false,
46+
}: FileDiffProps) {
47+
const theme = useTheme();
48+
49+
const { headerLine, renderDiffLines } = React.useMemo(() => {
50+
const fileDiff = diff.createPatch(filePath, oldValue, newValue, oldHeader, newHeader, {
51+
ignoreWhitespace,
52+
});
53+
54+
const lines = fileDiff.split('\n');
55+
const rawHeaderLine = lines[0] || '';
56+
const contentLines = lines.slice(2); // Skip first two lines
57+
58+
const renderedLines = contentLines.map((line, index) => {
59+
const className = getLineClass(line, index);
60+
const content = `${line}\n`;
61+
62+
if (className) {
63+
return (
64+
<span key={index} className={className}>
65+
{content}
66+
</span>
67+
);
68+
}
69+
70+
return content;
71+
});
72+
73+
return {
74+
headerLine: rawHeaderLine.replace(/^Index: /, ''),
75+
renderDiffLines: renderedLines,
76+
};
77+
}, [oldValue, newValue, filePath, oldHeader, newHeader, ignoreWhitespace]);
78+
79+
return (
80+
<Paper sx={{ overflow: 'hidden' }}>
81+
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
82+
<Typography variant="subtitle2" fontFamily="monospace" color="text.secondary">
83+
{headerLine}
84+
</Typography>
85+
</Box>
86+
<Box
87+
sx={{
88+
code: {
89+
minWidth: '100%',
90+
width: 'fit-content',
91+
display: 'block',
92+
},
93+
'& .diff-added': {
94+
backgroundColor: theme.palette.mode === 'dark' ? '#1e3a20' : '#d4edda',
95+
color: theme.palette.mode === 'dark' ? '#a3d9a5' : '#155724',
96+
display: 'block',
97+
},
98+
'& .diff-removed': {
99+
backgroundColor: theme.palette.mode === 'dark' ? '#3a1e1e' : '#f8d7da',
100+
color: theme.palette.mode === 'dark' ? '#f1a1a1' : '#721c24',
101+
display: 'block',
102+
},
103+
'& .diff-hunk': {
104+
color: theme.palette.primary.main,
105+
opacity: 0.8,
106+
fontWeight: 600,
107+
display: 'block',
108+
},
109+
'& .diff-preamble': {
110+
color: theme.palette.text.secondary,
111+
opacity: 0.6,
112+
display: 'block',
113+
},
114+
}}
115+
>
116+
<pre
117+
style={{
118+
padding: '16px',
119+
margin: 0,
120+
fontSize: '12px',
121+
lineHeight: '1.4',
122+
overflow: wrapLines ? 'visible' : 'auto',
123+
whiteSpace: wrapLines ? 'pre-wrap' : 'pre',
124+
wordBreak: wrapLines ? 'break-all' : 'normal',
125+
}}
126+
>
127+
<code>{renderDiffLines}</code>
128+
</pre>
129+
</Box>
130+
</Paper>
131+
);
132+
}

apps/code-infra-dashboard/src/components/PRList.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import Divider from '@mui/material/Divider';
1111
import Chip from '@mui/material/Chip';
1212
import Skeleton from '@mui/material/Skeleton';
1313
import GitPullRequestIcon from '@mui/icons-material/Commit';
14-
import BarChartIcon from '@mui/icons-material/BarChart';
1514
import { styled } from '@mui/material/styles';
1615
import { GitHubPRInfo } from '../hooks/useGitHubPR';
1716
import ErrorDisplay from './ErrorDisplay';
@@ -83,7 +82,7 @@ function PrRow({ pr, owner, repo, loading = false }: PrRowProps) {
8382
<StyledListItem
8483
// @ts-expect-error https://github.com/mui/material-ui/issues/29875
8584
component={RouterLink}
86-
to={`/size-comparison/${owner}/${repo}/diff?prNumber=${pr.number}`}
85+
to={`/repository/${owner}/${repo}/prs/${pr.number}`}
8786
sx={{
8887
py: 1.5,
8988
color: 'text.primary',
@@ -130,17 +129,6 @@ function PrRow({ pr, owner, repo, loading = false }: PrRowProps) {
130129
<Typography variant="caption" color="text.secondary">
131130
SHA: <code>{pr.head.sha.substring(0, 7)}</code>
132131
</Typography>
133-
<Box
134-
sx={{
135-
display: 'flex',
136-
alignItems: 'center',
137-
color: 'secondary.main',
138-
gap: 0.5,
139-
}}
140-
>
141-
<BarChartIcon fontSize="inherit" />
142-
<Typography variant="caption">View Bundle Size</Typography>
143-
</Box>
144132
</Box>
145133
}
146134
/>
@@ -170,7 +158,7 @@ export default function PRList({
170158
onLoadMore,
171159
}: PRListProps) {
172160
const displayItems = isLoading
173-
? Array.from({ length: 5 }, (_, index) => ({ id: `skeleton-${index}`, pr: null }))
161+
? Array.from({ length: 20 }, (_, index) => ({ id: `skeleton-${index}`, pr: null }))
174162
: prs.map((pr) => ({ id: pr.number, pr }));
175163

176164
return (
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
export interface Repository {
2+
owner: string;
3+
name: string;
4+
displayName: string;
5+
description: string;
6+
packages: string[];
7+
}
8+
9+
export const repositories: Repository[] = [
10+
{
11+
owner: 'mui',
12+
name: 'material-ui',
13+
displayName: 'MUI Core',
14+
description: "React components implementing Google's Material Design",
15+
packages: [
16+
'@mui/base',
17+
'@mui/codemod',
18+
'@mui/core-downloads-tracker',
19+
'@mui/docs',
20+
'@mui/envinfo',
21+
'@mui/icons-material',
22+
'@mui/internal-babel-macros',
23+
'@mui/internal-docs-utils',
24+
'@mui/internal-markdown',
25+
'@mui/internal-scripts',
26+
'@mui/internal-test-utils',
27+
'@mui/joy',
28+
'@mui/lab',
29+
'@mui/material',
30+
'@mui/material-next',
31+
'@mui/material-nextjs',
32+
'@mui/private-theming',
33+
'@mui/styled-engine',
34+
'@mui/styled-engine-sc',
35+
'@mui/stylis-plugin-rtl',
36+
'@mui/system',
37+
'@mui/types',
38+
'@mui/utils',
39+
],
40+
},
41+
{
42+
owner: 'mui',
43+
name: 'mui-x',
44+
displayName: 'MUI X',
45+
description: 'Advanced components for complex use cases',
46+
packages: [
47+
'@mui/x-charts',
48+
'@mui/x-charts-pro',
49+
'@mui/x-charts-vendor',
50+
'@mui/x-codemod',
51+
'@mui/x-data-grid',
52+
'@mui/x-data-grid-generator',
53+
'@mui/x-data-grid-premium',
54+
'@mui/x-data-grid-pro',
55+
'@mui/x-date-pickers',
56+
'@mui/x-date-pickers-pro',
57+
'@mui/x-internal-gestures',
58+
'@mui/x-internals',
59+
'@mui/x-license',
60+
'@mui/x-telemetry',
61+
'@mui/x-tree-view',
62+
'@mui/x-tree-view-pro',
63+
'@mui/x-virtualizer',
64+
],
65+
},
66+
{
67+
owner: 'mui',
68+
name: 'base-ui',
69+
displayName: 'Base UI',
70+
description: 'Unstyled React components and low-level hooks',
71+
packages: ['@base-ui-components/react', '@base-ui-components/utils'],
72+
},
73+
{
74+
owner: 'mui',
75+
name: 'mui-public',
76+
displayName: 'Code infra',
77+
description: 'Public monorepo with shared infrastructure and tooling',
78+
packages: [
79+
'@mui/internal-babel-plugin-display-name',
80+
'@mui/internal-babel-plugin-minify-errors',
81+
'@mui/internal-babel-plugin-resolve-imports',
82+
'@mui/internal-bundle-size-checker',
83+
'@mui/internal-code-infra',
84+
'@mui/internal-docs-infra',
85+
],
86+
},
87+
];

0 commit comments

Comments
 (0)