268 lines
7.7 KiB
JavaScript
268 lines
7.7 KiB
JavaScript
/**
|
|
* 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} = require('danger');
|
|
const {promisify} = require('util');
|
|
const glob = promisify(require('glob'));
|
|
const gzipSize = require('gzip-size');
|
|
|
|
const {readFileSync, statSync} = require('fs');
|
|
|
|
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.min.js',
|
|
'oss-experimental/react-dom/cjs/react-dom.production.min.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 kilobyteFormatter.format(bytes / 1000);
|
|
}
|
|
|
|
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 percentFormatter.format(decimal);
|
|
}
|
|
|
|
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})`,
|
|
`**${change(result.change)}**`,
|
|
`${kbs(result.baseSize)}`,
|
|
`${kbs(result.headSize)}`,
|
|
`${change(result.changeGzip)}`,
|
|
`${kbs(result.baseSizeGzip)}`,
|
|
`${kbs(result.headSizeGzip)}`,
|
|
];
|
|
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 = String(readFileSync(HEAD_DIR + '/COMMIT_SHA')).trim();
|
|
baseSha = String(readFileSync(BASE_DIR + '/COMMIT_SHA')).trim();
|
|
} catch {
|
|
warn(
|
|
"Failed to read build artifacts. It's possible a build configuration " +
|
|
'has changed upstream. Try pulling the latest changes from the ' +
|
|
'main branch.'
|
|
);
|
|
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 (
|
|
commitFiles.every(filename => filename.includes('packages/react-devtools'))
|
|
)
|
|
return;
|
|
|
|
const resultsMap = new Map();
|
|
|
|
// Find all the head (current) artifacts paths.
|
|
const headArtifactPaths = await glob('**/*.js', {cwd: 'build'});
|
|
for (const artifactPath of headArtifactPaths) {
|
|
try {
|
|
// This will throw if there's no matching base artifact
|
|
const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
|
|
const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
|
|
|
|
const headSize = statSync(HEAD_DIR + '/' + artifactPath).size;
|
|
const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
|
|
resultsMap.set(artifactPath, {
|
|
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 = statSync(HEAD_DIR + '/' + artifactPath).size;
|
|
const headSizeGzip = gzipSize.fileSync(HEAD_DIR + '/' + artifactPath);
|
|
resultsMap.set(artifactPath, {
|
|
path: artifactPath,
|
|
headSize,
|
|
headSizeGzip,
|
|
baseSize,
|
|
baseSizeGzip,
|
|
change: Infinity,
|
|
changeGzip: Infinity,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for base artifacts that were deleted in the head.
|
|
const baseArtifactPaths = await glob('**/*.js', {cwd: 'base-build'});
|
|
for (const artifactPath of baseArtifactPaths) {
|
|
if (!resultsMap.has(artifactPath)) {
|
|
const baseSize = statSync(BASE_DIR + '/' + artifactPath).size;
|
|
const baseSizeGzip = gzipSize.fileSync(BASE_DIR + '/' + artifactPath);
|
|
const headSize = 0;
|
|
const headSizeGzip = 0;
|
|
resultsMap.set(artifactPath, {
|
|
path: artifactPath,
|
|
headSize,
|
|
headSizeGzip,
|
|
baseSize,
|
|
baseSizeGzip,
|
|
change: -1,
|
|
changeGzip: -1,
|
|
});
|
|
}
|
|
}
|
|
|
|
const results = Array.from(resultsMap.values());
|
|
results.sort((a, b) => b.change - a.change);
|
|
|
|
let criticalResults = [];
|
|
for (const artifactPath of CRITICAL_ARTIFACT_PATHS) {
|
|
const result = resultsMap.get(artifactPath);
|
|
if (result === undefined) {
|
|
throw new Error(
|
|
'Missing expected bundle. If this was an intentional change to the ' +
|
|
'build configuration, update Dangerfile.js accordingly: ' +
|
|
artifactPath
|
|
);
|
|
}
|
|
criticalResults.push(row(result, baseSha, headSha));
|
|
}
|
|
|
|
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.
|
|
!CRITICAL_ARTIFACT_PATHS.has(result.path)
|
|
) {
|
|
criticalResults.push(row(result, baseSha, headSha));
|
|
}
|
|
|
|
// 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
|
|
) {
|
|
significantResults.push(row(result, baseSha, headSha));
|
|
}
|
|
}
|
|
|
|
markdown(`
|
|
Comparing: ${baseSha}...${headSha}
|
|
|
|
## Critical size changes
|
|
|
|
Includes critical production bundles, as well as any change greater than ${
|
|
CRITICAL_THRESHOLD * 100
|
|
}%:
|
|
|
|
${header}
|
|
${criticalResults.join('\n')}
|
|
|
|
## Significant size changes
|
|
|
|
Includes any change greater than ${SIGNIFICANCE_THRESHOLD * 100}%:
|
|
|
|
${
|
|
significantResults.length > 0
|
|
? `
|
|
<details>
|
|
<summary>Expand to show</summary>
|
|
${header}
|
|
${significantResults.join('\n')}
|
|
</details>
|
|
`
|
|
: '(No significant changes)'
|
|
}
|
|
`);
|
|
})();
|