1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  */
    
  7. 
    
  8. 'use strict';
    
  9. 
    
  10. /* eslint-disable no-for-of-loops/no-for-of-loops */
    
  11. 
    
  12. // Hi, if this is your first time editing/reading a Dangerfile, here's a summary:
    
  13. // It's a JS runtime which helps you provide continuous feedback inside GitHub.
    
  14. //
    
  15. // You can see the docs here: http://danger.systems/js/
    
  16. //
    
  17. // If you want to test changes Danger, I'd recommend checking out an existing PR
    
  18. // and then running the `danger pr` command.
    
  19. //
    
  20. // You'll need a GitHub token, you can re-use this one:
    
  21. //
    
  22. //  0a7d5c3cad9a6dbec2d9 9a5222cf49062a4c1ef7
    
  23. //
    
  24. // (Just remove the space)
    
  25. //
    
  26. // So, for example:
    
  27. //
    
  28. // `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865
    
  29. 
    
  30. const {markdown, danger, warn} = require('danger');
    
  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 = 'build';
    
  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.   // insignificant 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. ]);
    
  50. 
    
  51. const kilobyteFormatter = new Intl.NumberFormat('en', {
    
  52.   style: 'unit',
    
  53.   unit: 'kilobyte',
    
  54.   minimumFractionDigits: 2,
    
  55.   maximumFractionDigits: 2,
    
  56. });
    
  57. 
    
  58. function kbs(bytes) {
    
  59.   return kilobyteFormatter.format(bytes / 1000);
    
  60. }
    
  61. 
    
  62. const percentFormatter = new Intl.NumberFormat('en', {
    
  63.   style: 'percent',
    
  64.   signDisplay: 'exceptZero',
    
  65.   minimumFractionDigits: 2,
    
  66.   maximumFractionDigits: 2,
    
  67. });
    
  68. 
    
  69. function change(decimal) {
    
  70.   if (Number === Infinity) {
    
  71.     return 'New file';
    
  72.   }
    
  73.   if (decimal === -1) {
    
  74.     return 'Deleted';
    
  75.   }
    
  76.   if (decimal < 0.0001) {
    
  77.     return '=';
    
  78.   }
    
  79.   return percentFormatter.format(decimal);
    
  80. }
    
  81. 
    
  82. const header = `
    
  83.   | Name | +/- | Base | Current | +/- gzip | Base gzip | Current gzip |
    
  84.   | ---- | --- | ---- | ------- | -------- | --------- | ------------ |`;
    
  85. 
    
  86. function row(result, baseSha, headSha) {
    
  87.   const diffViewUrl = `https://react-builds.vercel.app/commits/${headSha}/files/${result.path}?compare=${baseSha}`;
    
  88.   const rowArr = [
    
  89.     `| [${result.path}](${diffViewUrl})`,
    
  90.     `**${change(result.change)}**`,
    
  91.     `${kbs(result.baseSize)}`,
    
  92.     `${kbs(result.headSize)}`,
    
  93.     `${change(result.changeGzip)}`,
    
  94.     `${kbs(result.baseSizeGzip)}`,
    
  95.     `${kbs(result.headSizeGzip)}`,
    
  96.   ];
    
  97.   return rowArr.join(' | ');
    
  98. }
    
  99. 
    
  100. (async function () {
    
  101.   // Use git locally to grab the commit which represents the place
    
  102.   // where the branches differ
    
  103. 
    
  104.   const upstreamRepo = danger.github.pr.base.repo.full_name;
    
  105.   if (upstreamRepo !== 'facebook/react') {
    
  106.     // Exit unless we're running in the main repo
    
  107.     return;
    
  108.   }
    
  109. 
    
  110.   let headSha;
    
  111.   let baseSha;
    
  112.   try {
    
  113.     headSha = String(readFileSync(HEAD_DIR + '/COMMIT_SHA')).trim();
    
  114.     baseSha = String(readFileSync(BASE_DIR + '/COMMIT_SHA')).trim();
    
  115.   } catch {
    
  116.     warn(
    
  117.       "Failed to read build artifacts. It's possible a build configuration " +
    
  118.         'has changed upstream. Try pulling the latest changes from the ' +
    
  119.         'main branch.'
    
  120.     );
    
  121.     return;
    
  122.   }
    
  123. 
    
  124.   // Disable sizeBot in a Devtools Pull Request. Because that doesn't affect production bundle size.
    
  125.   const commitFiles = [
    
  126.     ...danger.git.created_files,
    
  127.     ...danger.git.deleted_files,
    
  128.     ...danger.git.modified_files,
    
  129.   ];
    
  130.   if (
    
  131.     commitFiles.every(filename => filename.includes('packages/react-devtools'))
    
  132.   )
    
  133.     return;
    
  134. 
    
  135.   const resultsMap = new Map();
    
  136. 
    
  137.   // Find all the head (current) artifacts paths.
    
  138.   const headArtifactPaths = await glob('**/*.js', {cwd: 'build'});
    
  139.   for (const artifactPath of headArtifactPaths) {
    
  140.     try {
    
  141.       // This will throw if there's no matching base artifact
    
  142.       const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
    
  143.       const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
    
  144. 
    
  145.       const headSize = statSync(HEAD_DIR + '/' + artifactPath).size;
    
  146.       const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
    
  147.       resultsMap.set(artifactPath, {
    
  148.         path: artifactPath,
    
  149.         headSize,
    
  150.         headSizeGzip,
    
  151.         baseSize,
    
  152.         baseSizeGzip,
    
  153.         change: (headSize - baseSize) / baseSize,
    
  154.         changeGzip: (headSizeGzip - baseSizeGzip) / baseSizeGzip,
    
  155.       });
    
  156.     } catch {
    
  157.       // There's no matching base artifact. This is a new file.
    
  158.       const baseSize = 0;
    
  159.       const baseSizeGzip = 0;
    
  160.       const headSize = statSync(HEAD_DIR + '/' + artifactPath).size;
    
  161.       const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
    
  162.       resultsMap.set(artifactPath, {
    
  163.         path: artifactPath,
    
  164.         headSize,
    
  165.         headSizeGzip,
    
  166.         baseSize,
    
  167.         baseSizeGzip,
    
  168.         change: Infinity,
    
  169.         changeGzip: Infinity,
    
  170.       });
    
  171.     }
    
  172.   }
    
  173. 
    
  174.   // Check for base artifacts that were deleted in the head.
    
  175.   const baseArtifactPaths = await glob('**/*.js', {cwd: 'base-build'});
    
  176.   for (const artifactPath of baseArtifactPaths) {
    
  177.     if (!resultsMap.has(artifactPath)) {
    
  178.       const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
    
  179.       const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
    
  180.       const headSize = 0;
    
  181.       const headSizeGzip = 0;
    
  182.       resultsMap.set(artifactPath, {
    
  183.         path: artifactPath,
    
  184.         headSize,
    
  185.         headSizeGzip,
    
  186.         baseSize,
    
  187.         baseSizeGzip,
    
  188.         change: -1,
    
  189.         changeGzip: -1,
    
  190.       });
    
  191.     }
    
  192.   }
    
  193. 
    
  194.   const results = Array.from(resultsMap.values());
    
  195.   results.sort((a, b) => b.change - a.change);
    
  196. 
    
  197.   let criticalResults = [];
    
  198.   for (const artifactPath of CRITICAL_ARTIFACT_PATHS) {
    
  199.     const result = resultsMap.get(artifactPath);
    
  200.     if (result === undefined) {
    
  201.       throw new Error(
    
  202.         'Missing expected bundle. If this was an intentional change to the ' +
    
  203.           'build configuration, update Dangerfile.js accordingly: ' +
    
  204.           artifactPath
    
  205.       );
    
  206.     }
    
  207.     criticalResults.push(row(result, baseSha, headSha));
    
  208.   }
    
  209. 
    
  210.   let significantResults = [];
    
  211.   for (const result of results) {
    
  212.     // If result exceeds critical threshold, add to top section.
    
  213.     if (
    
  214.       (result.change > CRITICAL_THRESHOLD ||
    
  215.         0 - result.change > CRITICAL_THRESHOLD ||
    
  216.         // New file
    
  217.         result.change === Infinity ||
    
  218.         // Deleted file
    
  219.         result.change === -1) &&
    
  220.       // Skip critical artifacts. We added those earlier, in a fixed order.
    
  221.       !CRITICAL_ARTIFACT_PATHS.has(result.path)
    
  222.     ) {
    
  223.       criticalResults.push(row(result, baseSha, headSha));
    
  224.     }
    
  225. 
    
  226.     // Do the same for results that exceed the significant threshold. These
    
  227.     // will go into the bottom, collapsed section. Intentionally including
    
  228.     // critical artifacts in this section, too.
    
  229.     if (
    
  230.       result.change > SIGNIFICANCE_THRESHOLD ||
    
  231.       0 - result.change > SIGNIFICANCE_THRESHOLD ||
    
  232.       result.change === Infinity ||
    
  233.       result.change === -1
    
  234.     ) {
    
  235.       significantResults.push(row(result, baseSha, headSha));
    
  236.     }
    
  237.   }
    
  238. 
    
  239.   markdown(`
    
  240. Comparing: ${baseSha}...${headSha}
    
  241. 
    
  242. ## Critical size changes
    
  243. 
    
  244. Includes critical production bundles, as well as any change greater than ${
    
  245.     CRITICAL_THRESHOLD * 100
    
  246.   }%:
    
  247. 
    
  248. ${header}
    
  249. ${criticalResults.join('\n')}
    
  250. 
    
  251. ## Significant size changes
    
  252. 
    
  253. Includes any change greater than ${SIGNIFICANCE_THRESHOLD * 100}%:
    
  254. 
    
  255. ${
    
  256.   significantResults.length > 0
    
  257.     ? `
    
  258. <details>
    
  259. <summary>Expand to show</summary>
    
  260. ${header}
    
  261. ${significantResults.join('\n')}
    
  262. </details>
    
  263. `
    
  264.     : '(No significant changes)'
    
  265. }
    
  266. `);
    
  267. })();