diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0c6f557b46b804..7f698a2035b3b6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -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' diff --git a/scripts/sizeSnapshot/create.js b/scripts/sizeSnapshot/create.js index 88e018e75c0768..9c2b11eec0c6af 100644 --- a/scripts/sizeSnapshot/create.js +++ b/scripts/sizeSnapshot/create.js @@ -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); @@ -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 @@ -149,10 +161,12 @@ 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()), ]); @@ -160,7 +174,26 @@ async function run() { 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(); diff --git a/scripts/sizeSnapshot/webpack.config.js b/scripts/sizeSnapshot/webpack.config.js index 079c6d665e3fb4..5dbd325dcba86e 100644 --- a/scripts/sizeSnapshot/webpack.config.js +++ b/scripts/sizeSnapshot/webpack.config.js @@ -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) => { @@ -25,7 +26,6 @@ async function getSizeLimitBundles() { return { name: entryName, - webpack: true, path: path.relative(workspaceRoot, path.dirname(componentPath)), }; }, @@ -38,7 +38,6 @@ async function getSizeLimitBundles() { return { name: componentName, - webpack: true, path: path.relative(workspaceRoot, path.dirname(componentPath)), }; }, @@ -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 @@ -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; };