/** * Copyright (c) Facebook, Inc. and its 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'; // 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 fetch = require('node-fetch'); const {generateResultsArray} = require('./scripts/rollup/stats'); const {existsSync, readFileSync} = require('fs'); const {exec} = require('child_process'); // This must match the name of the CI job that creates the build artifacts const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL === 'experimental' ? 'experimental' : 'stable'; const artifactsJobName = process.env.RELEASE_CHANNEL === 'experimental' ? 'process_artifacts_experimental' : 'process_artifacts'; if (!existsSync('./build/bundle-sizes.json')) { // This indicates the build failed previously. // In that case, there's nothing for the Dangerfile to do. // Exit early to avoid leaving a redundant (and potentially confusing) PR comment. warn( 'No bundle size information found. This indicates the build ' + 'job failed.' ); process.exit(0); } const currentBuildResults = JSON.parse( readFileSync('./build/bundle-sizes.json') ); /** * Generates a Markdown table * @param {string[]} headers * @param {string[][]} body */ function generateMDTable(headers, body) { const tableHeaders = [ headers.join(' | '), headers.map(() => ' --- ').join(' | '), ]; const tablebody = body.map(r => r.join(' | ')); return tableHeaders.join('\n') + '\n' + tablebody.join('\n'); } /** * Generates a user-readable string from a percentage change * @param {number} change * @param {boolean} includeEmoji */ function addPercent(change, includeEmoji) { if (!isFinite(change)) { // When a new package is created return 'n/a'; } const formatted = (change * 100).toFixed(1); if (/^-|^0(?:\.0+)$/.test(formatted)) { return `${formatted}%`; } else { if (includeEmoji) { return `:small_red_triangle:+${formatted}%`; } else { return `+${formatted}%`; } } } function setBoldness(row, isBold) { if (isBold) { return row.map(element => `**${element}**`); } else { return row; } } /** * Gets the commit that represents the merge between the current branch * and master. */ function git(args) { return new Promise(res => { exec('git ' + args, (err, stdout, stderr) => { if (err) { throw err; } else { res(stdout.trim()); } }); }); } (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; } markdown(`## Size changes (${RELEASE_CHANNEL})`); const upstreamRef = danger.github.pr.base.ref; await git(`remote add upstream https://github.com/facebook/react.git`); await git('fetch upstream'); const baseCommit = await git(`merge-base HEAD upstream/${upstreamRef}`); let previousBuildResults = null; try { let baseCIBuildId = null; const statusesResponse = await fetch( `https://api.github.com/repos/facebook/react/commits/${baseCommit}/status` ); const {statuses, state} = await statusesResponse.json(); if (state === 'failure') { warn(`Base commit is broken: ${baseCommit}`); return; } for (let i = 0; i < statuses.length; i++) { const status = statuses[i]; if (status.context === `ci/circleci: ${artifactsJobName}`) { if (status.state === 'success') { baseCIBuildId = /\/facebook\/react\/([0-9]+)/.exec( status.target_url )[1]; break; } if (status.state === 'pending') { warn(`Build job for base commit is still pending: ${baseCommit}`); return; } } } if (baseCIBuildId === null) { warn(`Could not find build artifacts for base commit: ${baseCommit}`); return; } const baseArtifactsInfoResponse = await fetch( `https://circleci.com/api/v1.1/project/github/facebook/react/${baseCIBuildId}/artifacts` ); const baseArtifactsInfo = await baseArtifactsInfoResponse.json(); for (let i = 0; i < baseArtifactsInfo.length; i++) { const info = baseArtifactsInfo[i]; if (info.path.endsWith('bundle-sizes.json')) { const resultsResponse = await fetch(info.url); previousBuildResults = await resultsResponse.json(); break; } } } catch (error) { warn(`Failed to fetch build artifacts for base commit: ${baseCommit}`); return; } if (previousBuildResults === null) { warn(`Could not find build artifacts for base commit: ${baseCommit}`); return; } // Take the JSON of the build response and // make an array comparing the results for printing const results = generateResultsArray( currentBuildResults, previousBuildResults ); const packagesToShow = results .filter( r => Math.abs(r.prevFileSizeAbsoluteChange) >= 300 || // bytes Math.abs(r.prevGzipSizeAbsoluteChange) >= 100 // bytes ) .map(r => r.packageName); if (packagesToShow.length) { let allTables = []; // Highlight React and React DOM changes inline // e.g. react: `react.production.min.js`: -3%, `react.development.js`: +4% if (packagesToShow.includes('react')) { const reactProd = results.find( r => r.bundleType === 'UMD_PROD' && r.packageName === 'react' ); if ( reactProd.prevFileSizeChange !== 0 || reactProd.prevGzipSizeChange !== 0 ) { const changeSize = addPercent(reactProd.prevFileSizeChange, true); const changeGzip = addPercent(reactProd.prevGzipSizeChange, true); markdown(`React: size: ${changeSize}, gzip: ${changeGzip}`); } } if (packagesToShow.includes('react-dom')) { const reactDOMProd = results.find( r => r.bundleType === 'UMD_PROD' && r.packageName === 'react-dom' ); if ( reactDOMProd.prevFileSizeChange !== 0 || reactDOMProd.prevGzipSizeChange !== 0 ) { const changeSize = addPercent(reactDOMProd.prevFileSizeChange, true); const changeGzip = addPercent(reactDOMProd.prevGzipSizeChange, true); markdown(`ReactDOM: size: ${changeSize}, gzip: ${changeGzip}`); } } // Show a hidden summary table for all diffs // eslint-disable-next-line no-var,no-for-of-loops/no-for-of-loops for (var name of new Set(packagesToShow)) { const thisBundleResults = results.filter(r => r.packageName === name); const changedFiles = thisBundleResults.filter( r => r.prevFileSizeChange !== 0 || r.prevGzipSizeChange !== 0 ); const mdHeaders = [ 'File', 'Filesize Diff', 'Gzip Diff', 'Prev Size', 'Current Size', 'Prev Gzip', 'Current Gzip', 'ENV', ]; const mdRows = changedFiles.map(r => { const isProd = r.bundleType.includes('PROD'); return setBoldness( [ r.filename, addPercent(r.prevFileSizeChange, isProd), addPercent(r.prevGzipSizeChange, isProd), r.prevSize, r.prevFileSize, r.prevGzip, r.prevGzipSize, r.bundleType, ], isProd ); }); allTables.push(`\n## ${name}`); allTables.push(generateMDTable(mdHeaders, mdRows)); } const summary = `
Details of bundled changes.

Comparing: ${baseCommit}...${danger.github.pr.head.sha}

${allTables.join('\n')}
`; markdown(summary); } else { markdown('No significant bundle size changes to report.'); } })();