Skip to content

Commit

Permalink
[core] Prevent unstable chunks in size snapshot (#23181)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Oct 22, 2020
1 parent c60fbba commit d0c2a16
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 84 deletions.
6 changes: 6 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ steps:
yarn install
displayName: 'install dependencies'
- task: Cache@2
inputs:
key: 'node-modules-cache | yarn.lock'
path: node_modules/.cache
displayName: Cache node_modules/.cache

- script: |
yarn danger ci
displayName: 'prepare danger on PRs'
Expand Down
77 changes: 55 additions & 22 deletions scripts/sizeSnapshot/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const lodash = require('lodash');
const path = require('path');
const { promisify } = require('util');
const webpackCallbackBased = require('webpack');
const yargs = require('yargs');
const createWebpackConfig = require('./webpack.config');

const webpack = promisify(webpackCallbackBased);
Expand Down Expand Up @@ -31,27 +32,38 @@ async function getRollupSize(snapshotPath) {
/**
* creates size snapshot for every bundle that built with webpack
*/
async function getWebpackSizes() {
async function getWebpackSizes(webpackEnvironment) {
await fse.mkdirp(path.join(__dirname, 'build'));

// webpack --config $configPath --json > $statsPath
// will create a 300MB big json file which sometimes requires up to 1.5GB
// memory. This will sometimes crash node in azure pipelines with "heap out of memory"
const webpackStats = await webpack(await createWebpackConfig(webpack));
const stats = webpackStats.toJson();
if (stats.errors.length > 0) {
throw new Error(
`The following errors occured during bundling with webpack: \n${stats.errors.join('\n')}`,
);
}
const configurations = await createWebpackConfig(webpack, webpackEnvironment);
const webpackMultiStats = await webpack(configurations);

const sizes = [];
webpackMultiStats.stats.forEach((webpackStats) => {
if (webpackStats.hasErrors()) {
const { entrypoints, errors } = webpackStats.toJson({
all: false,
entrypoints: true,
errors: true,
});
throw new Error(
`The following errors occured during bundling of ${Object.keys(
entrypoints,
)} with webpack: \n${errors.join('\n')}`,
);
}

const assets = new Map(stats.assets.map((asset) => [asset.name, asset]));
const stats = webpackStats.toJson({ all: false, assets: true });
const assets = new Map(stats.assets.map((asset) => [asset.name, asset]));

return Object.entries(stats.assetsByChunkName).map(([chunkName, assetName]) => {
const parsedSize = assets.get(assetName).size;
const gzipSize = assets.get(`${assetName}.gz`).size;
return [chunkName, { parsed: parsedSize, gzip: gzipSize }];
Object.entries(stats.assetsByChunkName).forEach(([chunkName, assetName]) => {
const parsedSize = assets.get(assetName).size;
const gzipSize = assets.get(`${assetName}.gz`).size;
sizes.push([chunkName, { parsed: parsedSize, gzip: gzipSize }]);
});
});

return sizes;
}

// waiting for String.prototype.matchAll in node 10
Expand Down Expand Up @@ -149,18 +161,39 @@ async function getNextPagesSize() {
return entries;
}

async function run() {
async function run(argv) {
const { analyze } = argv;

const rollupBundles = [path.join(workspaceRoot, 'packages/material-ui/size-snapshot.json')];
const bundleSizes = lodash.fromPairs([
...(await getWebpackSizes()),
...(await getWebpackSizes({ analyze })),
...lodash.flatten(await Promise.all(rollupBundles.map(getRollupSize))),
...(await getNextPagesSize()),
]);

await fse.writeJSON(snapshotDestPath, bundleSizes, { spaces: 2 });
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
yargs
.command({
command: '$0',
description: 'Saves a size snapshot in size-snapshot.json',
builder: (command) => {
return command
.option('analyze', {
default: false,
describe: 'Creates a webpack-bundle-analyzer report for each bundle.',
type: 'boolean',
})
.option('accurateBundles', {
default: false,
describe: 'Displays used bundles accurately at the cost of accurate bundle size.',
type: 'boolean',
});
},
handler: run,
})
.help()
.strict(true)
.version(false)
.parse();
119 changes: 57 additions & 62 deletions scripts/sizeSnapshot/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
const globCallback = require('glob');
const path = require('path');
const { promisify } = require('util');
const CompressionPlugin = require('compression-webpack-plugin');
const globCallback = require('glob');
const TerserPlugin = require('terser-webpack-plugin');
const { promisify } = require('util');

const glob = promisify(globCallback);
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const workspaceRoot = path.join(__dirname, '..', '..');

async function getSizeLimitBundles() {
const glob = promisify(globCallback);

async function getWebpackEntries() {
const corePackagePath = path.join(workspaceRoot, 'packages/material-ui/build');
const coreComponents = (await glob(path.join(corePackagePath, '[A-Z]*/index.js'))).map(
(componentPath) => {
Expand All @@ -25,7 +26,6 @@ async function getSizeLimitBundles() {

return {
name: entryName,
webpack: true,
path: path.relative(workspaceRoot, path.dirname(componentPath)),
};
},
Expand All @@ -38,7 +38,6 @@ async function getSizeLimitBundles() {

return {
name: componentName,
webpack: true,
path: path.relative(workspaceRoot, path.dirname(componentPath)),
};
},
Expand All @@ -47,54 +46,44 @@ async function getSizeLimitBundles() {
return [
{
name: '@material-ui/core',
webpack: true,
path: path.join(path.relative(workspaceRoot, corePackagePath), 'index.js'),
},
{
name: '@material-ui/lab',
webpack: true,
path: path.join(path.relative(workspaceRoot, labPackagePath), 'index.js'),
},
{
name: '@material-ui/styles',
webpack: true,
path: 'packages/material-ui-styles/build/index.js',
},
{
name: '@material-ui/system',
webpack: true,
path: 'packages/material-ui-system/build/esm/index.js',
},
...coreComponents,
{
name: '@material-ui/core/styles/createMuiTheme',
webpack: true,
path: 'packages/material-ui/build/styles/createMuiTheme.js',
},
{
name: 'colorManipulator',
webpack: true,
path: 'packages/material-ui/build/styles/colorManipulator.js',
},
...labComponents,
{
name: 'useAutocomplete',
webpack: true,
path: 'packages/material-ui-lab/build/useAutocomplete/index.js',
},
{
name: '@material-ui/core/useMediaQuery',
webpack: true,
path: 'packages/material-ui/build/useMediaQuery/index.js',
},
{
name: '@material-ui/core/useScrollTrigger',
webpack: true,
path: 'packages/material-ui/build/useScrollTrigger/index.js',
},
{
name: '@material-ui/utils',
webpack: true,
path: 'packages/material-ui-utils/build/esm/index.js',
},
// TODO: Requires webpack v5
Expand All @@ -106,59 +95,65 @@ async function getSizeLimitBundles() {
// },
{
name: '@material-ui/core.legacy',
webpack: true,
path: path.join(path.relative(workspaceRoot, corePackagePath), 'legacy/index.js'),
},
];
}

module.exports = async function webpackConfig() {
const entries = await getSizeLimitBundles();
const entry = entries.reduce((acc, bundle) => {
acc[bundle.name] = path.join(workspaceRoot, bundle.path);
return acc;
}, {});
module.exports = async function webpackConfig(webpack, environment) {
const analyzerMode = environment.analyze ? 'static' : 'disabled';
const concatenateModules = !environment.accurateBundles;

const config = {
entry,
// ideally this would be computed from the bundles peer dependencies
externals: /^(react|react-dom|react\/jsx-runtime)$/,
mode: 'production',
optimization: {
// Otherwise bundles with that include chunks for which we track the size separately are penalized
// e.g. without this option `@material-ui/core.legacy` would be smaller since it could concatenate all modules
// while `@material-ui/core` had to import the chunks from all the components.
// Ideally we could just disable shared chunks but I couldn't figure out how.
concatenateModules: false,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
const entries = await getWebpackEntries();
const configurations = entries.map((entry) => {
return {
// ideally this would be computed from the bundles peer dependencies
externals: /^(react|react-dom|react\/jsx-runtime)$/,
mode: 'production',
optimization: {
concatenateModules,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
}),
],
},
output: {
filename: '[name].js',
path: path.join(__dirname, 'build'),
},
plugins: [
new CompressionPlugin(),
new BundleAnalyzerPlugin({
analyzerMode,
// We create a report for each bundle so around 120 reports.
// Opening them all is spam.
// If opened with `webpack --config . --analyze` it'll still open one new tab though.
openAnalyzer: false,
// '[name].html' not supported: https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/12
reportFilename: `${entry.name}.html`,
}),
],
},
output: {
filename: '[name].js',
path: path.join(__dirname, 'build'),
},
plugins: [new CompressionPlugin()],
resolve: {
alias: {
'@material-ui/core': path.join(workspaceRoot, 'packages/material-ui/build'),
'@material-ui/lab': path.join(workspaceRoot, 'packages/material-ui-lab/build'),
'@material-ui/styled-engine': path.join(
workspaceRoot,
'packages/material-ui-styled-engine/build',
),
'@material-ui/styled-engine-sc': path.join(
workspaceRoot,
'packages/material-ui-styles-sc/build',
),
'@material-ui/styles': path.join(workspaceRoot, 'packages/material-ui-styles/build'),
'@material-ui/system': path.join(workspaceRoot, 'packages/material-ui-system/build'),
'@material-ui/utils': path.join(workspaceRoot, 'packages/material-ui-utils/build'),
resolve: {
alias: {
'@material-ui/core': path.join(workspaceRoot, 'packages/material-ui/build'),
'@material-ui/lab': path.join(workspaceRoot, 'packages/material-ui-lab/build'),
'@material-ui/styled-engine': path.join(
workspaceRoot,
'packages/material-ui-styled-engine/build',
),
'@material-ui/styled-engine-sc': path.join(
workspaceRoot,
'packages/material-ui-styles-sc/build',
),
'@material-ui/styles': path.join(workspaceRoot, 'packages/material-ui-styles/build'),
'@material-ui/system': path.join(workspaceRoot, 'packages/material-ui-system/build'),
'@material-ui/utils': path.join(workspaceRoot, 'packages/material-ui-utils/build'),
},
},
},
};
entry: { [entry.name]: path.join(workspaceRoot, entry.path) },
};
});

return config;
return configurations;
};

0 comments on commit d0c2a16

Please sign in to comment.