├── .nvmrc ├── test ├── dev-server │ ├── .gitignore │ ├── src.js │ └── webpack.config.js ├── src │ ├── a.js │ ├── b.js │ ├── a-clone.js │ └── index.js ├── bundles │ ├── invalidBundle.js │ ├── validBundleWithArrowFunction.js │ ├── validExtraBundleWithModulesAsArray.js │ ├── validExtraBundleWithNamedChunk.js │ ├── validJsonpWithArrayConcatAndEntryPoint.js │ ├── validWebpack4AsyncChunk.modules.json │ ├── validBundleWithArrowFunction.modules.json │ ├── validExtraBundleWithNamedChunk.modules.json │ ├── validWebpack4AsyncChunk.js │ ├── validWebpack4AsyncChunkWithWebWorkerChunkTemplatePlugin.js │ ├── validWebpack4AsyncChunkAndEntryPoint.modules.json │ ├── validWebpack4AsyncChunkUsingSelfInsteadOfWindow.js │ ├── validWebpack4AsyncChunkUsingThisInsteadOfWindow.js │ ├── validExtraBundleWithModulesAsArray.modules.json │ ├── validWebpack4AsyncChunkUsingCustomGlobalObject.modules.json │ ├── validWebpack4AsyncChunkUsingSelfInsteadOfWindow.modules.json │ ├── validWebpack4AsyncChunkUsingThisInsteadOfWindow.modules.json │ ├── validWebpack4AsyncChunkWithOptimizedModulesArray.modules.json │ ├── validWebpack4AsyncChunkWithWebWorkerChunkTemplatePlugin.modules.json │ ├── validJsonpWithArrayConcatAndEntryPoint.modules.json │ ├── validWebpack4AsyncChunkAndEntryPoint.js │ ├── validWebpack4AsyncChunkWithOptimizedModulesArray.js │ ├── validWebpack5ModernBundle.modules.json │ ├── validWebpack5LegacyBundle.modules.json │ ├── validNodeBundle.js │ ├── validWebpack4AsyncChunkUsingCustomGlobalObject.js │ ├── validWebpack5ModernBundle.js │ ├── validCommonBundleWithDedupePlugin.modules.json │ ├── validNodeBundle.modules.json │ ├── validCommonBundleWithModulesAsArray.modules.json │ ├── validUmdLibraryBundleWithModulesAsArray.modules.json │ ├── validWebpack5LegacyBundle.js │ ├── validCommonBundleWithModulesAsObject.modules.json │ ├── validBundleWithEsNextFeatures.modules.json │ ├── validExtraBundleWithModulesInsideArrayConcat.js │ ├── validExtraBundleWithModulesInsideArrayConcat.modules.json │ ├── validBundleWithEsNextFeatures.js │ ├── validUmdLibraryBundleWithModulesAsArray.js │ ├── validCommonBundleWithDedupePlugin.js │ ├── validCommonBundleWithModulesAsArray.js │ └── validCommonBundleWithModulesAsObject.js ├── stats │ ├── with-invalid-chunk │ │ ├── invalid-chunk.js │ │ └── valid-chunk.js │ ├── extremely-optimized-webpack-5-bundle │ │ ├── bundle.js │ │ └── expected-chart-data.js │ ├── webpack-5-bundle-with-concatenated-entry-module │ │ ├── app.js │ │ └── expected-chart-data.json │ ├── with-worker-loader-dynamic-import │ │ ├── 1.bundle.worker.js │ │ ├── 1.bundle.js │ │ ├── bundle.js │ │ └── bundle.worker.js │ ├── with-non-asset-asset │ │ └── bundle.js │ ├── webpack-5-bundle-with-single-entry │ │ ├── bundle.js │ │ └── expected-chart-data.js │ ├── with-special-chars │ │ ├── expected-chart-data.js │ │ ├── bundle.js │ │ └── stats.json │ ├── webpack-5-bundle-with-multiple-entries │ │ ├── bundle.js │ │ └── expected-chart-data.js │ ├── with-missing-chunk │ │ └── valid-chunk.js │ ├── with-missing-module-chunks │ │ └── valid-chunk.js │ ├── with-module-concatenation-info │ │ ├── bundle.js │ │ └── expected-chart-data.js │ ├── with-array-config │ │ ├── config-1-main.js │ │ └── config-2-main.js │ ├── with-worker-loader │ │ └── bundle.js │ ├── with-modules-in-chunks │ │ └── expected-chart-data.js │ ├── with-no-entrypoints │ │ └── stats.json │ ├── minimal-stats │ │ └── stats.json │ ├── with-invalid-dynamic-require.json │ └── with-missing-parsed-module │ │ └── bundle.js ├── .gitignore ├── webpack-versions │ ├── 4.44.2 │ │ └── package.json │ └── 5.76.0 │ │ └── package.json ├── .eslintrc.json ├── dev-server.js ├── parseUtils.js ├── utils.js ├── statsUtils.js ├── Logger.js ├── viewer.js └── helpers.js ├── .eslintrc.json ├── .gitignore ├── client ├── components │ ├── ModulesList.css │ ├── Icon.css │ ├── Switcher.css │ ├── CheckboxList.css │ ├── Checkbox.css │ ├── ContextMenuItem.css │ ├── Tooltip.css │ ├── ContextMenu.css │ ├── SwitcherItem.jsx │ ├── ContextMenuItem.jsx │ ├── ThemeToggle.css │ ├── Dropdown.css │ ├── Button.css │ ├── Checkbox.jsx │ ├── Search.css │ ├── ThemeToggle.jsx │ ├── ModuleItem.css │ ├── Switcher.jsx │ ├── ModulesList.jsx │ ├── CheckboxListItem.jsx │ ├── ModulesTreemap.css │ ├── Button.jsx │ ├── Icon.jsx │ ├── Sidebar.css │ ├── Tooltip.jsx │ ├── Search.jsx │ ├── Dropdown.jsx │ ├── ModuleItem.jsx │ ├── CheckboxList.jsx │ ├── ContextMenu.jsx │ ├── Sidebar.jsx │ └── Treemap.jsx ├── assets │ ├── icon-moon.svg │ ├── icon-arrow-right.svg │ ├── icon-sun.svg │ ├── icon-folder.svg │ ├── icon-module.svg │ ├── icon-invisible.svg │ ├── icon-chunk.svg │ └── icon-pin.svg ├── .eslintrc.json ├── utils.js ├── lib │ └── PureComponent.jsx ├── localStorage.js ├── viewer.jsx ├── viewer.css └── store.js ├── .browserslistrc ├── bin └── install-test-webpack-versions.sh ├── src ├── index.js ├── sizeUtils.js ├── tree │ ├── Node.js │ ├── utils.js │ ├── ContentModule.js │ ├── ContentFolder.js │ ├── Module.js │ ├── Folder.js │ ├── ConcatenatedModule.js │ └── BaseFolder.js ├── Logger.js ├── utils.js ├── statsUtils.js ├── template.js ├── BundleAnalyzerPlugin.js ├── bin │ └── analyzer.js └── viewer.js ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .npm-upgrade.json ├── jest.config.js ├── LICENSE ├── .github └── workflows │ └── main.yml ├── gulpfile.js ├── CONTRIBUTING.md ├── package.json └── webpack.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.14.0 2 | -------------------------------------------------------------------------------- /test/dev-server/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | -------------------------------------------------------------------------------- /test/src/a.js: -------------------------------------------------------------------------------- 1 | module.exports = 'module a'; 2 | -------------------------------------------------------------------------------- /test/src/b.js: -------------------------------------------------------------------------------- 1 | module.exports = 'module b'; 2 | -------------------------------------------------------------------------------- /test/src/a-clone.js: -------------------------------------------------------------------------------- 1 | module.exports = 'module a'; 2 | -------------------------------------------------------------------------------- /test/dev-server/src.js: -------------------------------------------------------------------------------- 1 | export const chuck = 'norris'; 2 | -------------------------------------------------------------------------------- /test/bundles/invalidBundle.js: -------------------------------------------------------------------------------- 1 | module.exports = 'invalid bundle'; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "th0r" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /public 3 | /samples 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /test/src/index.js: -------------------------------------------------------------------------------- 1 | require('./a'); 2 | require('./b'); 3 | require('./a-clone'); 4 | -------------------------------------------------------------------------------- /test/stats/with-invalid-chunk/invalid-chunk.js: -------------------------------------------------------------------------------- 1 | console.log('invalid chunk'); 2 | -------------------------------------------------------------------------------- /client/components/ModulesList.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | } 4 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | 3 | # Sandbox config 4 | /webpack.config.js 5 | # Output of sandbox config 6 | /dist 7 | -------------------------------------------------------------------------------- /test/stats/extremely-optimized-webpack-5-bundle/bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";console.log("module a","module b")})(); -------------------------------------------------------------------------------- /test/bundles/validBundleWithArrowFunction.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],[(t,e,r)=>{ 2 | console.log("Hello world!"); 3 | }]); 4 | -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithModulesAsArray.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0,2],[,,function(t,e,r){'123'},,,function(t,e){'12345'},]); 2 | -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithNamedChunk.js: -------------------------------------------------------------------------------- 1 | webpackJsonp(["app"],{125:function(n,e,t){console.log("it works");}},[125]); 2 | -------------------------------------------------------------------------------- /test/webpack-versions/4.44.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "webpack": "4.44.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/webpack-versions/5.76.0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "webpack": "5.76.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Supported browsers 2 | 3 | last 2 Chrome major versions 4 | last 2 Firefox major versions 5 | last 1 Safari major version 6 | -------------------------------------------------------------------------------- /test/bundles/validJsonpWithArrayConcatAndEntryPoint.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([10],Array(10).concat([function(e,t,n){'abcd'},,,,function(e,t,n){'efgh'}]),[11]); -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunk.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "57iH": "function(e,n,t){console.log(\"hello world\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /bin/install-test-webpack-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for dir in "$(dirname "$0")"/../test/webpack-versions/*; do (cd "$dir" && npm i); done 4 | -------------------------------------------------------------------------------- /test/bundles/validBundleWithArrowFunction.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "(t,e,r)=>{\n console.log(\"Hello world!\");\n}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithNamedChunk.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "125": "function(n,e,t){console.log(\"it works\");}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunk.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[27],{"57iH":function(e,n,t){console.log("hello world")}}]); 2 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkWithWebWorkerChunkTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | self.chunkCallbackName([27],{1:function(e,n,t){console.log("Chuck Norris")}}); 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const {start} = require('./viewer'); 2 | 3 | module.exports = { 4 | start, 5 | BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin') 6 | }; 7 | -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-concatenated-entry-module/app.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";console.log("foo.js"),console.log("bar.js")})(),console.log("baz.js"); 2 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkAndEntryPoint.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "57iH": "function(e,n,t){console.log(\"hello world\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingSelfInsteadOfWindow.js: -------------------------------------------------------------------------------- 1 | (self.webpackJsonp=self.webpackJsonp||[]).push([[27],{1:function(e,n,t){console.log("Chuck Norris")}}]); 2 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingThisInsteadOfWindow.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonp=this.webpackJsonp||[]).push([[27],{1:function(e,n,t){console.log("Chuck Norris")}}]); 2 | -------------------------------------------------------------------------------- /test/stats/with-worker-loader-dynamic-import/1.bundle.worker.js: -------------------------------------------------------------------------------- 1 | self.webpackChunk([1],[,function(n,t,c){"use strict";c.r(t),c.d(t,"foo",(function(){return o}));const o=42}]); -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithModulesAsArray.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "2": "function(t,e,r){'123'}", 4 | "5": "function(t,e){'12345'}" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingCustomGlobalObject.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "1": "function(e,n,t){console.log(\"Chuck Norris\")}" 4 | } 5 | } 6 | 7 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingSelfInsteadOfWindow.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "1": "function(e,n,t){console.log(\"Chuck Norris\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingThisInsteadOfWindow.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "1": "function(e,n,t){console.log(\"Chuck Norris\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkWithOptimizedModulesArray.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "549": "function(e,n,t){console.log(\"hello world\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkWithWebWorkerChunkTemplatePlugin.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "1": "function(e,n,t){console.log(\"Chuck Norris\")}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/bundles/validJsonpWithArrayConcatAndEntryPoint.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "10": "function(e,t,n){'abcd'}", 4 | "14": "function(e,t,n){'efgh'}" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/assets/icon-moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkAndEntryPoint.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[27],{"57iH":function(e,n,t){console.log("hello world")}},[["57iH",19,24,25]]]); 2 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkWithOptimizedModulesArray.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([['chunkId1', 'chunkId2'],Array(549).concat([function(e,n,t){console.log("hello world")}])]); 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | // Babel config for Node 2 | // Compiles sources, gulpfile and tests 3 | { 4 | "presets": [ 5 | ["@babel/preset-env", { 6 | "targets": {"node": "16.20.2"} 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test/stats/with-worker-loader-dynamic-import/1.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[1],[,function(n,e,o){"use strict";o.r(e),e.default=function(){return new Worker(o.p+"bundle.worker.js")}}]]); -------------------------------------------------------------------------------- /client/components/Icon.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | background: no-repeat center/contain; 3 | display: inline-block; 4 | filter: invert(0); 5 | } 6 | 7 | [data-theme="dark"] .icon { 8 | filter: invert(1); 9 | } 10 | -------------------------------------------------------------------------------- /test/bundles/validWebpack5ModernBundle.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "631": "r=>{r.exports=\"module a\"}", 4 | "85": "r=>{r.exports=\"module a\"}", 5 | "326": "r=>{r.exports=\"module b\"}" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/bundles/validWebpack5LegacyBundle.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "631": "function(o){o.exports=\"module a\"}", 4 | "85": "function(o){o.exports=\"module a\"}", 5 | "326": "function(o){o.exports=\"module b\"}" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/assets/icon-arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | indent_style = space 7 | indent_size = 2 8 | 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /client/components/Switcher.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | white-space: nowrap; 4 | } 5 | 6 | .label { 7 | font-weight: bold; 8 | font-size: 11px; 9 | margin-bottom: 7px; 10 | } 11 | 12 | .item + .item { 13 | margin-left: 5px; 14 | } 15 | -------------------------------------------------------------------------------- /test/stats/with-non-asset-asset/bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{var r={146:r=>{r.exports="module a"},296:r=>{r.exports="module a"},260:r=>{r.exports="module b"}},e={};function o(t){if(e[t])return e[t].exports;var p=e[t]={exports:{}};return r[t](p,p.exports,o),p.exports}o(296),o(260),o(146)})(); -------------------------------------------------------------------------------- /client/components/CheckboxList.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | white-space: nowrap; 4 | } 5 | 6 | .label { 7 | font-size: 11px; 8 | font-weight: bold; 9 | margin-bottom: 7px; 10 | } 11 | 12 | .item + .item { 13 | margin-top: 1px; 14 | } 15 | -------------------------------------------------------------------------------- /test/bundles/validNodeBundle.js: -------------------------------------------------------------------------------- 1 | exports.ids = ["common"]; 2 | exports.modules = { 3 | 0: function(e,t,n){n(1),n(21),n(96),n(306),n(23),n(150),n(57),n(56),n(34),n(138),e.exports=n(348)}, 4 | 3: function(e,t,n){"use strict";e.exports=n(680)}, 5 | 5: function(e,t){} 6 | }; 7 | -------------------------------------------------------------------------------- /test/bundles/validWebpack4AsyncChunkUsingCustomGlobalObject.js: -------------------------------------------------------------------------------- 1 | (("undefined" != typeof self ? self : this).webpackJsonp_someCustomName = ("undefined" != typeof self ? self : this).webpackJsonp_someCustomName || []).push([[27],{1:function(e,n,t){console.log("Chuck Norris")}}]); 2 | -------------------------------------------------------------------------------- /test/bundles/validWebpack5ModernBundle.js: -------------------------------------------------------------------------------- 1 | (()=>{var r={631:r=>{r.exports="module a"},85:r=>{r.exports="module a"},326:r=>{r.exports="module b"}},e={};function o(t){if(e[t])return e[t].exports;var p=e[t]={exports:{}};return r[t](p,p.exports,o),p.exports}o(85),o(326),o(631)})(); 2 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc.json", 3 | "env": { 4 | "jest": true, 5 | "browser": true 6 | }, 7 | "globals": { 8 | "makeWebpackConfig": true, 9 | "webpackCompile": true, 10 | "forEachWebpackVersion": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithDedupePlugin.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "function(t,r,e){e(1),e(2)}", 4 | "1": "function(t,r){t.exports=1}", 5 | "2": "1", 6 | "4": "[2, 'arg1', 'arg2']", 7 | "6": "['module-id', 'arg']" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/bundles/validNodeBundle.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "function(e,t,n){n(1),n(21),n(96),n(306),n(23),n(150),n(57),n(56),n(34),n(138),e.exports=n(348)}", 4 | "3": "function(e,t,n){\"use strict\";e.exports=n(680)}", 5 | "5": "function(e,t){}" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/assets/icon-sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/components/Checkbox.css: -------------------------------------------------------------------------------- 1 | .label { 2 | cursor: pointer; 3 | display: inline-block; 4 | } 5 | 6 | .checkbox { 7 | cursor: pointer; 8 | } 9 | 10 | .itemText { 11 | margin-left: 3px; 12 | position: relative; 13 | top: -2px; 14 | vertical-align: middle; 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules 3 | 4 | # Generated code 5 | lib 6 | public 7 | 8 | # Test fixtures 9 | test/bundles 10 | test/stats 11 | test/webpack.config.js 12 | 13 | # Test results 14 | test/output 15 | 16 | # Webpack config 17 | webpack.config.js 18 | 19 | samples 20 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithModulesAsArray.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "function(e,t,n){n(1),n(21),n(96),n(306),n(23),n(150),n(57),n(56),n(34),n(138),e.exports=n(348)}", 4 | "3": "function(e,t,n){\"use strict\";e.exports=n(680)}", 5 | "5": "function(e,t){}" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-single-entry/bundle.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var o,t,n={85:function(o,t){t.Z="module a"},326:function(o,t){t.Z="module b"}},r={};function e(o){if(r[o])return r[o].exports;var t=r[o]={exports:{}};return n[o](t,t.exports,e),t.exports}o=e(85),t=e(326),console.log(o.Z,t.Z)}(); -------------------------------------------------------------------------------- /test/bundles/validUmdLibraryBundleWithModulesAsArray.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "function(e,o,t){t(1),t(2),t(3)}", 4 | "1": "function(e,o){e.exports=\"module a\"}", 5 | "2": "function(e,o){e.exports=\"module b\"}", 6 | "3": "function(e,o){e.exports=\"module a\"}" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/bundles/validWebpack5LegacyBundle.js: -------------------------------------------------------------------------------- 1 | !function(){var o={631:function(o){o.exports="module a"},85:function(o){o.exports="module a"},326:function(o){o.exports="module b"}},t={};function r(e){if(t[e])return t[e].exports;var n=t[e]={exports:{}};return o[e](n,n.exports,r),n.exports}r(85),r(326),r(631)}(); 2 | -------------------------------------------------------------------------------- /test/stats/with-special-chars/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | "groups": [ 4 | { 5 | "id": 0, 6 | "label": "index.js", 7 | "path": "./index.js", 8 | "statSize": 1021 9 | } 10 | ], 11 | "label": "bundle.js", 12 | "statSize": 1021 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /client/components/ContextMenuItem.css: -------------------------------------------------------------------------------- 1 | .item { 2 | cursor: pointer; 3 | margin: 0; 4 | padding: 8px 14px; 5 | user-select: none; 6 | } 7 | 8 | .item:hover { 9 | background: #ffefd7; 10 | } 11 | 12 | .disabled { 13 | cursor: default; 14 | color: gray; 15 | } 16 | 17 | .item.disabled:hover { 18 | background: transparent; 19 | } 20 | -------------------------------------------------------------------------------- /client/assets/icon-folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sizeUtils.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib'); 2 | 3 | export function getCompressedSize(compressionAlgorithm, input) { 4 | if (compressionAlgorithm === 'gzip') return zlib.gzipSync(input, {level: 9}).length; 5 | if (compressionAlgorithm === 'brotli') return zlib.brotliCompressSync(input).length; 6 | 7 | throw new Error(`Unsupported compression algorithm: ${compressionAlgorithm}.`); 8 | } 9 | -------------------------------------------------------------------------------- /.npm-upgrade.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "mobx": { 4 | "versions": ">=6", 5 | "reason": "v6 drops decorators" 6 | }, 7 | "mobx-react": { 8 | "versions": ">=7", 9 | "reason": "v7 requires MobX v6" 10 | }, 11 | "webpack-cli": { 12 | "versions": ">=4", 13 | "reason": "Current version of Webpack Dev Server doesn't work with v4" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/components/Tooltip.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | position: absolute; 4 | padding: 5px 10px; 5 | border-radius: 4px; 6 | background: #fff; 7 | border: 1px solid #aaa; 8 | opacity: 0.9; 9 | white-space: nowrap; 10 | visibility: visible; 11 | transition: opacity .2s ease, visibility .2s ease; 12 | } 13 | 14 | .hidden { 15 | opacity: 0; 16 | visibility: hidden; 17 | } 18 | -------------------------------------------------------------------------------- /client/components/ContextMenu.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | position: absolute; 4 | padding: 0; 5 | border-radius: 4px; 6 | background: #fff; 7 | border: 1px solid #aaa; 8 | list-style: none; 9 | opacity: 1; 10 | white-space: nowrap; 11 | visibility: visible; 12 | transition: opacity .2s ease, visibility .2s ease; 13 | } 14 | 15 | .hidden { 16 | opacity: 0; 17 | visibility: hidden; 18 | } 19 | -------------------------------------------------------------------------------- /client/assets/icon-module.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "th0r-react", 4 | "../.eslintrc.json" 5 | ], 6 | "settings": { 7 | "react": { 8 | "version": "16.2" 9 | } 10 | }, 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "legacyDecorators": true 14 | } 15 | }, 16 | "rules": { 17 | "react/jsx-key": "off", 18 | "react/jsx-no-bind": "off", 19 | "react/react-in-jsx-scope": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/components/SwitcherItem.jsx: -------------------------------------------------------------------------------- 1 | import Button from './Button'; 2 | import PureComponent from '../lib/PureComponent'; 3 | 4 | export default class SwitcherItem extends PureComponent { 5 | render({item, ...props}) { 6 | return ( 7 | 10 | ); 11 | } 12 | 13 | handleClick = () => { 14 | this.props.onClick(this.props.item); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tree/Node.js: -------------------------------------------------------------------------------- 1 | export default class Node { 2 | 3 | constructor(name, parent) { 4 | this.name = name; 5 | this.parent = parent; 6 | } 7 | 8 | get path() { 9 | const path = []; 10 | let node = this; 11 | 12 | while (node) { 13 | path.push(node.name); 14 | node = node.parent; 15 | } 16 | 17 | return path.reverse().join('/'); 18 | } 19 | 20 | get isRoot() { 21 | return !this.parent; 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /client/components/ContextMenuItem.jsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import s from './ContextMenuItem.css'; 3 | 4 | function noop() { 5 | return false; 6 | } 7 | 8 | export default function ContextMenuItem({children, disabled, onClick}) { 9 | const className = cls({ 10 | [s.item]: true, 11 | [s.disabled]: disabled 12 | }); 13 | const handler = disabled ? noop : onClick; 14 | return (
  • {children}
  • ); 15 | } 16 | -------------------------------------------------------------------------------- /test/dev-server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const BundleAnalyzerPlugin = require('../../lib/BundleAnalyzerPlugin'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: `${__dirname}/src.js`, 6 | output: { 7 | path: `${__dirname}/output`, 8 | filename: 'bundle.js' 9 | }, 10 | plugins: [ 11 | new BundleAnalyzerPlugin({ 12 | analyzerMode: 'static', 13 | reportFilename: 'report.html', 14 | openAnalyzer: false 15 | }) 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /client/assets/icon-invisible.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/components/ThemeToggle.css: -------------------------------------------------------------------------------- 1 | .themeToggle { 2 | background: transparent; 3 | border: none; 4 | cursor: pointer; 5 | padding: 8px; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | border-radius: 4px; 10 | transition: background-color 0.2s ease; 11 | } 12 | 13 | .themeToggle:hover { 14 | background: rgba(0, 0, 0, 0.1); 15 | } 16 | 17 | [data-theme="dark"] .themeToggle:hover { 18 | background: rgba(255, 255, 255, 0.1); 19 | } 20 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithModulesAsObject.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "2": "function(t,e,n){n(3),n(173),n(15),n(11),n(148),n(150),n(152),n(143),n(5),n(151),n(4),n(1),n(14),n(144),n(146),n(498),n(505),n(496),n(168),n(501),n(499),n(504),n(130),n(10),n(131),n(500),n(169),n(497),n(506),n(171),n(172),t.exports=n(170)}", 4 | "6": "function(t,e,n){(function(e){t.exports=e.$=n(167)}).call(e,function(){return this}())}", 5 | "/x1Yz5": "function(t,e,n){}" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/bundles/validBundleWithEsNextFeatures.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "0": "function(t,e,r){\n async function asyncFn() {\n return await Promise.resolve(1);\n }\n\n const arrowFn = arg => arg * 2;\n\n function* generatorFn() {\n yield 1;\n }\n\n class TestClass {\n static staticMethod() {}\n constructor() {}\n testMethod() {}\n }\n\n for (const i of [1, 2, 3]) {\n console.log(i);\n }\n\n let obj = {\n ['a' + 'b']: 1,\n func() {}\n };\n\n const [var1, var2] = [1, 2];\n}" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-multiple-entries/bundle.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var o,e,r={85:(o,e,r)=>{r.d(e,{Z:()=>t});const t="module a"},326:(o,e,r)=>{r.d(e,{Z:()=>t});const t="module b"}},t={};function n(o){if(t[o])return t[o].exports;var e=t[o]={exports:{}};return r[o](e,e.exports,n),e.exports}n.d=(o,e)=>{for(var r in e)n.o(e,r)&&!n.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:e[r]})},n.o=(o,e)=>Object.prototype.hasOwnProperty.call(o,e),o=n(85),e=n(326),console.log(o.Z,e.Z),(()=>{var o=n(85),e=n(326);console.log(o.Z,e.Z)})()})(); -------------------------------------------------------------------------------- /client/utils.js: -------------------------------------------------------------------------------- 1 | export function isChunkParsed(chunk) { 2 | return (typeof chunk.parsedSize === 'number'); 3 | } 4 | 5 | export function walkModules(modules, cb) { 6 | for (const module of modules) { 7 | if (cb(module) === false) return false; 8 | 9 | if (module.groups) { 10 | if (walkModules(module.groups, cb) === false) { 11 | return false; 12 | } 13 | } 14 | } 15 | } 16 | 17 | export function elementIsOutside(elem, container) { 18 | return !(elem === container || container.contains(elem)); 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // Jest configuration 2 | // Reference: https://jestjs.io/docs/configuration 3 | 4 | module.exports = { 5 | testMatch: [ 6 | '**/test/*.js' 7 | ], 8 | testPathIgnorePatterns: [ 9 | '/test/helpers.js' 10 | ], 11 | setupFilesAfterEnv: [ 12 | '/test/helpers.js' 13 | ], 14 | watchPathIgnorePatterns: [ 15 | // Ignore the output generated by plugin tests 16 | // when watching for changes to avoid the test 17 | // runner continuously re-running tests 18 | '/test/output' 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithModulesInsideArrayConcat.js: -------------------------------------------------------------------------------- 1 | webpackJsonplmn_ui_obe__name_Init([0],Array(104).concat([function(e,t){function n(e){return null==e}e.exports=n},,,function(e,t,n){function r(e){if("number"==typeof e)return e;if(i(e))return a;if(o(e)){var t="function"==typeof e.valueOf?e.valueOf():e;e=o(t)?t+"":t}if("string"!=typeof e)return 0===e?e:+e;e=e.replace(u,"");var n=l.test(e);return n||c.test(e)?d(e.slice(2),n?2:8):s.test(e)?a:+e}var o=n(2),i=n(20),a=NaN,u=/^\s+|\s+$/g,s=/^[-+]0x[0-9a-f]+$/i,l=/^0b[01]+$/i,c=/^0o[0-7]+$/i,d=parseInt;e.exports=r}])); 2 | -------------------------------------------------------------------------------- /test/bundles/validExtraBundleWithModulesInsideArrayConcat.modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "104": "function(e,t){function n(e){return null==e}e.exports=n}", 4 | "107": "function(e,t,n){function r(e){if(\"number\"==typeof e)return e;if(i(e))return a;if(o(e)){var t=\"function\"==typeof e.valueOf?e.valueOf():e;e=o(t)?t+\"\":t}if(\"string\"!=typeof e)return 0===e?e:+e;e=e.replace(u,\"\");var n=l.test(e);return n||c.test(e)?d(e.slice(2),n?2:8):s.test(e)?a:+e}var o=n(2),i=n(20),a=NaN,u=/^\\s+|\\s+$/g,s=/^[-+]0x[0-9a-f]+$/i,l=/^0b[01]+$/i,c=/^0o[0-7]+$/i,d=parseInt;e.exports=r}" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/bundles/validBundleWithEsNextFeatures.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],[function(t,e,r){ 2 | async function asyncFn() { 3 | return await Promise.resolve(1); 4 | } 5 | 6 | const arrowFn = arg => arg * 2; 7 | 8 | function* generatorFn() { 9 | yield 1; 10 | } 11 | 12 | class TestClass { 13 | static staticMethod() {} 14 | constructor() {} 15 | testMethod() {} 16 | } 17 | 18 | for (const i of [1, 2, 3]) { 19 | console.log(i); 20 | } 21 | 22 | let obj = { 23 | ['a' + 'b']: 1, 24 | func() {} 25 | }; 26 | 27 | const [var1, var2] = [1, 2]; 28 | }]); 29 | -------------------------------------------------------------------------------- /test/bundles/validUmdLibraryBundleWithModulesAsArray.js: -------------------------------------------------------------------------------- 1 | (function(e,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.Lib=o():e.Lib=o()})(this,function(){return function(e){function o(n){if(t[n])return t[n].exports;var r=t[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,o),r.loaded=!0,r.exports}var t={};return o.m=e,o.c=t,o.p="",o(0)}([function(e,o,t){t(1),t(2),t(3)},function(e,o){e.exports="module a"},function(e,o){e.exports="module b"},function(e,o){e.exports="module a"}])}); 2 | -------------------------------------------------------------------------------- /client/components/Dropdown.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | white-space: nowrap; 4 | } 5 | 6 | .label { 7 | font-size: 11px; 8 | font-weight: bold; 9 | margin-bottom: 7px; 10 | } 11 | 12 | .input { 13 | border: 1px solid var(--border-color); 14 | border-radius: 4px; 15 | display: block; 16 | width: 100%; 17 | color: var(--text-secondary); 18 | height: 27px; 19 | background: var(--bg-primary); 20 | transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; 21 | } 22 | 23 | .option { 24 | padding: 4px 0; 25 | cursor: pointer; 26 | } 27 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithDedupePlugin.js: -------------------------------------------------------------------------------- 1 | !function(t){function r(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}var e={};return r.m=t,r.c=e,r.p="",r(0)}(function(t){for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r))switch(typeof t[r]){case"function":break;case"object":t[r]=function(r){var e=r.slice(1),n=t[r[0]];return function(t,r,o){n.apply(this,[t,r,o].concat(e))}}(t[r]);break;default:t[r]=t[t[r]]}return t}([function(t,r,e){e(1),e(2)},function(t,r){t.exports=1},1,,[2, 'arg1', 'arg2'],,['module-id', 'arg']])); 2 | -------------------------------------------------------------------------------- /test/stats/with-invalid-chunk/valid-chunk.js: -------------------------------------------------------------------------------- 1 | !function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r);console.log("module a")}]); -------------------------------------------------------------------------------- /test/stats/with-missing-chunk/valid-chunk.js: -------------------------------------------------------------------------------- 1 | !function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r);console.log("module a")}]); -------------------------------------------------------------------------------- /client/assets/icon-chunk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/lib/PureComponent.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | 3 | export default class PureComponent extends Component { 4 | shouldComponentUpdate(nextProps, nextState) { 5 | return !isEqual(nextProps, this.props) || !isEqual(this.state, nextState); 6 | } 7 | } 8 | 9 | function isEqual(obj1, obj2) { 10 | if (obj1 === obj2) return true; 11 | const keys = Object.keys(obj1); 12 | if (keys.length !== Object.keys(obj2).length) return false; 13 | for (let i = 0; i < keys.length; i++) { 14 | const key = keys[i]; 15 | if (obj1[key] !== obj2[key]) return false; 16 | } 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /client/localStorage.js: -------------------------------------------------------------------------------- 1 | const KEY_PREFIX = 'wba'; 2 | 3 | export default { 4 | 5 | getItem(key) { 6 | try { 7 | return JSON.parse(window.localStorage.getItem(`${KEY_PREFIX}.${key}`)); 8 | } catch (err) { 9 | return null; 10 | } 11 | }, 12 | 13 | setItem(key, value) { 14 | try { 15 | window.localStorage.setItem(`${KEY_PREFIX}.${key}`, JSON.stringify(value)); 16 | } catch (err) { /* ignored */ } 17 | }, 18 | 19 | removeItem(key) { 20 | try { 21 | window.localStorage.removeItem(`${KEY_PREFIX}.${key}`); 22 | } catch (err) { /* ignored */ } 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /test/stats/with-missing-module-chunks/valid-chunk.js: -------------------------------------------------------------------------------- 1 | !function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r);console.log("module a")}]); -------------------------------------------------------------------------------- /test/stats/with-module-concatenation-info/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r);console.log("a1","b","c","d","e")}]); -------------------------------------------------------------------------------- /client/components/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: var(--bg-primary); 3 | border: 1px solid var(--border-color); 4 | border-radius: 4px; 5 | cursor: pointer; 6 | display: inline-block; 7 | font: var(--main-font); 8 | outline: none; 9 | padding: 5px 7px; 10 | transition: background .3s ease, border-color .3s ease, color .3s ease; 11 | white-space: nowrap; 12 | color: var(--text-primary); 13 | } 14 | 15 | .button:focus, 16 | .button:hover { 17 | background: var(--hover-bg); 18 | } 19 | 20 | .button.active { 21 | background: #ffa500; 22 | color: #000; 23 | } 24 | 25 | .button[disabled] { 26 | cursor: default; 27 | } 28 | -------------------------------------------------------------------------------- /client/assets/icon-pin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tree/utils.js: -------------------------------------------------------------------------------- 1 | const MULTI_MODULE_REGEXP = /^multi /u; 2 | 3 | export function getModulePathParts(moduleData) { 4 | if (MULTI_MODULE_REGEXP.test(moduleData.identifier)) { 5 | return [moduleData.identifier]; 6 | } 7 | 8 | const loaders = moduleData.name.split('!'); 9 | // Removing loaders from module path: they're joined by `!` and the last part is a raw module path 10 | const parsedPath = loaders[loaders.length - 1] 11 | // Splitting module path into parts 12 | .split('/') 13 | // Removing first `.` 14 | .slice(1) 15 | // Replacing `~` with `node_modules` 16 | .map(part => (part === '~' ? 'node_modules' : part)); 17 | 18 | return parsedPath.length ? parsedPath : null; 19 | } 20 | -------------------------------------------------------------------------------- /client/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | import cls from 'classnames'; 3 | 4 | import s from './Checkbox.css'; 5 | 6 | export default class Checkbox extends Component { 7 | 8 | render() { 9 | const {checked, className, children} = this.props; 10 | 11 | return ( 12 | 21 | ); 22 | } 23 | 24 | handleChange = () => { 25 | this.props.onChange(!this.props.checked); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /client/components/Search.css: -------------------------------------------------------------------------------- 1 | .container { 2 | font: var(--main-font); 3 | white-space: nowrap; 4 | } 5 | 6 | .label { 7 | font-weight: bold; 8 | margin-bottom: 7px; 9 | } 10 | 11 | .row { 12 | display: flex; 13 | } 14 | 15 | .input { 16 | border: 1px solid var(--border-color); 17 | border-radius: 4px; 18 | display: block; 19 | flex: 1; 20 | padding: 5px; 21 | background: var(--bg-primary); 22 | color: var(--text-primary); 23 | transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; 24 | } 25 | 26 | .input:focus { 27 | outline: none; 28 | border-color: var(--text-secondary); 29 | } 30 | 31 | .clear { 32 | flex: 0 0 auto; 33 | line-height: 1; 34 | margin-left: 3px; 35 | padding: 5px 8px 7px; 36 | } 37 | -------------------------------------------------------------------------------- /client/components/ThemeToggle.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | import {observer} from 'mobx-react'; 3 | 4 | import s from './ThemeToggle.css'; 5 | import Button from './Button'; 6 | import Icon from './Icon'; 7 | import {store} from '../store'; 8 | 9 | @observer 10 | export default class ThemeToggle extends Component { 11 | render() { 12 | const {darkMode} = store; 13 | 14 | return ( 15 | 21 | ); 22 | } 23 | 24 | handleToggle = () => { 25 | store.toggleDarkMode(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/components/ModuleItem.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background: no-repeat left center; 3 | cursor: pointer; 4 | margin-bottom: 4px; 5 | padding-left: 18px; 6 | position: relative; 7 | white-space: nowrap; 8 | } 9 | 10 | .container.module { 11 | background-image: url('../assets/icon-module.svg'); 12 | background-position-x: 1px; 13 | } 14 | 15 | .container.folder { 16 | background-image: url('../assets/icon-folder.svg'); 17 | } 18 | 19 | .container.chunk { 20 | background-image: url('../assets/icon-chunk.svg'); 21 | } 22 | 23 | .container.invisible:hover::before { 24 | background: url('../assets/icon-invisible.svg') no-repeat left center; 25 | content: ""; 26 | height: 100%; 27 | left: 0; 28 | top: 1px; 29 | position: absolute; 30 | width: 13px; 31 | } 32 | -------------------------------------------------------------------------------- /client/components/Switcher.jsx: -------------------------------------------------------------------------------- 1 | import SwitcherItem from './SwitcherItem'; 2 | import s from './Switcher.css'; 3 | import PureComponent from '../lib/PureComponent'; 4 | 5 | export default class Switcher extends PureComponent { 6 | 7 | render() { 8 | const {label, items, activeItem, onSwitch} = this.props; 9 | 10 | return ( 11 |
    12 |
    13 | {label}: 14 |
    15 |
    16 | {items.map(item => 17 | 22 | )} 23 |
    24 |
    25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /client/components/ModulesList.jsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import s from './ModulesList.css'; 3 | import ModuleItem from './ModuleItem'; 4 | import PureComponent from '../lib/PureComponent'; 5 | 6 | export default class ModulesList extends PureComponent { 7 | render({modules, showSize, highlightedText, isModuleVisible, className}) { 8 | return ( 9 |
    10 | {modules.map(module => 11 | 17 | )} 18 |
    19 | ); 20 | } 21 | 22 | handleModuleClick = module => this.props.onModuleClick(module); 23 | } 24 | -------------------------------------------------------------------------------- /client/components/CheckboxListItem.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | 3 | import Checkbox from './Checkbox'; 4 | import CheckboxList from './CheckboxList'; 5 | import s from './CheckboxList.css'; 6 | 7 | export default class CheckboxListItem extends Component { 8 | 9 | render() { 10 | return ( 11 |
    12 | 14 | {this.renderLabel()} 15 | 16 |
    17 | ); 18 | } 19 | 20 | renderLabel() { 21 | const {children, item} = this.props; 22 | 23 | if (children) { 24 | return children(item); 25 | } 26 | 27 | return (item === CheckboxList.ALL_ITEM) ? 'All' : item.label; 28 | } 29 | 30 | handleChange = () => { 31 | this.props.onChange(this.props.item); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/tree/ContentModule.js: -------------------------------------------------------------------------------- 1 | import Module from './Module'; 2 | 3 | export default class ContentModule extends Module { 4 | 5 | constructor(name, data, ownerModule, parent) { 6 | super(name, data, parent); 7 | this.ownerModule = ownerModule; 8 | } 9 | 10 | get parsedSize() { 11 | return this.getSize('parsedSize'); 12 | } 13 | 14 | get gzipSize() { 15 | return this.getSize('gzipSize'); 16 | } 17 | 18 | get brotliSize() { 19 | return this.getSize('brotliSize'); 20 | } 21 | 22 | getSize(sizeType) { 23 | const ownerModuleSize = this.ownerModule[sizeType]; 24 | 25 | if (ownerModuleSize !== undefined) { 26 | return Math.floor((this.size / this.ownerModule.size) * ownerModuleSize); 27 | } 28 | } 29 | 30 | toChartData() { 31 | return { 32 | ...super.toChartData(), 33 | inaccurateSizes: true 34 | }; 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithModulesAsArray.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n=window.webpackJsonp;window.webpackJsonp=function(i,a){for(var s,u,l=0,c=[];l 20 | {children} 21 | 22 | ); 23 | } 24 | 25 | get disabled() { 26 | const {props} = this; 27 | return ( 28 | props.disabled || 29 | (props.active && !props.toggle) 30 | ); 31 | } 32 | 33 | handleClick = (event) => { 34 | this.elem.blur(); 35 | this.props.onClick(event); 36 | } 37 | 38 | saveRef = elem => this.elem = elem; 39 | } 40 | -------------------------------------------------------------------------------- /test/bundles/validCommonBundleWithModulesAsObject.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(n){if(i[n])return i[n].exports;var r=i[n]={exports:{},id:n,loaded:!1};return t[n].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,s){for(var a,l,c=0,u=[];cconsole.log("hello world after 5 sec"),5e3)},function(e,t,n){e.exports=n(0)}]); -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-single-entry/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'label': 'bundle.js', 4 | 'isAsset': true, 5 | 'statSize': 142, 6 | 'parsedSize': 253, 7 | 'gzipSize': 179, 8 | 'groups': [ 9 | { 10 | 'label': 'src', 11 | 'path': './src', 12 | 'statSize': 142, 13 | 'groups': [ 14 | { 15 | 'id': 85, 16 | 'label': 'a.js', 17 | 'path': './src/a.js', 18 | 'statSize': 40, 19 | 'parsedSize': 29, 20 | 'gzipSize': 49 21 | }, { 22 | 'id': 326, 23 | 'label': 'b.js', 24 | 'path': './src/b.js', 25 | 'statSize': 40, 26 | 'parsedSize': 29, 27 | 'gzipSize': 49 28 | }, { 29 | 'id': 138, 30 | 'label': 'index.js', 31 | 'path': './src/index.js', 32 | 'statSize': 62, 33 | 'parsedSize': 195, 34 | 'gzipSize': 159 35 | } 36 | ], 37 | 'parsedSize': 253, 38 | 'gzipSize': 181 39 | } 40 | ] 41 | } 42 | ]; 43 | -------------------------------------------------------------------------------- /client/viewer.jsx: -------------------------------------------------------------------------------- 1 | import {render} from 'preact'; 2 | 3 | import {store} from './store'; 4 | import ModulesTreemap from './components/ModulesTreemap'; 5 | /* eslint no-unused-vars: "off" */ 6 | import styles from './viewer.css'; 7 | 8 | // Initializing WebSocket for live treemap updates 9 | let ws; 10 | try { 11 | if (window.enableWebSocket) { 12 | ws = new WebSocket(`ws://${location.host}`); 13 | } 14 | } catch (err) { 15 | console.warn( 16 | "Couldn't connect to analyzer websocket server so you'll have to reload page manually to see updates in the treemap" 17 | ); 18 | } 19 | 20 | window.addEventListener('load', () => { 21 | store.defaultSize = `${window.defaultSizes}Size`; 22 | store.setModules(window.chartData); 23 | store.setEntrypoints(window.entrypoints); 24 | store.updateTheme(); 25 | render( 26 | , 27 | document.getElementById('app') 28 | ); 29 | 30 | if (ws) { 31 | ws.addEventListener('message', event => { 32 | const msg = JSON.parse(event.data); 33 | 34 | if (msg.event === 'chartDataUpdated') { 35 | store.setModules(msg.data); 36 | } 37 | }); 38 | } 39 | }, false); 40 | -------------------------------------------------------------------------------- /client/viewer.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: normal 11px Verdana, sans-serif; 3 | --bg-primary: #fff; 4 | --bg-secondary: #f5f5f5; 5 | --text-primary: #000; 6 | --text-secondary: #666; 7 | --border-color: #aaa; 8 | --border-light: #ddd; 9 | --shadow: rgba(0, 0, 0, 0.1); 10 | --hover-bg: rgba(0, 0, 0, 0.05); 11 | } 12 | 13 | [data-theme="dark"] { 14 | --bg-primary: #1e1e1e; 15 | --bg-secondary: #252525; 16 | --text-primary: #e0e0e0; 17 | --text-secondary: #a0a0a0; 18 | --border-color: #404040; 19 | --border-light: #333; 20 | --shadow: rgba(0, 0, 0, 0.3); 21 | --hover-bg: rgba(255, 255, 255, 0.05); 22 | } 23 | 24 | :global html, 25 | :global body, 26 | :global #app { 27 | height: 100%; 28 | margin: 0; 29 | overflow: hidden; 30 | padding: 0; 31 | width: 100%; 32 | background: var(--bg-primary); 33 | color: var(--text-primary); 34 | transition: background-color 0.3s ease, color 0.3s ease; 35 | } 36 | 37 | :global body.resizing { 38 | user-select: none !important; 39 | } 40 | 41 | :global body.resizing * { 42 | pointer-events: none; 43 | } 44 | 45 | :global body.resizing.col { 46 | cursor: col-resize !important; 47 | } 48 | -------------------------------------------------------------------------------- /src/Logger.js: -------------------------------------------------------------------------------- 1 | const LEVELS = [ 2 | 'debug', 3 | 'info', 4 | 'warn', 5 | 'error', 6 | 'silent' 7 | ]; 8 | 9 | const LEVEL_TO_CONSOLE_METHOD = new Map([ 10 | ['debug', 'log'], 11 | ['info', 'log'], 12 | ['warn', 'log'] 13 | ]); 14 | 15 | class Logger { 16 | 17 | static levels = LEVELS; 18 | static defaultLevel = 'info'; 19 | 20 | constructor(level = Logger.defaultLevel) { 21 | this.activeLevels = new Set(); 22 | this.setLogLevel(level); 23 | } 24 | 25 | setLogLevel(level) { 26 | const levelIndex = LEVELS.indexOf(level); 27 | 28 | if (levelIndex === -1) throw new Error(`Invalid log level "${level}". Use one of these: ${LEVELS.join(', ')}`); 29 | 30 | this.activeLevels.clear(); 31 | 32 | for (const [i, level] of LEVELS.entries()) { 33 | if (i >= levelIndex) this.activeLevels.add(level); 34 | } 35 | } 36 | 37 | _log(level, ...args) { 38 | console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args); 39 | } 40 | 41 | }; 42 | 43 | LEVELS.forEach(level => { 44 | if (level === 'silent') return; 45 | 46 | Logger.prototype[level] = function (...args) { 47 | if (this.activeLevels.has(level)) this._log(level, ...args); 48 | }; 49 | }); 50 | 51 | module.exports = Logger; 52 | -------------------------------------------------------------------------------- /test/stats/with-modules-in-chunks/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'label': 'runtime.6afe30102d8fe7337431.js', 4 | 'statSize': 1053, 5 | 'groups': [] 6 | }, 7 | { 8 | 'label': 'polyfills.2903ad11212d7d797800.js', 9 | 'statSize': 101, 10 | 'groups': [ 11 | { 12 | 'label': 'node_modules/core-js/modules', 13 | 'path': './node_modules/core-js/modules', 14 | 'statSize': 101, 15 | 'groups': [ 16 | { 17 | 'id': '+rLv', 18 | 'label': '_html.js', 19 | 'path': './node_modules/core-js/modules/_html.js', 20 | 'statSize': 101 21 | } 22 | ] 23 | } 24 | ] 25 | }, 26 | { 27 | 'label': 'main.e339f68cc77f07c43589.js', 28 | 'statSize': 160, 29 | 'groups': [ 30 | { 31 | 'label': 'src', 32 | 'path': './src', 33 | 'statSize': 160, 34 | 'groups': [ 35 | { 36 | 'id': 'crnd', 37 | 'label': '$$_lazy_route_resource lazy namespace object', 38 | 'path': './src/$$_lazy_route_resource lazy namespace object', 39 | 'statSize': 160 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /test/stats/with-worker-loader-dynamic-import/bundle.worker.js: -------------------------------------------------------------------------------- 1 | !function(e){self.webpackChunk=function(r,n){for(var o in n)e[o]=n[o];for(;r.length;)t[r.pop()]=1};var r={},t={0:1};function n(t){if(r[t])return r[t].exports;var o=r[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.e=function(e){var r=[];return r.push(Promise.resolve().then((function(){t[e]||importScripts(n.p+""+e+".bundle.worker.js")}))),Promise.all(r)},n.m=e,n.c=r,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,r){if(1&r&&(e=n(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)n.d(t,o,function(r){return e[r]}.bind(null,o));return t},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="",n(n.s=0)}([function(e,r,t){"use strict";t.r(r),t.e(1).then(t.bind(null,1)),r.default={}}]); -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build-and-test: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node: 13 | - version: 20.x 14 | - version: 22.x 15 | - version: 24.x 16 | runs-on: ubuntu-22.04 17 | name: Tests on Node.js v${{ matrix.node.version }} 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v5 21 | 22 | - name: Setup node 23 | uses: actions/setup-node@v6 24 | with: 25 | node-version: ${{ matrix.node.version }} 26 | 27 | - name: Download deps 28 | uses: bahmutov/npm-install@v1 29 | 30 | - name: Build sources 31 | run: ${{ matrix.node.env }} npm run build 32 | 33 | - name: Run tests 34 | run: ${{ matrix.node.env }} npm run test 35 | 36 | lint: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout repo 40 | uses: actions/checkout@v5 41 | 42 | - name: Setup node 43 | uses: actions/setup-node@v6 44 | with: 45 | node-version: "22.x" 46 | 47 | - name: Download deps 48 | uses: bahmutov/npm-install@v1 49 | 50 | - name: Run lint 51 | run: npm run lint 52 | -------------------------------------------------------------------------------- /test/dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const {spawn} = require('child_process'); 3 | 4 | const del = require('del'); 5 | 6 | const ROOT = `${__dirname}/dev-server`; 7 | const WEBPACK_CONFIG_PATH = `${ROOT}/webpack.config.js`; 8 | const webpackConfig = require(WEBPACK_CONFIG_PATH); 9 | 10 | describe('Webpack Dev Server', function () { 11 | beforeAll(deleteOutputDirectory); 12 | afterEach(deleteOutputDirectory); 13 | 14 | const timeout = 15000; 15 | jest.setTimeout(timeout); 16 | 17 | it('should save report file to the output directory', function (done) { 18 | const startedAt = Date.now(); 19 | 20 | const devServer = spawn(`${__dirname}/../node_modules/.bin/webpack-dev-server`, ['--config', WEBPACK_CONFIG_PATH], { 21 | cwd: ROOT 22 | }); 23 | 24 | const reportCheckIntervalId = setInterval(() => { 25 | if (fs.existsSync(`${webpackConfig.output.path}/report.html`)) { 26 | finish(); 27 | } else if (Date.now() - startedAt > timeout - 1000) { 28 | finish(`report file wasn't found in "${webpackConfig.output.path}" directory`); 29 | } 30 | }, 300); 31 | 32 | function finish(errorMessage) { 33 | clearInterval(reportCheckIntervalId); 34 | devServer.kill(); 35 | done(errorMessage ? new Error(errorMessage) : null); 36 | } 37 | }); 38 | }); 39 | 40 | function deleteOutputDirectory() { 41 | del.sync(webpackConfig.output.path); 42 | } 43 | -------------------------------------------------------------------------------- /test/parseUtils.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-subset')); 3 | const {expect} = chai; 4 | const fs = require('fs'); 5 | 6 | const {parseBundle} = require('../lib/parseUtils'); 7 | 8 | const BUNDLES_DIR = `${__dirname}/bundles`; 9 | 10 | describe('parseBundle', function () { 11 | const bundles = fs 12 | .readdirSync(BUNDLES_DIR) 13 | .filter(filename => filename.endsWith('.js')) 14 | .map(filename => filename.replace(/\.js$/u, '')); 15 | 16 | bundles 17 | .filter(bundleName => bundleName.startsWith('valid')) 18 | .forEach(bundleName => { 19 | it(`should parse ${bundleName.toLocaleLowerCase()}`, function () { 20 | const bundleFile = `${BUNDLES_DIR}/${bundleName}.js`; 21 | const bundle = parseBundle(bundleFile); 22 | const expectedModules = JSON.parse(fs.readFileSync(`${BUNDLES_DIR}/${bundleName}.modules.json`)); 23 | 24 | expect(bundle.src).to.equal(fs.readFileSync(bundleFile, 'utf8')); 25 | expect(bundle.modules).to.deep.equal(expectedModules.modules); 26 | }); 27 | }); 28 | 29 | it("should parse invalid bundle and return it's content and empty modules hash", function () { 30 | const bundleFile = `${BUNDLES_DIR}/invalidBundle.js`; 31 | const bundle = parseBundle(bundleFile); 32 | expect(bundle.src).to.equal(fs.readFileSync(bundleFile, 'utf8')); 33 | expect(bundle.modules).to.deep.equal({}); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /client/components/Icon.jsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import s from './Icon.css'; 3 | import PureComponent from '../lib/PureComponent'; 4 | 5 | import iconArrowRight from '../assets/icon-arrow-right.svg'; 6 | import iconPin from '../assets/icon-pin.svg'; 7 | import iconMoon from '../assets/icon-moon.svg'; 8 | import iconSun from '../assets/icon-sun.svg'; 9 | 10 | const ICONS = { 11 | 'arrow-right': { 12 | src: iconArrowRight, 13 | size: [7, 13] 14 | }, 15 | 'pin': { 16 | src: iconPin, 17 | size: [12, 18] 18 | }, 19 | 'moon': { 20 | src: iconMoon, 21 | size: [24, 24] 22 | }, 23 | 'sun': { 24 | src: iconSun, 25 | size: [24, 24] 26 | } 27 | }; 28 | 29 | export default class Icon extends PureComponent { 30 | render({className}) { 31 | return ( 32 | 34 | ); 35 | } 36 | 37 | get style() { 38 | const {name, size, rotate} = this.props; 39 | const icon = ICONS[name]; 40 | 41 | if (!icon) throw new TypeError(`Can't find "${name}" icon.`); 42 | 43 | let [width, height] = icon.size; 44 | 45 | if (size) { 46 | const ratio = size / Math.max(width, height); 47 | width = Math.min(Math.ceil(width * ratio), size); 48 | height = Math.min(Math.ceil(height * ratio), size); 49 | } 50 | 51 | return { 52 | backgroundImage: `url(${icon.src})`, 53 | width: `${width}px`, 54 | height: `${height}px`, 55 | transform: rotate ? `rotate(${rotate}deg)` : '' 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-concatenated-entry-module/expected-chart-data.json: -------------------------------------------------------------------------------- 1 | [{"groups": [{"concatenated": true, "groups": [{"gzipSize": 8, "id": 613, "inaccurateSizes": true, "label": "baz.js", "parsedSize": 10, "path": "./entry modules (concatenated)/baz.js", "statSize": 23}, {"concatenated": true, "groups": [{"gzipSize": 5, "id": null, "inaccurateSizes": true, "label": "index.js", "parsedSize": 6, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)/index.js", "statSize": 14}, {"groups": [{"gzipSize": 12, "id": null, "inaccurateSizes": true, "label": "index.js", "parsedSize": 14, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)/dep/index.js", "statSize": 32}, {"gzipSize": 25, "id": null, "inaccurateSizes": true, "label": "foo.js", "parsedSize": 29, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)/dep/foo.js", "statSize": 66}, {"gzipSize": 25, "id": null, "inaccurateSizes": true, "label": "bar.js", "parsedSize": 29, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)/dep/bar.js", "statSize": 66}], "gzipSize": 62, "inaccurateSizes": true, "label": "dep", "parsedSize": 72, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)/dep", "statSize": 164}], "gzipSize": 68, "id": 469, "label": "index.js + 3 modules (concatenated)", "parsedSize": 79, "path": "./entry modules (concatenated)/index.js + 3 modules (concatenated)", "statSize": 178}], "gzipSize": 77, "label": "entry modules (concatenated)", "parsedSize": 90, "path": "./entry modules (concatenated)", "statSize": 201}], "gzipSize": 77, "isAsset": true, "isInitialByEntrypoint": {"app": true}, "label": "app.js", "parsedSize": 90, "statSize": 201}] 2 | -------------------------------------------------------------------------------- /client/components/Sidebar.css: -------------------------------------------------------------------------------- 1 | @value toggleTime: 200ms; 2 | 3 | .container { 4 | background: var(--bg-primary); 5 | border: none; 6 | border-right: 1px solid var(--border-color); 7 | box-sizing: border-box; 8 | max-width: calc(50% - 10px); 9 | opacity: 0.95; 10 | z-index: 1; 11 | transition: background-color 0.3s ease, border-color 0.3s ease; 12 | } 13 | 14 | .container:not(.hidden) { 15 | min-width: 200px; 16 | } 17 | 18 | .container:not(.pinned) { 19 | bottom: 0; 20 | position: absolute; 21 | top: 0; 22 | transition: transform toggleTime ease; 23 | } 24 | 25 | .container.pinned { 26 | position: relative; 27 | } 28 | 29 | .container.left { 30 | left: 0; 31 | } 32 | 33 | .container.left.hidden { 34 | transform: translateX(calc(-100% + 7px)); 35 | } 36 | 37 | .content { 38 | box-sizing: border-box; 39 | height: 100%; 40 | overflow-y: auto; 41 | padding: 25px 20px 20px; 42 | width: 100%; 43 | } 44 | 45 | .empty.pinned .content { 46 | padding: 0; 47 | } 48 | 49 | .container :global(.themeToggle) { 50 | position: absolute; 51 | top: 10px; 52 | left: 15px; 53 | z-index: 10; 54 | height: 26px; 55 | width: 27px; 56 | padding: 0; 57 | } 58 | 59 | .pinButton, 60 | .toggleButton { 61 | cursor: pointer; 62 | height: 26px; 63 | line-height: 0; 64 | position: absolute; 65 | top: 10px; 66 | width: 27px; 67 | } 68 | 69 | .pinButton { 70 | right: 47px; 71 | } 72 | 73 | .toggleButton { 74 | padding-left: 6px; 75 | right: 15px; 76 | } 77 | 78 | .hidden .toggleButton { 79 | right: -35px; 80 | transition: transform .2s ease; 81 | } 82 | 83 | .hidden .toggleButton:hover { 84 | transform: translateX(4px); 85 | } 86 | 87 | .resizer { 88 | bottom: 0; 89 | cursor: col-resize; 90 | position: absolute; 91 | right: 0; 92 | top: 0; 93 | width: 7px; 94 | } 95 | -------------------------------------------------------------------------------- /test/stats/with-special-chars/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t),console.log("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ˂˃˄˅ˆˇˈˉˊˋˌˍˎˏːˑ˒˓˔˕˖˗˘˙˚˛˜˝˞˟ˠˡˢˣˤ˥˦˧˨˩˪˫ˬ˭ˮ˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ͵Ͷͷͺͻͼͽ;Ϳ΄΅Ά·ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ϶ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈ")}]); -------------------------------------------------------------------------------- /src/tree/Module.js: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | import {getCompressedSize} from '../sizeUtils'; 3 | 4 | export default class Module extends Node { 5 | 6 | constructor(name, data, parent, opts) { 7 | super(name, parent); 8 | this.data = data; 9 | this.opts = opts; 10 | } 11 | 12 | get src() { 13 | return this.data.parsedSrc; 14 | } 15 | 16 | set src(value) { 17 | this.data.parsedSrc = value; 18 | delete this._gzipSize; 19 | delete this._brotliSize; 20 | } 21 | 22 | get size() { 23 | return this.data.size; 24 | } 25 | 26 | set size(value) { 27 | this.data.size = value; 28 | } 29 | 30 | get parsedSize() { 31 | return this.getParsedSize(); 32 | } 33 | 34 | get gzipSize() { 35 | return this.getGzipSize(); 36 | } 37 | 38 | get brotliSize() { 39 | return this.getBrotliSize(); 40 | } 41 | 42 | getParsedSize() { 43 | return this.src ? this.src.length : undefined; 44 | } 45 | 46 | getGzipSize() { 47 | return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined; 48 | } 49 | 50 | getBrotliSize() { 51 | return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined; 52 | } 53 | 54 | getCompressedSize(compressionAlgorithm) { 55 | const key = `_${compressionAlgorithm}Size`; 56 | if (!(key in this)) { 57 | this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : undefined; 58 | } 59 | 60 | return this[key]; 61 | } 62 | 63 | mergeData(data) { 64 | if (data.size) { 65 | this.size += data.size; 66 | } 67 | 68 | if (data.parsedSrc) { 69 | this.src = (this.src || '') + data.parsedSrc; 70 | } 71 | } 72 | 73 | toChartData() { 74 | return { 75 | id: this.data.id, 76 | label: this.name, 77 | path: this.path, 78 | statSize: this.size, 79 | parsedSize: this.parsedSize, 80 | gzipSize: this.gzipSize, 81 | brotliSize: this.brotliSize 82 | }; 83 | } 84 | 85 | }; 86 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const {inspect, types} = require('util'); 2 | const opener = require('opener'); 3 | 4 | const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 5 | 6 | exports.createAssetsFilter = createAssetsFilter; 7 | 8 | function createAssetsFilter(excludePatterns) { 9 | const excludeFunctions = (Array.isArray(excludePatterns) ? excludePatterns : [excludePatterns]) 10 | .filter(Boolean) 11 | .map(pattern => { 12 | if (typeof pattern === 'string') { 13 | pattern = new RegExp(pattern, 'u'); 14 | } 15 | 16 | if (types.isRegExp(pattern)) { 17 | return (asset) => pattern.test(asset); 18 | } 19 | 20 | if (typeof pattern !== 'function') { 21 | throw new TypeError( 22 | `Pattern should be either string, RegExp or a function, but "${inspect(pattern, {depth: 0})}" got.` 23 | ); 24 | } 25 | 26 | return pattern; 27 | }); 28 | 29 | if (excludeFunctions.length) { 30 | return (asset) => excludeFunctions.every(fn => fn(asset) !== true); 31 | } else { 32 | return () => true; 33 | } 34 | } 35 | 36 | /** 37 | * @desc get string of current time 38 | * format: dd/MMM HH:mm 39 | * */ 40 | exports.defaultTitle = function () { 41 | const time = new Date(); 42 | const year = time.getFullYear(); 43 | const month = MONTHS[time.getMonth()]; 44 | const day = time.getDate(); 45 | const hour = `0${time.getHours()}`.slice(-2); 46 | const minute = `0${time.getMinutes()}`.slice(-2); 47 | 48 | const currentTime = `${day} ${month} ${year} at ${hour}:${minute}`; 49 | 50 | return `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${currentTime}]`; 51 | }; 52 | 53 | exports.defaultAnalyzerUrl = function (options) { 54 | const {listenHost, boundAddress} = options; 55 | return `http://${listenHost}:${boundAddress.port}`; 56 | }; 57 | 58 | /** 59 | * Calls opener on a URI, but silently try / catches it. 60 | */ 61 | exports.open = function (uri, logger) { 62 | try { 63 | opener(uri); 64 | } catch (err) { 65 | logger.debug(`Opener failed to open "${uri}":\n${err}`); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /test/stats/webpack-5-bundle-with-multiple-entries/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'label': 'bundle.js', 4 | 'isAsset': true, 5 | 'statSize': 204, 6 | 'parsedSize': 488, 7 | 'gzipSize': 297, 8 | 'groups': [ 9 | { 10 | 'label': 'entry modules (concatenated)', 11 | 'path': './entry modules (concatenated)', 12 | 'statSize': 124, 13 | 'parsedSize': 396, 14 | 'gzipSize': 265, 15 | 'concatenated': true, 16 | 'groups': [ 17 | { 18 | 'label': 'src', 19 | 'path': './entry modules (concatenated)/src', 20 | 'statSize': 124, 21 | 'groups': [ 22 | { 23 | 'id': 138, 24 | 'label': 'index.js', 25 | 'path': './entry modules (concatenated)/src/index.js', 26 | 'statSize': 62, 27 | 'parsedSize': 198, 28 | 'gzipSize': 132, 29 | 'inaccurateSizes': true 30 | }, { 31 | 'id': 51, 32 | 'label': 'index2.js', 33 | 'path': './entry modules (concatenated)/src/index2.js', 34 | 'statSize': 62, 35 | 'parsedSize': 198, 36 | 'gzipSize': 132, 37 | 'inaccurateSizes': true 38 | } 39 | ], 40 | 'parsedSize': 396, 41 | 'gzipSize': 265, 42 | 'inaccurateSizes': true 43 | } 44 | ] 45 | }, { 46 | 'label': 'src', 47 | 'path': './src', 48 | 'statSize': 80, 49 | 'groups': [ 50 | { 51 | 'id': 85, 52 | 'label': 'a.js', 53 | 'path': './src/a.js', 54 | 'statSize': 40, 55 | 'parsedSize': 46, 56 | 'gzipSize': 66 57 | }, { 58 | 'id': 326, 59 | 'label': 'b.js', 60 | 'path': './src/b.js', 61 | 'statSize': 40, 62 | 'parsedSize': 46, 63 | 'gzipSize': 66 64 | } 65 | ], 66 | 'parsedSize': 92, 67 | 'gzipSize': 72 68 | } 69 | ] 70 | } 71 | ]; 72 | -------------------------------------------------------------------------------- /test/stats/with-no-entrypoints/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "0d30ee86a3a7e89aaace", 3 | "version": "5.74.0", 4 | "time": 42, 5 | "builtAt": 1660844314317, 6 | "publicPath": "auto", 7 | "outputPath": "/home/coder/webpack/examples/code-splitting-depend-on-advanced/dist", 8 | "assetsByChunkName": {}, 9 | "assets": [], 10 | "chunks": [], 11 | "modules": [], 12 | "entrypoints": {}, 13 | "namedChunkGroups": {}, 14 | "errors": [], 15 | "errorsCount": 0, 16 | "warnings": [ 17 | { 18 | "message": "configuration\nThe 'mode' option has not been set, webpack will fallback to 'production' for this value.\nSet 'mode' option to 'development' or 'production' to enable defaults for each environment.\nYou can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/", 19 | "stack": "NoModeWarning: configuration\nThe 'mode' option has not been set, webpack will fallback to 'production' for this value.\nSet 'mode' option to 'development' or 'production' to enable defaults for each environment.\nYou can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/\n at /home/coder/webpack/node_modules/webpack/lib/WarnNoModeSetPlugin.js:20:30\n at Hook.eval [as call] (eval at create (/home/coder/webpack/node_modules/tapable/lib/HookCodeFactory.js:19:10), :19:1)\n at Hook.CALL_DELEGATE [as _call] (/home/coder/webpack/node_modules/tapable/lib/Hook.js:14:14)\n at Compiler.newCompilation (/home/coder/webpack/node_modules/webpack/lib/Compiler.js:1121:30)\n at /home/coder/webpack/node_modules/webpack/lib/Compiler.js:1166:29\n at Hook.eval [as callAsync] (eval at create (/home/coder/webpack/node_modules/tapable/lib/HookCodeFactory.js:33:10), :4:1)\n at Hook.CALL_ASYNC_DELEGATE [as _callAsync] (/home/coder/webpack/node_modules/tapable/lib/Hook.js:18:14)\n at Compiler.compile (/home/coder/webpack/node_modules/webpack/lib/Compiler.js:1161:28)\n at /home/coder/webpack/node_modules/webpack/lib/Compiler.js:524:12\n at Compiler.readRecords (/home/coder/webpack/node_modules/webpack/lib/Compiler.js:986:5)" 20 | } 21 | ], 22 | "warningsCount": 1, 23 | "children": [] 24 | } -------------------------------------------------------------------------------- /client/components/Tooltip.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | import cls from 'classnames'; 3 | 4 | import s from './Tooltip.css'; 5 | 6 | export default class Tooltip extends Component { 7 | 8 | static marginX = 10; 9 | static marginY = 30; 10 | 11 | mouseCoords = { 12 | x: 0, 13 | y: 0 14 | }; 15 | 16 | state = { 17 | left: 0, 18 | top: 0 19 | }; 20 | 21 | componentDidMount() { 22 | document.addEventListener('mousemove', this.handleMouseMove, true); 23 | } 24 | 25 | shouldComponentUpdate(nextProps) { 26 | return this.props.visible || nextProps.visible; 27 | } 28 | 29 | componentWillUnmount() { 30 | document.removeEventListener('mousemove', this.handleMouseMove, true); 31 | } 32 | 33 | render() { 34 | const {children, visible} = this.props; 35 | const className = cls({ 36 | [s.container]: true, 37 | [s.hidden]: !visible 38 | }); 39 | 40 | return ( 41 |
    44 | {children} 45 |
    46 | ); 47 | } 48 | 49 | handleMouseMove = event => { 50 | Object.assign(this.mouseCoords, { 51 | x: event.pageX, 52 | y: event.pageY 53 | }); 54 | 55 | if (this.props.visible) { 56 | this.updatePosition(); 57 | } 58 | }; 59 | 60 | saveNode = node => (this.node = node); 61 | 62 | getStyle() { 63 | return { 64 | left: this.state.left, 65 | top: this.state.top 66 | }; 67 | } 68 | 69 | updatePosition() { 70 | if (!this.props.visible) return; 71 | 72 | const pos = { 73 | left: this.mouseCoords.x + Tooltip.marginX, 74 | top: this.mouseCoords.y + Tooltip.marginY 75 | }; 76 | 77 | const boundingRect = this.node.getBoundingClientRect(); 78 | 79 | if (pos.left + boundingRect.width > window.innerWidth) { 80 | // Shifting horizontally 81 | pos.left = window.innerWidth - boundingRect.width; 82 | } 83 | 84 | if (pos.top + boundingRect.height > window.innerHeight) { 85 | // Flipping vertically 86 | pos.top = this.mouseCoords.y - Tooltip.marginY - boundingRect.height; 87 | } 88 | 89 | this.setState(pos); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-subset')); 3 | const {expect} = chai; 4 | const {createAssetsFilter} = require('../lib/utils'); 5 | 6 | describe('createAssetsFilter', function () { 7 | 8 | it('should create a noop filter if pattern is not set', function () { 9 | for (const pattern of [undefined, null, []]) { 10 | const filter = createAssetsFilter(pattern); 11 | expect(filter('foo')).to.equal(true); 12 | } 13 | }); 14 | 15 | it('should allow a string as a pattern', function () { 16 | const filter = createAssetsFilter('^foo'); 17 | expect(filter('foo')).to.equal(false); 18 | expect(filter('foo-bar')).to.equal(false); 19 | expect(filter('bar')).to.equal(true); 20 | expect(filter('bar-foo')).to.equal(true); 21 | }); 22 | 23 | it('should allow a RegExp as a pattern', function () { 24 | const filter = createAssetsFilter(/^foo/iu); 25 | expect(filter('foo')).to.equal(false); 26 | expect(filter('FOO')).to.equal(false); 27 | expect(filter('foo-bar')).to.equal(false); 28 | expect(filter('bar')).to.equal(true); 29 | expect(filter('bar-foo')).to.equal(true); 30 | }); 31 | 32 | it('should allow a filter function as a pattern', function () { 33 | const filter = createAssetsFilter(asset => asset.startsWith('foo')); 34 | expect(filter('foo')).to.equal(false); 35 | expect(filter('foo-bar')).to.equal(false); 36 | expect(filter('bar')).to.equal(true); 37 | expect(filter('bar-foo')).to.equal(true); 38 | }); 39 | 40 | it('should throw on invalid pattern types', function () { 41 | expect(() => createAssetsFilter(5)).to.throw('but "5" got'); 42 | expect(() => createAssetsFilter({a: 1})).to.throw('but "{ a: 1 }" got'); 43 | expect(() => createAssetsFilter([true])).to.throw('but "true" got'); 44 | }); 45 | 46 | it('should allow an array of patterns', function () { 47 | const filter = createAssetsFilter([ 48 | '^foo', 49 | /bar$/iu, 50 | asset => asset.includes('baz') 51 | ]); 52 | expect(filter('foo')).to.equal(false); 53 | expect(filter('bar')).to.equal(false); 54 | expect(filter('fooBar')).to.equal(false); 55 | expect(filter('fooBARbaz')).to.equal(false); 56 | expect(filter('bar-foo')).to.equal(true); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /client/components/Search.jsx: -------------------------------------------------------------------------------- 1 | // TODO: switch to a more modern debounce package once we drop Node.js 10 support 2 | import debounce from 'debounce'; 3 | 4 | import s from './Search.css'; 5 | import Button from './Button'; 6 | import PureComponent from '../lib/PureComponent'; 7 | 8 | export default class Search extends PureComponent { 9 | 10 | componentDidMount() { 11 | if (this.props.autofocus) { 12 | this.focus(); 13 | } 14 | } 15 | 16 | componentWillUnmount() { 17 | this.handleValueChange.clear(); 18 | } 19 | 20 | render() { 21 | const {label, query} = this.props; 22 | 23 | return ( 24 |
    25 |
    26 | {label}: 27 |
    28 |
    29 | 37 | 38 |
    39 |
    40 | ); 41 | } 42 | 43 | handleValueChange = debounce((event) => { 44 | this.informChange(event.target.value); 45 | }, 400) 46 | 47 | handleInputBlur = () => { 48 | this.handleValueChange.flush(); 49 | } 50 | 51 | handleClearClick = () => { 52 | this.clear(); 53 | this.focus(); 54 | } 55 | 56 | handleKeyDown = event => { 57 | let handled = true; 58 | 59 | switch (event.key) { 60 | case 'Escape': 61 | this.clear(); 62 | break; 63 | case 'Enter': 64 | this.handleValueChange.flush(); 65 | break; 66 | default: 67 | handled = false; 68 | } 69 | 70 | if (handled) { 71 | event.stopPropagation(); 72 | } 73 | } 74 | 75 | focus() { 76 | if (this.input) { 77 | this.input.focus(); 78 | } 79 | } 80 | 81 | clear() { 82 | this.handleValueChange.clear(); 83 | this.informChange(''); 84 | this.input.value = ''; 85 | } 86 | 87 | informChange(value) { 88 | this.props.onQueryChange(value); 89 | } 90 | 91 | saveInputNode = node => this.input = node; 92 | } 93 | -------------------------------------------------------------------------------- /src/statsUtils.js: -------------------------------------------------------------------------------- 1 | const {createWriteStream} = require('fs'); 2 | const {Readable} = require('stream'); 3 | 4 | class StatsSerializeStream extends Readable { 5 | constructor(stats) { 6 | super(); 7 | this._indentLevel = 0; 8 | this._stringifier = this._stringify(stats); 9 | } 10 | 11 | get _indent() { 12 | return ' '.repeat(this._indentLevel); 13 | } 14 | 15 | _read() { 16 | let readMore = true; 17 | 18 | while (readMore) { 19 | const {value, done} = this._stringifier.next(); 20 | 21 | if (done) { 22 | this.push(null); 23 | readMore = false; 24 | } else { 25 | readMore = this.push(value); 26 | } 27 | } 28 | } 29 | 30 | * _stringify(obj) { 31 | if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || obj === null) { 32 | yield JSON.stringify(obj); 33 | } else if (Array.isArray(obj)) { 34 | yield '['; 35 | this._indentLevel++; 36 | 37 | let isFirst = true; 38 | for (let item of obj) { 39 | if (item === undefined) { 40 | item = null; 41 | } 42 | 43 | yield `${isFirst ? '' : ','}\n${this._indent}`; 44 | yield* this._stringify(item); 45 | isFirst = false; 46 | } 47 | 48 | this._indentLevel--; 49 | yield obj.length ? `\n${this._indent}]` : ']'; 50 | } else { 51 | yield '{'; 52 | this._indentLevel++; 53 | 54 | let isFirst = true; 55 | const entries = Object.entries(obj); 56 | for (const [itemKey, itemValue] of entries) { 57 | if (itemValue === undefined) { 58 | continue; 59 | } 60 | 61 | yield `${isFirst ? '' : ','}\n${this._indent}${JSON.stringify(itemKey)}: `; 62 | yield* this._stringify(itemValue); 63 | isFirst = false; 64 | } 65 | 66 | this._indentLevel--; 67 | yield entries.length ? `\n${this._indent}}` : '}'; 68 | } 69 | } 70 | } 71 | 72 | exports.StatsSerializeStream = StatsSerializeStream; 73 | exports.writeStats = writeStats; 74 | 75 | async function writeStats(stats, filepath) { 76 | return new Promise((resolve, reject) => { 77 | new StatsSerializeStream(stats) 78 | .on('end', resolve) 79 | .on('error', reject) 80 | .pipe(createWriteStream(filepath)); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /test/stats/with-module-concatenation-info/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | label: 'index.js + 5 modules (concatenated)', 3 | concatenated: true, 4 | statSize: 332, 5 | parsedSize: 0, 6 | gzipSize: 0, 7 | groups: [ 8 | { 9 | inaccurateSizes: true, 10 | gzipSize: 0, 11 | id: null, 12 | label: 'index.js', 13 | parsedSize: 0, 14 | path: './index.js + 5 modules (concatenated)/index.js', 15 | statSize: 196 16 | }, 17 | { 18 | inaccurateSizes: true, 19 | gzipSize: 0, 20 | id: null, 21 | label: 'a.js', 22 | parsedSize: 0, 23 | path: './index.js + 5 modules (concatenated)/a.js', 24 | statSize: 48 25 | }, 26 | { 27 | label: 'modules-1', 28 | gzipSize: 0, 29 | inaccurateSizes: true, 30 | parsedSize: 0, 31 | path: './index.js + 5 modules (concatenated)/modules-1', 32 | statSize: 44, 33 | groups: [ 34 | { 35 | inaccurateSizes: true, 36 | gzipSize: 0, 37 | id: null, 38 | label: 'b.js', 39 | parsedSize: 0, 40 | path: './index.js + 5 modules (concatenated)/modules-1/b.js', 41 | statSize: 22 42 | }, 43 | { 44 | inaccurateSizes: true, 45 | gzipSize: 0, 46 | id: null, 47 | label: 'c.js', 48 | parsedSize: 0, 49 | path: './index.js + 5 modules (concatenated)/modules-1/c.js', 50 | statSize: 22 51 | } 52 | ] 53 | }, 54 | { 55 | label: 'modules-2', 56 | inaccurateSizes: true, 57 | gzipSize: 0, 58 | parsedSize: 0, 59 | path: './index.js + 5 modules (concatenated)/modules-2', 60 | statSize: 44, 61 | groups: [ 62 | { 63 | inaccurateSizes: true, 64 | gzipSize: 0, 65 | id: null, 66 | label: 'd.js', 67 | parsedSize: 0, 68 | path: './index.js + 5 modules (concatenated)/modules-2/d.js', 69 | statSize: 22 70 | }, 71 | { 72 | inaccurateSizes: true, 73 | gzipSize: 0, 74 | id: null, 75 | label: 'e.js', 76 | parsedSize: 0, 77 | path: './index.js + 5 modules (concatenated)/modules-2/e.js', 78 | statSize: 22 79 | } 80 | ] 81 | } 82 | ] 83 | }; 84 | -------------------------------------------------------------------------------- /test/stats/extremely-optimized-webpack-5-bundle/expected-chart-data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'label': 'bundle.js', 4 | 'isAsset': true, 5 | 'statSize': 142, 6 | 'parsedSize': 58, 7 | 'gzipSize': 71, 8 | 'groups': [ 9 | { 10 | 'label': 'src', 11 | 'path': './src', 12 | 'statSize': 142, 13 | 'groups': [ 14 | { 15 | 'id': 602, 16 | 'label': 'index.js + 2 modules (concatenated)', 17 | 'path': './src/index.js + 2 modules (concatenated)', 18 | 'statSize': 142, 19 | 'parsedSize': 58, 20 | 'gzipSize': 71, 21 | 'concatenated': true, 22 | 'groups': [ 23 | { 24 | 'label': 'src', 25 | 'path': './src/index.js + 2 modules (concatenated)/src', 26 | 'statSize': 142, 27 | 'groups': [ 28 | { 29 | 'id': null, 30 | 'label': 'index.js', 31 | 'path': './src/index.js + 2 modules (concatenated)/src/index.js', 32 | 'statSize': 62, 33 | 'parsedSize': 25, 34 | 'gzipSize': 30, 35 | 'inaccurateSizes': true 36 | }, 37 | { 38 | 'id': null, 39 | 'label': 'a.js', 40 | 'path': './src/index.js + 2 modules (concatenated)/src/a.js', 41 | 'statSize': 40, 42 | 'parsedSize': 16, 43 | 'gzipSize': 20, 44 | 'inaccurateSizes': true 45 | }, 46 | { 47 | 'id': null, 48 | 'label': 'b.js', 49 | 'path': './src/index.js + 2 modules (concatenated)/src/b.js', 50 | 'statSize': 40, 51 | 'parsedSize': 16, 52 | 'gzipSize': 20, 53 | 'inaccurateSizes': true 54 | } 55 | ], 56 | 'parsedSize': 58, 57 | 'gzipSize': 71, 58 | 'inaccurateSizes': true 59 | } 60 | ] 61 | } 62 | ], 63 | 'parsedSize': 58, 64 | 'gzipSize': 71 65 | } 66 | ] 67 | } 68 | ]; 69 | -------------------------------------------------------------------------------- /test/statsUtils.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-subset')); 3 | const {expect} = chai; 4 | const path = require('path'); 5 | const {readFileSync} = require('fs'); 6 | const globby = require('globby'); 7 | 8 | const {StatsSerializeStream} = require('../lib/statsUtils'); 9 | 10 | describe('StatsSerializeStream', () => { 11 | it('should properly stringify primitives', function () { 12 | expectProperJson(0); 13 | expectProperJson(1); 14 | expectProperJson(-1); 15 | expectProperJson(42.42); 16 | expectProperJson(-42.42); 17 | expectProperJson(false); 18 | expectProperJson(true); 19 | expectProperJson(null); 20 | expectProperJson(null); 21 | expectProperJson(''); 22 | expectProperJson('"'); 23 | expectProperJson('foo bar'); 24 | expectProperJson('"foo bar"'); 25 | expectProperJson('Вива Лас-Вегас!'); 26 | }); 27 | 28 | it('should properly stringify simple arrays', function () { 29 | expectProperJson([]); 30 | expectProperJson([1, undefined, 2]); 31 | // eslint-disable-next-line 32 | expectProperJson([1, , 2]); 33 | expectProperJson([false, 'f\'o"o', -1, 42.42]); 34 | }); 35 | 36 | it('should properly stringify objects', function () { 37 | expectProperJson({}); 38 | expectProperJson({a: 1, 'foo-bar': null, undef: undefined, '"Гусь!"': true}); 39 | }); 40 | 41 | it('should properly stringify complex structures', function () { 42 | expectProperJson({ 43 | foo: [], 44 | bar: { 45 | baz: [ 46 | 1, 47 | {a: 1, b: ['foo', 'bar'], c: []}, 48 | 'foo', 49 | {a: 1, b: undefined, c: [{d: true}]}, 50 | null, 51 | undefined 52 | ] 53 | } 54 | }); 55 | }); 56 | 57 | globby.sync('stats/**/*.json', {cwd: __dirname}).forEach(filepath => { 58 | it(`should properly stringify JSON from "${filepath}"`, function () { 59 | const content = readFileSync(path.resolve(__dirname, filepath), 'utf8'); 60 | const json = JSON.parse(content); 61 | expectProperJson(json); 62 | }); 63 | }); 64 | }); 65 | 66 | async function expectProperJson(json) { 67 | expect(await stringify(json)).to.equal(JSON.stringify(json, null, 2)); 68 | } 69 | 70 | async function stringify(json) { 71 | return new Promise((resolve, reject) => { 72 | let result = ''; 73 | 74 | new StatsSerializeStream(json) 75 | .on('data', chunk => result += chunk) 76 | .on('end', () => resolve(result)) 77 | .on('error', reject); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/tree/Folder.js: -------------------------------------------------------------------------------- 1 | import Module from './Module'; 2 | import BaseFolder from './BaseFolder'; 3 | import ConcatenatedModule from './ConcatenatedModule'; 4 | import {getModulePathParts} from './utils'; 5 | import {getCompressedSize} from '../sizeUtils'; 6 | 7 | export default class Folder extends BaseFolder { 8 | 9 | constructor(name, opts) { 10 | super(name); 11 | this.opts = opts; 12 | } 13 | 14 | get parsedSize() { 15 | return this.src ? this.src.length : 0; 16 | } 17 | 18 | get gzipSize() { 19 | return this.opts.compressionAlgorithm === 'gzip' ? this.getCompressedSize('gzip') : undefined; 20 | } 21 | 22 | get brotliSize() { 23 | return this.opts.compressionAlgorithm === 'brotli' ? this.getCompressedSize('brotli') : undefined; 24 | } 25 | 26 | getCompressedSize(compressionAlgorithm) { 27 | const key = `_${compressionAlgorithm}Size`; 28 | 29 | if (!Object.prototype.hasOwnProperty.call(this, key)) { 30 | this[key] = this.src ? getCompressedSize(compressionAlgorithm, this.src) : 0; 31 | } 32 | 33 | return this[key]; 34 | } 35 | 36 | addModule(moduleData) { 37 | const pathParts = getModulePathParts(moduleData); 38 | 39 | if (!pathParts) { 40 | return; 41 | } 42 | 43 | const [folders, fileName] = [pathParts.slice(0, -1), pathParts[pathParts.length - 1]]; 44 | let currentFolder = this; 45 | 46 | folders.forEach(folderName => { 47 | let childNode = currentFolder.getChild(folderName); 48 | 49 | if ( 50 | // Folder is not created yet 51 | !childNode || 52 | // In some situations (invalid usage of dynamic `require()`) webpack generates a module with empty require 53 | // context, but it's moduleId points to a directory in filesystem. 54 | // In this case we replace this `File` node with `Folder`. 55 | // See `test/stats/with-invalid-dynamic-require.json` as an example. 56 | !(childNode instanceof Folder) 57 | ) { 58 | childNode = currentFolder.addChildFolder(new Folder(folderName, this.opts)); 59 | } 60 | 61 | currentFolder = childNode; 62 | }); 63 | 64 | const ModuleConstructor = moduleData.modules ? ConcatenatedModule : Module; 65 | const module = new ModuleConstructor(fileName, moduleData, this, this.opts); 66 | currentFolder.addChildModule(module); 67 | } 68 | 69 | toChartData() { 70 | return { 71 | ...super.toChartData(), 72 | parsedSize: this.parsedSize, 73 | gzipSize: this.gzipSize, 74 | brotliSize: this.brotliSize 75 | }; 76 | } 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | 5 | const NODE_SRC = './src/**/*.js'; 6 | const NODE_DEST = './lib'; 7 | 8 | const cli = require('commander') 9 | .usage(' [options]') 10 | .option('-e, --env ', 'Can be `prod` or `dev`. Default is `dev`', /^(dev|prod)$/u, 'dev') 11 | .option('-a, --analyze', 'Analyze client bundle. If set, `env` will be set to `prod`.') 12 | .parse(process.argv); 13 | 14 | const task = cli.args[0] || 'watch'; 15 | if (task === 'build' || cli.analyze) { 16 | cli.env = 'prod'; 17 | } 18 | 19 | gulp.task('clean', gulp.parallel(cleanNodeScripts, cleanViewerScripts)); 20 | gulp.task('build', gulp.series('clean', compileNodeScripts, compileViewerScripts)); 21 | gulp.task('watch', gulp.series('build', watch)); 22 | gulp.task('default', gulp.task('watch')); 23 | 24 | class TaskError extends Error { 25 | constructor(message) { 26 | super(message); 27 | this.name = 'TaskError'; 28 | // Internal Gulp flag that says "don't display error stack trace" 29 | this.showStack = false; 30 | } 31 | } 32 | 33 | function watch() { 34 | gulp 35 | .watch(NODE_SRC, gulp.series(cleanNodeScripts, compileNodeScripts)) 36 | // TODO: replace with `emitErrors: false` option after https://github.com/gulpjs/glob-watcher/pull/34 will be merged 37 | .on('error', () => {}); 38 | } 39 | 40 | function cleanViewerScripts() { 41 | const del = require('del'); 42 | return del('public'); 43 | } 44 | 45 | function cleanNodeScripts() { 46 | const del = require('del'); 47 | return del(NODE_DEST); 48 | } 49 | 50 | function compileNodeScripts() { 51 | const babel = require('gulp-babel'); 52 | 53 | return gulp 54 | .src(NODE_SRC) 55 | .pipe(babel()) 56 | .pipe(gulp.dest(NODE_DEST)); 57 | } 58 | 59 | function compileViewerScripts() { 60 | const webpack = require('webpack'); 61 | const config = require('./webpack.config')({ 62 | env: cli.env, 63 | analyze: cli.analyze 64 | }); 65 | 66 | return new Promise((resolve, reject) => { 67 | webpack(config, (err, stats) => { 68 | if (cli.env === 'dev') { 69 | if (err) { 70 | console.error(err); 71 | } else { 72 | console.log(stats.toString({colors: true})); 73 | } 74 | resolve(); 75 | } else { 76 | if (err) return reject(err); 77 | 78 | if (stats.hasErrors()) { 79 | reject( 80 | new TaskError('Webpack compilation error') 81 | ); 82 | } 83 | 84 | console.log(stats.toString({colors: true})); 85 | resolve(); 86 | } 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /client/components/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import {createRef} from 'preact'; 2 | import PureComponent from '../lib/PureComponent'; 3 | 4 | import s from './Dropdown.css'; 5 | 6 | export default class Dropdown extends PureComponent { 7 | input = createRef(); 8 | 9 | state = { 10 | query: '', 11 | showOptions: false 12 | }; 13 | 14 | componentDidMount() { 15 | document.addEventListener('click', this.handleClickOutside, true); 16 | } 17 | 18 | componentWillUnmount() { 19 | document.removeEventListener('click', this.handleClickOutside, true); 20 | } 21 | 22 | render() { 23 | const {label, options} = this.props; 24 | 25 | const filteredOptions = 26 | this.state.query 27 | ? options.filter((option) => 28 | option.toLowerCase().includes(this.state.query.toLowerCase()) 29 | ) 30 | : options; 31 | 32 | return ( 33 |
    34 |
    {label}:
    35 |
    36 | 42 | {this.state.showOptions ? ( 43 |
    44 | {filteredOptions.map((option) => ( 45 |
    48 | {option} 49 |
    50 | ))} 51 |
    52 | ) : null} 53 |
    54 |
    55 | ); 56 | } 57 | 58 | handleClickOutside = (event) => { 59 | const el = this.input.current; 60 | if (el && event && !el.contains(event.target)) { 61 | this.setState({showOptions: false}); 62 | // If the query is not in the options, reset the selection 63 | if (this.state.query && !this.props.options.some((option) => option === this.state.query)) { 64 | this.setState({query: ''}); 65 | this.props.onSelectionChange(undefined); 66 | } 67 | } 68 | }; 69 | 70 | handleInput = (event) => { 71 | const {value} = event.target; 72 | this.setState({query: value}); 73 | if (!value) { 74 | this.props.onSelectionChange(undefined); 75 | } 76 | } 77 | 78 | handleFocus = () => { 79 | // move the cursor to the end of the input 80 | this.input.current.value = this.state.query; 81 | this.setState({showOptions: true}); 82 | } 83 | 84 | getOptionClickHandler = (option) => () => { 85 | this.props.onSelectionChange(option); 86 | this.setState({query: option, showOptions: false}); 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /client/components/ModuleItem.jsx: -------------------------------------------------------------------------------- 1 | import escapeRegExp from 'escape-string-regexp'; 2 | import {escape} from 'html-escaper'; 3 | import filesize from 'filesize'; 4 | import cls from 'classnames'; 5 | 6 | import PureComponent from '../lib/PureComponent'; 7 | import s from './ModuleItem.css'; 8 | 9 | export default class ModuleItem extends PureComponent { 10 | state = { 11 | visible: true 12 | }; 13 | 14 | render({module, showSize}) { 15 | const invisible = !this.state.visible; 16 | const classes = cls(s.container, s[this.itemType], { 17 | [s.invisible]: invisible 18 | }); 19 | 20 | return ( 21 |
    26 | 27 | {showSize && [ 28 | ' (', 29 | {filesize(module[showSize])}, 30 | ')' 31 | ]} 32 |
    33 | ); 34 | } 35 | 36 | get itemType() { 37 | const {module} = this.props; 38 | if (!module.path) return 'chunk'; 39 | return module.groups ? 'folder' : 'module'; 40 | } 41 | 42 | get titleHtml() { 43 | let html; 44 | const {module} = this.props; 45 | const title = module.path || module.label; 46 | const term = this.props.highlightedText; 47 | 48 | if (term) { 49 | const regexp = (term instanceof RegExp) ? 50 | new RegExp(term.source, 'igu') : 51 | new RegExp(`(?:${escapeRegExp(term)})+`, 'iu'); 52 | let match; 53 | let lastMatch; 54 | 55 | do { 56 | lastMatch = match; 57 | match = regexp.exec(title); 58 | } while (match); 59 | 60 | if (lastMatch) { 61 | html = ( 62 | escape(title.slice(0, lastMatch.index)) + 63 | `${escape(lastMatch[0])}` + 64 | escape(title.slice(lastMatch.index + lastMatch[0].length)) 65 | ); 66 | } 67 | } 68 | 69 | if (!html) { 70 | html = escape(title); 71 | } 72 | 73 | return html; 74 | } 75 | 76 | get invisibleHint() { 77 | const itemType = this.itemType.charAt(0).toUpperCase() + this.itemType.slice(1); 78 | return `${itemType} is not rendered in the treemap because it's too small.`; 79 | } 80 | 81 | get isVisible() { 82 | const {isVisible} = this.props; 83 | return isVisible ? isVisible(this.props.module) : true; 84 | } 85 | 86 | handleClick = () => this.props.onClick(this.props.module); 87 | 88 | handleMouseEnter = () => { 89 | if (this.props.isVisible) { 90 | this.setState({visible: this.isVisible}); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/tree/ConcatenatedModule.js: -------------------------------------------------------------------------------- 1 | import Module from './Module'; 2 | import ContentModule from './ContentModule'; 3 | import ContentFolder from './ContentFolder'; 4 | import {getModulePathParts} from './utils'; 5 | 6 | export default class ConcatenatedModule extends Module { 7 | 8 | constructor(name, data, parent, opts) { 9 | super(name, data, parent, opts); 10 | this.name += ' (concatenated)'; 11 | this.children = Object.create(null); 12 | this.fillContentModules(); 13 | } 14 | 15 | get parsedSize() { 16 | return this.getParsedSize() ?? this.getEstimatedSize('parsedSize'); 17 | } 18 | 19 | get gzipSize() { 20 | return this.getGzipSize() ?? this.getEstimatedSize('gzipSize'); 21 | } 22 | 23 | get brotliSize() { 24 | return this.getBrotliSize() ?? this.getEstimatedSize('brotliSize'); 25 | } 26 | 27 | getEstimatedSize(sizeType) { 28 | const parentModuleSize = this.parent[sizeType]; 29 | 30 | if (parentModuleSize !== undefined) { 31 | return Math.floor((this.size / this.parent.size) * parentModuleSize); 32 | } 33 | } 34 | 35 | fillContentModules() { 36 | this.data.modules.forEach(moduleData => this.addContentModule(moduleData)); 37 | } 38 | 39 | addContentModule(moduleData) { 40 | const pathParts = getModulePathParts(moduleData); 41 | 42 | if (!pathParts) { 43 | return; 44 | } 45 | 46 | const [folders, fileName] = [pathParts.slice(0, -1), pathParts[pathParts.length - 1]]; 47 | let currentFolder = this; 48 | 49 | folders.forEach(folderName => { 50 | let childFolder = currentFolder.getChild(folderName); 51 | 52 | if (!childFolder) { 53 | childFolder = currentFolder.addChildFolder(new ContentFolder(folderName, this)); 54 | } 55 | 56 | currentFolder = childFolder; 57 | }); 58 | 59 | const ModuleConstructor = moduleData.modules ? ConcatenatedModule : ContentModule; 60 | const module = new ModuleConstructor(fileName, moduleData, this, this.opts); 61 | currentFolder.addChildModule(module); 62 | } 63 | 64 | getChild(name) { 65 | return this.children[name]; 66 | } 67 | 68 | addChildModule(module) { 69 | module.parent = this; 70 | this.children[module.name] = module; 71 | } 72 | 73 | addChildFolder(folder) { 74 | folder.parent = this; 75 | this.children[folder.name] = folder; 76 | return folder; 77 | } 78 | 79 | mergeNestedFolders() { 80 | Object.values(this.children).forEach(child => { 81 | if (child.mergeNestedFolders) { 82 | child.mergeNestedFolders(); 83 | } 84 | }); 85 | } 86 | 87 | toChartData() { 88 | return { 89 | ...super.toChartData(), 90 | concatenated: true, 91 | groups: Object.values(this.children).map(child => child.toChartData()) 92 | }; 93 | } 94 | 95 | }; 96 | -------------------------------------------------------------------------------- /test/Logger.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-subset')); 3 | const {expect} = chai; 4 | const Logger = require('../lib/Logger'); 5 | 6 | class TestLogger extends Logger { 7 | constructor(level) { 8 | super(level); 9 | this.logs = []; 10 | } 11 | 12 | clear() { 13 | this.logs = []; 14 | } 15 | 16 | _log(level, ...args) { 17 | this.logs.push([level, ...args]); 18 | } 19 | } 20 | 21 | let logger; 22 | 23 | describe('Logger', function () { 24 | describe('level', function () { 25 | for (const testingLevel of Logger.levels) { 26 | describe(`"${testingLevel}"`, function () { 27 | beforeEach(function () { 28 | logger = new TestLogger(testingLevel); 29 | }); 30 | 31 | for (const level of Logger.levels.filter(level => level !== 'silent')) { 32 | if (Logger.levels.indexOf(level) >= Logger.levels.indexOf(testingLevel)) { 33 | it(`should log "${level}" message`, function () { 34 | logger[level]('msg1', 'msg2'); 35 | expect(logger.logs).to.deep.equal([[level, 'msg1', 'msg2']]); 36 | }); 37 | } else { 38 | it(`should not log "${level}" message`, function () { 39 | logger[level]('msg1', 'msg2'); 40 | expect(logger.logs).to.be.empty; 41 | }); 42 | } 43 | } 44 | }); 45 | } 46 | 47 | it('should be set to "info" by default', function () { 48 | logger = new TestLogger(); 49 | expectLoggerLevel(logger, 'info'); 50 | }); 51 | 52 | it('should allow to change level', function () { 53 | logger = new TestLogger('warn'); 54 | expectLoggerLevel(logger, 'warn'); 55 | logger.setLogLevel('info'); 56 | expectLoggerLevel(logger, 'info'); 57 | logger.setLogLevel('silent'); 58 | expectLoggerLevel(logger, 'silent'); 59 | }); 60 | 61 | it('should throw if level is invalid on instance creation', function () { 62 | expect(() => new TestLogger('invalid')).to.throw(invalidLogLevelMessage('invalid')); 63 | }); 64 | 65 | it('should throw if level is invalid on `setLogLevel`', function () { 66 | expect(() => new TestLogger().setLogLevel('invalid')).to.throw(invalidLogLevelMessage('invalid')); 67 | }); 68 | }); 69 | }); 70 | 71 | function expectLoggerLevel(logger, level) { 72 | logger.clear(); 73 | 74 | const levels = Logger.levels.filter(level => level !== 'silent'); 75 | 76 | for (const level of levels) { 77 | logger[level]('msg1', 'msg2'); 78 | } 79 | 80 | const expectedLogs = levels 81 | .filter(testLevel => Logger.levels.indexOf(testLevel) >= Logger.levels.indexOf(level)) 82 | .map(testLevel => [testLevel, 'msg1', 'msg2']); 83 | 84 | expect(logger.logs).to.deep.equal(expectedLogs); 85 | } 86 | 87 | function invalidLogLevelMessage(level) { 88 | return `Invalid log level "${level}". Use one of these: ${Logger.levels.join(', ')}`; 89 | } 90 | -------------------------------------------------------------------------------- /client/components/CheckboxList.jsx: -------------------------------------------------------------------------------- 1 | import CheckboxListItem from './CheckboxListItem'; 2 | import s from './CheckboxList.css'; 3 | import PureComponent from '../lib/PureComponent'; 4 | 5 | const ALL_ITEM = Symbol('ALL_ITEM'); 6 | 7 | export default class CheckboxList extends PureComponent { 8 | 9 | static ALL_ITEM = ALL_ITEM; 10 | 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | checkedItems: props.checkedItems || props.items 15 | }; 16 | } 17 | 18 | componentWillReceiveProps(newProps) { 19 | if (newProps.items !== this.props.items) { 20 | if (this.isAllChecked()) { 21 | // Preserving `all checked` state 22 | this.setState({checkedItems: newProps.items}); 23 | this.informAboutChange(newProps.items); 24 | } else if (this.state.checkedItems.length) { 25 | // Checking only items that are in the new `items` array 26 | const checkedItems = newProps.items.filter(item => 27 | this.state.checkedItems.find(checkedItem => checkedItem.label === item.label) 28 | ); 29 | 30 | this.setState({checkedItems}); 31 | this.informAboutChange(checkedItems); 32 | } 33 | } else if (newProps.checkedItems !== this.props.checkedItems) { 34 | this.setState({checkedItems: newProps.checkedItems}); 35 | } 36 | } 37 | 38 | render() { 39 | const {label, items, renderLabel} = this.props; 40 | 41 | return ( 42 |
    43 |
    44 | {label}: 45 |
    46 |
    47 | 50 | {renderLabel} 51 | 52 | {items.map(item => 53 | 57 | {renderLabel} 58 | 59 | )} 60 |
    61 |
    62 | ); 63 | } 64 | 65 | handleToggleAllCheck = () => { 66 | const checkedItems = this.isAllChecked() ? [] : this.props.items; 67 | this.setState({checkedItems}); 68 | this.informAboutChange(checkedItems); 69 | }; 70 | 71 | handleItemCheck = item => { 72 | let checkedItems; 73 | 74 | if (this.isItemChecked(item)) { 75 | checkedItems = this.state.checkedItems.filter(checkedItem => checkedItem !== item); 76 | } else { 77 | checkedItems = [...this.state.checkedItems, item]; 78 | } 79 | 80 | this.setState({checkedItems}); 81 | this.informAboutChange(checkedItems); 82 | }; 83 | 84 | isItemChecked(item) { 85 | return this.state.checkedItems.includes(item); 86 | } 87 | 88 | isAllChecked() { 89 | return (this.props.items.length === this.state.checkedItems.length); 90 | } 91 | 92 | informAboutChange(checkedItems) { 93 | setTimeout(() => this.props.onChange(checkedItems)); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/tree/BaseFolder.js: -------------------------------------------------------------------------------- 1 | import Node from './Node'; 2 | 3 | export default class BaseFolder extends Node { 4 | 5 | constructor(name, parent) { 6 | super(name, parent); 7 | this.children = Object.create(null); 8 | } 9 | 10 | get src() { 11 | if (!Object.prototype.hasOwnProperty.call(this, '_src')) { 12 | this._src = this.walk((node, src) => (src += node.src || ''), '', false); 13 | } 14 | 15 | return this._src; 16 | } 17 | 18 | get size() { 19 | if (!Object.prototype.hasOwnProperty.call(this, '_size')) { 20 | this._size = this.walk((node, size) => (size + node.size), 0, false); 21 | } 22 | 23 | return this._size; 24 | } 25 | 26 | getChild(name) { 27 | return this.children[name]; 28 | } 29 | 30 | addChildModule(module) { 31 | const {name} = module; 32 | const currentChild = this.children[name]; 33 | 34 | // For some reason we already have this node in children and it's a folder. 35 | if (currentChild && currentChild instanceof BaseFolder) return; 36 | 37 | if (currentChild) { 38 | // We already have this node in children and it's a module. 39 | // Merging it's data. 40 | currentChild.mergeData(module.data); 41 | } else { 42 | // Pushing new module 43 | module.parent = this; 44 | this.children[name] = module; 45 | } 46 | 47 | delete this._size; 48 | delete this._src; 49 | } 50 | 51 | addChildFolder(folder) { 52 | folder.parent = this; 53 | this.children[folder.name] = folder; 54 | delete this._size; 55 | delete this._src; 56 | 57 | return folder; 58 | } 59 | 60 | walk(walker, state = {}, deep = true) { 61 | let stopped = false; 62 | 63 | Object.values(this.children).forEach(child => { 64 | if (deep && child.walk) { 65 | state = child.walk(walker, state, stop); 66 | } else { 67 | state = walker(child, state, stop); 68 | } 69 | 70 | if (stopped) return false; 71 | }); 72 | 73 | return state; 74 | 75 | function stop(finalState) { 76 | stopped = true; 77 | return finalState; 78 | } 79 | } 80 | 81 | mergeNestedFolders() { 82 | if (!this.isRoot) { 83 | let childNames; 84 | 85 | while ((childNames = Object.keys(this.children)).length === 1) { 86 | const childName = childNames[0]; 87 | const onlyChild = this.children[childName]; 88 | 89 | if (onlyChild instanceof this.constructor) { 90 | this.name += `/${onlyChild.name}`; 91 | this.children = onlyChild.children; 92 | } else { 93 | break; 94 | } 95 | } 96 | } 97 | 98 | this.walk(child => { 99 | child.parent = this; 100 | 101 | if (child.mergeNestedFolders) { 102 | child.mergeNestedFolders(); 103 | } 104 | }, null, false); 105 | } 106 | 107 | toChartData() { 108 | return { 109 | label: this.name, 110 | path: this.path, 111 | statSize: this.size, 112 | groups: Object.values(this.children).map(child => child.toChartData()) 113 | }; 114 | } 115 | 116 | }; 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute to `webpack-bundle-analyzer`, fork the repository and clone it to your machine. [See this GitHub help page for what forking and cloning means](https://help.github.com/articles/fork-a-repo/) 4 | 5 | ## Setup packages 6 | 7 | Next, install this package's dependencies: 8 | 9 | ```sh 10 | npm i 11 | ``` 12 | 13 | ## Develop with your own project 14 | 15 | Run the following to build this library and watch its source files for changes: 16 | 17 | ```sh 18 | npm run start 19 | ``` 20 | 21 | You will now have a fully functioning local build of this library ready to be used. **Leave the `start` script running**, and continue with a new Terminal/shell window. 22 | 23 | Link the local package with `yarn` and/or `npm` to use it in your own projects: 24 | 25 | ```sh 26 | # Needed if your own project uses `yarn` to handle dependencies: 27 | yarn link 28 | # Needed if your own project uses `npm` to handle dependencies: 29 | npm link 30 | ``` 31 | 32 | Now go to your own project directory, and tell `npm` or `yarn` to use the local copy of `webpack-bundle-analyzer` package: 33 | 34 | ```sh 35 | cd /path/to/my/own/project 36 | # If you're using yarn, run this: 37 | yarn link webpack-bundle-analyzer 38 | # ...and if you're not, and you're using just npm in your own 39 | # project, run this: 40 | npm link webpack-bundle-analyzer 41 | ``` 42 | 43 | Now when you call `require('webpack-bundle-analyzer')` in your own project, you will actually be using the local copy of the `webpack-bundle-analyzer` project. 44 | 45 | If your own project's Webpack config has `BundleAnalyzerPlugin` configured with `analyzerMode: 'server'`, the changes you do inside `client` folder within your local copy of `webpack-bundle-analyzer` should now be immediately visible after you refresh your browser page. Hack away! 46 | 47 | ## Send your changes back to us! :revolving_hearts: 48 | 49 | We'd love for you to contribute your changes back to `webpack-bundle-analyzer`! To do that, it would be ace if you could commit your changes to a separate feature branch and open a Pull Request for those changes. 50 | 51 | Point your feature branch to use the `main` branch as the base of this PR. The exact commands used depends on how you've setup your local git copy, but the flow could look like this: 52 | 53 | ```sh 54 | # Inside your own copy of `webpack-bundle-analyzer` package... 55 | git checkout --branch feature-branch-name-here upstream/main 56 | # Then hack away, and commit your changes: 57 | git add -A 58 | git commit -m "Few words about the changes I did" 59 | # Push your local changes back to your fork 60 | git push --set-upstream origin feature-branch-name-here 61 | ``` 62 | 63 | After these steps, you should be able to create a new Pull Request for this repository. If you hit any issues following these instructions, please open an issue and we'll see if we can improve these instructions even further. 64 | 65 | ## Add tests for your changes :tada: 66 | 67 | It would be really great if the changes you did could be tested somehow. Our tests live inside the `test` directory, and they can be run with the following command: 68 | 69 | ```sh 70 | npm run test-dev 71 | ``` 72 | 73 | Now whenever you change some files, the tests will be rerun immediately. If you don't want that, and want to run tests as a one-off operation, you can use: 74 | 75 | ```sh 76 | npm test 77 | ``` 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-bundle-analyzer", 3 | "version": "5.1.0", 4 | "description": "Webpack plugin and CLI utility that represents bundle content as convenient interactive zoomable treemap", 5 | "author": "Yury Grunin ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/webpack/webpack-bundle-analyzer", 8 | "changelog": "https://github.com/webpack/webpack-bundle-analyzer/blob/main/CHANGELOG.md", 9 | "bugs": { 10 | "url": "https://github.com/webpack/webpack-bundle-analyzer/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/webpack/webpack-bundle-analyzer.git" 15 | }, 16 | "main": "lib/index.js", 17 | "bin": "lib/bin/analyzer.js", 18 | "engines": { 19 | "node": ">= 20.9.0" 20 | }, 21 | "packageManager": "npm@6.14.8", 22 | "scripts": { 23 | "start": "gulp watch", 24 | "build": "gulp build", 25 | "npm-publish": "npm run lint && npm run build && npm test && npm publish", 26 | "lint": "eslint --ext js,jsx .", 27 | "install-test-webpack-versions": "./bin/install-test-webpack-versions.sh", 28 | "test": "npm run install-test-webpack-versions && NODE_OPTIONS=--openssl-legacy-provider jest --runInBand", 29 | "test-dev": "npm run install-test-webpack-versions && NODE_OPTIONS=--openssl-legacy-provider jest --watch --runInBand" 30 | }, 31 | "files": [ 32 | "public", 33 | "lib" 34 | ], 35 | "dependencies": { 36 | "@discoveryjs/json-ext": "0.5.7", 37 | "acorn": "^8.0.4", 38 | "acorn-walk": "^8.0.0", 39 | "commander": "^7.2.0", 40 | "debounce": "^1.2.1", 41 | "escape-string-regexp": "^4.0.0", 42 | "html-escaper": "^2.0.2", 43 | "opener": "^1.5.2", 44 | "picocolors": "^1.0.0", 45 | "sirv": "^2.0.3", 46 | "ws": "^7.3.1" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "7.26.9", 50 | "@babel/plugin-proposal-decorators": "7.25.9", 51 | "@babel/plugin-transform-class-properties": "^7.27.1", 52 | "@babel/plugin-transform-runtime": "7.26.9", 53 | "@babel/preset-env": "7.26.9", 54 | "@babel/preset-react": "7.26.3", 55 | "@babel/runtime": "7.26.9", 56 | "@carrotsearch/foamtree": "3.5.0", 57 | "autoprefixer": "10.2.5", 58 | "babel-eslint": "10.1.0", 59 | "babel-loader": "9.2.1", 60 | "babel-plugin-lodash": "3.3.4", 61 | "chai": "4.3.4", 62 | "chai-subset": "1.6.0", 63 | "classnames": "2.3.1", 64 | "core-js": "3.12.1", 65 | "css-loader": "5.2.5", 66 | "cssnano": "5.0.4", 67 | "del": "6.0.0", 68 | "eslint": "5.16.0", 69 | "eslint-config-th0r": "2.0.0", 70 | "eslint-config-th0r-react": "2.0.1", 71 | "eslint-plugin-react": "7.23.2", 72 | "filesize": "^6.3.0", 73 | "globby": "11.0.3", 74 | "gulp": "4.0.2", 75 | "gulp-babel": "8.0.0", 76 | "jest": "^30.2.0", 77 | "lodash.memoize": "^4.1.2", 78 | "lodash.merge": "^4.6.2", 79 | "lodash.partial": "^4.2.1", 80 | "mobx": "5.15.7", 81 | "mobx-react": "6.3.1", 82 | "postcss": "8.3.0", 83 | "postcss-icss-values": "2.0.2", 84 | "postcss-loader": "5.3.0", 85 | "preact": "10.5.13", 86 | "puppeteer": "^24.30.0", 87 | "stream-combiner2": "1.1.1", 88 | "style-loader": "2.0.0", 89 | "terser-webpack-plugin": "5.1.2", 90 | "url-loader": "4.1.1", 91 | "webpack": "5.98.0", 92 | "webpack-cli": "6.0.1", 93 | "webpack-dev-server": "5.2.0" 94 | }, 95 | "keywords": [ 96 | "webpack", 97 | "bundle", 98 | "analyzer", 99 | "modules", 100 | "size", 101 | "interactive", 102 | "chart", 103 | "treemap", 104 | "zoomable", 105 | "zoom" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /client/components/ContextMenu.jsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import ContextMenuItem from './ContextMenuItem'; 3 | import PureComponent from '../lib/PureComponent'; 4 | import {store} from '../store'; 5 | import {elementIsOutside} from '../utils'; 6 | 7 | import s from './ContextMenu.css'; 8 | 9 | export default class ContextMenu extends PureComponent { 10 | componentDidMount() { 11 | this.boundingRect = this.node.getBoundingClientRect(); 12 | } 13 | 14 | componentDidUpdate(prevProps) { 15 | if (this.props.visible && !prevProps.visible) { 16 | document.addEventListener('mousedown', this.handleDocumentMousedown, true); 17 | } else if (prevProps.visible && !this.props.visible) { 18 | document.removeEventListener('mousedown', this.handleDocumentMousedown, true); 19 | } 20 | } 21 | 22 | render() { 23 | const {visible} = this.props; 24 | const containerClassName = cls({ 25 | [s.container]: true, 26 | [s.hidden]: !visible 27 | }); 28 | const multipleChunksSelected = store.selectedChunks.length > 1; 29 | return ( 30 |
      31 | 33 | Hide chunk 34 | 35 | 37 | Hide all other chunks 38 | 39 |
      40 | 42 | Show all chunks 43 | 44 |
    45 | ); 46 | } 47 | 48 | handleClickHideChunk = () => { 49 | const {chunk: selectedChunk} = this.props; 50 | if (selectedChunk && selectedChunk.label) { 51 | const filteredChunks = store.selectedChunks.filter(chunk => chunk.label !== selectedChunk.label); 52 | store.selectedChunks = filteredChunks; 53 | } 54 | this.hide(); 55 | } 56 | 57 | handleClickFilterToChunk = () => { 58 | const {chunk: selectedChunk} = this.props; 59 | if (selectedChunk && selectedChunk.label) { 60 | const filteredChunks = store.allChunks.filter(chunk => chunk.label === selectedChunk.label); 61 | store.selectedChunks = filteredChunks; 62 | } 63 | this.hide(); 64 | } 65 | 66 | handleClickShowAllChunks = () => { 67 | store.selectedChunks = store.allChunks; 68 | this.hide(); 69 | } 70 | 71 | /** 72 | * Handle document-wide `mousedown` events to detect clicks 73 | * outside the context menu. 74 | * @param {MouseEvent} e - DOM mouse event object 75 | * @returns {void} 76 | */ 77 | handleDocumentMousedown = (e) => { 78 | const isSecondaryClick = e.ctrlKey || e.button === 2; 79 | if (!isSecondaryClick && elementIsOutside(e.target, this.node)) { 80 | e.preventDefault(); 81 | e.stopPropagation(); 82 | this.hide(); 83 | } 84 | } 85 | 86 | hide() { 87 | if (this.props.onHide) { 88 | this.props.onHide(); 89 | } 90 | } 91 | 92 | saveNode = node => (this.node = node); 93 | 94 | getStyle() { 95 | const {boundingRect} = this; 96 | 97 | // Upon the first render of this component, we don't yet know 98 | // its dimensions, so can't position it yet 99 | if (!boundingRect) return; 100 | 101 | const {coords} = this.props; 102 | 103 | const pos = { 104 | left: coords.x, 105 | top: coords.y 106 | }; 107 | 108 | if (pos.left + boundingRect.width > window.innerWidth) { 109 | // Shifting horizontally 110 | pos.left = window.innerWidth - boundingRect.width; 111 | } 112 | 113 | if (pos.top + boundingRect.height > window.innerHeight) { 114 | // Flipping vertically 115 | pos.top = coords.y - boundingRect.height; 116 | } 117 | return pos; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /test/viewer.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-subset')); 3 | const {expect} = chai; 4 | const crypto = require('crypto'); 5 | const net = require('net'); 6 | 7 | const Logger = require('../lib/Logger'); 8 | const {getEntrypoints, startServer} = require('../lib/viewer.js'); 9 | 10 | describe('WebSocket server', function () { 11 | it('should not crash when an error is emitted on the websocket', function (done) { 12 | const bundleStats = { 13 | assets: [{name: 'bundle.js', chunks: [0]}] 14 | }; 15 | 16 | const options = { 17 | openBrowser: false, 18 | logger: new Logger('silent'), 19 | port: 0, 20 | analyzerUrl: () => '' 21 | }; 22 | 23 | startServer(bundleStats, options) 24 | .then(function ({http: server}) { 25 | // The GUID constant defined in WebSocket protocol 26 | // https://tools.ietf.org/html/rfc6455#section-1.3 27 | const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 28 | 29 | // The client-generated "Sec-WebSocket-Key" header field value. 30 | const key = crypto.randomBytes(16).toString('base64'); 31 | 32 | // The server-generated "Sec-WebSocket-Accept" header field value. 33 | const accept = crypto.createHash('sha1') 34 | .update(key + GUID) 35 | .digest('base64'); 36 | 37 | const socket = net.createConnection(server.address().port, function () { 38 | socket.write([ 39 | 'GET / HTTP/1.1', 40 | 'Host: localhost', 41 | 'Upgrade: websocket', 42 | 'Connection: Upgrade', 43 | `Sec-WebSocket-Key: ${key}`, 44 | 'Sec-WebSocket-Version: 13', 45 | '', 46 | '' 47 | ].join('\r\n')); 48 | }); 49 | 50 | socket.on('data', function (chunk) { 51 | const expected = Buffer.from([ 52 | 'HTTP/1.1 101 Switching Protocols', 53 | 'Upgrade: websocket', 54 | 'Connection: Upgrade', 55 | `Sec-WebSocket-Accept: ${accept}`, 56 | '', 57 | '' 58 | ].join('\r\n')); 59 | 60 | expect(chunk.equals(expected)).to.be.true; 61 | 62 | // Send a WebSocket frame with a reserved opcode (5) to trigger an error 63 | // to be emitted on the server. 64 | socket.write(Buffer.from([0x85, 0x00])); 65 | socket.on('close', function () { 66 | server.close(done); 67 | }); 68 | }); 69 | }) 70 | .catch(done); 71 | }); 72 | }); 73 | 74 | describe('getEntrypoints', () => { 75 | it('should get all entrypoints', () => { 76 | const bundleStats = { 77 | entrypoints: { 78 | 'A': { 79 | name: 'A', 80 | assets: [ 81 | { 82 | name: 'chunkA.js' 83 | } 84 | ] 85 | }, 86 | 'B': { 87 | name: 'B', 88 | assets: [ 89 | { 90 | name: 'chunkA.js' 91 | }, 92 | { 93 | name: 'chunkB.js' 94 | } 95 | ] 96 | } 97 | } 98 | }; 99 | expect(JSON.stringify(getEntrypoints(bundleStats))).to.equal(JSON.stringify(['A', 'B'])); 100 | }); 101 | 102 | it('should handle when bundlestats is null or undefined ', function () { 103 | expect(JSON.stringify(getEntrypoints(null))).to.equal(JSON.stringify([])); 104 | expect(JSON.stringify(getEntrypoints(undefined))).to.equal(JSON.stringify([])); 105 | }); 106 | 107 | it('should handle when bundlestats is empty', function () { 108 | const bundleStatsWithoutEntryPoints = {}; 109 | expect(JSON.stringify(getEntrypoints(bundleStatsWithoutEntryPoints))).to.equal(JSON.stringify([])); 110 | }); 111 | 112 | it('should handle when entrypoints is empty', function () { 113 | const bundleStatsEmptyEntryPoint = {entrypoints: {}}; 114 | expect(JSON.stringify(getEntrypoints(bundleStatsEmptyEntryPoint))).to.equal(JSON.stringify([])); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | const {readdirSync} = require('fs'); 2 | const webpack = require('webpack'); 3 | const memoize = require('lodash.memoize'); 4 | const partial = require('lodash.partial'); 5 | const merge = require('lodash.merge'); 6 | 7 | global.webpackCompile = webpackCompile; 8 | global.makeWebpackConfig = makeWebpackConfig; 9 | global.forEachWebpackVersion = forEachWebpackVersion; 10 | 11 | const BundleAnalyzerPlugin = require('../lib/BundleAnalyzerPlugin'); 12 | 13 | const getAvailableWebpackVersions = memoize(() => 14 | readdirSync(`${__dirname}/webpack-versions`, {withFileTypes: true}) 15 | .filter(entry => entry.isDirectory()) 16 | .map(dir => dir.name) 17 | ); 18 | 19 | function forEachWebpackVersion(versions, cb) { 20 | const availableVersions = getAvailableWebpackVersions(); 21 | 22 | if (typeof versions === 'function') { 23 | cb = versions; 24 | versions = availableVersions; 25 | } else { 26 | const notFoundVersions = versions.filter(version => !availableVersions.includes(version)); 27 | 28 | if (notFoundVersions.length) { 29 | throw new Error( 30 | `These Webpack versions are not currently available for testing: ${notFoundVersions.join(', ')}\n` + 31 | 'You need to install them manually into "test/webpack-versions" directory.' 32 | ); 33 | } 34 | } 35 | 36 | for (const version of versions) { 37 | const itFn = function (testDescription, ...args) { 38 | return it.call(this, `${testDescription} (Webpack ${version})`, ...args); 39 | }; 40 | 41 | itFn.only = function (testDescription, ...args) { 42 | return it.only.call(this, `${testDescription} (Webpack ${version})`, ...args); 43 | }; 44 | 45 | cb({ 46 | it: itFn, 47 | version, 48 | webpackCompile: partial(webpackCompile, partial.placeholder, version) 49 | }); 50 | } 51 | } 52 | 53 | async function webpackCompile(config, version) { 54 | if (version === undefined || version === null) { 55 | throw new Error('Webpack version is not specified'); 56 | } 57 | 58 | if (!getAvailableWebpackVersions().includes(version)) { 59 | throw new Error(`Webpack version "${version}" is not available for testing`); 60 | } 61 | 62 | let webpack; 63 | 64 | try { 65 | webpack = require(`./webpack-versions/${version}/node_modules/webpack`); 66 | } catch (err) { 67 | throw new Error( 68 | `Error requiring Webpack ${version}:\n${err}\n\n` + 69 | 'Try running "npm run install-test-webpack-versions".' 70 | ); 71 | } 72 | 73 | await new Promise((resolve, reject) => { 74 | webpack(config, (err, stats) => { 75 | if (err) { 76 | return reject(err); 77 | } 78 | 79 | if (stats.hasErrors()) { 80 | return reject(stats.toJson({source: false}).errors); 81 | } 82 | 83 | resolve(); 84 | }); 85 | }); 86 | // Waiting for the next tick (for analyzer report to be generated) 87 | await wait(1); 88 | } 89 | 90 | function makeWebpackConfig(opts) { 91 | opts = merge({ 92 | analyzerOpts: { 93 | analyzerMode: 'static', 94 | openAnalyzer: false, 95 | logLevel: 'error' 96 | }, 97 | minify: false, 98 | multipleChunks: false 99 | }, opts); 100 | 101 | return { 102 | context: __dirname, 103 | mode: 'development', 104 | entry: { 105 | bundle: './src' 106 | }, 107 | output: { 108 | path: `${__dirname}/output`, 109 | filename: '[name].js' 110 | }, 111 | optimization: { 112 | runtimeChunk: { 113 | name: 'manifest' 114 | } 115 | }, 116 | plugins: (plugins => { 117 | plugins.push( 118 | new BundleAnalyzerPlugin(opts.analyzerOpts) 119 | ); 120 | 121 | if (opts.minify) { 122 | plugins.push( 123 | new webpack.optimize.UglifyJsPlugin({ 124 | comments: false, 125 | mangle: true, 126 | compress: { 127 | warnings: false, 128 | negate_iife: false 129 | } 130 | }) 131 | ); 132 | } 133 | 134 | return plugins; 135 | })([]) 136 | }; 137 | } 138 | 139 | function wait(ms) { 140 | return new Promise(resolve => setTimeout(resolve, ms)); 141 | } 142 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const compact = require('lodash/compact'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const BundleAnalyzePlugin = require('./lib/BundleAnalyzerPlugin'); 5 | 6 | module.exports = opts => { 7 | opts = Object.assign({ 8 | env: 'dev', 9 | analyze: false 10 | }, opts); 11 | 12 | const isDev = (opts.env === 'dev'); 13 | 14 | return { 15 | mode: isDev ? 'development' : 'production', 16 | context: __dirname, 17 | entry: './client/viewer', 18 | output: { 19 | path: `${__dirname}/public`, 20 | filename: 'viewer.js', 21 | publicPath: '/' 22 | }, 23 | 24 | resolve: { 25 | extensions: ['.js', '.jsx'], 26 | alias: { 27 | react: 'preact/compat', 28 | 'react-dom/test-utils': 'preact/test-utils', 29 | 'react-dom': 'preact/compat', 30 | mobx: require.resolve('mobx/lib/mobx.es6.js') 31 | } 32 | }, 33 | 34 | devtool: isDev ? 'eval' : 'source-map', 35 | watch: isDev, 36 | 37 | performance: { 38 | hints: false 39 | }, 40 | optimization: { 41 | minimize: !isDev, 42 | minimizer: [ 43 | new TerserPlugin({ 44 | parallel: true, 45 | terserOptions: { 46 | output: { 47 | comments: /copyright/iu 48 | }, 49 | safari10: true 50 | } 51 | }) 52 | ] 53 | }, 54 | 55 | module: { 56 | rules: [ 57 | { 58 | test: /\.jsx?$/u, 59 | exclude: /node_modules/u, 60 | loader: 'babel-loader', 61 | options: { 62 | babelrc: false, 63 | presets: [ 64 | ['@babel/preset-env', { 65 | // Target browsers are specified in .browserslistrc 66 | 67 | modules: false, 68 | useBuiltIns: 'usage', 69 | corejs: require('./package.json').devDependencies['core-js'], 70 | debug: true 71 | }], 72 | ['@babel/preset-react', { 73 | runtime: 'automatic', 74 | importSource: 'preact' 75 | }] 76 | ], 77 | plugins: [ 78 | 'lodash', 79 | ['@babel/plugin-proposal-decorators', {legacy: true}], 80 | ['@babel/plugin-transform-class-properties', {loose: true}], 81 | ['@babel/plugin-transform-runtime', { 82 | useESModules: true 83 | }] 84 | ] 85 | } 86 | }, 87 | { 88 | test: /\.css$/u, 89 | use: [ 90 | 'style-loader', 91 | { 92 | loader: 'css-loader', 93 | options: { 94 | modules: { 95 | localIdentName: '[name]__[local]' 96 | }, 97 | importLoaders: 1 98 | } 99 | }, 100 | { 101 | loader: 'postcss-loader', 102 | options: { 103 | postcssOptions: { 104 | plugins: compact([ 105 | require('postcss-icss-values'), 106 | require('autoprefixer'), 107 | !isDev && require('cssnano')() 108 | ]) 109 | } 110 | } 111 | } 112 | ] 113 | }, 114 | { 115 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/u, 116 | loader: 'url-loader' 117 | } 118 | ] 119 | }, 120 | 121 | plugins: (plugins => { 122 | if (!isDev) { 123 | if (opts.analyze) { 124 | plugins.push( 125 | new BundleAnalyzePlugin({ 126 | generateStatsFile: true 127 | }) 128 | ); 129 | } 130 | 131 | plugins.push( 132 | new webpack.DefinePlugin({ 133 | 'process': JSON.stringify({ 134 | env: { 135 | NODE_ENV: 'production' 136 | } 137 | }), 138 | // Fixes "ModuleConcatenation bailout" for some modules (e.g. Preact and MobX) 139 | 'global': 'undefined' 140 | }) 141 | ); 142 | } 143 | 144 | return plugins; 145 | })([]) 146 | }; 147 | }; 148 | -------------------------------------------------------------------------------- /src/template.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const {escape} = require('html-escaper'); 6 | 7 | const projectRoot = path.resolve(__dirname, '..'); 8 | const assetsRoot = path.join(projectRoot, 'public'); 9 | 10 | exports.renderViewer = renderViewer; 11 | 12 | /** 13 | * Escapes `<` characters in JSON to safely use it in ``; 37 | } else { 38 | return ``; 39 | } 40 | } 41 | 42 | function renderViewer({title, enableWebSocket, chartData, entrypoints, defaultSizes, compressionAlgorithm, mode} = {}) { 43 | return html` 44 | 45 | 46 | 47 | 48 | ${escape(title)} 49 | 50 | 51 | 54 | ${getScript('viewer.js', mode)} 55 | 56 | 57 | 58 |
    59 | 65 | 66 | `; 67 | } 68 | -------------------------------------------------------------------------------- /client/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | import cls from 'classnames'; 3 | 4 | import s from './Sidebar.css'; 5 | import Button from './Button'; 6 | import Icon from './Icon'; 7 | import ThemeToggle from './ThemeToggle'; 8 | 9 | const toggleTime = parseInt(s.toggleTime); 10 | 11 | export default class Sidebar extends Component { 12 | static defaultProps = { 13 | pinned: false, 14 | position: 'left' 15 | }; 16 | 17 | allowHide = true; 18 | toggling = false; 19 | hideContentTimeout = null; 20 | width = null; 21 | state = { 22 | visible: true, 23 | renderContent: true 24 | }; 25 | 26 | componentDidMount() { 27 | this.hideTimeoutId = setTimeout(() => this.toggleVisibility(false), 3000); 28 | } 29 | 30 | componentWillUnmount() { 31 | clearTimeout(this.hideTimeoutId); 32 | clearTimeout(this.hideContentTimeout); 33 | } 34 | 35 | render() { 36 | const {position, pinned, children} = this.props; 37 | const {visible, renderContent} = this.state; 38 | 39 | const className = cls({ 40 | [s.container]: true, 41 | [s.pinned]: pinned, 42 | [s.left]: (position === 'left'), 43 | [s.hidden]: !visible, 44 | [s.empty]: !renderContent 45 | }); 46 | 47 | return ( 48 |
    52 | 53 | {visible && 54 | 62 | } 63 | 69 | {pinned && visible && 70 |
    71 | } 72 |
    75 | {renderContent ? children : null} 76 |
    77 |
    78 | ); 79 | } 80 | 81 | handleClick = () => { 82 | this.allowHide = false; 83 | } 84 | 85 | handleMouseEnter = () => { 86 | if (!this.toggling && !this.props.pinned) { 87 | clearTimeout(this.hideTimeoutId); 88 | this.toggleVisibility(true); 89 | } 90 | }; 91 | 92 | handleMouseMove = () => { 93 | this.allowHide = true; 94 | } 95 | 96 | handleMouseLeave = () => { 97 | if (this.allowHide && !this.toggling && !this.props.pinned) { 98 | this.toggleVisibility(false); 99 | } 100 | } 101 | 102 | handleToggleButtonClick = () => { 103 | this.toggleVisibility(); 104 | } 105 | 106 | handlePinButtonClick = () => { 107 | const pinned = !this.props.pinned; 108 | this.width = pinned ? this.node.getBoundingClientRect().width : null; 109 | this.updateNodeWidth(); 110 | this.props.onPinStateChange(pinned); 111 | } 112 | 113 | handleResizeStart = event => { 114 | this.resizeInfo = { 115 | startPageX: event.pageX, 116 | initialWidth: this.width 117 | }; 118 | document.body.classList.add('resizing', 'col'); 119 | document.addEventListener('mousemove', this.handleResize, true); 120 | document.addEventListener('mouseup', this.handleResizeEnd, true); 121 | } 122 | 123 | handleResize = event => { 124 | this.width = this.resizeInfo.initialWidth + (event.pageX - this.resizeInfo.startPageX); 125 | this.updateNodeWidth(); 126 | } 127 | 128 | handleResizeEnd = () => { 129 | document.body.classList.remove('resizing', 'col'); 130 | document.removeEventListener('mousemove', this.handleResize, true); 131 | document.removeEventListener('mouseup', this.handleResizeEnd, true); 132 | this.props.onResize(); 133 | } 134 | 135 | toggleVisibility(flag) { 136 | clearTimeout(this.hideContentTimeout); 137 | 138 | const {visible} = this.state; 139 | const {onToggle, pinned} = this.props; 140 | 141 | if (flag === undefined) { 142 | flag = !visible; 143 | } else if (flag === visible) { 144 | return; 145 | } 146 | 147 | this.setState({visible: flag}); 148 | this.toggling = true; 149 | setTimeout(() => { 150 | this.toggling = false; 151 | }, toggleTime); 152 | 153 | if (pinned) { 154 | this.updateNodeWidth(flag ? this.width : null); 155 | } 156 | 157 | if (flag || pinned) { 158 | this.setState({renderContent: flag}); 159 | onToggle(flag); 160 | } else if (!flag) { 161 | // Waiting for the CSS animation to finish and hiding content 162 | this.hideContentTimeout = setTimeout(() => { 163 | this.hideContentTimeout = null; 164 | this.setState({renderContent: false}); 165 | onToggle(false); 166 | }, toggleTime); 167 | } 168 | } 169 | 170 | saveNode = node => this.node = node; 171 | 172 | updateNodeWidth(width = this.width) { 173 | this.node.style.width = width ? `${width}px` : ''; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /test/stats/minimal-stats/stats.json: -------------------------------------------------------------------------------- 1 | {"logging":{"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./simple-entry.js":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/viewer.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/store.js":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/ModulesTreemap.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/utils.js":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/localStorage.js":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Tooltip.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Treemap.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Sidebar.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/CheckboxList.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Checkbox.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Dropdown.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Switcher.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/ModulesList.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Search.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/ContextMenu.jsx":{"entries":[],"filteredEntries":3,"debug":false},"webpack.DefinePlugin":{"entries":[],"filteredEntries":137,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/SwitcherItem.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Button.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/ModuleItem.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/Icon.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/CheckboxListItem.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/components/ContextMenuItem.jsx":{"entries":[],"filteredEntries":3,"debug":false},"./node_modules/babel-loader/lib/index.js babel-loader ./node_modules/babel-loader/lib/index.js??ruleSet[1].rules[0]!./client/lib/PureComponent.jsx":{"entries":[],"filteredEntries":3,"debug":false},"webpack.Compiler":{"entries":[],"filteredEntries":7,"debug":false},"webpack.Compilation":{"entries":[],"filteredEntries":27,"debug":false},"webpack.FlagDependencyExportsPlugin":{"entries":[],"filteredEntries":4,"debug":false},"webpack.InnerGraphPlugin":{"entries":[],"filteredEntries":1,"debug":false},"webpack.SideEffectsFlagPlugin":{"entries":[],"filteredEntries":1,"debug":false},"webpack.FlagDependencyUsagePlugin":{"entries":[],"filteredEntries":2,"debug":false},"webpack.buildChunkGraph":{"entries":[],"filteredEntries":9,"debug":false},"webpack.SplitChunksPlugin":{"entries":[],"filteredEntries":4,"debug":false},"webpack.ModuleConcatenationPlugin":{"entries":[],"filteredEntries":8,"debug":false},"webpack.FileSystemInfo":{"entries":[],"filteredEntries":11,"debug":false},"webpack.Watching":{"entries":[],"filteredEntries":1,"debug":false}},"version":"5.102.1","time":4182,"assetsByChunkName":{"main":["viewer.js"]},"filteredAssets":1,"filteredModules":172,"filteredErrorDetailsCount":0,"errors":[],"errorsCount":0,"filteredWarningDetailsCount":0,"warnings":[],"warningsCount":0} 2 | -------------------------------------------------------------------------------- /src/BundleAnalyzerPlugin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const {bold} = require('picocolors'); 4 | 5 | const Logger = require('./Logger'); 6 | const viewer = require('./viewer'); 7 | const utils = require('./utils'); 8 | const {writeStats} = require('./statsUtils'); 9 | 10 | class BundleAnalyzerPlugin { 11 | constructor(opts = {}) { 12 | this.opts = { 13 | analyzerMode: 'server', 14 | analyzerHost: '127.0.0.1', 15 | compressionAlgorithm: 'gzip', 16 | reportFilename: null, 17 | reportTitle: utils.defaultTitle, 18 | defaultSizes: 'parsed', 19 | openAnalyzer: true, 20 | generateStatsFile: false, 21 | statsFilename: 'stats.json', 22 | statsOptions: null, 23 | excludeAssets: null, 24 | logLevel: 'info', 25 | // deprecated 26 | startAnalyzer: true, 27 | analyzerUrl: utils.defaultAnalyzerUrl, 28 | ...opts, 29 | analyzerPort: 'analyzerPort' in opts ? (opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort) : 8888 30 | }; 31 | 32 | this.server = null; 33 | this.logger = new Logger(this.opts.logLevel); 34 | } 35 | 36 | apply(compiler) { 37 | this.compiler = compiler; 38 | 39 | const done = (stats, callback) => { 40 | callback = callback || (() => {}); 41 | 42 | const actions = []; 43 | 44 | if (this.opts.generateStatsFile) { 45 | actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions))); 46 | } 47 | 48 | // Handling deprecated `startAnalyzer` flag 49 | if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) { 50 | this.opts.analyzerMode = 'disabled'; 51 | } 52 | 53 | if (this.opts.analyzerMode === 'server') { 54 | actions.push(() => this.startAnalyzerServer(stats.toJson())); 55 | } else if (this.opts.analyzerMode === 'static') { 56 | actions.push(() => this.generateStaticReport(stats.toJson())); 57 | } else if (this.opts.analyzerMode === 'json') { 58 | actions.push(() => this.generateJSONReport(stats.toJson())); 59 | } 60 | 61 | if (actions.length) { 62 | // Making analyzer logs to be after all webpack logs in the console 63 | setImmediate(async () => { 64 | try { 65 | await Promise.all(actions.map(action => action())); 66 | callback(); 67 | } catch (e) { 68 | callback(e); 69 | } 70 | }); 71 | } else { 72 | callback(); 73 | } 74 | }; 75 | 76 | if (compiler.hooks) { 77 | compiler.hooks.done.tapAsync('webpack-bundle-analyzer', done); 78 | } else { 79 | compiler.plugin('done', done); 80 | } 81 | } 82 | 83 | async generateStatsFile(stats) { 84 | const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename); 85 | await fs.promises.mkdir(path.dirname(statsFilepath), {recursive: true}); 86 | 87 | try { 88 | await writeStats(stats, statsFilepath); 89 | 90 | this.logger.info( 91 | `${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}` 92 | ); 93 | } catch (error) { 94 | this.logger.error( 95 | `${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}` 96 | ); 97 | } 98 | } 99 | 100 | async startAnalyzerServer(stats) { 101 | if (this.server) { 102 | (await this.server).updateChartData(stats); 103 | } else { 104 | this.server = viewer.startServer(stats, { 105 | openBrowser: this.opts.openAnalyzer, 106 | host: this.opts.analyzerHost, 107 | port: this.opts.analyzerPort, 108 | reportTitle: this.opts.reportTitle, 109 | compressionAlgorithm: this.opts.compressionAlgorithm, 110 | bundleDir: this.getBundleDirFromCompiler(), 111 | logger: this.logger, 112 | defaultSizes: this.opts.defaultSizes, 113 | excludeAssets: this.opts.excludeAssets, 114 | analyzerUrl: this.opts.analyzerUrl 115 | }); 116 | } 117 | } 118 | 119 | async generateJSONReport(stats) { 120 | await viewer.generateJSONReport(stats, { 121 | reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.json'), 122 | compressionAlgorithm: this.opts.compressionAlgorithm, 123 | bundleDir: this.getBundleDirFromCompiler(), 124 | logger: this.logger, 125 | excludeAssets: this.opts.excludeAssets 126 | }); 127 | } 128 | 129 | async generateStaticReport(stats) { 130 | await viewer.generateReport(stats, { 131 | openBrowser: this.opts.openAnalyzer, 132 | reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename || 'report.html'), 133 | reportTitle: this.opts.reportTitle, 134 | compressionAlgorithm: this.opts.compressionAlgorithm, 135 | bundleDir: this.getBundleDirFromCompiler(), 136 | logger: this.logger, 137 | defaultSizes: this.opts.defaultSizes, 138 | excludeAssets: this.opts.excludeAssets 139 | }); 140 | } 141 | 142 | getBundleDirFromCompiler() { 143 | if (typeof this.compiler.outputFileSystem.constructor === 'undefined') { 144 | return this.compiler.outputPath; 145 | } 146 | switch (this.compiler.outputFileSystem.constructor.name) { 147 | case 'MemoryFileSystem': 148 | return null; 149 | // Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development 150 | // Related: #274 151 | case 'AsyncMFS': 152 | return null; 153 | default: 154 | return this.compiler.outputPath; 155 | } 156 | } 157 | 158 | } 159 | 160 | module.exports = BundleAnalyzerPlugin; 161 | -------------------------------------------------------------------------------- /test/stats/with-invalid-dynamic-require.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "warnings": [ 4 | "./src/invalid-require-usage.js\n2:9-28 Critical dependency: the request of a dependency is an expression\n at CommonJsRequireContextDependency.getWarnings (/Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/dependencies/CommonJsRequireContextDependency.js:27:4)\n at Compilation.reportDependencyErrorsAndWarnings (/Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/Compilation.js:668:24)\n at Compilation.finish (/Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/Compilation.js:531:9)\n at /Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/Compiler.js:486:16\n at /Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/node_modules/tapable/lib/Tapable.js:225:11\n at _addModuleChain (/Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/Compilation.js:477:11)\n at processModuleDependencies.err (/Users/th0r/.nvm/versions/node/v7.8.0/lib/node_modules/webpack/lib/Compilation.js:448:13)\n at _combinedTickCallback (internal/process/next_tick.js:73:7)\n at process._tickCallback (internal/process/next_tick.js:104:9)" 5 | ], 6 | "version": "2.3.3", 7 | "hash": "6f90cfe22237ea1b46c7", 8 | "time": 161, 9 | "publicPath": "", 10 | "assetsByChunkName": { 11 | "bundle": "bundle.js" 12 | }, 13 | "assets": [ 14 | { 15 | "name": "bundle.js", 16 | "size": 793, 17 | "chunks": [ 18 | 0 19 | ], 20 | "chunkNames": [ 21 | "bundle" 22 | ], 23 | "emitted": true 24 | } 25 | ], 26 | "entrypoints": { 27 | "bundle": { 28 | "chunks": [ 29 | 0 30 | ], 31 | "assets": [ 32 | "bundle.js" 33 | ] 34 | } 35 | }, 36 | "chunks": [ 37 | { 38 | "id": 0, 39 | "rendered": true, 40 | "initial": true, 41 | "entry": true, 42 | "extraAsync": false, 43 | "size": 296, 44 | "names": [ 45 | "bundle" 46 | ], 47 | "files": [ 48 | "bundle.js" 49 | ], 50 | "hash": "af2ae029ed2063f5780c", 51 | "parents": [], 52 | "origins": [ 53 | { 54 | "moduleId": 2, 55 | "module": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 56 | "moduleIdentifier": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 57 | "moduleName": "./src/invalid-require-usage.js", 58 | "loc": "", 59 | "name": "bundle", 60 | "reasons": [] 61 | } 62 | ] 63 | } 64 | ], 65 | "modules": [ 66 | { 67 | "id": 0, 68 | "identifier": "/Volumes/Work/webpack-bundle-analyzer/test/src", 69 | "name": "./src", 70 | "index": 1, 71 | "index2": 0, 72 | "size": 160, 73 | "cacheable": true, 74 | "built": true, 75 | "optional": false, 76 | "prefetched": false, 77 | "chunks": [ 78 | 0 79 | ], 80 | "assets": [], 81 | "issuer": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 82 | "issuerId": 2, 83 | "issuerName": "./src/invalid-require-usage.js", 84 | "failed": false, 85 | "errors": 0, 86 | "warnings": 0, 87 | "reasons": [ 88 | { 89 | "moduleId": 2, 90 | "moduleIdentifier": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 91 | "module": "./src/invalid-require-usage.js", 92 | "moduleName": "./src/invalid-require-usage.js", 93 | "type": "cjs require context", 94 | "userRequest": ".", 95 | "loc": "2:9-28" 96 | } 97 | ], 98 | "usedExports": true, 99 | "providedExports": null, 100 | "depth": 1 101 | }, 102 | { 103 | "id": 1, 104 | "identifier": "/Volumes/Work/webpack-bundle-analyzer/test/src/a.js", 105 | "name": "./src/a.js", 106 | "index": 2, 107 | "index2": 1, 108 | "size": 29, 109 | "cacheable": true, 110 | "built": true, 111 | "optional": false, 112 | "prefetched": false, 113 | "chunks": [ 114 | 0 115 | ], 116 | "assets": [], 117 | "issuer": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 118 | "issuerId": 2, 119 | "issuerName": "./src/invalid-require-usage.js", 120 | "failed": false, 121 | "errors": 0, 122 | "warnings": 0, 123 | "reasons": [ 124 | { 125 | "moduleId": 2, 126 | "moduleIdentifier": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 127 | "module": "./src/invalid-require-usage.js", 128 | "moduleName": "./src/invalid-require-usage.js", 129 | "type": "cjs require", 130 | "userRequest": "./a", 131 | "loc": "6:0-14" 132 | } 133 | ], 134 | "usedExports": true, 135 | "providedExports": null, 136 | "depth": 1, 137 | "source": "module.exports = 'module a';\n" 138 | }, 139 | { 140 | "id": 2, 141 | "identifier": "/Volumes/Work/webpack-bundle-analyzer/test/src/invalid-require-usage.js", 142 | "name": "./src/invalid-require-usage.js", 143 | "index": 0, 144 | "index2": 2, 145 | "size": 107, 146 | "cacheable": true, 147 | "built": true, 148 | "optional": false, 149 | "prefetched": false, 150 | "chunks": [ 151 | 0 152 | ], 153 | "assets": [], 154 | "issuer": null, 155 | "issuerId": null, 156 | "issuerName": null, 157 | "failed": false, 158 | "errors": 0, 159 | "warnings": 0, 160 | "reasons": [], 161 | "usedExports": true, 162 | "providedExports": null, 163 | "depth": 0, 164 | "source": "function dynamicRequire(moduleName) {\n return require(moduleName);\n}\n\ndynamicRequire('');\nrequire('./a');\n" 165 | } 166 | ], 167 | "filteredModules": 0, 168 | "children": [] 169 | } 170 | -------------------------------------------------------------------------------- /src/bin/analyzer.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const {resolve, dirname} = require('path'); 4 | 5 | const commander = require('commander'); 6 | const {magenta} = require('picocolors'); 7 | 8 | const analyzer = require('../analyzer'); 9 | const viewer = require('../viewer'); 10 | const Logger = require('../Logger'); 11 | const utils = require('../utils'); 12 | 13 | const SIZES = new Set(['stat', 'parsed', 'gzip']); 14 | const COMPRESSION_ALGORITHMS = new Set(['gzip', 'brotli']); 15 | 16 | const program = commander 17 | .version(require('../../package.json').version) 18 | .usage( 19 | ` [bundleDir] [options] 20 | 21 | Arguments: 22 | 23 | bundleStatsFile Path to Webpack Stats JSON file. 24 | bundleDir Directory containing all generated bundles. 25 | You should provided it if you want analyzer to show you the real parsed module sizes. 26 | By default a directory of stats file is used.` 27 | ) 28 | .option( 29 | '-m, --mode ', 30 | 'Analyzer mode. Should be `server`,`static` or `json`.' + 31 | br('In `server` mode analyzer will start HTTP server to show bundle report.') + 32 | br('In `static` mode single HTML file with bundle report will be generated.') + 33 | br('In `json` mode single JSON file with bundle report will be generated.'), 34 | 'server' 35 | ) 36 | .option( 37 | // Had to make `host` parameter optional in order to let `-h` flag output help message 38 | // Fixes https://github.com/webpack/webpack-bundle-analyzer/issues/239 39 | '-h, --host [host]', 40 | 'Host that will be used in `server` mode to start HTTP server.', 41 | '127.0.0.1' 42 | ) 43 | .option( 44 | '-p, --port ', 45 | 'Port that will be used in `server` mode to start HTTP server.', 46 | 8888 47 | ) 48 | .option( 49 | '-r, --report ', 50 | 'Path to bundle report file that will be generated in `static` mode.' 51 | ) 52 | .option( 53 | '-t, --title ', 54 | 'String to use in title element of html report.' 55 | ) 56 | .option( 57 | '-s, --default-sizes <type>', 58 | 'Module sizes to show in treemap by default.' + 59 | br(`Possible values: ${[...SIZES].join(', ')}`), 60 | 'parsed' 61 | ) 62 | .option( 63 | '--compression-algorithm <type>', 64 | 'Compression algorithm that will be used to calculate the compressed module sizes.' + 65 | br(`Possible values: ${[...COMPRESSION_ALGORITHMS].join(', ')}`), 66 | 'gzip' 67 | ) 68 | .option( 69 | '-O, --no-open', 70 | "Don't open report in default browser automatically." 71 | ) 72 | .option( 73 | '-e, --exclude <regexp>', 74 | 'Assets that should be excluded from the report.' + 75 | br('Can be specified multiple times.'), 76 | array() 77 | ) 78 | .option( 79 | '-l, --log-level <level>', 80 | 'Log level.' + 81 | br(`Possible values: ${[...Logger.levels].join(', ')}`), 82 | Logger.defaultLevel 83 | ) 84 | .parse(process.argv); 85 | 86 | let [bundleStatsFile, bundleDir] = program.args; 87 | let { 88 | mode, 89 | host, 90 | port, 91 | report: reportFilename, 92 | title: reportTitle, 93 | defaultSizes, 94 | compressionAlgorithm, 95 | logLevel, 96 | open: openBrowser, 97 | exclude: excludeAssets 98 | } = program.opts(); 99 | const logger = new Logger(logLevel); 100 | 101 | if (typeof reportTitle === 'undefined') { 102 | reportTitle = utils.defaultTitle; 103 | } 104 | 105 | if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument'); 106 | if (mode !== 'server' && mode !== 'static' && mode !== 'json') { 107 | showHelp('Invalid mode. Should be either `server`, `static` or `json`.'); 108 | } 109 | if (mode === 'server') { 110 | if (!host) showHelp('Invalid host name'); 111 | 112 | port = port === 'auto' ? 0 : Number(port); 113 | if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`'); 114 | } 115 | if (!COMPRESSION_ALGORITHMS.has(compressionAlgorithm)) { 116 | showHelp(`Invalid compression algorithm option. Possible values are: ${[...COMPRESSION_ALGORITHMS].join(', ')}`); 117 | } 118 | if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`); 119 | 120 | bundleStatsFile = resolve(bundleStatsFile); 121 | 122 | if (!bundleDir) bundleDir = dirname(bundleStatsFile); 123 | 124 | parseAndAnalyse(bundleStatsFile); 125 | 126 | async function parseAndAnalyse(bundleStatsFile) { 127 | try { 128 | const bundleStats = await analyzer.readStatsFromFile(bundleStatsFile); 129 | if (mode === 'server') { 130 | viewer.startServer(bundleStats, { 131 | openBrowser, 132 | port, 133 | host, 134 | defaultSizes, 135 | compressionAlgorithm, 136 | reportTitle, 137 | bundleDir, 138 | excludeAssets, 139 | logger: new Logger(logLevel), 140 | analyzerUrl: utils.defaultAnalyzerUrl 141 | }); 142 | } else if (mode === 'static') { 143 | viewer.generateReport(bundleStats, { 144 | openBrowser, 145 | reportFilename: resolve(reportFilename || 'report.html'), 146 | reportTitle, 147 | defaultSizes, 148 | compressionAlgorithm, 149 | bundleDir, 150 | excludeAssets, 151 | logger: new Logger(logLevel) 152 | }); 153 | } else if (mode === 'json') { 154 | viewer.generateJSONReport(bundleStats, { 155 | reportFilename: resolve(reportFilename || 'report.json'), 156 | compressionAlgorithm, 157 | bundleDir, 158 | excludeAssets, 159 | logger: new Logger(logLevel) 160 | }); 161 | } 162 | } catch (err) { 163 | logger.error(`Couldn't read webpack bundle stats from "${bundleStatsFile}":\n${err}`); 164 | logger.debug(err.stack); 165 | process.exit(1); 166 | } 167 | } 168 | 169 | function showHelp(error) { 170 | if (error) console.log(`\n ${magenta(error)}\n`); 171 | program.outputHelp(); 172 | process.exit(1); 173 | } 174 | 175 | function br(str) { 176 | return `\n${' '.repeat(32)}${str}`; 177 | } 178 | 179 | function array() { 180 | const arr = []; 181 | return (val) => { 182 | arr.push(val); 183 | return arr; 184 | }; 185 | } 186 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import {observable, computed} from 'mobx'; 2 | import {isChunkParsed, walkModules} from './utils'; 3 | import localStorage from './localStorage'; 4 | 5 | export class Store { 6 | cid = 0; 7 | sizes = new Set(['statSize', 'parsedSize', 'gzipSize', 'brotliSize']); 8 | 9 | @observable.ref allChunks; 10 | @observable.shallow selectedChunks; 11 | @observable searchQuery = ''; 12 | @observable defaultSize; 13 | @observable selectedSize; 14 | @observable showConcatenatedModulesContent = (localStorage.getItem('showConcatenatedModulesContent') === true); 15 | @observable darkMode = (() => { 16 | const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 17 | 18 | try { 19 | const saved = localStorage.getItem('darkMode'); 20 | if (saved !== null) return saved === 'true'; 21 | } catch (e) { 22 | // Some browsers might not have localStorage available and we can fail silently 23 | } 24 | 25 | return systemPrefersDark; 26 | })(); 27 | 28 | 29 | setModules(modules) { 30 | walkModules(modules, module => { 31 | module.cid = this.cid++; 32 | }); 33 | 34 | this.allChunks = modules; 35 | this.selectedChunks = this.allChunks; 36 | } 37 | 38 | setEntrypoints(entrypoints) { 39 | this.entrypoints = entrypoints; 40 | } 41 | 42 | @computed get hasParsedSizes() { 43 | return this.allChunks.some(isChunkParsed); 44 | } 45 | 46 | @computed get activeSize() { 47 | const activeSize = this.selectedSize || this.defaultSize; 48 | 49 | if (!this.hasParsedSizes || !this.sizes.has(activeSize)) { 50 | return 'statSize'; 51 | } 52 | 53 | return activeSize; 54 | } 55 | 56 | @computed get visibleChunks() { 57 | const visibleChunks = this.allChunks.filter(chunk => 58 | this.selectedChunks.includes(chunk) 59 | ); 60 | 61 | return this.filterModulesForSize(visibleChunks, this.activeSize); 62 | } 63 | 64 | @computed get allChunksSelected() { 65 | return this.visibleChunks.length === this.allChunks.length; 66 | } 67 | 68 | @computed get totalChunksSize() { 69 | return this.allChunks.reduce((totalSize, chunk) => 70 | totalSize + (chunk[this.activeSize] || 0), 71 | 0); 72 | } 73 | 74 | @computed get searchQueryRegexp() { 75 | const query = this.searchQuery.trim(); 76 | 77 | if (!query) { 78 | return null; 79 | } 80 | 81 | try { 82 | return new RegExp(query, 'iu'); 83 | } catch (err) { 84 | return null; 85 | } 86 | } 87 | 88 | @computed get isSearching() { 89 | return !!this.searchQueryRegexp; 90 | } 91 | 92 | @computed get foundModulesByChunk() { 93 | if (!this.isSearching) { 94 | return []; 95 | } 96 | 97 | const query = this.searchQueryRegexp; 98 | 99 | return this.visibleChunks 100 | .map(chunk => { 101 | let foundGroups = []; 102 | 103 | walkModules(chunk.groups, module => { 104 | let weight = 0; 105 | 106 | /** 107 | * Splitting found modules/directories into groups: 108 | * 109 | * 1) Module with matched label (weight = 4) 110 | * 2) Directory with matched label (weight = 3) 111 | * 3) Module with matched path (weight = 2) 112 | * 4) Directory with matched path (weight = 1) 113 | */ 114 | if (query.test(module.label)) { 115 | weight += 3; 116 | } else if (module.path && query.test(module.path)) { 117 | weight++; 118 | } 119 | 120 | if (!weight) return; 121 | 122 | if (!module.groups) { 123 | weight += 1; 124 | } 125 | 126 | const foundModules = foundGroups[weight - 1] = foundGroups[weight - 1] || []; 127 | foundModules.push(module); 128 | }); 129 | 130 | const {activeSize} = this; 131 | 132 | // Filtering out missing groups 133 | foundGroups = foundGroups.filter(Boolean).reverse(); 134 | // Sorting each group by active size 135 | foundGroups.forEach(modules => 136 | modules.sort((m1, m2) => m2[activeSize] - m1[activeSize]) 137 | ); 138 | 139 | return { 140 | chunk, 141 | modules: [].concat(...foundGroups) 142 | }; 143 | }) 144 | .filter(result => result.modules.length > 0) 145 | .sort((c1, c2) => c1.modules.length - c2.modules.length); 146 | } 147 | 148 | @computed get foundModules() { 149 | return this.foundModulesByChunk.reduce((arr, chunk) => arr.concat(chunk.modules), []); 150 | } 151 | 152 | @computed get hasFoundModules() { 153 | return this.foundModules.length > 0; 154 | } 155 | 156 | @computed get hasConcatenatedModules() { 157 | let result = false; 158 | 159 | walkModules(this.visibleChunks, module => { 160 | if (module.concatenated) { 161 | result = true; 162 | return false; 163 | } 164 | }); 165 | 166 | return result; 167 | } 168 | 169 | @computed get foundModulesSize() { 170 | return this.foundModules.reduce( 171 | (summ, module) => summ + module[this.activeSize], 172 | 0 173 | ); 174 | } 175 | 176 | filterModulesForSize(modules, sizeProp) { 177 | return modules.reduce((filteredModules, module) => { 178 | if (module[sizeProp]) { 179 | if (module.groups) { 180 | const showContent = (!module.concatenated || this.showConcatenatedModulesContent); 181 | 182 | module = { 183 | ...module, 184 | groups: showContent ? this.filterModulesForSize(module.groups, sizeProp) : null 185 | }; 186 | } 187 | 188 | module.weight = module[sizeProp]; 189 | filteredModules.push(module); 190 | } 191 | 192 | return filteredModules; 193 | }, []); 194 | } 195 | 196 | toggleDarkMode() { 197 | this.darkMode = !this.darkMode; 198 | try { 199 | localStorage.setItem('darkMode', this.darkMode); 200 | } catch (e) { 201 | // Some browsers might not have localStorage available and we can fail silently 202 | } 203 | this.updateTheme(); 204 | } 205 | 206 | updateTheme() { 207 | if (this.darkMode) { 208 | document.documentElement.setAttribute('data-theme', 'dark'); 209 | } else { 210 | document.documentElement.removeAttribute('data-theme'); 211 | } 212 | } 213 | } 214 | 215 | export const store = new Store(); 216 | -------------------------------------------------------------------------------- /test/stats/with-special-chars/stats.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "warnings": [], 4 | "version": "4.25.1", 5 | "hash": "6a0006856f4405101aa5", 6 | "time": 279, 7 | "builtAt": 1542409324382, 8 | "publicPath": "", 9 | "outputPath": "/tmp/with-special-chars", 10 | "assetsByChunkName": { 11 | "main": "bundle.js" 12 | }, 13 | "assets": [ 14 | { 15 | "name": "bundle.js", 16 | "size": 1972, 17 | "chunks": [ 18 | 0 19 | ], 20 | "chunkNames": [ 21 | "main" 22 | ], 23 | "emitted": true 24 | } 25 | ], 26 | "filteredAssets": 0, 27 | "entrypoints": { 28 | "main": { 29 | "chunks": [ 30 | 0 31 | ], 32 | "assets": [ 33 | "bundle.js" 34 | ], 35 | "children": {}, 36 | "childAssets": {} 37 | } 38 | }, 39 | "namedChunkGroups": { 40 | "main": { 41 | "chunks": [ 42 | 0 43 | ], 44 | "assets": [ 45 | "bundle.js" 46 | ], 47 | "children": {}, 48 | "childAssets": {} 49 | } 50 | }, 51 | "chunks": [ 52 | { 53 | "id": 0, 54 | "rendered": true, 55 | "initial": true, 56 | "entry": true, 57 | "size": 1021, 58 | "names": [ 59 | "main" 60 | ], 61 | "files": [ 62 | "bundle.js" 63 | ], 64 | "hash": "ad9a5baaeb4c63ce54e3", 65 | "siblings": [], 66 | "parents": [], 67 | "children": [], 68 | "childrenByOrder": {}, 69 | "modules": [ 70 | { 71 | "id": 0, 72 | "identifier": "/tmp/with-special-chars/index.js", 73 | "name": "./index.js", 74 | "index": 0, 75 | "index2": 0, 76 | "size": 1021, 77 | "cacheable": true, 78 | "built": true, 79 | "optional": false, 80 | "prefetched": false, 81 | "chunks": [ 82 | 0 83 | ], 84 | "issuer": null, 85 | "issuerId": null, 86 | "issuerName": null, 87 | "issuerPath": null, 88 | "failed": false, 89 | "errors": 0, 90 | "warnings": 0, 91 | "assets": [], 92 | "reasons": [ 93 | { 94 | "moduleId": null, 95 | "moduleIdentifier": null, 96 | "module": null, 97 | "moduleName": null, 98 | "type": "single entry", 99 | "userRequest": "/tmp/with-special-chars/index.js", 100 | "loc": "main" 101 | } 102 | ], 103 | "usedExports": true, 104 | "providedExports": [], 105 | "optimizationBailout": [ 106 | "ModuleConcatenation bailout: Module is an entry point" 107 | ], 108 | "depth": 0, 109 | "source": "console.log(`!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ˂˃˄˅ˆˇˈˉˊˋˌˍˎˏːˑ˒˓˔˕˖˗˘˙˚˛˜˝˞˟ˠˡˢˣˤ˥˦˧˨˩˪˫ˬ˭ˮ˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ͵Ͷͷͺͻͼͽ;Ϳ΄΅Ά·ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ϶ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈ`)\n" 110 | } 111 | ], 112 | "filteredModules": 0, 113 | "origins": [ 114 | { 115 | "module": "", 116 | "moduleIdentifier": "", 117 | "moduleName": "", 118 | "loc": "main", 119 | "request": "/tmp/with-special-chars/index.js", 120 | "reasons": [] 121 | } 122 | ] 123 | } 124 | ], 125 | "modules": [ 126 | { 127 | "id": 0, 128 | "identifier": "/tmp/with-special-chars/index.js", 129 | "name": "./index.js", 130 | "index": 0, 131 | "index2": 0, 132 | "size": 1021, 133 | "cacheable": true, 134 | "built": true, 135 | "optional": false, 136 | "prefetched": false, 137 | "chunks": [ 138 | 0 139 | ], 140 | "issuer": null, 141 | "issuerId": null, 142 | "issuerName": null, 143 | "issuerPath": null, 144 | "failed": false, 145 | "errors": 0, 146 | "warnings": 0, 147 | "assets": [], 148 | "reasons": [ 149 | { 150 | "moduleId": null, 151 | "moduleIdentifier": null, 152 | "module": null, 153 | "moduleName": null, 154 | "type": "single entry", 155 | "userRequest": "/tmp/with-special-chars/index.js", 156 | "loc": "main" 157 | } 158 | ], 159 | "usedExports": true, 160 | "providedExports": [], 161 | "optimizationBailout": [ 162 | "ModuleConcatenation bailout: Module is an entry point" 163 | ], 164 | "depth": 0, 165 | "source": "console.log(`!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ˂˃˄˅ˆˇˈˉˊˋˌˍˎˏːˑ˒˓˔˕˖˗˘˙˚˛˜˝˞˟ˠˡˢˣˤ˥˦˧˨˩˪˫ˬ˭ˮ˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ͵Ͷͷͺͻͼͽ;Ϳ΄΅Ά·ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ϶ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈ`)\n" 166 | } 167 | ], 168 | "filteredModules": 0, 169 | "children": [] 170 | } 171 | -------------------------------------------------------------------------------- /src/viewer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const http = require('http'); 4 | 5 | const WebSocket = require('ws'); 6 | const sirv = require('sirv'); 7 | const {bold} = require('picocolors'); 8 | 9 | const Logger = require('./Logger'); 10 | const analyzer = require('./analyzer'); 11 | const {open} = require('./utils'); 12 | const {renderViewer} = require('./template'); 13 | 14 | const projectRoot = path.resolve(__dirname, '..'); 15 | 16 | function resolveTitle(reportTitle) { 17 | if (typeof reportTitle === 'function') { 18 | return reportTitle(); 19 | } else { 20 | return reportTitle; 21 | } 22 | } 23 | 24 | function resolveDefaultSizes(defaultSizes, compressionAlgorithm) { 25 | if (['gzip', 'brotli'].includes(defaultSizes)) return compressionAlgorithm; 26 | return defaultSizes; 27 | } 28 | 29 | module.exports = { 30 | startServer, 31 | generateReport, 32 | generateJSONReport, 33 | getEntrypoints, 34 | // deprecated 35 | start: startServer 36 | }; 37 | 38 | async function startServer(bundleStats, opts) { 39 | const { 40 | port = 8888, 41 | host = '127.0.0.1', 42 | openBrowser = true, 43 | bundleDir = null, 44 | logger = new Logger(), 45 | defaultSizes = 'parsed', 46 | compressionAlgorithm, 47 | excludeAssets = null, 48 | reportTitle, 49 | analyzerUrl 50 | } = opts || {}; 51 | 52 | const analyzerOpts = {logger, excludeAssets, compressionAlgorithm}; 53 | 54 | let chartData = getChartData(analyzerOpts, bundleStats, bundleDir); 55 | const entrypoints = getEntrypoints(bundleStats); 56 | 57 | if (!chartData) return; 58 | 59 | const sirvMiddleware = sirv(`${projectRoot}/public`, { 60 | // disables caching and traverse the file system on every request 61 | dev: true 62 | }); 63 | 64 | const server = http.createServer((req, res) => { 65 | if (req.method === 'GET' && req.url === '/') { 66 | const html = renderViewer({ 67 | mode: 'server', 68 | title: resolveTitle(reportTitle), 69 | chartData, 70 | entrypoints, 71 | defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), 72 | compressionAlgorithm, 73 | enableWebSocket: true 74 | }); 75 | res.writeHead(200, {'Content-Type': 'text/html'}); 76 | res.end(html); 77 | } else { 78 | sirvMiddleware(req, res); 79 | } 80 | }); 81 | 82 | await new Promise(resolve => { 83 | server.listen(port, host, () => { 84 | resolve(); 85 | 86 | const url = analyzerUrl({ 87 | listenPort: port, 88 | listenHost: host, 89 | boundAddress: server.address() 90 | }); 91 | 92 | logger.info( 93 | `${bold('Webpack Bundle Analyzer')} is started at ${bold(url)}\n` + 94 | `Use ${bold('Ctrl+C')} to close it` 95 | ); 96 | 97 | if (openBrowser) { 98 | open(url, logger); 99 | } 100 | }); 101 | }); 102 | 103 | const wss = new WebSocket.Server({server}); 104 | 105 | wss.on('connection', ws => { 106 | ws.on('error', err => { 107 | // Ignore network errors like `ECONNRESET`, `EPIPE`, etc. 108 | if (err.errno) return; 109 | 110 | logger.info(err.message); 111 | }); 112 | }); 113 | 114 | return { 115 | ws: wss, 116 | http: server, 117 | updateChartData 118 | }; 119 | 120 | function updateChartData(bundleStats) { 121 | const newChartData = getChartData(analyzerOpts, bundleStats, bundleDir); 122 | 123 | if (!newChartData) return; 124 | 125 | chartData = newChartData; 126 | 127 | wss.clients.forEach(client => { 128 | if (client.readyState === WebSocket.OPEN) { 129 | client.send(JSON.stringify({ 130 | event: 'chartDataUpdated', 131 | data: newChartData 132 | })); 133 | } 134 | }); 135 | } 136 | } 137 | 138 | async function generateReport(bundleStats, opts) { 139 | const { 140 | openBrowser = true, 141 | reportFilename, 142 | reportTitle, 143 | bundleDir = null, 144 | logger = new Logger(), 145 | defaultSizes = 'parsed', 146 | compressionAlgorithm, 147 | excludeAssets = null 148 | } = opts || {}; 149 | 150 | const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir); 151 | const entrypoints = getEntrypoints(bundleStats); 152 | 153 | if (!chartData) return; 154 | 155 | const reportHtml = renderViewer({ 156 | mode: 'static', 157 | title: resolveTitle(reportTitle), 158 | chartData, 159 | entrypoints, 160 | defaultSizes: resolveDefaultSizes(defaultSizes, compressionAlgorithm), 161 | compressionAlgorithm, 162 | enableWebSocket: false 163 | }); 164 | const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename); 165 | 166 | fs.mkdirSync(path.dirname(reportFilepath), {recursive: true}); 167 | fs.writeFileSync(reportFilepath, reportHtml); 168 | 169 | logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`); 170 | 171 | if (openBrowser) { 172 | open(`file://${reportFilepath}`, logger); 173 | } 174 | } 175 | 176 | async function generateJSONReport(bundleStats, opts) { 177 | const { 178 | reportFilename, 179 | bundleDir = null, 180 | logger = new Logger(), 181 | excludeAssets = null, 182 | compressionAlgorithm 183 | } = opts || {}; 184 | 185 | const chartData = getChartData({logger, excludeAssets, compressionAlgorithm}, bundleStats, bundleDir); 186 | 187 | if (!chartData) return; 188 | 189 | await fs.promises.mkdir(path.dirname(reportFilename), {recursive: true}); 190 | await fs.promises.writeFile(reportFilename, JSON.stringify(chartData)); 191 | 192 | logger.info(`${bold('Webpack Bundle Analyzer')} saved JSON report to ${bold(reportFilename)}`); 193 | } 194 | 195 | function getChartData(analyzerOpts, ...args) { 196 | let chartData; 197 | const {logger} = analyzerOpts; 198 | 199 | try { 200 | chartData = analyzer.getViewerData(...args, analyzerOpts); 201 | } catch (err) { 202 | logger.error(`Couldn't analyze webpack bundle:\n${err}`); 203 | logger.debug(err.stack); 204 | chartData = null; 205 | } 206 | 207 | // chartData can either be an array (bundleInfo[]) or null. It can't be an plain object anyway 208 | if ( 209 | // analyzer.getViewerData() doesn't failed in the previous step 210 | chartData 211 | && !Array.isArray(chartData) 212 | ) { 213 | logger.error("Couldn't find any javascript bundles in provided stats file"); 214 | chartData = null; 215 | } 216 | 217 | return chartData; 218 | } 219 | 220 | function getEntrypoints(bundleStats) { 221 | if (bundleStats === null || bundleStats === undefined || !bundleStats.entrypoints) { 222 | return []; 223 | } 224 | return Object.values(bundleStats.entrypoints).map(entrypoint => entrypoint.name); 225 | } 226 | -------------------------------------------------------------------------------- /test/stats/with-missing-parsed-module/bundle.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // define __esModule on exports 48 | /******/ __webpack_require__.r = function(exports) { 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = "/static/bundles/"; 66 | /******/ 67 | /******/ 68 | /******/ // Load entry module and return exports 69 | /******/ return __webpack_require__(__webpack_require__.s = "./client/index.js"); 70 | /******/ }) 71 | /************************************************************************/ 72 | /******/ ({ 73 | 74 | /***/ "./client/App.vue": 75 | /*!************************!*\ 76 | !*** ./client/App.vue ***! 77 | \************************/ 78 | /*! exports provided: default */ 79 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 80 | 81 | "use strict"; 82 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=278f674b */ \"./client/App.vue?vue&type=template&id=278f674b\");\n/* harmony import */ var _App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&lang=js */ \"./client/App.vue?vue&type=script&lang=js\");\n/* empty/unused harmony star reexport *//* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"client/App.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./client/App.vue?"); 83 | 84 | /***/ }), 85 | 86 | /***/ "./client/App.vue?vue&type=script&lang=js": 87 | /*!************************************************!*\ 88 | !*** ./client/App.vue?vue&type=script&lang=js ***! 89 | \************************************************/ 90 | /*! exports provided: default */ 91 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 92 | 93 | "use strict"; 94 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../node_modules/babel-loader/lib!../node_modules/vue-loader/lib??vue-loader-options!./App.vue?vue&type=script&lang=js */ \"./node_modules/babel-loader/lib/index.js!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/App.vue?vue&type=script&lang=js\");\n/* empty/unused harmony star reexport */ /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]); \n\n//# sourceURL=webpack:///./client/App.vue?"); 95 | 96 | /***/ }), 97 | 98 | /***/ "./client/App.vue?vue&type=template&id=278f674b": 99 | /*!******************************************************!*\ 100 | !*** ./client/App.vue?vue&type=template&id=278f674b ***! 101 | \******************************************************/ 102 | /*! exports provided: render, staticRenderFns */ 103 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 104 | 105 | "use strict"; 106 | eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../node_modules/vue-loader/lib??vue-loader-options!./App.vue?vue&type=template&id=278f674b */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/App.vue?vue&type=template&id=278f674b\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_278f674b__WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./client/App.vue?"); 107 | 108 | /***/ }) 109 | /******/ }); 110 | -------------------------------------------------------------------------------- /client/components/Treemap.jsx: -------------------------------------------------------------------------------- 1 | import {Component} from 'preact'; 2 | import FoamTree from '@carrotsearch/foamtree'; 3 | 4 | export default class Treemap extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.treemap = null; 9 | this.zoomOutDisabled = false; 10 | this.findChunkNamePartIndex(); 11 | } 12 | 13 | componentDidMount() { 14 | this.treemap = this.createTreemap(); 15 | window.addEventListener('resize', this.resize); 16 | } 17 | 18 | componentWillReceiveProps(nextProps) { 19 | if (nextProps.data !== this.props.data) { 20 | this.findChunkNamePartIndex(); 21 | this.treemap.set({ 22 | dataObject: this.getTreemapDataObject(nextProps.data) 23 | }); 24 | } else if (nextProps.highlightGroups !== this.props.highlightGroups) { 25 | setTimeout(() => this.treemap.redraw()); 26 | } 27 | } 28 | 29 | shouldComponentUpdate() { 30 | return false; 31 | } 32 | 33 | componentWillUnmount() { 34 | window.removeEventListener('resize', this.resize); 35 | this.treemap.dispose(); 36 | } 37 | 38 | render() { 39 | return ( 40 | <div {...this.props} ref={this.saveNodeRef}/> 41 | ); 42 | } 43 | 44 | saveNodeRef = node => (this.node = node); 45 | 46 | getTreemapDataObject(data = this.props.data) { 47 | return {groups: data}; 48 | } 49 | 50 | createTreemap() { 51 | const component = this; 52 | const {props} = this; 53 | 54 | return new FoamTree({ 55 | element: this.node, 56 | layout: 'squarified', 57 | stacking: 'flattened', 58 | pixelRatio: window.devicePixelRatio || 1, 59 | maxGroups: Infinity, 60 | maxGroupLevelsDrawn: Infinity, 61 | maxGroupLabelLevelsDrawn: Infinity, 62 | maxGroupLevelsAttached: Infinity, 63 | wireframeLabelDrawing: 'always', 64 | groupMinDiameter: 0, 65 | groupLabelVerticalPadding: 0.2, 66 | rolloutDuration: 0, 67 | pullbackDuration: 0, 68 | fadeDuration: 0, 69 | groupExposureZoomMargin: 0.2, 70 | zoomMouseWheelDuration: 300, 71 | openCloseDuration: 200, 72 | dataObject: this.getTreemapDataObject(), 73 | titleBarDecorator(opts, props, vars) { 74 | vars.titleBarShown = false; 75 | }, 76 | groupColorDecorator(options, properties, variables) { 77 | const root = component.getGroupRoot(properties.group); 78 | const chunkName = component.getChunkNamePart(root.label); 79 | const hash = /[^0-9]/u.test(chunkName) 80 | ? hashCode(chunkName) 81 | : (parseInt(chunkName) / 1000) * 360; 82 | variables.groupColor = { 83 | model: 'hsla', 84 | h: Math.round(Math.abs(hash) % 360), 85 | s: 60, 86 | l: 50, 87 | a: 0.9 88 | }; 89 | 90 | const {highlightGroups} = component.props; 91 | const module = properties.group; 92 | 93 | if (highlightGroups && highlightGroups.has(module)) { 94 | variables.groupColor = { 95 | model: 'rgba', 96 | r: 255, 97 | g: 0, 98 | b: 0, 99 | a: 0.8 100 | }; 101 | } else if (highlightGroups && highlightGroups.size > 0) { 102 | // this means a search (e.g.) is active, but this module 103 | // does not match; gray it out 104 | // https://github.com/webpack/webpack-bundle-analyzer/issues/553 105 | variables.groupColor.s = 10; 106 | } 107 | }, 108 | /** 109 | * Handle Foamtree's "group clicked" event 110 | * @param {FoamtreeEvent} event - Foamtree event object 111 | * (see https://get.carrotsearch.com/foamtree/demo/api/index.html#event-details) 112 | * @returns {void} 113 | */ 114 | onGroupClick(event) { 115 | preventDefault(event); 116 | if ((event.ctrlKey || event.secondary) && props.onGroupSecondaryClick) { 117 | props.onGroupSecondaryClick.call(component, event); 118 | return; 119 | } 120 | component.zoomOutDisabled = false; 121 | this.zoom(event.group); 122 | }, 123 | onGroupDoubleClick: preventDefault, 124 | onGroupHover(event) { 125 | // Ignoring hovering on `FoamTree` branding group and the root group 126 | if (event.group && (event.group.attribution || event.group === this.get('dataObject'))) { 127 | event.preventDefault(); 128 | if (props.onMouseLeave) { 129 | props.onMouseLeave.call(component, event); 130 | } 131 | return; 132 | } 133 | 134 | if (props.onGroupHover) { 135 | props.onGroupHover.call(component, event); 136 | } 137 | }, 138 | onGroupMouseWheel(event) { 139 | const {scale} = this.get('viewport'); 140 | const isZoomOut = (event.delta < 0); 141 | 142 | if (isZoomOut) { 143 | if (component.zoomOutDisabled) return preventDefault(event); 144 | if (scale < 1) { 145 | component.zoomOutDisabled = true; 146 | preventDefault(event); 147 | } 148 | } else { 149 | component.zoomOutDisabled = false; 150 | } 151 | } 152 | }); 153 | } 154 | 155 | getGroupRoot(group) { 156 | let nextParent; 157 | while (!group.isAsset && (nextParent = this.treemap.get('hierarchy', group).parent)) { 158 | group = nextParent; 159 | } 160 | return group; 161 | } 162 | 163 | zoomToGroup(group) { 164 | this.zoomOutDisabled = false; 165 | 166 | while (group && !this.treemap.get('state', group).revealed) { 167 | group = this.treemap.get('hierarchy', group).parent; 168 | } 169 | 170 | if (group) { 171 | this.treemap.zoom(group); 172 | } 173 | } 174 | 175 | isGroupRendered(group) { 176 | const groupState = this.treemap.get('state', group); 177 | return !!groupState && groupState.revealed; 178 | } 179 | 180 | update() { 181 | this.treemap.update(); 182 | } 183 | 184 | resize = () => { 185 | const {props} = this; 186 | this.treemap.resize(); 187 | 188 | if (props.onResize) { 189 | props.onResize(); 190 | } 191 | }; 192 | 193 | /** 194 | * Finds patterns across all chunk names to identify the unique "name" part. 195 | */ 196 | findChunkNamePartIndex() { 197 | const splitChunkNames = this.props.data.map((chunk) => chunk.label.split(/[^a-z0-9]/iu)); 198 | const longestSplitName = Math.max(...splitChunkNames.map((parts) => parts.length)); 199 | const namePart = { 200 | index: 0, 201 | votes: 0 202 | }; 203 | for (let i = longestSplitName - 1; i >= 0; i--) { 204 | const identifierVotes = { 205 | name: 0, 206 | hash: 0, 207 | ext: 0 208 | }; 209 | let lastChunkPart = ''; 210 | for (const splitChunkName of splitChunkNames) { 211 | const part = splitChunkName[i]; 212 | if (part === undefined || part === '') { 213 | continue; 214 | } 215 | if (part === lastChunkPart) { 216 | identifierVotes.ext++; 217 | } else if (/[a-z]/u.test(part) && /[0-9]/u.test(part) && part.length === lastChunkPart.length) { 218 | identifierVotes.hash++; 219 | } else if (/^[a-z]+$/iu.test(part) || /^[0-9]+$/u.test(part)) { 220 | identifierVotes.name++; 221 | } 222 | lastChunkPart = part; 223 | } 224 | if (identifierVotes.name >= namePart.votes) { 225 | namePart.index = i; 226 | namePart.votes = identifierVotes.name; 227 | } 228 | } 229 | this.chunkNamePartIndex = namePart.index; 230 | } 231 | 232 | getChunkNamePart(chunkLabel) { 233 | return chunkLabel.split(/[^a-z0-9]/iu)[this.chunkNamePartIndex] || chunkLabel; 234 | } 235 | } 236 | 237 | function preventDefault(event) { 238 | event.preventDefault(); 239 | } 240 | 241 | function hashCode(str) { 242 | let hash = 0; 243 | for (let i = 0; i < str.length; i++) { 244 | const code = str.charCodeAt(i); 245 | hash = (hash << 5) - hash + code; 246 | hash = hash & hash; 247 | } 248 | return hash; 249 | } 250 | --------------------------------------------------------------------------------