|
1 |
| -// inspire by reacts dangerfile |
2 | 1 | // danger has to be the first thing required!
|
3 | 2 | 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 |
| -} |
186 | 3 |
|
187 | 4 | 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/`; |
223 | 6 |
|
224 | 7 | markdown(`
|
225 | 8 | ## Netlify deploy preview
|
226 | 9 |
|
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} |
237 | 11 | `);
|
238 | 12 | }
|
239 | 13 |
|
240 | 14 | async function run() {
|
241 | 15 | 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 |
| - } |
257 | 16 | }
|
258 | 17 |
|
259 | 18 | run().catch((error) => {
|
|
0 commit comments