Skip to content

Commit 1980ee7

Browse files
[docs] Link to docs on PRs (mui#394)
Signed-off-by: Olivier Tassinari <[email protected]> Co-authored-by: Michał Dudak <[email protected]>
1 parent 7159b04 commit 1980ee7

File tree

2 files changed

+15
-243
lines changed

2 files changed

+15
-243
lines changed

.circleci/config.yml

+13
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,15 @@ jobs:
405405
- run:
406406
name: pnpm test:e2e
407407
command: pnpm test:e2e
408+
test_bundle_size_monitor:
409+
<<: *default-job
410+
steps:
411+
- checkout
412+
- install_js
413+
- run:
414+
name: Run danger on PRs
415+
command: pnpm danger ci --fail-on-errors
416+
# TODO test bundle size https://github.com/mui/base-ui/issues/201
408417
workflows:
409418
version: 2
410419
pipeline:
@@ -441,6 +450,10 @@ workflows:
441450
<<: *default-context
442451
requires:
443452
- checkout
453+
- test_bundle_size_monitor:
454+
<<: *default-context
455+
requires:
456+
- checkout
444457
profile:
445458
when:
446459
equal: [profile, << pipeline.parameters.workflow >>]

dangerfile.ts

+2-243
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,18 @@
1-
// inspire by reacts dangerfile
21
// danger has to be the first thing required!
32
import { danger, markdown } from 'danger';
4-
// eslint-disable-next-line no-restricted-imports
5-
import replaceUrl from '@mui/monorepo/packages/api-docs-builder/utils/replaceUrl';
6-
import { exec } from 'child_process';
7-
import { loadComparison } from './scripts/sizeSnapshot';
8-
9-
const circleCIBuildNumber = process.env.CIRCLE_BUILD_NUM;
10-
const circleCIBuildUrl = `https://app.circleci.com/pipelines/github/mui/base-ui/jobs/${circleCIBuildNumber}`;
11-
const dangerCommand = process.env.DANGER_COMMAND;
12-
13-
const parsedSizeChangeThreshold = 300;
14-
const gzipSizeChangeThreshold = 100;
15-
16-
/**
17-
* executes a git subcommand
18-
* @param {any} args
19-
*/
20-
function git(args: any) {
21-
return new Promise((resolve, reject) => {
22-
exec(`git ${args}`, (err, stdout) => {
23-
if (err) {
24-
reject(err);
25-
} else {
26-
resolve(stdout.trim());
27-
}
28-
});
29-
});
30-
}
31-
32-
const UPSTREAM_REMOTE = 'danger-upstream';
33-
34-
/**
35-
* This is mainly used for local development. It should be executed before the
36-
* scripts exit to avoid adding internal remotes to the local machine. This is
37-
* not an issue in CI.
38-
*/
39-
async function reportBundleSizeCleanup() {
40-
await git(`remote remove ${UPSTREAM_REMOTE}`);
41-
}
42-
43-
/**
44-
* creates a callback for Object.entries(comparison).filter that excludes every
45-
* entry that does not exceed the given threshold values for parsed and gzip size
46-
* @param {number} parsedThreshold
47-
* @param {number} gzipThreshold
48-
*/
49-
function createComparisonFilter(parsedThreshold: number, gzipThreshold: number) {
50-
return (comparisonEntry: any) => {
51-
const [, snapshot] = comparisonEntry;
52-
return (
53-
Math.abs(snapshot.parsed.absoluteDiff) >= parsedThreshold ||
54-
Math.abs(snapshot.gzip.absoluteDiff) >= gzipThreshold
55-
);
56-
};
57-
}
58-
59-
/**
60-
* checks if the bundle is of a package e.b. `@mui/material` but not
61-
* `@mui/material/Paper`
62-
* @param {[string, any]} comparisonEntry
63-
*/
64-
function isPackageComparison(comparisonEntry: [string, any]) {
65-
const [bundleKey] = comparisonEntry;
66-
return /^@[\w-]+\/[\w-]+$/.test(bundleKey);
67-
}
68-
69-
/**
70-
* Generates a user-readable string from a percentage change
71-
* @param {number} change
72-
* @param {string} goodEmoji emoji on reduction
73-
* @param {string} badEmoji emoji on increase
74-
*/
75-
function addPercent(change: number, goodEmoji = '', badEmoji = ':small_red_triangle:') {
76-
const formatted = (change * 100).toFixed(2);
77-
if (/^-|^0(?:\.0+)$/.test(formatted)) {
78-
return `${formatted}% ${goodEmoji}`;
79-
}
80-
return `+${formatted}% ${badEmoji}`;
81-
}
82-
83-
function generateEmphasizedChange([bundle, { parsed, gzip }]: [
84-
string,
85-
{ parsed: { relativeDiff: number }; gzip: { relativeDiff: number } },
86-
]) {
87-
// increase might be a bug fix which is a nice thing. reductions are always nice
88-
const changeParsed = addPercent(parsed.relativeDiff, ':heart_eyes:', '');
89-
const changeGzip = addPercent(gzip.relativeDiff, ':heart_eyes:', '');
90-
91-
return `**${bundle}**: parsed: ${changeParsed}, gzip: ${changeGzip}`;
92-
}
93-
94-
/**
95-
* Puts results in different buckets wh
96-
* @param {*} results
97-
*/
98-
function sieveResults<T>(results: Array<[string, T]>) {
99-
const main: [string, T][] = [];
100-
const pages: [string, T][] = [];
101-
102-
results.forEach((entry) => {
103-
const [bundleId] = entry;
104-
105-
if (bundleId.startsWith('docs:')) {
106-
pages.push(entry);
107-
} else {
108-
main.push(entry);
109-
}
110-
});
111-
112-
return { all: results, main, pages };
113-
}
114-
115-
function prepareBundleSizeReport() {
116-
markdown(
117-
`## Bundle size report
118-
119-
Bundle size will be reported once [CircleCI build #${circleCIBuildNumber}](${circleCIBuildUrl}) finishes.`,
120-
);
121-
}
122-
123-
// A previous build might have failed to produce a snapshot
124-
// Let's walk up the tree a bit until we find a commit that has a successful snapshot
125-
async function loadLastComparison(
126-
upstreamRef: any,
127-
n = 0,
128-
): Promise<Awaited<ReturnType<typeof loadComparison>>> {
129-
const mergeBaseCommit = await git(`merge-base HEAD~${n} ${UPSTREAM_REMOTE}/${upstreamRef}`);
130-
try {
131-
return await loadComparison(mergeBaseCommit, upstreamRef);
132-
} catch (err) {
133-
if (n >= 5) {
134-
throw err;
135-
}
136-
return loadLastComparison(upstreamRef, n + 1);
137-
}
138-
}
139-
140-
async function reportBundleSize() {
141-
// Use git locally to grab the commit which represents the place
142-
// where the branches differ
143-
const upstreamRepo = danger.github.pr.base.repo.full_name;
144-
const upstreamRef = danger.github.pr.base.ref;
145-
try {
146-
await git(`remote add ${UPSTREAM_REMOTE} https://github.com/${upstreamRepo}.git`);
147-
} catch (err) {
148-
// ignore if it already exist for local testing
149-
}
150-
await git(`fetch ${UPSTREAM_REMOTE}`);
151-
152-
const comparison = await loadLastComparison(upstreamRef);
153-
154-
const detailedComparisonQuery = `circleCIBuildNumber=${circleCIBuildNumber}&baseRef=${danger.github.pr.base.ref}&baseCommit=${comparison.previous}&prNumber=${danger.github.pr.number}`;
155-
const detailedComparisonToolpadUrl = `https://tools-public.onrender.com/prod/pages/h71gdad?${detailedComparisonQuery}`;
156-
const detailedComparisonRoute = `/size-comparison?${detailedComparisonQuery}`;
157-
const detailedComparisonUrl = `https://mui-dashboard.netlify.app${detailedComparisonRoute}`;
158-
159-
const { all: allResults, main: mainResults } = sieveResults(Object.entries(comparison.bundles));
160-
const anyResultsChanges = allResults.filter(createComparisonFilter(1, 1));
161-
162-
if (anyResultsChanges.length > 0) {
163-
const importantChanges = mainResults
164-
.filter(createComparisonFilter(parsedSizeChangeThreshold, gzipSizeChangeThreshold))
165-
.filter(isPackageComparison)
166-
.map(generateEmphasizedChange);
167-
168-
// have to guard against empty strings
169-
if (importantChanges.length > 0) {
170-
markdown(importantChanges.join('\n'));
171-
}
172-
173-
const details = `## Bundle size report
174-
175-
[Details of bundle changes (Toolpad)](${detailedComparisonToolpadUrl})
176-
[Details of bundle changes](${detailedComparisonUrl})`;
177-
178-
markdown(details);
179-
} else {
180-
markdown(`## Bundle size report
181-
182-
[No bundle size changes (Toolpad)](${detailedComparisonToolpadUrl})
183-
[No bundle size changes](${detailedComparisonUrl})`);
184-
}
185-
}
1863

1874
function addDeployPreviewUrls() {
188-
/**
189-
* The incoming path from danger does not start with `/`
190-
* e.g. ['docs/data/joy/components/button/button.md']
191-
*/
192-
function formatFileToLink(path: string) {
193-
let url = path.replace('docs/data', '').replace(/\.md$/, '');
194-
195-
const fragments = url.split('/').reverse();
196-
if (fragments[0] === fragments[1]) {
197-
// check if the end of pathname is the same as the one before
198-
// for example `/data/material/getting-started/overview/overview.md
199-
url = fragments.slice(1).reverse().join('/');
200-
}
201-
202-
if (url.startsWith('/material')) {
203-
// needs to convert to correct material legacy folder structure to the existing url.
204-
url = replaceUrl(url.replace('/material', ''), '/material-ui').replace(/^\//, '');
205-
} else {
206-
url = url
207-
.replace(/^\//, '') // remove initial `/`
208-
.replace('joy/', 'joy-ui/')
209-
.replace('components/', 'react-');
210-
}
211-
212-
return url;
213-
}
214-
215-
const netlifyPreview = `https://deploy-preview-${danger.github.pr.number}--material-ui.netlify.app/`;
216-
217-
const files = [...danger.git.created_files, ...danger.git.modified_files];
218-
219-
// limit to the first 5 docs
220-
const docs = files
221-
.filter((file) => file.startsWith('docs/data') && file.endsWith('.md'))
222-
.slice(0, 5);
5+
const netlifyPreview = `https://deploy-preview-${danger.github.pr.number}--base-ui.netlify.app/`;
2236

2247
markdown(`
2258
## Netlify deploy preview
2269
227-
${
228-
docs.length
229-
? docs
230-
.map((path) => {
231-
const formattedUrl = formatFileToLink(path);
232-
return `- [${path}](${netlifyPreview}${formattedUrl})`;
233-
})
234-
.join('\n')
235-
: netlifyPreview
236-
}
10+
${netlifyPreview}
23711
`);
23812
}
23913

24014
async function run() {
24115
addDeployPreviewUrls();
242-
243-
switch (dangerCommand) {
244-
case 'prepareBundleSizeReport':
245-
prepareBundleSizeReport();
246-
break;
247-
case 'reportBundleSize':
248-
try {
249-
await reportBundleSize();
250-
} finally {
251-
await reportBundleSizeCleanup();
252-
}
253-
break;
254-
default:
255-
throw new TypeError(`Unrecognized danger command '${dangerCommand}'`);
256-
}
25716
}
25817

25918
run().catch((error) => {

0 commit comments

Comments
 (0)