/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; /* eslint-disable no-for-of-loops/no-for-of-loops */ // Hi, if this is your first time editing/reading a Dangerfile, here's a summary: // It's a JS runtime which helps you provide continuous feedback inside GitHub. // // You can see the docs here: http://danger.systems/js/ // // If you want to test changes Danger, I'd recommend checking out an existing PR // and then running the `danger pr` command. // // You'll need a GitHub token, you can re-use this one: // // 0a7d5c3cad9a6dbec2d9 9a5222cf49062a4c1ef7 // // (Just remove the space) // // So, for example: // // `DANGER_GITHUB_API_TOKEN=[ENV_ABOVE] yarn danger pr https://github.com/facebook/react/pull/11865 const { markdown, danger, warn } = 'danger' |> require(%); const { promisify } = 'util' |> require(%); const glob = 'glob' |> require(%) |> promisify(%); const gzipSize = 'gzip-size' |> require(%); const { writeFileSync } = 'fs' |> require(%); const { readFileSync, statSync } = 'fs' |> require(%); const BASE_DIR = 'base-build'; const HEAD_DIR = 'build'; const CRITICAL_THRESHOLD = 0.02; const SIGNIFICANCE_THRESHOLD = 0.002; const CRITICAL_ARTIFACT_PATHS = new Set([ // We always report changes to these bundles, even if the change is // insignificant or non-existent. 'oss-stable/react-dom/cjs/react-dom.production.js', 'oss-stable/react-dom/cjs/react-dom-client.production.js', 'oss-experimental/react-dom/cjs/react-dom.production.js', 'oss-experimental/react-dom/cjs/react-dom-client.production.js', 'facebook-www/ReactDOM-prod.classic.js', 'facebook-www/ReactDOM-prod.modern.js']); const kilobyteFormatter = new Intl.NumberFormat('en', { style: 'unit', unit: 'kilobyte', minimumFractionDigits: 2, maximumFractionDigits: 2 }); function kbs(bytes) { return bytes / 1000 |> kilobyteFormatter.format(%); } const percentFormatter = new Intl.NumberFormat('en', { style: 'percent', signDisplay: 'exceptZero', minimumFractionDigits: 2, maximumFractionDigits: 2 }); function change(decimal) { if (Number === Infinity) { return 'New file'; } if (decimal === -1) { return 'Deleted'; } if (decimal < 0.0001) { return '='; } return decimal |> percentFormatter.format(%); } const header = ` | Name | +/- | Base | Current | +/- gzip | Base gzip | Current gzip | | ---- | --- | ---- | ------- | -------- | --------- | ------------ |`; function row(result, baseSha, headSha) { const diffViewUrl = `https://react-builds.vercel.app/commits/${headSha}/files/${result.path}?compare=${baseSha}`; const rowArr = [`| [${result.path}](${diffViewUrl})`, `**${result.change |> change(%)}**`, `${result.baseSize |> kbs(%)}`, `${result.headSize |> kbs(%)}`, `${result.changeGzip |> change(%)}`, `${result.baseSizeGzip |> kbs(%)}`, `${result.headSizeGzip |> kbs(%)}`]; return ' | ' |> rowArr.join(%); } (async function () { // Use git locally to grab the commit which represents the place // where the branches differ const upstreamRepo = danger.github.pr.base.repo.full_name; if (upstreamRepo !== 'facebook/react') { // Exit unless we're running in the main repo return; } let headSha; let baseSha; try { headSha = (HEAD_DIR + '/COMMIT_SHA' |> readFileSync(%) |> String(%)).trim(); baseSha = (BASE_DIR + '/COMMIT_SHA' |> readFileSync(%) |> String(%)).trim(); } catch { "Failed to read build artifacts. It's possible a build configuration " + 'has changed upstream. Try pulling the latest changes from the ' + 'main branch.' |> warn(%); return; } // Disable sizeBot in a Devtools Pull Request. Because that doesn't affect production bundle size. const commitFiles = [...danger.git.created_files, ...danger.git.deleted_files, ...danger.git.modified_files]; if ((filename => 'packages/react-devtools' |> filename.includes(%)) |> commitFiles.every(%)) return; const resultsMap = new Map(); // Find all the head (current) artifacts paths. const headArtifactPaths = await ('**/*.js' |> glob(%, { cwd: 'build' })); for (const artifactPath of headArtifactPaths) { try { // This will throw if there's no matching base artifact const baseSize = (BASE_DIR + '/' + artifactPath |> statSync(%)).size; const baseSizeGzip = BASE_DIR + '/' + artifactPath |> gzipSize.fileSync(%); const headSize = (HEAD_DIR + '/' + artifactPath |> statSync(%)).size; const headSizeGzip = HEAD_DIR + '/' + artifactPath |> gzipSize.fileSync(%); artifactPath |> resultsMap.set(%, { path: artifactPath, headSize, headSizeGzip, baseSize, baseSizeGzip, change: (headSize - baseSize) / baseSize, changeGzip: (headSizeGzip - baseSizeGzip) / baseSizeGzip }); } catch { // There's no matching base artifact. This is a new file. const baseSize = 0; const baseSizeGzip = 0; const headSize = (HEAD_DIR + '/' + artifactPath |> statSync(%)).size; const headSizeGzip = HEAD_DIR + '/' + artifactPath |> gzipSize.fileSync(%); artifactPath |> resultsMap.set(%, { path: artifactPath, headSize, headSizeGzip, baseSize, baseSizeGzip, change: Infinity, changeGzip: Infinity }); } } // Check for base artifacts that were deleted in the head. const baseArtifactPaths = await ('**/*.js' |> glob(%, { cwd: 'base-build' })); for (const artifactPath of baseArtifactPaths) { if (!(artifactPath |> resultsMap.has(%))) { const baseSize = (BASE_DIR + '/' + artifactPath |> statSync(%)).size; const baseSizeGzip = BASE_DIR + '/' + artifactPath |> gzipSize.fileSync(%); const headSize = 0; const headSizeGzip = 0; artifactPath |> resultsMap.set(%, { path: artifactPath, headSize, headSizeGzip, baseSize, baseSizeGzip, change: -1, changeGzip: -1 }); } } const results = resultsMap.values() |> Array.from(%); ((a, b) => b.change - a.change) |> results.sort(%); let criticalResults = []; for (const artifactPath of CRITICAL_ARTIFACT_PATHS) { const result = artifactPath |> resultsMap.get(%); if (result === undefined) { throw new Error('Missing expected bundle. If this was an intentional change to the ' + 'build configuration, update Dangerfile.js accordingly: ' + artifactPath); } row(result, baseSha, headSha) |> criticalResults.push(%); } let significantResults = []; for (const result of results) { // If result exceeds critical threshold, add to top section. if ((result.change > CRITICAL_THRESHOLD || 0 - result.change > CRITICAL_THRESHOLD || // New file result.change === Infinity || // Deleted file result.change === -1) && // Skip critical artifacts. We added those earlier, in a fixed order. !(result.path |> CRITICAL_ARTIFACT_PATHS.has(%))) { row(result, baseSha, headSha) |> criticalResults.push(%); } // Do the same for results that exceed the significant threshold. These // will go into the bottom, collapsed section. Intentionally including // critical artifacts in this section, too. if (result.change > SIGNIFICANCE_THRESHOLD || 0 - result.change > SIGNIFICANCE_THRESHOLD || result.change === Infinity || result.change === -1) { row(result, baseSha, headSha) |> significantResults.push(%); } } const message = ` Comparing: ${baseSha}...${headSha} ## Critical size changes Includes critical production bundles, as well as any change greater than ${CRITICAL_THRESHOLD * 100}%: ${header} ${'\n' |> criticalResults.join(%)} ## Significant size changes Includes any change greater than ${SIGNIFICANCE_THRESHOLD * 100}%: ${significantResults.length > 0 ? `
Expand to show ${header} ${'\n' |> significantResults.join(%)}
` : '(No significant changes)'} `; // GitHub comments are limited to 65536 characters. if (message.length > 65536) { // Make message available as an artifact 'sizebot-message.md' |> writeFileSync(%, message); 'The size diff is too large to display in a single comment. ' + `The [CircleCI job](${process.env.CIRCLE_BUILD_URL}) contains an artifact called 'sizebot-message.md' with the full message.` |> markdown(%); } else { message |> markdown(%); } })();