diff --git a/packages/snaps-cli/package.json b/packages/snaps-cli/package.json index b4f00a9c49..8bc2bccb8c 100644 --- a/packages/snaps-cli/package.json +++ b/packages/snaps-cli/package.json @@ -112,6 +112,7 @@ "util": "^0.12.5", "vm-browserify": "^1.1.2", "webpack": "^5.88.0", + "webpack-bundle-analyzer": "^4.10.2", "webpack-merge": "^5.9.0", "yargs": "^17.7.1" }, diff --git a/packages/snaps-cli/src/builders.ts b/packages/snaps-cli/src/builders.ts index 8a4b002220..00020e40bf 100644 --- a/packages/snaps-cli/src/builders.ts +++ b/packages/snaps-cli/src/builders.ts @@ -6,8 +6,12 @@ export enum TranspilationModes { None = 'none', } -const builders: Record> = { - // eslint-disable-next-line @typescript-eslint/naming-convention +const builders = { + analyze: { + describe: 'Analyze the Snap bundle', + type: 'boolean', + }, + config: { alias: 'c', describe: 'Path to config file', @@ -146,6 +150,6 @@ const builders: Record> = { type: 'boolean', deprecated: true, }, -}; +} as const satisfies Record>; export default builders; diff --git a/packages/snaps-cli/src/commands/build/build.test.ts b/packages/snaps-cli/src/commands/build/build.test.ts index f8c774304c..cb915416b4 100644 --- a/packages/snaps-cli/src/commands/build/build.test.ts +++ b/packages/snaps-cli/src/commands/build/build.test.ts @@ -1,5 +1,7 @@ import { DEFAULT_SNAP_BUNDLE } from '@metamask/snaps-utils/test-utils'; import fs from 'fs'; +import type { Compiler } from 'webpack'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import { getMockConfig } from '../../test-utils'; import { evaluate } from '../eval'; @@ -10,6 +12,10 @@ jest.mock('fs'); jest.mock('../eval'); jest.mock('./implementation'); +jest.mock('webpack-bundle-analyzer', () => ({ + BundleAnalyzerPlugin: jest.fn(), +})); + describe('buildHandler', () => { it('builds a snap', async () => { await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE); @@ -27,6 +33,7 @@ describe('buildHandler', () => { expect(process.exitCode).not.toBe(1); expect(build).toHaveBeenCalledWith(config, { + analyze: false, evaluate: false, spinner: expect.any(Object), }); @@ -36,7 +43,54 @@ describe('buildHandler', () => { ); }); - it('does note evaluate if the evaluate option is set to false', async () => { + it('analyzes a snap bundle', async () => { + await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE); + + jest.spyOn(console, 'log').mockImplementation(); + const config = getMockConfig('webpack', { + input: '/input.js', + output: { + path: '/foo', + filename: 'output.js', + }, + }); + + const compiler: Compiler = { + // @ts-expect-error: Mock `Compiler` object. + options: { + plugins: [new BundleAnalyzerPlugin()], + }, + }; + + const plugin = jest.mocked(BundleAnalyzerPlugin); + const instance = plugin.mock.instances[0]; + + // @ts-expect-error: Partial `server` mock. + instance.server = Promise.resolve({ + http: { + address: () => 'http://localhost:8888', + }, + }); + + jest.mocked(build).mockResolvedValueOnce(compiler); + + await buildHandler(config, true); + + expect(process.exitCode).not.toBe(1); + expect(build).toHaveBeenCalledWith(config, { + analyze: true, + evaluate: false, + spinner: expect.any(Object), + }); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining( + 'Bundle analyzer running at http://localhost:8888.', + ), + ); + }); + + it('does not evaluate if the evaluate option is set to false', async () => { await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE); jest.spyOn(console, 'log').mockImplementation(); diff --git a/packages/snaps-cli/src/commands/build/build.ts b/packages/snaps-cli/src/commands/build/build.ts index c10aecb491..5e7aa28cbf 100644 --- a/packages/snaps-cli/src/commands/build/build.ts +++ b/packages/snaps-cli/src/commands/build/build.ts @@ -1,15 +1,19 @@ import { isFile } from '@metamask/snaps-utils/node'; +import { assert } from '@metamask/utils'; import { resolve as pathResolve } from 'path'; import type { ProcessedConfig, ProcessedWebpackConfig } from '../../config'; import { CommandError } from '../../errors'; import type { Steps } from '../../utils'; -import { executeSteps, info } from '../../utils'; +import { success, executeSteps, info } from '../../utils'; import { evaluate } from '../eval'; import { build } from './implementation'; +import { getBundleAnalyzerPort } from './utils'; type BuildContext = { + analyze: boolean; config: ProcessedWebpackConfig; + port?: number; }; const steps: Steps = [ @@ -27,10 +31,25 @@ const steps: Steps = [ }, { name: 'Building the snap bundle.', - task: async ({ config, spinner }) => { + task: async ({ analyze, config, spinner }) => { // We don't evaluate the bundle here, because it's done in a separate // step. - return await build(config, { evaluate: false, spinner }); + const compiler = await build(config, { + analyze, + evaluate: false, + spinner, + }); + + if (analyze) { + return { + analyze, + config, + spinner, + port: await getBundleAnalyzerPort(compiler), + }; + } + + return undefined; }, }, { @@ -48,6 +67,16 @@ const steps: Steps = [ info(`Snap bundle evaluated successfully.`, spinner); }, }, + { + name: 'Running analyser.', + condition: ({ analyze }) => analyze, + task: async ({ spinner, port }) => { + assert(port, 'Port is not defined.'); + success(`Bundle analyzer running at http://localhost:${port}.`, spinner); + + spinner.stop(); + }, + }, ] as const; /** @@ -57,10 +86,15 @@ const steps: Steps = [ * This creates the destination directory if it doesn't exist. * * @param config - The config object. + * @param analyze - Whether to analyze the bundle. * @returns Nothing. */ -export async function buildHandler(config: ProcessedConfig): Promise { +export async function buildHandler( + config: ProcessedConfig, + analyze = false, +): Promise { return await executeSteps(steps, { config, + analyze, }); } diff --git a/packages/snaps-cli/src/commands/build/implementation.ts b/packages/snaps-cli/src/commands/build/implementation.ts index 3139a22c18..07ed9b9655 100644 --- a/packages/snaps-cli/src/commands/build/implementation.ts +++ b/packages/snaps-cli/src/commands/build/implementation.ts @@ -1,3 +1,5 @@ +import type { Compiler } from 'webpack'; + import type { ProcessedWebpackConfig } from '../../config'; import type { WebpackOptions } from '../../webpack'; import { getCompiler } from '../../webpack'; @@ -14,7 +16,7 @@ export async function build( options?: WebpackOptions, ) { const compiler = await getCompiler(config, options); - return await new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { compiler.run((runError) => { if (runError) { reject(runError); @@ -27,7 +29,7 @@ export async function build( return; } - resolve(); + resolve(compiler); }); }); }); diff --git a/packages/snaps-cli/src/commands/build/index.test.ts b/packages/snaps-cli/src/commands/build/index.test.ts index 85a7c82082..a400072577 100644 --- a/packages/snaps-cli/src/commands/build/index.test.ts +++ b/packages/snaps-cli/src/commands/build/index.test.ts @@ -10,8 +10,8 @@ describe('build command', () => { const config = getMockConfig('webpack'); // @ts-expect-error - Partial `YargsArgs` is fine for testing. - await command.handler({ context: { config } }); + await command.handler({ analyze: false, context: { config } }); - expect(buildHandler).toHaveBeenCalledWith(config); + expect(buildHandler).toHaveBeenCalledWith(config, false); }); }); diff --git a/packages/snaps-cli/src/commands/build/index.ts b/packages/snaps-cli/src/commands/build/index.ts index 28b8c687c3..31fcc08b01 100644 --- a/packages/snaps-cli/src/commands/build/index.ts +++ b/packages/snaps-cli/src/commands/build/index.ts @@ -9,6 +9,7 @@ const command = { desc: 'Build snap from source', builder: (yarg: yargs.Argv) => { yarg + .option('analyze', builders.analyze) .option('dist', builders.dist) .option('eval', builders.eval) .option('manifest', builders.manifest) @@ -22,7 +23,8 @@ const command = { .implies('writeManifest', 'manifest') .implies('depsToTranspile', 'transpilationMode'); }, - handler: async (argv: YargsArgs) => buildHandler(argv.context.config), + handler: async (argv: YargsArgs) => + buildHandler(argv.context.config, argv.analyze), }; export * from './implementation'; diff --git a/packages/snaps-cli/src/commands/build/utils.test.ts b/packages/snaps-cli/src/commands/build/utils.test.ts new file mode 100644 index 0000000000..e5208d912f --- /dev/null +++ b/packages/snaps-cli/src/commands/build/utils.test.ts @@ -0,0 +1,70 @@ +import type { Compiler } from 'webpack'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; + +import { getBundleAnalyzerPort } from './utils'; + +jest.mock('webpack-bundle-analyzer', () => ({ + BundleAnalyzerPlugin: jest.fn(), +})); + +describe('getBundleAnalyzerPort', () => { + it('returns the port of the bundle analyzer server', async () => { + const compiler: Compiler = { + // @ts-expect-error: Mock `Compiler` object. + options: { + plugins: [new BundleAnalyzerPlugin()], + }, + }; + + const plugin = jest.mocked(BundleAnalyzerPlugin); + const instance = plugin.mock.instances[0]; + + // @ts-expect-error: Partial `server` mock. + instance.server = Promise.resolve({ + http: { + address: () => 'http://localhost:8888', + }, + }); + + const port = await getBundleAnalyzerPort(compiler); + expect(port).toBe(8888); + }); + + it('returns the port of the bundle analyzer server that returns an object', async () => { + const compiler: Compiler = { + // @ts-expect-error: Mock `Compiler` object. + options: { + plugins: [new BundleAnalyzerPlugin()], + }, + }; + + const plugin = jest.mocked(BundleAnalyzerPlugin); + const instance = plugin.mock.instances[0]; + + // @ts-expect-error: Partial `server` mock. + instance.server = Promise.resolve({ + http: { + address: () => { + return { + port: 8888, + }; + }, + }, + }); + + const port = await getBundleAnalyzerPort(compiler); + expect(port).toBe(8888); + }); + + it('returns undefined if the bundle analyzer server is not available', async () => { + const compiler: Compiler = { + // @ts-expect-error: Mock `Compiler` object. + options: { + plugins: [new BundleAnalyzerPlugin()], + }, + }; + + const port = await getBundleAnalyzerPort(compiler); + expect(port).toBeUndefined(); + }); +}); diff --git a/packages/snaps-cli/src/commands/build/utils.ts b/packages/snaps-cli/src/commands/build/utils.ts new file mode 100644 index 0000000000..d570e777e4 --- /dev/null +++ b/packages/snaps-cli/src/commands/build/utils.ts @@ -0,0 +1,29 @@ +import type { Compiler } from 'webpack'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; + +/** + * Get the port of the bundle analyzer server. + * + * @param compiler - The Webpack compiler. + * @returns The port of the bundle analyzer server. + */ +export async function getBundleAnalyzerPort(compiler: Compiler) { + const analyzerPlugin = compiler.options.plugins.find( + (plugin): plugin is BundleAnalyzerPlugin => + plugin instanceof BundleAnalyzerPlugin, + ); + + if (analyzerPlugin?.server) { + const { http } = await analyzerPlugin.server; + + const address = http.address(); + if (typeof address === 'string') { + const { port } = new URL(address); + return parseInt(port, 10); + } + + return address?.port; + } + + return undefined; +} diff --git a/packages/snaps-cli/src/types/webpack-bundle-analyzer.d.ts b/packages/snaps-cli/src/types/webpack-bundle-analyzer.d.ts new file mode 100644 index 0000000000..0e47c2b4c6 --- /dev/null +++ b/packages/snaps-cli/src/types/webpack-bundle-analyzer.d.ts @@ -0,0 +1,22 @@ +declare module 'webpack-bundle-analyzer' { + import type { Server } from 'http'; + import type { Compiler, WebpackPluginInstance } from 'webpack'; + + export type BundleAnalyzerPluginOptions = { + analyzerPort?: number | undefined; + logLevel?: 'info' | 'warn' | 'error' | 'silent' | undefined; + openAnalyzer?: boolean | undefined; + }; + + export class BundleAnalyzerPlugin implements WebpackPluginInstance { + readonly opts: BundleAnalyzerPluginOptions; + + server?: Promise<{ + http: Server; + }>; + + constructor(options?: BundleAnalyzerPluginOptions); + + apply(compiler: Compiler): void; + } +} diff --git a/packages/snaps-cli/src/types/yargs.d.ts b/packages/snaps-cli/src/types/yargs.d.ts index 3a55d5e928..cfcfe31003 100644 --- a/packages/snaps-cli/src/types/yargs.d.ts +++ b/packages/snaps-cli/src/types/yargs.d.ts @@ -20,6 +20,7 @@ type YargsArgs = { config: ProcessedConfig; }; + analyze?: boolean; fix?: boolean; input?: string; diff --git a/packages/snaps-cli/src/utils/logging.test.ts b/packages/snaps-cli/src/utils/logging.test.ts index 5a3f3683e0..19a8d6f633 100644 --- a/packages/snaps-cli/src/utils/logging.test.ts +++ b/packages/snaps-cli/src/utils/logging.test.ts @@ -1,7 +1,26 @@ -import { blue, dim, red, yellow } from 'chalk'; +import { blue, dim, green, red, yellow } from 'chalk'; import type { Ora } from 'ora'; -import { error, info, warn } from './logging'; +import { error, info, success, warn } from './logging'; + +describe('success', () => { + it('logs a success message', () => { + const log = jest.spyOn(console, 'log').mockImplementation(); + + success('foo'); + expect(log).toHaveBeenCalledWith(`${green('✔')} foo`); + }); + + it('clears a spinner if provided', () => { + jest.spyOn(console, 'warn').mockImplementation(); + + const spinner = { clear: jest.fn(), frame: jest.fn() } as unknown as Ora; + success('foo', spinner); + + expect(spinner.clear).toHaveBeenCalled(); + expect(spinner.frame).toHaveBeenCalled(); + }); +}); describe('warn', () => { it('logs a warning message', () => { diff --git a/packages/snaps-cli/src/utils/logging.ts b/packages/snaps-cli/src/utils/logging.ts index 2d88dab9f4..28a759c67c 100644 --- a/packages/snaps-cli/src/utils/logging.ts +++ b/packages/snaps-cli/src/utils/logging.ts @@ -1,9 +1,24 @@ import { logError, logInfo, logWarning } from '@metamask/snaps-utils'; -import { blue, dim, red, yellow } from 'chalk'; +import { blue, dim, green, red, yellow } from 'chalk'; import type { Ora } from 'ora'; /** - * Log a warning message. The message is prefixed with "Warning:". + * Log a success message. The message is prefixed with a green checkmark. + * + * @param message - The message to log. + * @param spinner - The spinner to clear. + */ +export function success(message: string, spinner?: Ora) { + if (spinner) { + spinner.clear(); + spinner.frame(); + } + + logInfo(`${green('✔')} ${message}`); +} + +/** + * Log a warning message. The message is prefixed with a yellow warning sign. * * @param message - The message to log. * @param spinner - The spinner to clear. diff --git a/packages/snaps-cli/src/utils/steps.test.ts b/packages/snaps-cli/src/utils/steps.test.ts index 971f5e19ef..af0641b234 100644 --- a/packages/snaps-cli/src/utils/steps.test.ts +++ b/packages/snaps-cli/src/utils/steps.test.ts @@ -63,6 +63,44 @@ describe('executeSteps', () => { }); }); + it('updates the context if a step returns an object', async () => { + const steps = [ + { + name: 'Step 1', + task: jest.fn(), + }, + { + name: 'Step 2', + task: jest.fn().mockResolvedValue({ foo: 'baz' }), + }, + { + name: 'Step 3', + task: jest.fn(), + }, + ]; + + const context = { + foo: 'bar', + }; + + await executeSteps(steps, context); + + expect(steps[0].task).toHaveBeenCalledWith({ + ...context, + spinner: expect.any(Object), + }); + + expect(steps[0].task).toHaveBeenCalledWith({ + ...context, + spinner: expect.any(Object), + }); + + expect(steps[2].task).toHaveBeenCalledWith({ + foo: 'baz', + spinner: expect.any(Object), + }); + }); + it('sets the exit code to 1 if a step throws an error', async () => { const log = jest.spyOn(console, 'error').mockImplementation(); diff --git a/packages/snaps-cli/src/utils/steps.ts b/packages/snaps-cli/src/utils/steps.ts index d75074d0de..67509d0af6 100644 --- a/packages/snaps-cli/src/utils/steps.ts +++ b/packages/snaps-cli/src/utils/steps.ts @@ -8,7 +8,7 @@ import { error } from './logging'; export type Step> = { name: string; condition?: (context: Context) => boolean; - task: (context: Context & { spinner: Ora }) => Promise; + task: (context: Context & { spinner: Ora }) => Promise; }; export type Steps> = Readonly< @@ -34,21 +34,27 @@ export async function executeSteps>( spinner.start(); try { - for (const step of steps) { + await steps.reduce>(async (contextPromise, step) => { + const currentContext = await contextPromise; + // If the step has a condition, and it returns false, we skip the step. - if (step.condition && !step.condition(context)) { - continue; + if (step.condition && !step.condition(currentContext)) { + return currentContext; } // Calling `start` here instead of setting `spinner.text` seems to work // better when the process doesn't have a TTY. spinner.start(dim(step.name)); - await step.task({ - ...context, + const newContext = await step.task({ + ...currentContext, spinner, }); - } + + // If the task returns a new context, we use it. Otherwise, we keep the + // current context. + return newContext ?? currentContext; + }, Promise.resolve(context)); // The spinner may have been stopped by a step, so we only succeed if it's // still spinning. @@ -58,6 +64,8 @@ export async function executeSteps>( } catch (_error) { error(getErrorMessage(_error), spinner); spinner.stop(); + + // eslint-disable-next-line require-atomic-updates process.exitCode = 1; } } diff --git a/packages/snaps-cli/src/webpack/__snapshots__/config.test.ts.snap b/packages/snaps-cli/src/webpack/__snapshots__/config.test.ts.snap index a55cc5eeae..e418d5dc33 100644 --- a/packages/snaps-cli/src/webpack/__snapshots__/config.test.ts.snap +++ b/packages/snaps-cli/src/webpack/__snapshots__/config.test.ts.snap @@ -3440,3 +3440,199 @@ exports[`getDefaultConfiguration returns the default Webpack configuration for t "target": "browserslist:/foo/bar/.browserslistrc", } `; + +exports[`getDefaultConfiguration returns the default Webpack configuration when \`analyze\` is \`true\` 1`] = ` +{ + "devtool": false, + "entry": "/foo/bar/src/index.js", + "infrastructureLogging": { + "level": "none", + }, + "mode": "production", + "module": { + "rules": [ + { + "exclude": /node_modules/u, + "test": /\\\\\\.\\(js\\|jsx\\|mjs\\|cjs\\|ts\\|tsx\\)\\$/u, + "use": { + "loader": "/foo/bar/node_modules/swc-loader/index.js", + "options": { + "env": { + "targets": "chrome >= 90, firefox >= 91", + }, + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + }, + "transform": { + "react": { + "importSource": "@metamask/snaps-sdk", + "runtime": "automatic", + "useBuiltins": true, + }, + }, + }, + "module": { + "type": "es6", + }, + "sourceMaps": false, + "sync": false, + }, + }, + }, + { + "resolve": { + "fullySpecified": false, + }, + "test": /\\\\\\.m\\?js\\$/u, + }, + { + "test": /\\\\\\.svg\\$/u, + "type": "asset/source", + }, + { + "generator": { + "dataUrl": [Function], + }, + "test": /\\\\\\.png\\$/u, + "type": "asset/inline", + }, + { + "generator": { + "dataUrl": [Function], + }, + "test": /\\\\\\.jpe\\?g\\$/u, + "type": "asset/inline", + }, + false, + ], + }, + "optimization": { + "minimize": true, + "minimizer": [ + TerserPlugin { + "options": { + "exclude": undefined, + "extractComments": true, + "include": undefined, + "minimizer": { + "implementation": [Function], + "options": {}, + }, + "parallel": true, + "test": /\\\\\\.\\[cm\\]\\?js\\(\\\\\\?\\.\\*\\)\\?\\$/i, + }, + }, + ], + "nodeEnv": false, + }, + "output": { + "chunkFormat": "commonjs", + "clean": false, + "filename": "bundle.js", + "library": { + "name": "module.exports", + "type": "assign", + }, + "path": "/foo/bar/dist", + "publicPath": "/", + }, + "performance": { + "hints": false, + }, + "plugins": [ + SnapsWebpackPlugin { + "options": { + "eval": undefined, + "manifestPath": "/foo/bar/snap.manifest.json", + "writeManifest": true, + }, + }, + SnapsStatsPlugin { + "options": { + "verbose": false, + }, + }, + DefinePlugin { + "definitions": { + "process.env.DEBUG": ""false"", + "process.env.NODE_DEBUG": ""false"", + "process.env.NODE_ENV": ""production"", + }, + }, + ProgressPlugin { + "dependenciesCount": 10000, + "handler": [Function], + "modulesCount": 5000, + "percentBy": undefined, + "profile": false, + "showActiveModules": false, + "showDependencies": true, + "showEntries": true, + "showModules": true, + }, + SnapsBundleWarningsPlugin { + "options": { + "buffer": true, + "builtInResolver": SnapsBuiltInResolver { + "options": { + "ignore": [], + }, + "unresolvedModules": Set {}, + }, + "builtIns": true, + }, + }, + BundleAnalyzerPlugin { + "logger": Logger { + "activeLevels": Set { + "silent", + }, + }, + "opts": { + "analyzerHost": "127.0.0.1", + "analyzerMode": "server", + "analyzerPort": 0, + "analyzerUrl": [Function], + "defaultSizes": "parsed", + "excludeAssets": null, + "generateStatsFile": false, + "logLevel": "silent", + "openAnalyzer": false, + "reportFilename": null, + "reportTitle": [Function], + "startAnalyzer": true, + "statsFilename": "stats.json", + "statsOptions": null, + }, + "server": null, + }, + ], + "resolve": { + "extensions": [ + ".js", + ".jsx", + ".mjs", + ".cjs", + ".ts", + ".tsx", + ], + "fallback": { + "buffer": false, + "fs": false, + "path": false, + }, + "plugins": [ + SnapsBuiltInResolver { + "options": { + "ignore": [], + }, + "unresolvedModules": Set {}, + }, + ], + }, + "stats": "none", + "target": "browserslist:/foo/bar/.browserslistrc", +} +`; diff --git a/packages/snaps-cli/src/webpack/config.test.ts b/packages/snaps-cli/src/webpack/config.test.ts index 0bc5bc56e4..168e4cebff 100644 --- a/packages/snaps-cli/src/webpack/config.test.ts +++ b/packages/snaps-cli/src/webpack/config.test.ts @@ -200,6 +200,25 @@ describe('getDefaultConfiguration', () => { }, ); + it('returns the default Webpack configuration when `analyze` is `true`', async () => { + const config = getMockConfig('webpack', { + input: 'src/index.js', + output: { + path: 'dist', + }, + manifest: { + path: 'snap.manifest.json', + }, + }); + + jest.spyOn(process, 'cwd').mockReturnValue('/foo/bar'); + + const output = await getDefaultConfiguration(config, { analyze: true }); + + // eslint-disable-next-line jest/no-restricted-matchers + expect(normalizeConfig(output)).toMatchSnapshot(); + }); + it.each([ getMockConfig('browserify', { cliOptions: { diff --git a/packages/snaps-cli/src/webpack/config.ts b/packages/snaps-cli/src/webpack/config.ts index 8fd69173ca..65fb5b00c4 100644 --- a/packages/snaps-cli/src/webpack/config.ts +++ b/packages/snaps-cli/src/webpack/config.ts @@ -5,6 +5,7 @@ import { resolve } from 'path'; import TerserPlugin from 'terser-webpack-plugin'; import type { Configuration } from 'webpack'; import { DefinePlugin, ProgressPlugin, ProvidePlugin } from 'webpack'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import type { ProcessedWebpackConfig } from '../config'; import { getFunctionLoader, wasm } from './loaders'; @@ -25,6 +26,11 @@ import { } from './utils'; export type WebpackOptions = { + /** + * Whether to analyze the bundle. + */ + analyze?: boolean; + /** * Whether to watch for changes. */ @@ -354,6 +360,13 @@ export async function getDefaultConfiguration( options.spinner, ), + options.analyze && + new BundleAnalyzerPlugin({ + analyzerPort: 0, + logLevel: 'silent', + openAnalyzer: false, + }), + /** * The `ProviderPlugin` is a Webpack plugin that automatically load * modules instead of having to import or require them everywhere. diff --git a/yarn.lock b/yarn.lock index dac0223492..c8d6359a8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2645,10 +2645,10 @@ __metadata: languageName: node linkType: hard -"@discoveryjs/json-ext@npm:^0.5.0": - version: 0.5.6 - resolution: "@discoveryjs/json-ext@npm:0.5.6" - checksum: 10/61f84f6098f5ae31128e98ff0e9415d1af4c2b61fcfb01a23800e2863a0a2a08ddc187a2152d68b7f4dcff6982f60f4e684bddda1edbbc55775ba9af58ca160b +"@discoveryjs/json-ext@npm:0.5.7, @discoveryjs/json-ext@npm:^0.5.0": + version: 0.5.7 + resolution: "@discoveryjs/json-ext@npm:0.5.7" + checksum: 10/b95682a852448e8ef50d6f8e3b7ba288aab3fd98a2bafbe46881a3db0c6e7248a2debe9e1ee0d4137c521e4743ca5bbcb1c0765c9d7b3e0ef53231506fec42b4 languageName: node linkType: hard @@ -5697,6 +5697,7 @@ __metadata: util: "npm:^0.12.5" vm-browserify: "npm:^1.1.2" webpack: "npm:^5.88.0" + webpack-bundle-analyzer: "npm:^4.10.2" webpack-merge: "npm:^5.9.0" yargs: "npm:^17.7.1" bin: @@ -6916,6 +6917,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.28 + resolution: "@polka/url@npm:1.0.0-next.28" + checksum: 10/7402aaf1de781d0eb0870d50cbcd394f949aee11b38a267a5c3b4e3cfee117e920693e6e93ce24c87ae2d477a59634f39d9edde8e86471cae756839b07c79af7 + languageName: node + linkType: hard + "@popperjs/core@npm:^2.11.6, @popperjs/core@npm:^2.11.8, @popperjs/core@npm:^2.9.3": version: 2.11.8 resolution: "@popperjs/core@npm:2.11.8" @@ -9120,10 +9128,12 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": - version: 8.2.0 - resolution: "acorn-walk@npm:8.2.0" - checksum: 10/e69f7234f2adfeb16db3671429a7c80894105bd7534cb2032acf01bb26e6a847952d11a062d071420b43f8d82e33d2e57f26fe87d9cce0853e8143d8910ff1de +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10/871386764e1451c637bb8ab9f76f4995d408057e9909be6fb5ad68537ae3375d85e6a6f170b98989f44ab3ff6c74ad120bc2779a3d577606e7a0cd2b4efcaf77 languageName: node linkType: hard @@ -9136,12 +9146,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": - version: 8.12.1 - resolution: "acorn@npm:8.12.1" +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" bin: acorn: bin/acorn - checksum: 10/d08c2d122bba32d0861e0aa840b2ee25946c286d5dc5990abca991baf8cdbfbe199b05aacb221b979411a2fea36f83e26b5ac4f6b4e0ce49038c62316c1848f0 + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 languageName: node linkType: hard @@ -10952,6 +10962,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 10/9973af10727ad4b44f26703bf3e9fdc323528660a7590efe3aa9ad5042b4584c0deed84ba443f61c9d6f02dade54a5a5d3c95e306a1e1630f8374ae6db16c06d + languageName: node + linkType: hard + "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -12146,7 +12163,7 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:~0.1.1": +"duplexer@npm:^0.1.2, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 10/62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 @@ -14591,6 +14608,15 @@ __metadata: languageName: node linkType: hard +"gzip-size@npm:^6.0.0": + version: 6.0.0 + resolution: "gzip-size@npm:6.0.0" + dependencies: + duplexer: "npm:^0.1.2" + checksum: 10/2df97f359696ad154fc171dcb55bc883fe6e833bca7a65e457b9358f3cb6312405ed70a8da24a77c1baac0639906cd52358dc0ce2ec1a937eaa631b934c94194 + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -14795,7 +14821,7 @@ __metadata: languageName: node linkType: hard -"html-escaper@npm:^2.0.0": +"html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" checksum: 10/034d74029dcca544a34fb6135e98d427acd73019796ffc17383eaa3ec2fe1c0471dcbbc8f8ed39e46e86d43ccd753a160631615e4048285e313569609b66d5b7 @@ -17908,6 +17934,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.0 + resolution: "mrmime@npm:2.0.0" + checksum: 10/8d95f714ea200c6cf3e3777cbc6168be04b05ac510090a9b41eef5ec081efeb1d1de3e535ffb9c9689fffcc42f59864fd52a500e84a677274f070adeea615c45 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -18483,6 +18516,15 @@ __metadata: languageName: node linkType: hard +"opener@npm:^1.5.2": + version: 1.5.2 + resolution: "opener@npm:1.5.2" + bin: + opener: bin/opener-bin.js + checksum: 10/0504efcd6546e14c016a261f58a68acf9f2e5c23d84865d7d5470d5169788327ceaa5386253682f533b3fba4821748aa37ecb395f3dae7acb3261b9b22e36814 + languageName: node + linkType: hard + "optionator@npm:^0.9.1": version: 0.9.1 resolution: "optionator@npm:0.9.1" @@ -20990,6 +21032,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^2.0.3": + version: 2.0.4 + resolution: "sirv@npm:2.0.4" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10/24f42cf06895017e589c9d16fc3f1c6c07fe8b0dbafce8a8b46322cfba67b7f2498610183954cb0e9d089c8cb60002a7ee7e8bca6a91a0d7042bfbc3473c95c3 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -21968,6 +22021,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10/5132d562cf88ff93fd710770a92f31dbe67cc19b5c6ccae2efc0da327f0954d211bbfd9456389655d726c624f284b4a23112f56d1da931ca7cfabbe1f45e778a + languageName: node + linkType: hard + "tough-cookie@npm:^4.1.2": version: 4.1.3 resolution: "tough-cookie@npm:4.1.3" @@ -23060,6 +23120,28 @@ __metadata: languageName: node linkType: hard +"webpack-bundle-analyzer@npm:^4.10.2": + version: 4.10.2 + resolution: "webpack-bundle-analyzer@npm:4.10.2" + dependencies: + "@discoveryjs/json-ext": "npm:0.5.7" + acorn: "npm:^8.0.4" + acorn-walk: "npm:^8.0.0" + commander: "npm:^7.2.0" + debounce: "npm:^1.2.1" + escape-string-regexp: "npm:^4.0.0" + gzip-size: "npm:^6.0.0" + html-escaper: "npm:^2.0.2" + opener: "npm:^1.5.2" + picocolors: "npm:^1.0.0" + sirv: "npm:^2.0.3" + ws: "npm:^7.3.1" + bin: + webpack-bundle-analyzer: lib/bin/analyzer.js + checksum: 10/cb7ff9d01dc04ef23634f439ab9fe739e022cce5595cb340e01d106ed474605ce4ef50b11b47e444507d341b16650dcb3610e88944020ca6c1c38e88072d43ba + languageName: node + linkType: hard + "webpack-cli@npm:^5.1.4": version: 5.1.4 resolution: "webpack-cli@npm:5.1.4" @@ -23465,6 +23547,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^7.3.1": + version: 7.5.10 + resolution: "ws@npm:7.5.10" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/9c796b84ba80ffc2c2adcdfc9c8e9a219ba99caa435c9a8d45f9ac593bba325563b3f83edc5eb067cc6d21b9a6bf2c930adf76dd40af5f58a5ca6859e81858f0 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0"