├── .github └── workflows │ └── azure-static-web-apps-happy-water-0887b0b1e.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── SECURITY.md ├── package-lock.json ├── package.json ├── public └── samples │ ├── sample1.msp.gz │ └── sample2.msp.gz ├── readme.md ├── screenshot.png ├── src ├── client │ ├── components │ │ ├── bundlephobia-stats.component.scss │ │ ├── bundlephobia-stats.component.tsx │ │ ├── but-wait-theres-more.component.tsx │ │ ├── button.component.scss │ │ ├── button.component.tsx │ │ ├── dashboard-chunk-page.component.tsx │ │ ├── dashboard-header.component.scss │ │ ├── dashboard-header.component.tsx │ │ ├── dashboard-node-module-page.component.tsx │ │ ├── dashboard-overview.component.scss │ │ ├── dashboard-overview.tsx │ │ ├── dashboard-own-module-page.component.tsx │ │ ├── dashboard-tabs.component.scss │ │ ├── dashboard.component.scss │ │ ├── dashboard.component.tsx │ │ ├── enter-urls.component.scss │ │ ├── enter-urls.component.tsx │ │ ├── errors.component.scss │ │ ├── errors.component.tsx │ │ ├── graphs │ │ │ ├── base-graph.component.scss │ │ │ ├── base-graph.component.tsx │ │ │ ├── changed-module-graph.component.tsx │ │ │ ├── chunk-graph.component.tsx │ │ │ ├── dependent-graph.component.tsx │ │ │ └── graph-tool.tsx │ │ ├── hints │ │ │ ├── hint-button.component.scss │ │ │ ├── hint-button.component.tsx │ │ │ └── hints.component.tsx │ │ ├── imports-list.component.scss │ │ ├── imports-list.component.tsx │ │ ├── imports-stats-row.component.tsx │ │ ├── module-table.component.scss │ │ ├── module-table.component.tsx │ │ ├── overview-suggestions.component.scss │ │ ├── overview-suggestions.tsx │ │ ├── panels │ │ │ ├── base-panel.component.tsx │ │ │ ├── boolean-panel.component.tsx │ │ │ ├── counter-panel.component.tsx │ │ │ ├── node-module-panel.component.tsx │ │ │ ├── panel-arrangement.component.tsx │ │ │ ├── panels.component.scss │ │ │ └── stat-delta.component.tsx │ │ ├── placeholder.component.scss │ │ ├── placeholder.component.tsx │ │ ├── progress-bar.component.scss │ │ ├── progress-bar.component.tsx │ │ ├── root.component.tsx │ │ ├── scroll-to-top.component.tsx │ │ ├── util.component.scss │ │ ├── util.ts │ │ └── variables.scss │ ├── index.css │ ├── index.tsx │ ├── redux │ │ ├── actions.ts │ │ ├── epics.ts │ │ ├── reducer.ts │ │ └── services │ │ │ ├── bundlephobia-api.ts │ │ │ └── http-error.ts │ ├── stat-reducers.ts │ └── worker │ │ ├── download.ts │ │ ├── index.worker.ts │ │ └── semaphore.ts ├── index.ts ├── plugin.ts └── types │ ├── bfj.d.ts │ ├── scss.d.ts │ ├── webpack.d.ts │ └── worker.d.ts ├── tsconfig.json ├── tsconfig.package.json ├── tslint.json ├── webpack.config.js └── webpack.production.js /.github/workflows/azure-static-web-apps-happy-water-0887b0b1e.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build And Deploy 22 | id: builddeploy 23 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 24 | with: 25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_HAPPY_WATER_0887B0B1E }} 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 27 | action: "upload" 28 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 30 | app_build_command: "npm run build" 31 | app_location: "/" # App source code path 32 | api_location: "api" # Api source code path - optional 33 | output_location: "dist" # Built app content directory - optional 34 | ###### End of Repository/Build Configurations ###### 35 | 36 | close_pull_request_job: 37 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 38 | runs-on: ubuntu-latest 39 | name: Close Pull Request Job 40 | steps: 41 | - name: Close Pull Request 42 | id: closepullrequest 43 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 44 | with: 45 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_HAPPY_WATER_0887B0B1E }} 46 | action: "close" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | #### node #### 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | /dist 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | /dist/public/samples 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mixer/webpack-bundle-compare", 3 | "version": "0.1.1", 4 | "description": "Tool to compare webpack bundle files", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "fmt": "prettier --write \"src/**/*.{tsx,ts}\" && npm run test:lint -- --fix", 8 | "test": "mocha --opts mocha.opts && npm run test:fmt && npm run test:lint", 9 | "test:fmt": "prettier --list-different \"src/**/*.{tsx,ts}\" || echo \"Run npm run fmt to fix formatting on these files\"", 10 | "test:lint": "tslint --project tsconfig.json \"src/**/*.{ts,tsx}\"", 11 | "build": "tsc -p tsconfig.package.json && webpack --config webpack.production.js", 12 | "start": "webpack serve", 13 | "prepare": "tsc -p tsconfig.package.json" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/mixer/webpack-bundle-compare.git" 18 | }, 19 | "keywords": [ 20 | "webpack", 21 | "bundle", 22 | "compare", 23 | "comparison", 24 | "analyzer" 25 | ], 26 | "author": "Connor Peet ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/mixer/webpack-bundle-compare/issues" 30 | }, 31 | "homepage": "https://github.com/mixer/webpack-bundle-compare#readme", 32 | "dependencies": { 33 | "bfj": "^7.0.2", 34 | "js-base64": "^3.6.0", 35 | "msgpack-lite": "^0.1.26" 36 | }, 37 | "devDependencies": { 38 | "@mixer/retrieval": "^2.0.3", 39 | "@types/chai": "^4.2.17", 40 | "@types/cytoscape": "^3.14.15", 41 | "@types/filesize": "^4.2.0", 42 | "@types/fixed-data-table-2": "^0.8.4", 43 | "@types/js-base64": "^3.0.0", 44 | "@types/mocha": "^8.2.2", 45 | "@types/msgpack-lite": "^0.1.7", 46 | "@types/node": "^15.0.2", 47 | "@types/pako": "^1.0.1", 48 | "@types/react": "^17.0.5", 49 | "@types/react-dom": "^17.0.3", 50 | "@types/react-redux": "^7.1.16", 51 | "@types/react-router-dom": "^5.1.7", 52 | "@types/react-tabs": "^2.3.2", 53 | "@types/sigmajs": "^1.0.27", 54 | "@types/webpack": "^5.28.0", 55 | "chai": "^4.3.4", 56 | "copy-webpack-plugin": "^8.1.1", 57 | "css-loader": "^5.2.4", 58 | "cytoscape": "^3.18.2", 59 | "cytoscape-dagre": "^2.3.2", 60 | "cytoscape-fcose": "^2.0.0", 61 | "dayjs": "^1.10.4", 62 | "filesize": "^6.3.0", 63 | "fixed-data-table-2": "^1.1.3", 64 | "flexboxgrid": "^6.3.1", 65 | "fs-extra": "^10.0.0", 66 | "html-webpack-plugin": "^5.3.1", 67 | "mocha": "^8.4.0", 68 | "node-sass": "^5.0.0", 69 | "normalize.css": "^8.0.1", 70 | "pako": "^2.0.3", 71 | "prettier": "^2.2.1", 72 | "react": "^17.0.2", 73 | "react-dom": "^17.0.2", 74 | "react-github-corner": "^2.5.0", 75 | "react-icons": "^4.2.0", 76 | "react-redux": "^7.2.4", 77 | "react-router-dom": "^5.2.0", 78 | "react-tabs": "^3.2.2", 79 | "redux": "^4.1.0", 80 | "redux-devtools-extension": "^2.13.9", 81 | "redux-observable": "^1.2.0", 82 | "reselect": "^4.0.0", 83 | "rxjs": "^6.0.0", 84 | "sass-loader": "^11.0.1", 85 | "sigma": "^1.2.1", 86 | "style-loader": "^2.0.0", 87 | "ts-loader": "^9.1.2", 88 | "ts-node": "^9.1.1", 89 | "tslint": "^5.20.1", 90 | "tslint-config-prettier": "^1.18.0", 91 | "typesafe-actions": "^5.1.0", 92 | "typescript": "^4.2.4", 93 | "webpack": "^5.36.2", 94 | "webpack-cli": "^4.7.0", 95 | "webpack-dev-server": "^3.11.2", 96 | "worker-loader": "^3.0.8" 97 | }, 98 | "prettier": { 99 | "trailingComma": "all", 100 | "singleQuote": true, 101 | "printWidth": 100, 102 | "arrowParens": "avoid", 103 | "tabWidth": 2 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /public/samples/sample1.msp.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/webpack-bundle-compare/f8a0c6fe5cd96ed7b35aa5324a30f35b53c754c6/public/samples/sample1.msp.gz -------------------------------------------------------------------------------- /public/samples/sample2.msp.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/webpack-bundle-compare/f8a0c6fe5cd96ed7b35aa5324a30f35b53c754c6/public/samples/sample2.msp.gz -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # @mixer/webpack-bundle-compare 2 | 3 | This is a tool that allows you to compare webpack bundle analysis files over time. Check it out [here](https://happy-water-0887b0b1e.azurestaticapps.net). 4 | 5 | ![](./screenshot.png) 6 | 7 | ## Usage 8 | 9 | The bundle comparison tool takes URLs of webpack stat outputs and displays them. You can use the JSON output from the [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer), but we also include a plugin here if you don't use that package. 10 | 11 | ### Using the Plugin 12 | 13 | First, install the plugin. 14 | 15 | ``` 16 | npm install --save-dev @mixer/webpack-bundle-compare 17 | ``` 18 | 19 | Then, add it to your webpack.config.js file. 20 | 21 | ```diff 22 | +const { BundleComparisonPlugin } = require('@mixer/webpack-bundle-compare'); 23 | 24 | ... 25 | 26 | plugins: [ 27 | + new BundleComparisonPlugin() 28 | ] 29 | ``` 30 | 31 | By default, this'll output a file named `stats.msp.gz` in your build output. This is a gzipped msgpack file--for large projects webpack bundle stats can get pretty big and slow down your build, so we try to make it fast. You can configure the output format and filename by passing options to the plugin: 32 | 33 | ```js 34 | new BundleComparisonPlugin({ 35 | // File to create, relative to the webpack build output path: 36 | file: 'myStatsFile.msp.gz', 37 | // Format to output, "msgpack" by default: 38 | format: 'msgpack' | 'json', 39 | // Whether to gzip the output, defaults to true: 40 | gzip: true | false, 41 | }) 42 | ``` 43 | 44 | Once you have this file, you can upload it somewhere such as Azure blob storage, and serve it in the tool. The module exposes a convenient way to get a direct preloaded link in the tool: 45 | 46 | ```js 47 | const { getComparisonAddress } = require('@mixer/webpack-bundle-compare'); 48 | const link = getComparisonAddress([ 49 | 'http://example.com/stats1.msp.gz', 50 | 'http://example.com/stats1.msp.gz', 51 | ]) 52 | 53 | console.log('See your comparison at:', link); 54 | ``` 55 | 56 | ## Contributing 57 | 58 | ### Architecture 59 | 60 | It's a React/Redux app. We pull webpack analysis JSON down from URLs, process them, and output UI. State is driven via Redux, and the parsing and unzipping of the (potentially very large) webpack stat files happen in a webworker. Actions sent in the redux state are mirrored to the webworker, and in turn the webworker and send actions which get fired back to the host application. The work we do atop the stats file is not particularly interesting--an assortment of parsing, walking, and graph generation functions. 61 | 62 | ### Iteration 63 | 64 | To develop against the UI: 65 | 66 | 1. Create a folder called "public/samples", and place JSON files in there. Or use the ones we already have preloaded. 67 | 2. Set the `WBC_FILES` environment variable to a comma-delimited list of the filenames you placed in there. 68 | 3. Running the webpack dev server via `npm start` will now serve the files you have placed in there. 69 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/webpack-bundle-compare/f8a0c6fe5cd96ed7b35aa5324a30f35b53c754c6/screenshot.png -------------------------------------------------------------------------------- /src/client/components/bundlephobia-stats.component.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-size: 0.9em; 3 | color: rgba(255, 255, 255, 0.8); 4 | 5 | :global .row { 6 | margin-top: 16px; 7 | } 8 | 9 | p { 10 | line-height: 1.5em; 11 | } 12 | } 13 | 14 | .icons { 15 | text-align: center; 16 | margin-top: 16px; 17 | font-size: 2.5em; 18 | 19 | a { 20 | margin-right: 0.5em; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/client/components/bundlephobia-stats.component.tsx: -------------------------------------------------------------------------------- 1 | import { Retrieval, RetrievalState, shouldAttempt } from '@mixer/retrieval'; 2 | import * as filesize from 'filesize'; 3 | import * as React from 'react'; 4 | import { IoLogoGithub, IoLogoNpm } from 'react-icons/io'; 5 | import { connect } from 'react-redux'; 6 | import { fetchBundlephobiaData } from '../redux/actions'; 7 | import { getBundlephobiaData, IAppState } from '../redux/reducer'; 8 | import { IBundlephobiaStats } from '../redux/services/bundlephobia-api'; 9 | import styles from './bundlephobia-stats.component.scss'; 10 | import { Errors } from './errors.component'; 11 | import { SideEffectHint, TreeShakeHint } from './hints/hints.component'; 12 | import { BasePanel } from './panels/base-panel.component'; 13 | import { BooleanPanel } from './panels/boolean-panel.component'; 14 | import { CounterPanel } from './panels/counter-panel.component'; 15 | import { Placeholder } from './placeholder.component'; 16 | import { IndefiniteProgressBar } from './progress-bar.component'; 17 | import { color } from './util'; 18 | 19 | interface IProps { 20 | name: string; 21 | stats: Retrieval; 22 | retrieve(name: string): void; 23 | } 24 | 25 | class BundlephobiaStatsComponent extends React.PureComponent { 26 | public componentDidMount() { 27 | if (shouldAttempt(this.props.stats)) { 28 | this.props.retrieve(this.props.name); 29 | } 30 | } 31 | public componentDidUpdate(prevProps: IProps) { 32 | if (this.props.name !== prevProps.name && shouldAttempt(this.props.stats)) { 33 | this.props.retrieve(this.props.name); 34 | } 35 | } 36 | 37 | public render() { 38 | const stats = this.props.stats; 39 | 40 | switch (stats.state) { 41 | case RetrievalState.Errored: 42 | if (stats.error.statusCode === 404) { 43 | return ( 44 | 45 | This package does not appear to be published on the npm registry. 46 | 47 | ); 48 | } 49 | 50 | return ; 51 | case RetrievalState.Succeeded: 52 | return ( 53 |
54 |

