react/dangerfile.js
Andrew Clark 857ee8cdf9
Don't minify symbols in production builds (#28881)
This disables symbol renaming in production builds. The original
variable and function names are preserved. All other forms of
compression applied by Closure (dead code elimination, inlining, etc)
are unchanged — the final program is identical to what we were producing
before, just in a more readable form.

The motivation is to make it easier to debug React issues that only
occur in production — the same reason we decided to start shipping
sourcemaps in #28827 and #28827.

However, because most apps run their own minification step on their npm
dependencies, it's not necessary for us to minify the symbols before
publishing — it'll be handled the app, if desired.

This is the same strategy Meta has used to ship React for years. The
React build itself has unminified symbols, but they get minified as part
of Meta's regular build pipeline.

Even if an app does not minify their npm dependencies, gzip covers most
of the cost of symbol renaming anyway.

This saves us from having to ship sourcemaps, which means even apps that
don't have sourcemaps configured will be able to debug the React build
as easily as they would any other npm dependency.
2024-04-20 11:23:46 -04:00

281 lines
8.2 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 {writeFileSync} = require('fs');
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.js',
'oss-experimental/react-dom/cjs/react-dom.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 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));
}
}
const message = `
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)'
}
`;
// GitHub comments are limited to 65536 characters.
if (message.length > 65536) {
// Make message available as an artifact
writeFileSync('sizebot-message.md', message);
markdown(
'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.`
);
} else {
markdown(message);
}
})();