77
88'use strict' ;
99
10+ /* eslint-disable no-for-of-loops/no-for-of-loops */
11+
1012// Hi, if this is your first time editing/reading a Dangerfile, here's a summary:
1113// It's a JS runtime which helps you provide continuous feedback inside GitHub.
1214//
2628// `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865
2729
2830const { markdown, danger, warn} = require ( 'danger' ) ;
29-
30- const { generateResultsArray} = require ( './scripts/rollup/stats' ) ;
31- const { readFileSync, readdirSync} = require ( 'fs' ) ;
32- const path = require ( 'path' ) ;
33-
34- /**
35- * Generates a Markdown table
36- * @param {string[] } headers
37- * @param {string[][] } body
38- */
39- function generateMDTable ( headers , body ) {
40- const tableHeaders = [
41- headers . join ( ' | ' ) ,
42- headers . map ( ( ) => ' --- ' ) . join ( ' | ' ) ,
43- ] ;
44-
45- const tablebody = body . map ( r => r . join ( ' | ' ) ) ;
46- return tableHeaders . join ( '\n' ) + '\n' + tablebody . join ( '\n' ) ;
31+ const { promisify} = require ( 'util' ) ;
32+ const glob = promisify ( require ( 'glob' ) ) ;
33+ const gzipSize = require ( 'gzip-size' ) ;
34+
35+ const { readFileSync, statSync} = require ( 'fs' ) ;
36+
37+ const BASE_DIR = 'base-build' ;
38+ const HEAD_DIR = 'build2' ;
39+
40+ const CRITICAL_THRESHOLD = 0.02 ;
41+ const SIGNIFICANCE_THRESHOLD = 0.002 ;
42+ const CRITICAL_ARTIFACT_PATHS = new Set ( [
43+ // We always report changes to these bundles, even if the change is
44+ // insiginificant or non-existent.
45+ 'oss-stable/react-dom/cjs/react-dom.production.min.js' ,
46+ 'oss-experimental/react-dom/cjs/react-dom.production.min.js' ,
47+ 'facebook-www/ReactDOM-prod.classic.js' ,
48+ 'facebook-www/ReactDOM-prod.modern.js' ,
49+ 'facebook-www/ReactDOMForked-prod.classic.js' ,
50+ ] ) ;
51+
52+ const kilobyteFormatter = new Intl . NumberFormat ( 'en' , {
53+ style : 'unit' ,
54+ unit : 'kilobyte' ,
55+ minimumFractionDigits : 2 ,
56+ maximumFractionDigits : 2 ,
57+ } ) ;
58+
59+ function kbs ( bytes ) {
60+ return kilobyteFormatter . format ( bytes / 1000 ) ;
4761}
4862
49- /**
50- * Generates a user-readable string from a percentage change
51- * @param {number } change
52- * @param {boolean } includeEmoji
53- */
54- function addPercent ( change , includeEmoji ) {
55- if ( ! isFinite ( change ) ) {
56- // When a new package is created
57- return 'n/a' ;
58- }
59- const formatted = ( change * 100 ) . toFixed ( 1 ) ;
60- if ( / ^ - | ^ 0 (?: \. 0 + ) $ / . test ( formatted ) ) {
61- return `${ formatted } %` ;
62- } else {
63- if ( includeEmoji ) {
64- return `:small_red_triangle:+${ formatted } %` ;
65- } else {
66- return `+${ formatted } %` ;
67- }
68- }
69- }
63+ const percentFormatter = new Intl . NumberFormat ( 'en' , {
64+ style : 'percent' ,
65+ signDisplay : 'exceptZero' ,
66+ minimumFractionDigits : 2 ,
67+ maximumFractionDigits : 2 ,
68+ } ) ;
7069
71- function setBoldness ( row , isBold ) {
72- if ( isBold ) {
73- return row . map ( element => `**${ element } **` ) ;
74- } else {
75- return row ;
70+ function change ( decimal ) {
71+ if ( Number === Infinity ) {
72+ return '(new bundle)' ;
7673 }
77- }
78-
79- function getBundleSizes ( pathToSizesDir ) {
80- const filenames = readdirSync ( pathToSizesDir ) ;
81- let bundleSizes = [ ] ;
82- for ( let i = 0 ; i < filenames . length ; i ++ ) {
83- const filename = filenames [ i ] ;
84- if ( filename . endsWith ( '.json' ) ) {
85- const json = readFileSync ( path . join ( pathToSizesDir , filename ) ) ;
86- bundleSizes . push ( ...JSON . parse ( json ) . bundleSizes ) ;
87- }
74+ if ( decimal === - 1 ) {
75+ return '(deleted)' ;
8876 }
89- return { bundleSizes} ;
90- }
91-
92- async function printResultsForChannel ( baseResults , headResults ) {
93- // Take the JSON of the build response and
94- // make an array comparing the results for printing
95- const results = generateResultsArray ( headResults , baseResults ) ;
96-
97- const packagesToShow = results
98- . filter (
99- r =>
100- Math . abs ( r . prevFileSizeAbsoluteChange ) >= 300 || // bytes
101- Math . abs ( r . prevGzipSizeAbsoluteChange ) >= 100 // bytes
102- )
103- . map ( r => r . packageName ) ;
104-
105- if ( packagesToShow . length ) {
106- let allTables = [ ] ;
107-
108- // Highlight React and React DOM changes inline
109- // e.g. react: `react.production.min.js`: -3%, `react.development.js`: +4%
110-
111- if ( packagesToShow . includes ( 'react' ) ) {
112- const reactProd = results . find (
113- r => r . bundleType === 'UMD_PROD' && r . packageName === 'react'
114- ) ;
115- if (
116- reactProd . prevFileSizeChange !== 0 ||
117- reactProd . prevGzipSizeChange !== 0
118- ) {
119- const changeSize = addPercent ( reactProd . prevFileSizeChange , true ) ;
120- const changeGzip = addPercent ( reactProd . prevGzipSizeChange , true ) ;
121- markdown ( `React: size: ${ changeSize } , gzip: ${ changeGzip } ` ) ;
122- }
123- }
124-
125- if ( packagesToShow . includes ( 'react-dom' ) ) {
126- const reactDOMProd = results . find (
127- r => r . bundleType === 'UMD_PROD' && r . packageName === 'react-dom'
128- ) ;
129- if (
130- reactDOMProd . prevFileSizeChange !== 0 ||
131- reactDOMProd . prevGzipSizeChange !== 0
132- ) {
133- const changeSize = addPercent ( reactDOMProd . prevFileSizeChange , true ) ;
134- const changeGzip = addPercent ( reactDOMProd . prevGzipSizeChange , true ) ;
135- markdown ( `ReactDOM: size: ${ changeSize } , gzip: ${ changeGzip } ` ) ;
136- }
137- }
138-
139- // Show a hidden summary table for all diffs
140-
141- // eslint-disable-next-line no-var,no-for-of-loops/no-for-of-loops
142- for ( var name of new Set ( packagesToShow ) ) {
143- const thisBundleResults = results . filter ( r => r . packageName === name ) ;
144- const changedFiles = thisBundleResults . filter (
145- r => r . prevFileSizeChange !== 0 || r . prevGzipSizeChange !== 0
146- ) ;
147-
148- const mdHeaders = [
149- 'File' ,
150- 'Filesize Diff' ,
151- 'Gzip Diff' ,
152- 'Prev Size' ,
153- 'Current Size' ,
154- 'Prev Gzip' ,
155- 'Current Gzip' ,
156- 'ENV' ,
157- ] ;
158-
159- const mdRows = changedFiles . map ( r => {
160- const isProd = r . bundleType . includes ( 'PROD' ) ;
161- return setBoldness (
162- [
163- r . filename ,
164- addPercent ( r . prevFileSizeChange , isProd ) ,
165- addPercent ( r . prevGzipSizeChange , isProd ) ,
166- r . prevSize ,
167- r . prevFileSize ,
168- r . prevGzip ,
169- r . prevGzipSize ,
170- r . bundleType ,
171- ] ,
172- isProd
173- ) ;
174- } ) ;
175-
176- allTables . push ( `\n## ${ name } ` ) ;
177- allTables . push ( generateMDTable ( mdHeaders , mdRows ) ) ;
178- }
179-
180- const summary = `
181- <details>
182- <summary>Details of bundled changes.</summary>
183-
184- ${ allTables . join ( '\n' ) }
185-
186- </details>
187- ` ;
188- return summary ;
189- } else {
190- return 'No significant bundle size changes to report.' ;
77+ if ( decimal === 0 ) {
78+ return '(no change)' ;
19179 }
80+ return percentFormatter . format ( decimal ) ;
19281}
19382
19483( async function ( ) {
@@ -202,21 +91,10 @@ async function printResultsForChannel(baseResults, headResults) {
20291 }
20392
20493 let headSha ;
205- let headSizesStable ;
206- let headSizesExperimental ;
207-
20894 let baseSha ;
209- let baseSizesStable ;
210- let baseSizesExperimental ;
211-
21295 try {
213- headSha = ( readFileSync ( './build2/COMMIT_SHA' ) + '' ) . trim ( ) ;
214- headSizesStable = getBundleSizes ( './build2/sizes-stable' ) ;
215- headSizesExperimental = getBundleSizes ( './build2/sizes-experimental' ) ;
216-
217- baseSha = ( readFileSync ( './base-build/COMMIT_SHA' ) + '' ) . trim ( ) ;
218- baseSizesStable = getBundleSizes ( './base-build/sizes-stable' ) ;
219- baseSizesExperimental = getBundleSizes ( './base-build/sizes-experimental' ) ;
96+ headSha = ( readFileSync ( HEAD_DIR + '/COMMIT_SHA' ) + '' ) . trim ( ) ;
97+ baseSha = ( readFileSync ( BASE_DIR + '/COMMIT_SHA' ) + '' ) . trim ( ) ;
22098 } catch {
22199 warn (
222100 "Failed to read build artifacts. It's possible a build configuration " +
@@ -226,17 +104,121 @@ async function printResultsForChannel(baseResults, headResults) {
226104 return ;
227105 }
228106
107+ const resultsMap = new Map ( ) ;
108+
109+ // Find all the head (current) artifacts paths.
110+ const headArtifactPaths = await glob ( '**/*.js' , { cwd : 'build2' } ) ;
111+ for ( const artifactPath of headArtifactPaths ) {
112+ try {
113+ // This will throw if there's no matching base artifact
114+ const baseSize = statSync ( BASE_DIR + '/' + artifactPath ) . size ;
115+ const baseSizeGzip = gzipSize . fileSync ( BASE_DIR + '/' + artifactPath ) ;
116+
117+ const headSize = statSync ( HEAD_DIR + '/' + artifactPath ) . size ;
118+ const headSizeGzip = gzipSize . fileSync ( HEAD_DIR + '/' + artifactPath ) ;
119+ resultsMap . set ( artifactPath , {
120+ path : artifactPath ,
121+ headSize,
122+ headSizeGzip,
123+ baseSize,
124+ baseSizeGzip,
125+ change : ( headSize - baseSize ) / baseSize ,
126+ changeGzip : ( headSizeGzip - baseSizeGzip ) / baseSizeGzip ,
127+ } ) ;
128+ } catch {
129+ // There's no matching base artifact. This is a new file.
130+ const baseSize = 0 ;
131+ const baseSizeGzip = 0 ;
132+ const headSize = statSync ( HEAD_DIR + '/' + artifactPath ) . size ;
133+ const headSizeGzip = gzipSize . fileSync ( HEAD_DIR + '/' + artifactPath ) ;
134+ resultsMap . set ( artifactPath , {
135+ path : artifactPath ,
136+ headSize,
137+ headSizeGzip,
138+ baseSize,
139+ baseSizeGzip,
140+ change : Infinity ,
141+ changeGzip : Infinity ,
142+ } ) ;
143+ }
144+ }
145+
146+ // Check for base artifacts that were deleted in the head.
147+ const baseArtifactPaths = await glob ( '**/*.js' , { cwd : 'base-build' } ) ;
148+ for ( const artifactPath of baseArtifactPaths ) {
149+ if ( ! resultsMap . has ( artifactPath ) ) {
150+ const baseSize = statSync ( BASE_DIR + '/' + artifactPath ) . size ;
151+ const baseSizeGzip = gzipSize . fileSync ( BASE_DIR + '/' + artifactPath ) ;
152+ const headSize = 0 ;
153+ const headSizeGzip = 0 ;
154+ resultsMap . set ( artifactPath , {
155+ path : artifactPath ,
156+ headSize,
157+ headSizeGzip,
158+ baseSize,
159+ baseSizeGzip,
160+ change : - 1 ,
161+ changeGzip : - 1 ,
162+ } ) ;
163+ }
164+ }
165+
166+ const results = Array . from ( resultsMap . values ( ) ) ;
167+ results . sort ( ( a , b ) => b . change - a . change ) ;
168+
169+ const header = `
170+ | Name | +/- | Base | Current | +/- gzip | Base gzip | Current gzip |
171+ | ---- | --- | ---- | ------- | -------- | --------- | ------------ |` ;
172+
173+ let criticalResults = [ ] ;
174+ let significantResults = [ ] ;
175+ for ( const result of results ) {
176+ // prettier-ignore
177+ const row = `| ${ result . path } | **${ change ( result . change ) } ** | ${ kbs ( result . baseSize ) } | ${ kbs ( result . headSize ) } | ${ change ( result . changeGzip ) } | ${ kbs ( result . baseSizeGzip ) } | ${ kbs ( result . headSizeGzip ) } ` ;
178+ if (
179+ CRITICAL_ARTIFACT_PATHS . has ( result . path ) ||
180+ result . change > CRITICAL_THRESHOLD ||
181+ 0 - result . change > CRITICAL_THRESHOLD ||
182+ result . change === Infinity ||
183+ result . change === - 1
184+ ) {
185+ criticalResults . push ( row ) ;
186+ }
187+ if (
188+ result . change > SIGNIFICANCE_THRESHOLD ||
189+ 0 - result . change > SIGNIFICANCE_THRESHOLD ||
190+ result . change === Infinity ||
191+ result . change === - 1
192+ ) {
193+ significantResults . push ( row ) ;
194+ }
195+ }
196+
229197 markdown ( `
230- ## Size changes
198+ Comparing: ${ baseSha } ... ${ headSha }
231199
232- <p>Comparing: ${ baseSha } ... ${ headSha } </p>
200+ ## Critical size changes
233201
234- ### Stable channel
202+ Includes critical production bundles, as well as any change greater
203+ than ${ CRITICAL_THRESHOLD * 100 } %:
235204
236- ${ await printResultsForChannel ( baseSizesStable , headSizesStable ) }
205+ ${ header }
206+ ${ criticalResults . join ( '\n' ) }
237207
238- ### Experimental channel
208+ ## Significant size changes
239209
240- ${ await printResultsForChannel ( baseSizesExperimental , headSizesExperimental ) }
210+ Includes any change greater than ${ SIGNIFICANCE_THRESHOLD * 100 } %:
211+
212+ ${
213+ significantResults . length > 0
214+ ? `
215+ <details>
216+ <summary>Expand to show</summary>
217+ ${ header }
218+ ${ significantResults . join ( '\n' ) }
219+ </details>
220+ `
221+ : '(none)'
222+ }
241223` ) ;
242224} ) ( ) ;
0 commit comments