55 | The following information was retrieved from the latest version of this package. You 56 | may be using an older version of the package in your build--Webpack stats do not tell 57 | us your package version.{' '} 58 | 59 | View this package on Bundlephobia 60 | {' '} 61 | for more information. 62 |

63 | 64 |
65 |
66 | 71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 | 83 |
84 |
85 | 90 |
91 |
92 |
93 |
94 | 99 |
100 |
101 | 106 |
107 |
108 |
109 | {stats.value.repository && ( 110 | 111 | 112 | 113 | )} 114 | 115 | 116 | 117 |
118 |
119 | ); 120 | default: 121 | return ; 122 | } 123 | } 124 | } 125 | export const BundlephobiaStats = connect( 126 | (state: IAppState, { name }: { name: string }) => ({ 127 | stats: getBundlephobiaData(state, name), 128 | }), 129 | dispatch => ({ 130 | retrieve(name: string) { 131 | dispatch(fetchBundlephobiaData.request({ name })); 132 | }, 133 | }), 134 | )(BundlephobiaStatsComponent); 135 | -------------------------------------------------------------------------------- /src/client/components/but-wait-theres-more.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IProps { 4 | count: number; 5 | defaultDisplay?: number; 6 | children(index: number): React.ReactNode; 7 | } 8 | 9 | /** 10 | * Shows more children behind a "show more" link, when there's very many of them. 11 | */ 12 | export class ButWaitTheresMore extends React.PureComponent { 13 | public state = { limit: Math.min(this.props.defaultDisplay || 100, this.props.count) }; 14 | 15 | public render() { 16 | const output: React.ReactNode[] = []; 17 | for (let i = 0; i < this.state.limit; i++) { 18 | output.push(this.props.children(i)); 19 | } 20 | 21 | return ( 22 | <> 23 | {output} 24 | {this.state.limit < this.props.count && ( 25 | Show More ({this.props.count - this.state.limit} left) 26 | )} 27 | 28 | ); 29 | } 30 | 31 | private readonly showMore = () => { 32 | this.setState({ 33 | limit: Math.min(this.props.count, this.state.limit + (this.props.defaultDisplay || 100)), 34 | }); 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/client/components/button.component.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | @mixin variant($name, $color) { 4 | &[data-variant*="#{$name}"] { 5 | background: linear-gradient(lighten($color, 10%), darken($color, 10%)); 6 | 7 | &:hover { 8 | background: darken($color, 15%); 9 | } 10 | 11 | &[disabled] { 12 | background: darken($color, 15%); 13 | } 14 | } 15 | } 16 | 17 | @mixin size($name, $hpad, $wpad, $min-width) { 18 | &[data-variant*="#{$name}"] { 19 | padding: $hpad $wpad; 20 | min-width: $min-width; 21 | } 22 | } 23 | 24 | .button { 25 | color: #fff; 26 | border: none; 27 | border-radius: 100px; 28 | outline: 0 !important; 29 | cursor: pointer; 30 | text-transform: uppercase; 31 | letter-spacing: 1px; 32 | box-shadow: 0 3px 4px rgba(0, 0, 0, 0.5), inset 0 1px rgba(255, 255, 255, 0.3); 33 | 34 | @include variant('pink', $color-pink); 35 | @include variant('yellow', $color-yellow); 36 | @include variant('blue', $color-blue); 37 | 38 | @include size('small', 8px, 16px, 100px); 39 | @include size('normal', 8px, 16px, 100px); 40 | 41 | &[disabled] { 42 | color: rgba(#fff, 0.5); 43 | cursor: default; 44 | box-shadow: none; 45 | } 46 | 47 | &:active { 48 | box-shadow: 0 3px 4px rgba(0, 0, 0, 0.5), inset 0 4px 8px rgba(0, 0, 0, 0.3); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/client/components/button.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './button.component.scss'; 3 | import { classes } from './util'; 4 | 5 | type Props = React.DetailedHTMLProps< 6 | React.ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | > & { variant?: string; size?: 'small' | 'large' | 'normal' }; 9 | 10 | export const Button: React.FC = props => { 11 | const { variant = 'pink', size = 'normal', ...nested } = props; 12 | return ( 13 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/client/components/dashboard-chunk-page.component.tsx: -------------------------------------------------------------------------------- 1 | import * as filesize from 'filesize'; 2 | import * as React from 'react'; 3 | import { StatsCompilation } from 'webpack'; 4 | import { 5 | compareNodeModules, 6 | getNodeModuleCount, 7 | getNodeModuleSize, 8 | getTotalModuleCount, 9 | getTreeShakablePercent, 10 | } from '../stat-reducers'; 11 | import { ChangedModuleGraph } from './graphs/changed-module-graph.component'; 12 | import { TreeShakeHint } from './hints/hints.component'; 13 | import { ModuleTable } from './module-table.component'; 14 | import { CounterPanel } from './panels/counter-panel.component'; 15 | import { NodeModulePanel } from './panels/node-module-panel.component'; 16 | import { PanelArrangement } from './panels/panel-arrangement.component'; 17 | import { Placeholder } from './placeholder.component'; 18 | import { formatPercent } from './util'; 19 | 20 | export const DashboardChunkPage: React.FC<{ 21 | chunk: number; 22 | first: StatsCompilation; 23 | last: StatsCompilation; 24 | }> = ({ first, last, chunk }) => { 25 | const firstObj = first.chunks!.find(c => c.id === chunk); 26 | const lastSize = last.chunks!.find(c => c.id === chunk); 27 | const nodeModules = compareNodeModules(first, last); 28 | 29 | return ( 30 | <> 31 |
32 |
33 |

Chunk Stats

34 | 35 | 41 | 46 | 51 | 57 | 64 | 69 | 70 | 71 |

72 | Node Modules 73 | Click on a module to view more information about it. 74 |

75 | 76 | {nodeModules 77 | .sort((a, b) => (b.new ? b.new.totalSize : 0) - (a.new ? a.new.totalSize : 0)) 78 | .map(comparison => ( 79 | 80 | ))} 81 | 82 | {nodeModules.length === 0 && Huzzah! You have no dependencies!} 83 |
84 |
85 |

Module List

86 | 87 |
88 |
89 |

90 | Bundle TreeOnly changed files, and their parents, are displayed. 91 |

92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/client/components/dashboard-header.component.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .item { 4 | font-weight: bold; 5 | color: rgba(#fff, 0.8); 6 | text-decoration: none; 7 | line-height: 40px; 8 | display: flex; 9 | align-items: center; 10 | 11 | + .item { 12 | margin-left: 32px; 13 | } 14 | } 15 | 16 | a.item:hover { 17 | color: #fff; 18 | } 19 | 20 | .icon { 21 | color: $color-blue; 22 | font-size: 1.5em; 23 | margin-right: 0.4em; 24 | } 25 | -------------------------------------------------------------------------------- /src/client/components/dashboard-header.component.tsx: -------------------------------------------------------------------------------- 1 | import * as dayjs from 'dayjs'; 2 | import * as React from 'react'; 3 | import { IconType } from 'react-icons'; 4 | import { 5 | IoIosArrowBack, 6 | IoIosArrowRoundForward, 7 | IoIosCalendar, 8 | IoIosCloseCircleOutline, 9 | IoIosWarning, 10 | } from 'react-icons/io'; 11 | import { Link, withRouter } from 'react-router-dom'; 12 | import { StatsCompilation } from 'webpack'; 13 | import styles from './dashboard-header.component.scss'; 14 | 15 | const DashboardHeaderItem: React.FC<{ icon: IconType; href?: string | (() => void) }> = props => { 16 | const inner = ( 17 | <> 18 | {props.children} 19 | 20 | ); 21 | return typeof props.href === 'string' ? ( 22 | 23 | {inner} 24 | 25 | ) : ( 26 | 31 | {inner} 32 | 33 | ); 34 | }; 35 | 36 | export const DashboardBuildDate: React.FC<{ 37 | first: StatsCompilation; 38 | last: StatsCompilation; 39 | }> = ({ first, last }) => { 40 | const from = first.builtAt; 41 | const to = last.builtAt; 42 | if (!from || !to) { 43 | return null; 44 | } 45 | 46 | let fromFmt = dayjs(from).format('YYYY-MM-DD'); 47 | let toFmt = dayjs(to).format('YYYY-MM-DD'); 48 | if (fromFmt === toFmt) { 49 | fromFmt = dayjs(from).format('YYYY-MM-DD HH:mm'); 50 | toFmt = dayjs(to).format('HH:mm'); 51 | } 52 | return ( 53 | 54 | Built {fromFmt} {toFmt} 55 | 56 | ); 57 | }; 58 | 59 | export const DashboardWarningCount: React.FC<{ last: StatsCompilation }> = ({ last }) => ( 60 | 64 | {last.warnings?.length ?? 0} Warnings 65 | 66 | ); 67 | 68 | export const DashboardErrorCount: React.FC<{ last: StatsCompilation }> = ({ last }) => ( 69 | 73 | {last.errors?.length ?? 0} Errors 74 | 75 | ); 76 | 77 | export const DashboardClose = withRouter(props => ( 78 | props.history.goBack()}> 79 | Back 80 | 81 | )); 82 | -------------------------------------------------------------------------------- /src/client/components/dashboard-node-module-page.component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StatsCompilation } from 'webpack'; 3 | import { getDirectImportsOfNodeModule } from '../stat-reducers'; 4 | import { BundlephobiaStats } from './bundlephobia-stats.component'; 5 | import { NodeModuleDependentGraph } from './graphs/dependent-graph.component'; 6 | import { ImportsList, IssuerTree } from './imports-list.component'; 7 | import { ImportsStatsRow } from './imports-stats-row.component'; 8 | 9 | export const DashboardNodeModulePage: React.FC<{ 10 | name: string; 11 | first: StatsCompilation; 12 | last: StatsCompilation; 13 | }> = ({ first, last, name }) => { 14 | const firstImports = getDirectImportsOfNodeModule(first, name); 15 | const lastImports = getDirectImportsOfNodeModule(last, name); 16 | return ( 17 | <> 18 |
19 |
20 | 21 | 22 |

Imports of "{name}"

23 | 24 |
25 |
26 |

Bundlephobia Stats

27 | 28 | 29 |

30 | Issuer Tree 31 | The shortest from the entrypoint to this module 32 |

33 | 34 |
35 |
36 | 37 |

38 | Import TreeA graph of all files that depend on the module. 39 |

40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/client/components/dashboard-overview.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/webpack-bundle-compare/f8a0c6fe5cd96ed7b35aa5324a30f35b53c754c6/src/client/components/dashboard-overview.component.scss -------------------------------------------------------------------------------- /src/client/components/dashboard-overview.tsx: -------------------------------------------------------------------------------- 1 | import * as filesize from 'filesize'; 2 | import * as React from 'react'; 3 | import { StatsCompilation } from 'webpack'; 4 | import { 5 | getAverageChunkSize, 6 | getEntryChunkSize, 7 | getNodeModuleCount, 8 | getNodeModuleSize, 9 | getTotalChunkSize, 10 | getTotalModuleCount, 11 | getTreeShakablePercent, 12 | } from '../stat-reducers'; 13 | import { ChunkGraph } from './graphs/chunk-graph.component'; 14 | import { 15 | AverageChunkSize, 16 | TotalModules, 17 | TreeShakeHint, 18 | WhatIsAnEntrypoint, 19 | } from './hints/hints.component'; 20 | import { ModuleTable } from './module-table.component'; 21 | import { OverviewSuggestions } from './overview-suggestions'; 22 | import { CounterPanel } from './panels/counter-panel.component'; 23 | import { PanelArrangement } from './panels/panel-arrangement.component'; 24 | import { formatDuration, formatPercent } from './util'; 25 | 26 | export const DashboardOverview: React.FC<{ 27 | first: StatsCompilation; 28 | last: StatsCompilation; 29 | }> = ({ first, last }) => { 30 | return ( 31 | <> 32 |
33 |
34 |

Suggestions

35 | 36 | 37 |

Stats

38 | 39 | 45 | 51 | 58 | 65 | 71 | 77 | 84 | 89 | 95 | 96 |
97 |
98 |

Module List

99 | 100 |
101 |
102 |

103 | Chunk Graph 104 | 105 | Larger chunks are shown in red, smaller ones in green. Click on a chunk to drill down. 106 | 107 |

108 | 109 | 110 | ); 111 | }; 112 | -------------------------------------------------------------------------------- /src/client/components/dashboard-own-module-page.component.tsx: -------------------------------------------------------------------------------- 1 | import * as filesize from 'filesize'; 2 | import * as React from 'react'; 3 | import { StatsCompilation } from 'webpack'; 4 | import { 5 | getImportsOfName, 6 | getWebpackModulesMap, 7 | replaceLoaderInIdentifier, 8 | } from '../stat-reducers'; 9 | import { GenericDependentGraph } from './graphs/dependent-graph.component'; 10 | import { DependentModules } from './hints/hints.component'; 11 | import { ImportsList, IssuerTree } from './imports-list.component'; 12 | import { CounterPanel } from './panels/counter-panel.component'; 13 | import { PanelArrangement } from './panels/panel-arrangement.component'; 14 | 15 | export const DashboardOwnModulePage: React.FC<{ 16 | name: string; 17 | first: StatsCompilation; 18 | last: StatsCompilation; 19 | }> = ({ first, last, name }) => { 20 | const firstRoot = getWebpackModulesMap(first)[name]; 21 | const lastRoot = getWebpackModulesMap(last)[name]; 22 | const anyRoot = lastRoot || firstRoot; 23 | if (!anyRoot) { 24 | return null; 25 | } 26 | 27 | const firstImports = getImportsOfName(first, name); 28 | const lastImports = getImportsOfName(last, name); 29 | 30 | return ( 31 | <> 32 |
33 |
34 | 35 | 41 | 47 | 54 | 55 | 56 |

Imports of "{replaceLoaderInIdentifier(anyRoot.name)}"

57 | 58 |
59 |
60 |

61 | Issuer Tree 62 | The shortest from the entrypoint to this module 63 |

64 | 65 |
66 |
67 | 68 |

69 | Import TreeA graph of all files that depend on the module. 70 |

71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/client/components/dashboard-tabs.component.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .tabs { 4 | :global { 5 | .react-tabs__tab-list { 6 | display: flex; 7 | padding: 8px; 8 | background: rgba(#000, 0.2); 9 | } 10 | 11 | .react-tabs__tab { 12 | list-style-type: none; 13 | padding: 8px 32px; 14 | border-radius: 3px; 15 | cursor: pointer; 16 | text-transform: uppercase; 17 | font-family: var(--font-family-mono); 18 | font-size: 0.8em; 19 | 20 | &--selected { 21 | background: $color-pink; 22 | color: $color-dark; 23 | } 24 | } 25 | 26 | 27 | .react-tabs__tab-panel { 28 | display: none; 29 | background: rgba(#000, .1); 30 | 31 | &--selected { 32 | display: block; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/client/components/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .wrapper { 4 | margin: 32px; 5 | padding: 16px; 6 | background: rgba(#000, 0.4); 7 | border-radius: 4px; 8 | box-shadow: 0 10px 50px rgba(#000, 0.3); 9 | } 10 | 11 | .header { 12 | display: flex; 13 | align-items: center; 14 | border-bottom: 1px solid rgba(#fff, 0.1); 15 | padding-bottom: 16px; 16 | margin-bottom: 16px; 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/dashboard.component.tsx: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import * as React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Redirect, Route, RouteChildrenProps } from 'react-router'; 5 | import { StatsCompilation } from 'webpack'; 6 | import { getKnownStats, IAppState } from '../redux/reducer'; 7 | import { DashboardChunkPage } from './dashboard-chunk-page.component'; 8 | import { 9 | DashboardBuildDate, 10 | DashboardClose, 11 | DashboardErrorCount, 12 | DashboardWarningCount, 13 | } from './dashboard-header.component'; 14 | import { DashboardNodeModulePage } from './dashboard-node-module-page.component'; 15 | import { DashboardOverview } from './dashboard-overview'; 16 | import { DashboardOwnModulePage } from './dashboard-own-module-page.component'; 17 | import styles from './dashboard.component.scss'; 18 | 19 | interface IProps { 20 | stats: StatsCompilation[]; 21 | } 22 | 23 | class DashboardComponent extends React.PureComponent { 24 | private get first() { 25 | return this.props.stats[0]; 26 | } 27 | 28 | private get last() { 29 | return this.props.stats[this.props.stats.length - 1]; 30 | } 31 | 32 | public render() { 33 | const { first, last } = this; 34 | if (!first) { 35 | return ; 36 | } 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 |
45 | 46 |
47 | 48 | 49 | 54 | 59 |
60 | ); 61 | } 62 | 63 | private readonly renderOverview: React.FC = () => ( 64 | 65 | ); 66 | 67 | private readonly renderChunkPage: React.FC> = ({ 68 | match, 69 | }) => 70 | match && ( 71 | 76 | ); 77 | 78 | private readonly renderNodeModule: React.FC> = ({ 79 | match, 80 | }) => 81 | match && ( 82 | 87 | ); 88 | 89 | private readonly renderGenericModule: React.FC> = ({ 90 | match, 91 | }) => 92 | match && ( 93 | 98 | ); 99 | } 100 | 101 | export default connect((state: IAppState) => ({ 102 | stats: getKnownStats(state), 103 | }))(DashboardComponent); 104 | -------------------------------------------------------------------------------- /src/client/components/enter-urls.component.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | .entry { 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | width: 600px; 8 | max-width: 100vh; 9 | background: rgba(#000, 0.4); 10 | z-index: 0; 11 | text-align: center; 12 | transform: translate(-50%, -50%); 13 | padding: 50px; 14 | border-radius: 4px; 15 | box-shadow: 0 10px 50px rgba(0, 0, 0, 0.3); 16 | 17 | h1 { 18 | margin: 0 0 20px; 19 | } 20 | 21 | textarea, 22 | .error { 23 | margin: 20px 0; 24 | } 25 | } 26 | 27 | 28 | .upload { 29 | position: relative; 30 | display: inline-block; 31 | margin-left: 16px; 32 | overflow: hidden; 33 | 34 | input { 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | bottom: 0; 40 | opacity: 0; 41 | cursor: pointer; 42 | padding: 100px; 43 | } 44 | } -------------------------------------------------------------------------------- /src/client/components/enter-urls.component.tsx: -------------------------------------------------------------------------------- 1 | import { IRetrievalError, RetrievalState } from '@mixer/retrieval'; 2 | import * as React from 'react'; 3 | import GithubCorner from 'react-github-corner'; 4 | import { connect } from 'react-redux'; 5 | import { Redirect } from 'react-router-dom'; 6 | import { clearLoadedBundles, ILoadableResource, loadAllUrls } from '../redux/actions'; 7 | import { 8 | BundleStateMap, 9 | getBundleErrors, 10 | getBundleUrls, 11 | getGroupedBundleState, 12 | IAppState, 13 | } from '../redux/reducer'; 14 | import { Button } from './button.component'; 15 | import styles from './enter-urls.component.scss'; 16 | import { Errors } from './errors.component'; 17 | import { ProgressBar } from './progress-bar.component'; 18 | 19 | interface IProps { 20 | defaultUrls: string[]; 21 | bundleStates: BundleStateMap; 22 | bundleErrors: IRetrievalError[]; 23 | load(urls: ILoadableResource[]): void; 24 | complete(): void; 25 | cancel(): void; 26 | } 27 | 28 | interface IState { 29 | urls: string; 30 | } 31 | 32 | class EnterUrlsComponent extends React.PureComponent { 33 | public state = { urls: this.props.defaultUrls.join('\n') }; 34 | 35 | public componentDidMount() { 36 | this.props.cancel(); 37 | } 38 | 39 | public render() { 40 | const retrievingCount = this.props.bundleStates[RetrievalState.Retrieving]; 41 | const progress = 1 - retrievingCount / this.props.defaultUrls.length; 42 | 43 | let contents: React.ReactNode; 44 | if (retrievingCount) { 45 | contents = ( 46 | <> 47 | Downloading and parsing bundles... 48 | 49 | 50 | 51 | ); 52 | } else if ( 53 | this.props.defaultUrls.length > 0 && 54 | this.props.bundleStates[RetrievalState.Succeeded] === this.props.defaultUrls.length 55 | ) { 56 | contents = ( 57 | <> 58 | Opening the dashboard... 59 | 60 | 61 | 62 | ); 63 | } else { 64 | contents = ( 65 | <> 66 | Enter line-separated URLs of bundle JSON or msgpack files to compare 67 | {this.props.bundleErrors.length ? ( 68 | 69 | ) : null} 70 |