├── .git-blame-ignore-revs ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── deploy.sh ├── deploy └── bundle-buddy │ └── app.yaml ├── package.json ├── public ├── css │ └── react-table.css ├── icon.png ├── img │ ├── attach_icon.svg │ ├── copy_icon.svg │ ├── details.png │ ├── file.png │ ├── folder.png │ ├── ok_icon.svg │ ├── parcel_logo.png │ ├── ripple.png │ ├── rollup_logo.png │ ├── rome_logo.png │ ├── warn_icon.svg │ └── webpack_logo.png ├── index.html ├── manifest.json └── mappings.wasm ├── samples ├── esbuild │ └── stats.json ├── parcel │ ├── bundle-buddy.json │ └── index.js.map ├── rollup │ ├── graph.json │ ├── rollup.browser.js.map │ └── rollup.json ├── rome │ ├── bundlebuddy.json │ ├── index.js.map │ └── rome.json └── webpack │ ├── main.0a07234d.js.map │ ├── stats.json │ └── webpack.json ├── service_worker_postfix_shim.js ├── src ├── App.tsx ├── ErrorBoundry.tsx ├── Header.tsx ├── Home.test.js ├── TestProcess.tsx ├── bundle │ ├── Analyze.tsx │ ├── BarChart.tsx │ ├── Bundle.tsx │ ├── FileDetails.tsx │ ├── Header.tsx │ ├── Report.tsx │ ├── RippleChart.tsx │ ├── Treemap.js │ ├── TreemapComponent.js │ ├── bundle.css │ ├── prototype-semiotic │ │ └── hierarchy.json │ └── stringFormats.js ├── declarations.d.ts ├── graph │ ├── index.test.ts │ └── index.ts ├── home │ ├── Home.tsx │ └── home.css ├── import │ ├── ImportSelector.tsx │ ├── Importer.tsx │ ├── builtins.ts │ ├── clipboard.ts │ ├── esbuild │ │ ├── index.test.ts │ │ └── index.ts │ ├── file_reader.ts │ ├── graph_process.test.ts │ ├── graph_process.ts │ ├── prefix_cleaner.ts │ ├── process_imports.ts │ ├── process_sourcemaps.ts │ ├── stats_to_graph.test.ts │ └── stats_to_graph.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── report_error.ts ├── resolve │ ├── Resolve.tsx │ ├── duplicateModules.test.ts │ ├── duplicateModules.ts │ ├── process.test.ts │ ├── process.ts │ ├── trim.test.ts │ └── trim.ts ├── routes │ └── index.ts ├── serviceWorker.ts ├── storage │ ├── compile.sh │ ├── import.proto │ ├── import_pb.d.ts │ └── import_pb.js ├── stories │ ├── data │ │ ├── duplicateNodeModules.json │ │ ├── filedetails.json │ │ └── filetype.json │ └── index.js ├── test-process │ ├── edges.json │ ├── names.json │ └── sizes.json ├── theme.js └── types.ts ├── tsconfig.json └── yarn.lock /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # reformat 2 | fee40fb985bbfbcab36d38595554de0fbd7d390d 3 | 4 | # reformat 5 | bff2cacc6ccc30a14ade80b985f2f3bd0f44996c 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | .DS_STORE 3 | node_modules 4 | /*.js 5 | output_large.json 6 | src/bundle/prototype/* 7 | src/bundle/prototype-*/* 8 | build/* 9 | deploy/* 10 | samples/* 11 | yarn-error* 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | viz/src 2 | viz/build/output_large.json 3 | viz/public/output_large.json 4 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | cache: yarn 5 | script: 6 | - yarn test 7 | - CI=false yarn build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 Sam Saccone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | MIT License 24 | 25 | Copyright (c) 2019 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/samccone/bundle-buddy.svg?branch=bundle-explorer)](https://travis-ci.org/samccone/bundle-buddy) 2 | 3 | The design document for the project is here: 4 | https://docs.google.com/document/d/1ycGVBJmwIVs34yhC0oTqv_WH5f0fs2SAFmzyTiBK99k/edit 5 | 6 | The TODO list for the project is here: 7 | https://docs.google.com/document/d/1lStU7UmfwqgSmyhAgM7jtpQNcoI2czmPeLuC0-gNn7g/edit 8 | 9 | The legacy version of bundle-buddy (still installable via npm) can be viewed here: 10 | https://github.com/samccone/bundle-buddy/tree/5b79c7645677e78d29f0201aad582a346b88122a 11 | 12 | ### Deploy 13 | 14 | For the `gcloud` command you will need to install: https://cloud.google.com/sdk/docs/#install_the_latest_cloud_tools_version_cloudsdk_current_version 15 | 16 | Then you can deploy the project using: 17 | 18 | ./deploy.sh 19 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | yarn build 4 | cp -r build/* deploy/bundle-buddy/www 5 | cd deploy/bundle-buddy 6 | gcloud app deploy --project=bundle-buddy 7 | -------------------------------------------------------------------------------- /deploy/bundle-buddy/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python27 2 | api_version: 1 3 | threadsafe: true 4 | 5 | handlers: 6 | - url: / 7 | static_files: www/index.html 8 | upload: www/index.html 9 | 10 | - url: /(.*) 11 | static_files: www/\1 12 | upload: www/(.*) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundle-buddy", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@rehooks/window-size": "^1.0.2", 7 | "@sentry/browser": "^4.4.2", 8 | "@storybook/preset-create-react-app": "^2.1.1", 9 | "@types/d3-scale": "^2.2.0", 10 | "@types/history": "^4.7.5", 11 | "@types/jest": "^23.3.10", 12 | "@types/node": "^10.12.18", 13 | "@types/pako": "^1.0.1", 14 | "@types/react": "^16.7.18", 15 | "@types/react-dom": "^16.0.11", 16 | "@types/react-table": "^7.0.18", 17 | "d3-scale": "2.1.2", 18 | "d3-voronoi-treemap": "^1.1.0", 19 | "dagre": "0.8.2", 20 | "pako": "^1.0.11", 21 | "prettier": "^2.0.5", 22 | "react": "^16.6.1", 23 | "react-annotation": "2.1.6", 24 | "react-dom": "^16.6.1", 25 | "react-router-dom": "^4.3.1", 26 | "react-scripts": "^3.4.1", 27 | "react-table": "^7.1.0", 28 | "source-map": "^0.7.3", 29 | "storybook": "^5.3.17", 30 | "typescript": "^3.8.3", 31 | "webtreemap": "2.0.1" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/samccone/bundle-buddy.git" 36 | }, 37 | "scripts": { 38 | "start": "react-scripts start", 39 | "build": "GENERATE_SOURCEMAP=1 react-scripts build -- --stats && cat service_worker_postfix_shim.js >> build/service-worker.js", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject", 42 | "storybook": "start-storybook -p 9009 -s public", 43 | "build-storybook": "build-storybook -s public" 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "pretty-quick --staged" 48 | } 49 | }, 50 | "eslintConfig": { 51 | "extends": "react-app" 52 | }, 53 | "browserslist": [ 54 | ">0.2%", 55 | "not dead", 56 | "not ie <= 11", 57 | "not op_mini all" 58 | ], 59 | "devDependencies": { 60 | "@babel/core": "^7.9.0", 61 | "@storybook/addon-actions": "^5.3.17", 62 | "@storybook/addon-links": "^5.3.17", 63 | "@storybook/addons": "^5.3.17", 64 | "@storybook/react": "^5.3.17", 65 | "@types/google-protobuf": "^3.2.7", 66 | "@types/react-router-dom": "^4.3.1", 67 | "d3-hierarchy": "1.1.9", 68 | "google-protobuf": "^3.6.1", 69 | "husky": "^4.2.3", 70 | "pretty-quick": "^2.0.1", 71 | "ts-protoc-gen": "^0.9.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/css/react-table.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --grey100: #f1f2f4; 3 | --grey200: #dfe1e5; 4 | --grey300: #ccd1d7; 5 | --grey400: #a8b1bc; 6 | --grey500: #8892a0; 7 | --grey600: #6a7585; 8 | --grey700: #4e5a6a; 9 | --grey800: #36404e; 10 | --grey900: #212833; 11 | } 12 | 13 | .ReactTable { 14 | position: relative; 15 | display: -webkit-box; 16 | display: -ms-flexbox; 17 | display: flex; 18 | -webkit-box-orient: vertical; 19 | -webkit-box-direction: normal; 20 | -ms-flex-direction: column; 21 | flex-direction: column; 22 | border: 1px solid rgba(0, 0, 0, 0.1); 23 | } 24 | .ReactTable * { 25 | box-sizing: border-box; 26 | } 27 | .ReactTable .rt-table { 28 | -webkit-box-flex: 1; 29 | -ms-flex: auto 1; 30 | flex: auto 1; 31 | display: -webkit-box; 32 | display: -ms-flexbox; 33 | display: flex; 34 | -webkit-box-orient: vertical; 35 | -webkit-box-direction: normal; 36 | -ms-flex-direction: column; 37 | flex-direction: column; 38 | -webkit-box-align: stretch; 39 | -ms-flex-align: stretch; 40 | align-items: stretch; 41 | width: 100%; 42 | border-collapse: collapse; 43 | overflow: auto; 44 | } 45 | .ReactTable .rt-thead { 46 | -webkit-box-flex: 1; 47 | -ms-flex: 1 0 auto; 48 | flex: 1 0 auto; 49 | display: -webkit-box; 50 | display: -ms-flexbox; 51 | display: flex; 52 | -webkit-box-orient: vertical; 53 | -webkit-box-direction: normal; 54 | -ms-flex-direction: column; 55 | flex-direction: column; 56 | -webkit-user-select: none; 57 | -moz-user-select: none; 58 | -ms-user-select: none; 59 | user-select: none; 60 | background: var(--grey100); 61 | } 62 | .ReactTable .rt-thead.-headerGroups { 63 | background: rgba(0, 0, 0, 0.03); 64 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 65 | } 66 | .ReactTable .rt-thead.-filters { 67 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 68 | } 69 | .ReactTable .rt-thead.-filters input, 70 | .ReactTable .rt-thead.-filters select { 71 | border: 1px solid rgba(0, 0, 0, 0.1); 72 | background: #fff; 73 | padding: 5px 7px; 74 | font-size: inherit; 75 | border-radius: 3px; 76 | font-weight: normal; 77 | outline: none; 78 | } 79 | .ReactTable .rt-thead.-filters .rt-th { 80 | border-right: 1px solid rgba(0, 0, 0, 0.02); 81 | } 82 | .ReactTable .rt-thead.-header { 83 | box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.15); 84 | } 85 | .ReactTable .rt-thead .rt-tr { 86 | text-align: center; 87 | } 88 | .ReactTable .rt-thead .rt-th, 89 | .ReactTable .rt-thead .rt-td { 90 | line-height: normal; 91 | position: relative; 92 | border-right: 1px solid rgba(0, 0, 0, 0.05); 93 | transition: box-shadow 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); 94 | box-shadow: inset 0 0 0 0 transparent; 95 | padding: 12px; 96 | font-weight: 500; 97 | 98 | text-transform: uppercase; 99 | font-size: 12px; 100 | letter-spacing: 0.5px; 101 | color: var(--grey700); 102 | } 103 | .ReactTable .rt-thead .rt-th.-sort-asc, 104 | .ReactTable .rt-thead .rt-td.-sort-asc { 105 | box-shadow: inset 0 3px 0 0 rgba(0, 0, 0, 0.6); 106 | } 107 | .ReactTable .rt-thead .rt-th.-sort-desc, 108 | .ReactTable .rt-thead .rt-td.-sort-desc { 109 | box-shadow: inset 0 -3px 0 0 rgba(0, 0, 0, 0.6); 110 | } 111 | .ReactTable .rt-thead .rt-th.-cursor-pointer, 112 | .ReactTable .rt-thead .rt-td.-cursor-pointer { 113 | cursor: pointer; 114 | } 115 | .ReactTable .rt-thead .rt-th:last-child, 116 | .ReactTable .rt-thead .rt-td:last-child { 117 | border-right: 0; 118 | } 119 | .ReactTable .rt-thead .rt-resizable-header { 120 | overflow: visible; 121 | } 122 | .ReactTable .rt-thead .rt-resizable-header:last-child { 123 | overflow: hidden; 124 | } 125 | .ReactTable .rt-thead .rt-resizable-header-content { 126 | overflow: hidden; 127 | text-overflow: ellipsis; 128 | } 129 | .ReactTable .rt-thead .rt-header-pivot { 130 | border-right-color: #f7f7f7; 131 | } 132 | .ReactTable .rt-thead .rt-header-pivot:after, 133 | .ReactTable .rt-thead .rt-header-pivot:before { 134 | left: 100%; 135 | top: 50%; 136 | border: solid transparent; 137 | content: " "; 138 | height: 0; 139 | width: 0; 140 | position: absolute; 141 | pointer-events: none; 142 | } 143 | .ReactTable .rt-thead .rt-header-pivot:after { 144 | border-color: rgba(255, 255, 255, 0); 145 | border-left-color: #fff; 146 | border-width: 8px; 147 | margin-top: -8px; 148 | } 149 | .ReactTable .rt-thead .rt-header-pivot:before { 150 | border-color: rgba(102, 102, 102, 0); 151 | border-left-color: #f7f7f7; 152 | border-width: 10px; 153 | margin-top: -10px; 154 | } 155 | .ReactTable .rt-tbody { 156 | -webkit-box-flex: 99999; 157 | -ms-flex: 99999 1 auto; 158 | flex: 99999 1 auto; 159 | display: -webkit-box; 160 | display: -ms-flexbox; 161 | display: flex; 162 | -webkit-box-orient: vertical; 163 | -webkit-box-direction: normal; 164 | -ms-flex-direction: column; 165 | flex-direction: column; 166 | overflow: auto; 167 | } 168 | .ReactTable .rt-tbody .rt-tr-group { 169 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.1); 170 | } 171 | .ReactTable .rt-tbody .rt-tr-group:last-child { 172 | border-bottom: 0; 173 | } 174 | .ReactTable .rt-tbody .rt-td { 175 | border-right: 1px solid rgba(0, 0, 0, 0.02); 176 | } 177 | .ReactTable .rt-tbody .rt-td:last-child { 178 | border-right: 0; 179 | } 180 | .ReactTable .rt-tbody .rt-expandable { 181 | cursor: pointer; 182 | text-overflow: clip; 183 | } 184 | .ReactTable .rt-tr-group { 185 | -webkit-box-flex: 1; 186 | -ms-flex: 1 0 auto; 187 | flex: 1 0 auto; 188 | display: -webkit-box; 189 | display: -ms-flexbox; 190 | display: flex; 191 | -webkit-box-orient: vertical; 192 | -webkit-box-direction: normal; 193 | -ms-flex-direction: column; 194 | flex-direction: column; 195 | -webkit-box-align: stretch; 196 | -ms-flex-align: stretch; 197 | align-items: stretch; 198 | } 199 | .ReactTable .rt-tr { 200 | -webkit-box-flex: 1; 201 | -ms-flex: 1 0 auto; 202 | flex: 1 0 auto; 203 | display: -webkit-inline-box; 204 | display: -ms-inline-flexbox; 205 | display: inline-flex; 206 | } 207 | .ReactTable .rt-th, 208 | .ReactTable .rt-td { 209 | -webkit-box-flex: 1; 210 | -ms-flex: 1 0 0px; 211 | flex: 1 0 0; 212 | white-space: nowrap; 213 | text-overflow: ellipsis; 214 | padding: 7px 5px; 215 | overflow: hidden; 216 | transition: 0.3s ease; 217 | transition-property: width, min-width, padding, opacity; 218 | } 219 | .ReactTable .rt-th.-hidden, 220 | .ReactTable .rt-td.-hidden { 221 | width: 0 !important; 222 | min-width: 0 !important; 223 | padding: 0 !important; 224 | border: 0 !important; 225 | opacity: 0 !important; 226 | } 227 | .ReactTable .rt-expander { 228 | display: inline-block; 229 | position: relative; 230 | margin: 0; 231 | color: transparent; 232 | margin: 0 10px; 233 | } 234 | .ReactTable .rt-expander:after { 235 | content: ""; 236 | position: absolute; 237 | width: 0; 238 | height: 0; 239 | top: 50%; 240 | left: 50%; 241 | -webkit-transform: translate(-50%, -50%) rotate(-90deg); 242 | transform: translate(-50%, -50%) rotate(-90deg); 243 | border-left: 5.04px solid transparent; 244 | border-right: 5.04px solid transparent; 245 | border-top: 7px solid rgba(0, 0, 0, 0.8); 246 | transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); 247 | cursor: pointer; 248 | } 249 | .ReactTable .rt-expander.-open:after { 250 | -webkit-transform: translate(-50%, -50%) rotate(0); 251 | transform: translate(-50%, -50%) rotate(0); 252 | } 253 | .ReactTable .rt-resizer { 254 | display: inline-block; 255 | position: absolute; 256 | width: 36px; 257 | top: 0; 258 | bottom: 0; 259 | right: -18px; 260 | cursor: col-resize; 261 | z-index: 10; 262 | } 263 | .ReactTable .rt-tfoot { 264 | -webkit-box-flex: 1; 265 | -ms-flex: 1 0 auto; 266 | flex: 1 0 auto; 267 | display: -webkit-box; 268 | display: -ms-flexbox; 269 | display: flex; 270 | -webkit-box-orient: vertical; 271 | -webkit-box-direction: normal; 272 | -ms-flex-direction: column; 273 | flex-direction: column; 274 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.15); 275 | } 276 | .ReactTable .rt-tfoot .rt-td { 277 | border-right: 1px solid rgba(0, 0, 0, 0.05); 278 | } 279 | .ReactTable .rt-tfoot .rt-td:last-child { 280 | border-right: 0; 281 | } 282 | .ReactTable.-striped .rt-tr.-odd { 283 | background: rgba(0, 0, 0, 0.03); 284 | } 285 | .ReactTable.-highlight .rt-tbody .rt-tr:not(.-padRow):hover { 286 | background: rgba(0, 0, 0, 0.05); 287 | } 288 | .ReactTable .-pagination { 289 | z-index: 1; 290 | display: -webkit-box; 291 | display: -ms-flexbox; 292 | display: flex; 293 | -webkit-box-pack: justify; 294 | -ms-flex-pack: justify; 295 | justify-content: space-between; 296 | -webkit-box-align: stretch; 297 | -ms-flex-align: stretch; 298 | align-items: stretch; 299 | -ms-flex-wrap: wrap; 300 | flex-wrap: wrap; 301 | padding: 3px; 302 | text-transform: uppercase; 303 | font-size: 12px; 304 | letter-spacing: 0.5px; 305 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1); 306 | border-top: 2px solid rgba(0, 0, 0, 0.1); 307 | } 308 | .ReactTable .-pagination input, 309 | .ReactTable .-pagination select { 310 | border: 1px solid rgba(0, 0, 0, 0.1); 311 | background: #fff; 312 | padding: 5px 7px; 313 | font-size: inherit; 314 | border-radius: 3px; 315 | font-weight: normal; 316 | outline: none; 317 | height: 32px; 318 | color: var(--grey700); 319 | } 320 | 321 | .ReactTable .-pagination span { 322 | color: var(--grey700); 323 | } 324 | .ReactTable .-pagination .-btn { 325 | -webkit-appearance: none; 326 | -moz-appearance: none; 327 | appearance: none; 328 | display: block; 329 | width: 100%; 330 | height: 100%; 331 | border: 0; 332 | border-radius: 3px; 333 | padding: 6px; 334 | text-transform: uppercase; 335 | color: var(--grey700); 336 | background: var(--grey200); 337 | transition: all 0.1s ease; 338 | cursor: pointer; 339 | outline: none; 340 | } 341 | .ReactTable .-pagination .-btn[disabled] { 342 | opacity: 0.5; 343 | cursor: default; 344 | } 345 | .ReactTable .-pagination .-btn:not([disabled]):hover { 346 | background: var(--grey400); 347 | color: #fff; 348 | } 349 | .ReactTable .-pagination .-previous, 350 | .ReactTable .-pagination .-next { 351 | -webkit-box-flex: 1; 352 | -ms-flex: 1; 353 | flex: 1; 354 | text-align: center; 355 | } 356 | .ReactTable .-pagination .-center { 357 | -webkit-box-flex: 1.5; 358 | -ms-flex: 1.5; 359 | flex: 1.5; 360 | text-align: center; 361 | margin-bottom: 0; 362 | display: -webkit-box; 363 | display: -ms-flexbox; 364 | display: flex; 365 | -webkit-box-orient: horizontal; 366 | -webkit-box-direction: normal; 367 | -ms-flex-direction: row; 368 | flex-direction: row; 369 | -ms-flex-wrap: wrap; 370 | flex-wrap: wrap; 371 | -webkit-box-align: center; 372 | -ms-flex-align: center; 373 | align-items: center; 374 | -ms-flex-pack: distribute; 375 | justify-content: space-around; 376 | } 377 | .ReactTable .-pagination .-pageInfo { 378 | display: inline-block; 379 | margin: 3px 10px; 380 | white-space: nowrap; 381 | } 382 | .ReactTable .-pagination .-pageJump { 383 | display: inline-block; 384 | } 385 | .ReactTable .-pagination .-pageJump input { 386 | width: 70px; 387 | text-align: center; 388 | } 389 | .ReactTable .-pagination .-pageSizeOptions { 390 | margin: 3px 10px; 391 | } 392 | .ReactTable .rt-noData { 393 | display: block; 394 | position: absolute; 395 | left: 50%; 396 | top: 50%; 397 | -webkit-transform: translate(-50%, -50%); 398 | transform: translate(-50%, -50%); 399 | background: rgba(255, 255, 255, 0.8); 400 | transition: all 0.3s ease; 401 | z-index: 1; 402 | pointer-events: none; 403 | padding: 20px; 404 | color: rgba(0, 0, 0, 0.5); 405 | } 406 | .ReactTable .-loading { 407 | display: block; 408 | position: absolute; 409 | left: 0; 410 | right: 0; 411 | top: 0; 412 | bottom: 0; 413 | background: rgba(255, 255, 255, 0.8); 414 | transition: all 0.3s ease; 415 | z-index: -1; 416 | opacity: 0; 417 | pointer-events: none; 418 | } 419 | .ReactTable .-loading > div { 420 | position: absolute; 421 | display: block; 422 | text-align: center; 423 | width: 100%; 424 | top: 50%; 425 | left: 0; 426 | font-size: 15px; 427 | color: rgba(0, 0, 0, 0.6); 428 | -webkit-transform: translateY(-52%); 429 | transform: translateY(-52%); 430 | transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); 431 | } 432 | .ReactTable .-loading.-active { 433 | opacity: 1; 434 | z-index: 2; 435 | pointer-events: all; 436 | } 437 | .ReactTable .-loading.-active > div { 438 | -webkit-transform: translateY(50%); 439 | transform: translateY(50%); 440 | } 441 | .ReactTable .rt-resizing .rt-th, 442 | .ReactTable .rt-resizing .rt-td { 443 | transition: none !important; 444 | cursor: col-resize; 445 | -webkit-user-select: none; 446 | -moz-user-select: none; 447 | -ms-user-select: none; 448 | user-select: none; 449 | } 450 | 451 | .ReactTable .rt-tr .rt-td { 452 | padding: 12px; 453 | font-size: 14px; 454 | } 455 | 456 | .ReactTable .right { 457 | float: right; 458 | text-align: right; 459 | font-variant: tabular-nums; 460 | } 461 | 462 | .ReactTable .rt-th { 463 | height: 100px; 464 | } 465 | 466 | .ReactTable .rt-thead .rotated .rt-resizable-header-content { 467 | transform: translateY(46px) translateX(4px) rotate(-30deg); 468 | overflow: visible; 469 | text-overflow: unset; 470 | } 471 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/icon.png -------------------------------------------------------------------------------- /public/img/attach_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/copy_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/details.png -------------------------------------------------------------------------------- /public/img/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/file.png -------------------------------------------------------------------------------- /public/img/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/folder.png -------------------------------------------------------------------------------- /public/img/ok_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/parcel_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/parcel_logo.png -------------------------------------------------------------------------------- /public/img/ripple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/ripple.png -------------------------------------------------------------------------------- /public/img/rollup_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/rollup_logo.png -------------------------------------------------------------------------------- /public/img/rome_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/rome_logo.png -------------------------------------------------------------------------------- /public/img/warn_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/webpack_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/img/webpack_logo.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 21 | 30 | Bundle Buddy 31 | 32 | 33 | 34 |
35 | 36 | 40 | 41 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Bundle Buddy", 3 | "name": "Bundle Buddy", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/mappings.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samccone/bundle-buddy/bc6e8760519976485faf65d172bf9e2fe35a6ede/public/mappings.wasm -------------------------------------------------------------------------------- /service_worker_postfix_shim.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("message", event => { 2 | if (!event.data) { 3 | return; 4 | } 5 | 6 | switch (event.data) { 7 | case "skipWaiting": 8 | console.info("Skipping waiting at user's request"); 9 | self.skipWaiting(); 10 | break; 11 | default: 12 | // NOOP 13 | break; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 2 | import React, { Suspense, lazy } from "react"; 3 | import Header from "./Header"; 4 | import TestProcess from "./TestProcess"; 5 | import ErrorBoundry from "./ErrorBoundry"; 6 | import { Location } from "history"; 7 | import { 8 | ImportResolveState, 9 | ProcessedImportState, 10 | ImportHistory, 11 | } from "./types"; 12 | import { stateFromProcessedKey, stateFromResolveKey } from "./routes"; 13 | 14 | const Bundle = lazy(() => import("./bundle/Bundle")); 15 | const Home = lazy(() => import("./home/Home")); 16 | 17 | export default function App() { 18 | return ( 19 | 20 | 21 |
22 |
23 | Loading...
}> 24 | 25 | ; 31 | }) => { 32 | const state = stateFromProcessedKey( 33 | (location.state as any).key 34 | ); 35 | if (state == null) { 36 | throw new Error("invalid state"); 37 | } 38 | 39 | let params = new URLSearchParams(location.search); 40 | return ( 41 |
42 |
43 | 50 |
51 | ); 52 | }} 53 | /> 54 | 55 | {/* TODO remove this test route */} 56 | ; 62 | }) => { 63 | return ; 64 | }} 65 | /> 66 | 67 | ; 71 | history: ImportHistory; 72 | }) => { 73 | const state = stateFromResolveKey( 74 | ((h.location.state as any) || { key: "" }).key 75 | ); 76 | 77 | return ( 78 | 85 | ); 86 | }} 87 | /> 88 |
89 | 90 |
91 | 92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/ErrorBoundry.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import * as Sentry from "@sentry/browser"; 3 | import { ReportErrorUri } from "./report_error"; 4 | 5 | class ErrorBoundry extends Component<{}, { error: Error | null }> { 6 | constructor(props: {}) { 7 | super(props); 8 | if (process.env.NODE_ENV === "production") { 9 | Sentry.init({ 10 | dsn: "https://9e475abe454047779775876c0d1af187@sentry.io/1365297" 11 | }); 12 | } 13 | this.state = { error: null }; 14 | } 15 | 16 | static getDerivedStateFromError(error: Error) { 17 | return { error }; 18 | } 19 | 20 | componentDidCatch(error: Error, errorInfo: any) { 21 | if (process.env.NODE_ENV === "production") { 22 | Sentry.withScope(scope => { 23 | Object.keys(errorInfo).forEach(key => { 24 | scope.setExtra(key, errorInfo[key]); 25 | }); 26 | Sentry.captureException(error); 27 | }); 28 | } else { 29 | console.error(error, errorInfo); 30 | } 31 | } 32 | 33 | render() { 34 | if (this.state.error) { 35 | const errorReport = new ReportErrorUri(); 36 | errorReport.addError("Uncaught application error", this.state.error); 37 | 38 | return ( 39 |
40 |

41 | 42 | 🤷 43 | {" "} 44 | error encountered, please  45 | 50 | file a bug! 51 | 52 |

53 |
54 |             {this.state.error.message}
55 |             
56 | --------------- 57 |
58 | {this.state.error.stack} 59 |
60 |
61 | ); 62 | } 63 | 64 | return this.props.children; 65 | } 66 | } 67 | 68 | export default ErrorBoundry; 69 | -------------------------------------------------------------------------------- /src/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | // noopener noreferrer 5 | export default function Header() { 6 | return ( 7 |
8 | 9 | Bundle Buddy logo 16 |

Bundle Buddy

17 | 18 | 25 | 52 | 53 |
54 | 55 | ⚠ 56 | {" "} 57 | ️Project is in an experimental phase{" "} 58 | 59 | ⚠ 60 | {" "} 61 | ️ 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/TestProcess.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { transform } from "./resolve/process"; 3 | 4 | import edges from "./test-process/edges.json"; 5 | import sizes from "./test-process/sizes.json"; 6 | import names from "./test-process/names.json"; 7 | 8 | export default function TestProcess() { 9 | transform(edges, sizes, names); 10 | 11 | return
Test Process Route
; 12 | } 13 | -------------------------------------------------------------------------------- /src/bundle/Analyze.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FileDetails from "./FileDetails"; 3 | import RippleChart from "./RippleChart"; 4 | import { ProcessedImportState } from "../types"; 5 | 6 | type Props = { 7 | total?: number; 8 | changeSelected: React.Dispatch; 9 | directoryColors: { [dir: string]: string }; 10 | svgDirectoryColors: { [dir: string]: string }; 11 | network: ProcessedImportState["trimmedNetwork"]; 12 | hierarchy: ProcessedImportState["hierarchy"]; 13 | selected: string | null; 14 | directories: string[]; 15 | }; 16 | 17 | export default function Analyze(props: Props) { 18 | const { 19 | network, 20 | total, 21 | hierarchy, 22 | directoryColors, 23 | directories, 24 | changeSelected, 25 | svgDirectoryColors, 26 | selected, 27 | } = props; 28 | 29 | const { nodes = [], edges = [] } = network; 30 | 31 | const max = 32 | network && 33 | network.nodes && 34 | network.nodes.sort((a, b) => { 35 | if (a.totalBytes == null || b.totalBytes == null) { 36 | return 0; 37 | } 38 | return b.totalBytes - a.totalBytes; 39 | })[0].totalBytes; 40 | 41 | let withNodeModules = 0; 42 | let withoutNodeModules = 0; 43 | 44 | nodes.forEach((n) => { 45 | if (n.id.indexOf("node_modules") !== -1) withNodeModules++; 46 | else withoutNodeModules++; 47 | }); 48 | 49 | return ( 50 |
51 |
52 |
53 |

Analyze

54 |
55 |
56 |

57 | details 58 | Details 59 |

60 |

61 | This project bundled{" "} 62 | {withNodeModules && ( 63 | 64 | {withNodeModules} node_modules 65 | 66 | )}{" "} 67 | {withNodeModules && "with "} 68 | {withoutNodeModules} files 69 |

70 |
71 |
72 |
73 | 74 | 83 | {selected && ( 84 |
85 | Object.assign({}, d))} 88 | edges={edges.map((d) => Object.assign({}, d))} 89 | max={max} 90 | selected={selected} 91 | directoryColors={svgDirectoryColors} 92 | /> 93 |
94 | )} 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/bundle/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { scaleLinear } from "d3-scale"; 3 | 4 | //handle pattern 5 | 6 | type Props = { 7 | margin: { top?: number; bottom?: number; left?: number }; 8 | barHeight?: number; 9 | rExtent?: [number, number]; 10 | oAccessor: (d: any) => string; 11 | oLabel: (d: any, o: string, r: number) => string | JSX.Element; 12 | rAccessor: (d: any) => number; 13 | bar?: (d: any, width: number | string | undefined) => JSX.Element; 14 | oPadding?: number; 15 | foregroundGraphics?: JSX.Element | JSX.Element[]; 16 | data?: any[]; 17 | onBarClick?: (id: string) => {} | React.Dispatch | undefined | void; 18 | }; 19 | 20 | export default function BarChart(props: Props) { 21 | const { 22 | data = [], 23 | rAccessor, 24 | oAccessor, 25 | margin = {} as Props["margin"], 26 | oLabel, 27 | bar, 28 | oPadding = 3, 29 | barHeight = 45, 30 | onBarClick, 31 | rExtent 32 | } = props; 33 | 34 | const max = data.reduce((p, c) => { 35 | const r = rAccessor(c); 36 | if (r > p) p = r; 37 | return p; 38 | }, 0); 39 | 40 | const percentScale = scaleLinear() 41 | .domain(rExtent || [0, max || 1]) 42 | .range([0, 100]); 43 | 44 | return ( 45 |
46 |
47 | {data.map(d => { 48 | const o = oAccessor(d); 49 | const r = rAccessor(d); 50 | return ( 51 |
onBarClick(d.id))} 54 | style={{ 55 | height: barHeight, 56 | padding: oPadding 57 | }} 58 | key={o} 59 | > 60 |
61 | {oLabel && oLabel(d, o, r)} 62 |
63 |
64 | {bar && bar(d, `${Math.round(percentScale(r))}%`)} 65 |
66 |
67 | ); 68 | })} 69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/bundle/Bundle.tsx: -------------------------------------------------------------------------------- 1 | import * as pako from "pako"; 2 | import React, { useState, useEffect, useMemo } from "react"; 3 | import Header from "./Header"; 4 | import Report from "./Report"; 5 | import Analyze from "./Analyze"; 6 | import "./bundle.css"; 7 | 8 | import { colors } from "../theme"; 9 | import { ProcessedImportState } from "../types"; 10 | 11 | function storeSelected(selected?: string | null) { 12 | if (selected) { 13 | window.history.pushState( 14 | { ...window.history.state, selected }, 15 | "", 16 | selected 17 | ? `${window.location.origin}${ 18 | window.location.pathname 19 | }?selected=${encodeURIComponent(selected)}` 20 | : `${window.location.origin}${window.location.pathname}` 21 | ); 22 | } 23 | } 24 | 25 | function download(props: Props) { 26 | const deflate = new pako.Deflate({ level: 3 }); 27 | deflate.push(new TextEncoder().encode(JSON.stringify(props)).buffer, true); 28 | 29 | const blob = new Blob([deflate.result as Uint8Array], { 30 | type: "application/json", 31 | }); 32 | const objectURL = URL.createObjectURL(blob); 33 | const a: HTMLAnchorElement = document.createElement("a"); 34 | a.setAttribute("download", "bundle-buddy-share.json"); 35 | a.href = objectURL; 36 | a.click(); 37 | } 38 | 39 | interface Props extends ProcessedImportState { 40 | selected: string | null; 41 | } 42 | 43 | function getDirectories(rollups: Props["rollups"]) { 44 | const directories = rollups.directories 45 | .sort((a, b) => b.totalBytes - a.totalBytes) 46 | .map((d) => d.name); 47 | 48 | const directoryColors: { [dir: string]: string } = {}; 49 | const svgDirectoryColors: { [dir: string]: string } = {}; 50 | let i = 0; 51 | directories.forEach((d) => { 52 | if (d.indexOf("node_modules") !== -1) { 53 | directoryColors[d] = `repeating-linear-gradient( 54 | 45deg, 55 | #dfe1e5, 56 | #dfe1e5 2px, 57 | #fff 2px, 58 | #fff 4px 59 | )`; 60 | svgDirectoryColors[d] = "url(#dags)"; 61 | } else { 62 | directoryColors[d] = colors[i] || "black"; 63 | svgDirectoryColors[d] = colors[i] || "black"; 64 | i++; 65 | } 66 | }); 67 | 68 | return { 69 | directories, 70 | directoryColors, 71 | svgDirectoryColors, 72 | }; 73 | } 74 | 75 | export default function Bundle(props: Props) { 76 | const { trimmedNetwork, rollups, hierarchy, duplicateNodeModules } = props; 77 | 78 | const [selected, changeSelected] = useState(props.selected); 79 | useEffect(() => storeSelected(selected), [selected]); 80 | 81 | const network = trimmedNetwork; 82 | 83 | const { directories, directoryColors, svgDirectoryColors } = useMemo( 84 | () => getDirectories(rollups), 85 | [rollups] 86 | ); 87 | 88 | rollups.directories.forEach((d) => { 89 | d.color = directoryColors[d.name]; 90 | }); 91 | 92 | const downloadButton = ( 93 | 96 | ); 97 | 98 | return ( 99 |
100 |
101 |
102 |
103 |
104 | 105 |
106 |
107 | 117 |
118 |
119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/bundle/FileDetails.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useTable, useSortBy } from "react-table"; 3 | 4 | import { getCSSPercent, getFileSize } from "./stringFormats"; 5 | import { ProcessedImportState, TrimmedDataNode } from "../types"; 6 | import Treemap from "./Treemap"; 7 | 8 | type Column = { 9 | value: number; 10 | row: { original: TrimmedDataNode }; 11 | }; 12 | 13 | type Maxes = { 14 | totalBytes: number; 15 | requires: number; 16 | requiredBy: number; 17 | transitiveRequiresSize: number; 18 | transitiveRequiredBy: number; 19 | transitiveRequires: number; 20 | }; 21 | 22 | function getColumns( 23 | directoryColors: Props["directoryColors"], 24 | data: TrimmedDataNode[], 25 | total?: number 26 | ) { 27 | const maxes = { 28 | totalBytes: 0, 29 | requires: 0, 30 | requiredBy: 0, 31 | transitiveRequiresSize: 0, 32 | transitiveRequiredBy: 0, 33 | transitiveRequires: 0, 34 | }; 35 | 36 | data.forEach((d) => { 37 | maxes.totalBytes = Math.max(maxes.totalBytes, d.totalBytes); 38 | maxes.transitiveRequiresSize = Math.max( 39 | maxes.transitiveRequiresSize, 40 | d.transitiveRequiresSize 41 | ); 42 | maxes.requires = Math.max(maxes.requires, d.requires.length); 43 | maxes.transitiveRequires = Math.max( 44 | maxes.transitiveRequires, 45 | d.transitiveRequires.length - d.requires.length 46 | ); 47 | maxes.requiredBy = Math.max(maxes.requiredBy, d.requiredBy.length); 48 | maxes.transitiveRequiredBy = Math.max( 49 | maxes.transitiveRequiredBy, 50 | d.transitiveRequiredBy.length - d.requiredBy.length 51 | ); 52 | }); 53 | 54 | function getBar( 55 | // d: Column, 56 | d: any, 57 | accessor: (d: TrimmedDataNode) => number, 58 | id: keyof Maxes 59 | ) { 60 | return ( 61 |
72 | ); 73 | } 74 | 75 | return [ 76 | { 77 | accessor: "text" as any, 78 | Header: "Name", 79 | className: "name", 80 | Cell: (d: Column) => { 81 | return {d.value}; 82 | }, 83 | }, 84 | { 85 | id: "totalBytes", 86 | accessor: (d: TrimmedDataNode) => d.totalBytes, 87 | Header: "File Size", 88 | minWidth: 150, 89 | label: (d: Column) => ( 90 |
91 |
92 | 95 | {getCSSPercent(d.value, total)} 96 | 97 |
98 |
99 | 102 | {getFileSize(d.value)} 103 | 104 |
105 |
106 | ), 107 | }, 108 | { 109 | id: "requires", 110 | accessor: (d: TrimmedDataNode) => d.requires.length, 111 | Header: "Direct Imports", 112 | minWidth: 25, 113 | }, 114 | { 115 | id: "transitiveRequires", 116 | accessor: (d: TrimmedDataNode) => 117 | d.transitiveRequires.length - d.requires.length, 118 | Header: "Indirect Imports", 119 | minWidth: 25, 120 | }, 121 | { 122 | id: "transitiveRequiresSize", 123 | accessor: (d: TrimmedDataNode) => d.transitiveRequiresSize, 124 | Header: "All Imported Size", 125 | minWidth: 50, 126 | format: (d: Column) => getFileSize(d.value), 127 | }, 128 | { 129 | id: "requiredBy", 130 | accessor: (d: TrimmedDataNode) => d.requiredBy.length, 131 | Header: "Directly Imported By", 132 | minWidth: 25, 133 | }, 134 | 135 | { 136 | id: "transitiveRequiredBy", 137 | accessor: (d: TrimmedDataNode) => 138 | d.transitiveRequiredBy.length - d.requiredBy.length, 139 | Header: "Indirectly Imported By", 140 | minWidth: 25, 141 | }, 142 | ].map((d, i) => { 143 | return { 144 | ...d, 145 | sortDescFirst: i === 0 ? false : true, 146 | Cell: 147 | d.Cell || 148 | ((c: Column) => { 149 | return ( 150 |
151 | {getBar(c, d.accessor, d.id as keyof Maxes)} 152 | 153 | {d.label ? ( 154 | d.label(c) 155 | ) : ( 156 | 164 | 165 | {d.format ? d.format(c) : !c.value ? "--" : c.value} 166 | 167 | 168 | )} 169 |
170 | ); 171 | }), 172 | }; 173 | }); 174 | } 175 | 176 | type Props = { 177 | total?: number; 178 | changeSelected: React.Dispatch; 179 | directoryColors: { [dir: string]: string }; 180 | network: ProcessedImportState["trimmedNetwork"]; 181 | hierarchy: ProcessedImportState["hierarchy"]; 182 | selected: string | null; 183 | directories: string[]; 184 | }; 185 | 186 | export default function FileDetails(props: Props) { 187 | const { 188 | network = {} as ProcessedImportState["trimmedNetwork"], 189 | changeSelected, 190 | directoryColors, 191 | total, 192 | hierarchy, 193 | selected, 194 | directories, 195 | } = props; 196 | const { nodes = [] } = network; 197 | 198 | const columns = useMemo(() => getColumns(directoryColors, nodes, total), [ 199 | directoryColors, 200 | nodes, 201 | total, 202 | ]); 203 | 204 | const data = useMemo(() => nodes, [nodes]); 205 | 206 | const { 207 | getTableProps, 208 | getTableBodyProps, 209 | headerGroups, 210 | rows, 211 | prepareRow, 212 | } = useTable( 213 | { 214 | columns: columns, 215 | data, 216 | defaultCanSort: true, 217 | disableSortRemove: true, 218 | initialState: { sortBy: [{ id: "totalBytes", desc: true }] } as any, 219 | } as any, 220 | useSortBy 221 | ); 222 | 223 | const selectedNode = nodes.find((d) => d.id === selected); 224 | 225 | return ( 226 | 227 | 228 | 229 | 238 | 239 | {headerGroups.map((headerGroup) => ( 240 | 241 | {headerGroup.headers.map((column) => ( 242 | 286 | ))} 287 | 288 | ))} 289 | 290 | 291 | {rows.map((row: any) => { 292 | prepareRow(row); 293 | const id = row.original.id; 294 | return ( 295 | { 299 | if (id === selected) changeSelected(null); 300 | else changeSelected(id); 301 | }} 302 | > 303 | {row.cells.map((cell: any) => { 304 | return ( 305 | 317 | ); 318 | })} 319 | 320 | ); 321 | })} 322 | 323 |
230 | 237 |
249 | {column.render("Header")} 250 | 251 | {(column as any).isSorted ? ( 252 | (column as any).isSortedDesc ? ( 253 | 259 | 260 | 264 | 265 | 266 | ) : ( 267 | 273 | 274 | 278 | 279 | 280 | ) 281 | ) : ( 282 | "" 283 | )} 284 | 285 |
315 | {cell.render("Cell")} 316 |
324 | ); 325 | } 326 | -------------------------------------------------------------------------------- /src/bundle/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { primary, mainFileColor, secondaryFileColor } from "../theme"; 4 | 5 | import { getFileSize, getFileSizeSplit, getPercent } from "./stringFormats"; 6 | import BarChart from "./BarChart"; 7 | import { ProcessedImportState, SizeData } from "../types"; 8 | 9 | export const typeColors = { 10 | js: mainFileColor, 11 | ts: mainFileColor, 12 | jsx: mainFileColor, 13 | tsx: mainFileColor, 14 | }; 15 | 16 | const frameProps = { 17 | margin: { top: 5, right: 50, left: 140 }, 18 | oAccessor: (d: SizeData) => d.name, 19 | rAccessor: (d: SizeData) => d.totalBytes, 20 | oPadding: 10, 21 | barHeight: 45, 22 | bar: (d: SizeData, width: number | string | undefined) => { 23 | return ( 24 |
34 | ); 35 | }, 36 | oLabel: (d: SizeData, o: string, r: number) => { 37 | return ( 38 |
39 | {o} 40 |
41 | 42 | {getPercent(d.pct)}{" "} 43 | {getFileSize(r)} 44 | 45 |
46 | ); 47 | }, 48 | }; 49 | 50 | const directoryProps = { 51 | ...frameProps, 52 | additionalDefs: [ 53 | 60 | 66 | , 67 | 74 | 80 | , 81 | ], 82 | }; 83 | 84 | type Props = { 85 | download: JSX.Element; 86 | rollups?: ProcessedImportState["rollups"]; 87 | }; 88 | 89 | export default function ByTypeBarChart(props: Props) { 90 | const { rollups = {} as ProcessedImportState["rollups"], download } = props; 91 | const totalSize = rollups.value; 92 | const types = rollups.fileTypes || []; 93 | const folders = rollups.directories || []; 94 | 95 | const fileTypes = types.sort((a, b) => b.totalBytes - a.totalBytes); 96 | const directories = folders.sort((a, b) => b.totalBytes - a.totalBytes); 97 | 98 | let fileTypeMessage; 99 | 100 | if (fileTypes.length === 1) { 101 | fileTypeMessage = ( 102 | 103 | Your project only has one file type: {fileTypes[0].name} 104 | 105 | ); 106 | } else { 107 | if (fileTypes[0] && fileTypes[0].pct >= 0.6) { 108 | fileTypeMessage = ( 109 | 110 | Your project has {fileTypes.length} file types, but it is 111 | mostly {fileTypes[0].name} files. 112 | 113 | ); 114 | } else { 115 | fileTypeMessage = ( 116 | 117 | Your bundle has {fileTypes.length} file types 118 | 119 | ); 120 | } 121 | } 122 | 123 | const size = getFileSizeSplit(totalSize); 124 | return ( 125 |
126 |
127 |

Overview

128 | 129 | {totalSize && ( 130 |
131 |

132 | {size.value}{" "} 133 | {size.type} 134 |

135 |
136 | )} 137 | {download} 138 |
139 |
140 |

141 | file types 142 | 143 | By Bundle 144 | 145 |

146 | 147 |
148 |
149 |
150 |
151 |

152 | file types 153 | 154 | By File Type 155 | 156 |

157 |

{fileTypeMessage}

158 |
159 | 160 |
161 |
162 |
163 |
164 |
165 |

166 | directories 171 | 172 | By Directory 173 | 174 |

175 |

176 | Your project is made up {directories.length} top-level 177 | directories 178 |

179 |
180 | 181 |
182 |
183 |
184 |
185 |
186 | ); 187 | } 188 | -------------------------------------------------------------------------------- /src/bundle/Report.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ProcessedImportState} from '../types'; 3 | 4 | type Props = { 5 | duplicateNodeModules: ProcessedImportState['duplicateNodeModules']; 6 | }; 7 | 8 | export default function Report(props: Props) { 9 | const {duplicateNodeModules} = props; 10 | return ( 11 |
12 |
13 |

Health Checks

14 |
15 |
16 |
17 |
18 |
19 |
20 | directories 21 | 22 |

23 | 24 | Duplicate Node Modules 25 | 26 |

27 |
28 |
29 |
30 | {duplicateNodeModules && duplicateNodeModules.length > 0 ? ( 31 |
32 |
33 | 34 | 35 | 36 | 39 | 42 | 43 | 44 | 45 | {duplicateNodeModules.map(k => { 46 | return ( 47 | 48 | 51 | 52 | 53 | ); 54 | })} 55 | 56 |
37 | Module 38 | 40 | Dependencies 41 |
49 | {k.key} 50 | {k.value.join(', ')}
57 |
58 | ) : ( 59 |

60 | No duplicated node modules found{' '} 61 | 62 | 🙌 63 | 64 |

65 | )} 66 |
67 |

68 | Run {`(npm or yarn) list `} with the duplicated module 69 | name to see the associations between duplicated modules. To prevent this 70 | automatically see{' '} 71 | 76 | inspectpack 77 | 78 | . 79 |

80 |
81 |
82 |
83 |
84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/bundle/RippleChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from "react"; 2 | import { scaleSqrt, scaleLinear, ScaleLinear } from "d3-scale"; 3 | import { primary } from "../theme"; 4 | 5 | import { TrimmedDataNode, Imported } from "../types"; 6 | 7 | type Props = { 8 | selected: string; 9 | directoryColors: { [key: string]: string }; 10 | max?: number; 11 | changeSelected: React.Dispatch; 12 | nodes: TrimmedDataNode[]; 13 | edges: Imported[]; 14 | }; 15 | 16 | type InOut = "in" | "out"; 17 | 18 | const OFFSET = 150; 19 | 20 | interface NodeWithPosition extends TrimmedDataNode { 21 | x: number; 22 | y: number; 23 | r: number; 24 | degrees: number | undefined; 25 | } 26 | 27 | function mapLocation( 28 | radiusScale: ScaleLinear, 29 | inOrOut: InOut, 30 | files: TrimmedDataNode[], 31 | rOffset = OFFSET 32 | ) { 33 | let translateX = 0, 34 | translateY = 50, 35 | maxX = 0, 36 | minR = OFFSET; 37 | 38 | let total = 0; 39 | const requires = files 40 | .sort((a, b) => b.totalBytes - a.totalBytes) 41 | .map((d, i) => { 42 | const spacing = Math.max(radiusScale(d.totalBytes) * 2, 0); 43 | const value = { 44 | ...d, 45 | r: radiusScale(d.totalBytes), 46 | spacing, 47 | offset: total + spacing / 2, 48 | }; 49 | 50 | total += value.spacing; 51 | 52 | return value; 53 | }); 54 | 55 | const rSize = Math.max(minR, (total * 2.4) / 2 / Math.PI); 56 | if (rSize > minR) { 57 | minR = rSize; 58 | } 59 | const circumfrence = 2 * Math.PI * rSize; 60 | 61 | const overallArcStart = (total / circumfrence / 2) * 360; 62 | 63 | const center = inOrOut === "in" ? 270 : 90; 64 | const sign = inOrOut === "in" ? 1 : -1; 65 | 66 | const angleScale = scaleLinear() 67 | .domain([0, total || 0]) 68 | .range([ 69 | center + overallArcStart * sign, 70 | center - overallArcStart * sign, 71 | ]), 72 | yScale = (degrees: number, r: number) => 73 | -Math.cos(degrees * (Math.PI / 180)) * r, 74 | xScale = (degrees: number, r: number) => 75 | Math.sin(degrees * (Math.PI / 180)) * r; 76 | 77 | const nodesWithPosition = requires.map((d) => { 78 | const degrees = angleScale(d.offset); 79 | const node: NodeWithPosition = { 80 | ...d, 81 | x: xScale(degrees, rSize) + sign * (rSize - rOffset), 82 | y: yScale(degrees, rSize), 83 | r: radiusScale(d.totalBytes), 84 | degrees: undefined, 85 | }; 86 | 87 | if (-(node.x + node.r) > translateX) translateX = -(node.x + node.r); 88 | if (node.y > translateY) translateY = node.y; 89 | if (node.x > maxX) maxX = node.x; 90 | node.degrees = degrees; 91 | 92 | return node; 93 | }); 94 | 95 | return { nodesWithPosition, x: translateX, y: translateY, maxX }; 96 | } 97 | 98 | function getPlaceCircles( 99 | changeSelected: Props["changeSelected"], 100 | updateHover: React.Dispatch, 101 | directoryColors: Props["directoryColors"] 102 | ) { 103 | const getFill = (d: TrimmedDataNode) => { 104 | return directoryColors[d.directory]; 105 | }; 106 | 107 | return { 108 | placeCircles: (inOrOut: InOut, files: NodeWithPosition[]) => { 109 | return files 110 | .sort((a, b) => a.r - b.r) 111 | .map((d) => { 112 | return ( 113 | changeSelected(d.id)} 116 | onMouseEnter={() => updateHover(d.id)} 117 | onMouseLeave={() => updateHover(undefined)} 118 | > 119 | 120 | 121 | {d.r > 8 && ( 122 | 127 | {d.fileName} 128 | 129 | )} 130 | 131 | 132 | ); 133 | }); 134 | }, 135 | getFill, 136 | }; 137 | } 138 | 139 | export default function RippleChart(props: Props) { 140 | const { 141 | edges, 142 | nodes, 143 | max, 144 | selected, 145 | directoryColors, 146 | changeSelected, 147 | } = props; 148 | 149 | const [hover, updateHover] = useState(); 150 | const selectedNode = nodes.find((d) => d.id === selected); 151 | const { placeCircles, getFill } = useMemo( 152 | () => getPlaceCircles(changeSelected, updateHover, directoryColors), 153 | [changeSelected, updateHover, directoryColors] 154 | ); 155 | 156 | if (!selectedNode || !max) return null; 157 | 158 | let requires = { 159 | nodesWithPosition: [] as NodeWithPosition[], 160 | x: 0, 161 | y: 0, 162 | maxX: 0, 163 | }, 164 | requiredBy = { 165 | nodesWithPosition: [] as NodeWithPosition[], 166 | x: 0, 167 | y: 0, 168 | maxX: 0, 169 | }, 170 | nextLevelNodes: NodeWithPosition[] = [], 171 | nextLevelEdges: Imported[] = []; 172 | 173 | const radiusScale = scaleSqrt().domain([0, max]).range([0, 100]); 174 | 175 | let usedNodes: { [key: string]: NodeWithPosition } = {}; 176 | let selectedXPos = 0; 177 | let selectedYPos = 0; 178 | let maxXPos = 0; 179 | requires = mapLocation( 180 | radiusScale, 181 | "in", 182 | nodes.filter((d) => selectedNode.requires.indexOf(d.id) !== -1) 183 | ); 184 | requiredBy = mapLocation( 185 | radiusScale, 186 | "out", 187 | nodes.filter((d) => selectedNode.requiredBy.indexOf(d.id) !== -1) 188 | ); 189 | 190 | nextLevelEdges = [ 191 | ...selectedNode.requires.map((d) => ({ imported: d, fileName: selected })), 192 | ...selectedNode.requiredBy.map((d) => ({ 193 | fileName: d, 194 | imported: selected, 195 | })), 196 | ]; 197 | 198 | usedNodes = [ 199 | ...requires.nodesWithPosition, 200 | ...requiredBy.nodesWithPosition, 201 | ].reduce((p: { [key: string]: NodeWithPosition }, c: NodeWithPosition) => { 202 | p[c.id] = c; 203 | return p; 204 | }, {}); 205 | 206 | selectedXPos = Math.max(requires.x, requiredBy.x); 207 | selectedYPos = Math.max(requires.y, requiredBy.y); 208 | maxXPos = Math.max(requires.maxX, requiredBy.maxX); 209 | 210 | const getNextLevel = (requiredByKeys: string[], level = 0) => { 211 | const edgeLevel = edges.filter( 212 | (d) => requiredByKeys.indexOf(d.imported) !== -1 213 | ); 214 | nextLevelEdges.push(...edgeLevel); 215 | 216 | const edgeLevelKeys = edgeLevel.map((d) => d.fileName); 217 | 218 | const matchingNodes = nodes.filter( 219 | (d) => edgeLevelKeys.indexOf(d.id) !== -1 && !usedNodes[d.id] 220 | ); 221 | 222 | if (matchingNodes.length > 0) { 223 | const newNodes = mapLocation( 224 | radiusScale, 225 | "out", 226 | matchingNodes, 227 | OFFSET * (level + 2) 228 | ); 229 | 230 | newNodes.nodesWithPosition.forEach((n) => { 231 | usedNodes[n.id] = n; 232 | nextLevelNodes.push(n); 233 | }); 234 | 235 | selectedXPos = Math.max(selectedXPos, newNodes.x); 236 | selectedYPos = Math.max(selectedYPos, newNodes.y); 237 | maxXPos = Math.max(maxXPos, newNodes.maxX); 238 | 239 | getNextLevel( 240 | newNodes.nodesWithPosition.map((d) => d.id), 241 | level + 1 242 | ); 243 | } 244 | }; 245 | 246 | getNextLevel(selectedNode.requiredBy); 247 | 248 | if (requires.nodesWithPosition.length === 0) { 249 | selectedXPos = 150; 250 | } else { 251 | selectedXPos += 150; 252 | } 253 | 254 | usedNodes[selected] = { 255 | ...selectedNode, 256 | x: 0, 257 | y: 0, 258 | r: radiusScale(selectedNode.totalBytes), 259 | degrees: undefined, 260 | }; 261 | 262 | const primaryRadius = radiusScale(selectedNode.totalBytes); 263 | 264 | let showEdges: Imported[] = []; 265 | let showNodes: { id: string; anchor: string }[] = []; 266 | if (hover) { 267 | showEdges = nextLevelEdges.filter( 268 | (d) => d.imported === hover || d.fileName === hover 269 | ); 270 | 271 | showNodes = Object.values( 272 | showEdges.reduce( 273 | ( 274 | p: { 275 | [key: string]: { 276 | id: string; 277 | anchor: string; 278 | }; 279 | }, 280 | c 281 | ) => { 282 | p[c.imported] = { 283 | id: c.imported, 284 | anchor: c.fileName === selected ? "end" : "start", 285 | }; 286 | p[c.fileName] = { id: c.fileName, anchor: "start" }; 287 | return p; 288 | }, 289 | {} 290 | ) 291 | ); 292 | } 293 | 294 | return ( 295 |
296 |
297 |
298 |

299 | details 300 | {selected} 301 |

302 |
303 |
304 |
305 | 311 | 312 | 319 | 325 | 326 | 327 | ] 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 345 | {placeCircles("in", requires.nodesWithPosition)} 346 | {placeCircles("out", requiredBy.nodesWithPosition)} 347 | {placeCircles("out", nextLevelNodes)} 348 | {requires.nodesWithPosition.length !== 0 && ( 349 | 350 | {" "} 351 | 358 | Import{selectedNode.requires.length > 1 && "s"} 359 | 360 | 361 | 362 | {selectedNode.requires.length} 363 | {" "} 364 | items 365 | 366 | 367 | )} 368 | 369 | {requiredBy.nodesWithPosition.length !== 0 && ( 370 | 371 | 372 | 373 | Imported 374 | 375 | 376 | 377 | {selectedNode.requiredBy.length} 378 | {" "} 379 | times 380 | 381 | 382 | )} 383 | 384 | 393 | 394 | {hover && 395 | showEdges.map((d, i) => { 396 | const imported = usedNodes[d.imported]; 397 | const fileName = usedNodes[d.fileName]; 398 | 399 | const color = "black"; 400 | 401 | return ( 402 | 403 | 410 | 417 | 424 | 425 | ); 426 | })} 427 | {hover && ( 428 | 429 | {showNodes.map((d, i) => { 430 | const n = usedNodes[d.id]; 431 | return ( 432 | 437 | 438 | 439 | {d.id !== selected ? ( 440 | 441 | 449 | {n.fileName} 450 | 451 | 452 | {n.fileName} 453 | 454 | 455 | ) : null} 456 | 457 | ); 458 | })} 459 | 460 | )} 461 | 462 | 463 |
464 |
465 | ); 466 | } 467 | -------------------------------------------------------------------------------- /src/bundle/Treemap.js: -------------------------------------------------------------------------------- 1 | //Code by @stil From https://github.com/stil/treemap-multilevel 2 | /* 3 | MIT License 4 | 5 | Copyright (c) 2019 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | import React, { Fragment, useState, useMemo } from "react"; 27 | import { stratify } from "d3-hierarchy"; 28 | import TreemapComponent from "./TreemapComponent"; 29 | import { getFileSize } from "./stringFormats"; 30 | import useWindowSize from "@rehooks/window-size"; 31 | 32 | function getHandleClick(changeSelected, setZoom) { 33 | return (zoomed, node) => { 34 | if (node.children) { 35 | if (node.depth === 0) { 36 | setZoom(undefined); 37 | } 38 | zoomed === node.data.name 39 | ? setZoom(node.parent.data.name) 40 | : setZoom(node.data.name); 41 | } else if (node) { 42 | changeSelected(node.data.name); 43 | } 44 | }; 45 | } 46 | const padding = [20, 2, 2, 2]; 47 | 48 | function getNodeComponent(name, bgColorsMap, zoomed, handleClick) { 49 | return (node, i, posStyle) => { 50 | const name = node.data.name; 51 | const index = name.lastIndexOf("/"); 52 | let fileName = name.slice(index !== -1 ? index + 1 : 0); 53 | 54 | if (fileName === "rootNode") fileName = name; 55 | const dirIndex = name.indexOf("/"); 56 | let directory = name; 57 | if (dirIndex !== -1) directory = name.slice(0, dirIndex); 58 | 59 | return ( 60 |
67 |
{ 72 | handleClick(zoomed, node); 73 | }} 74 | style={{ 75 | lineHeight: `${padding[0] - 2}px`, 76 | fontWeight: node.children ? "bold" : "300", 77 | color: "black", //textColorsMap.get(node.data.key), 78 | }} 79 | > 80 | {posStyle.height > 5 && ( 81 |
82 | {fileName} 83 | {posStyle.height > 2 * padding[0] && 84 | (!node.children || node.children.length === 0) ? ( 85 | 86 |
87 | {getFileSize(node.value)} 88 |
89 | ) : ( 90 | ({getFileSize(node.value)}) 91 | )} 92 |
93 | )} 94 |
95 |
96 | ); 97 | }; 98 | } 99 | 100 | export default function Treemap(props) { 101 | const [zoomed, setZoom] = useState(); 102 | const { hierarchy, bgColorsMap, changeSelected, name, directories } = props; 103 | 104 | const { innerWidth, innerHeight } = useWindowSize(); 105 | const width = innerWidth - 80, 106 | height = innerHeight * 0.3; 107 | 108 | const handleClick = useMemo(() => getHandleClick(changeSelected, setZoom), [ 109 | changeSelected, 110 | setZoom, 111 | ]); 112 | 113 | const tree = useMemo( 114 | () => 115 | stratify() 116 | .id(function (d) { 117 | return d.name; 118 | }) 119 | .parentId(function (d) { 120 | return d.parent; 121 | })(hierarchy) 122 | .sum((d) => d.totalBytes) 123 | .sort((a, b) => b.height - a.height || b.value - a.value), 124 | [hierarchy] 125 | ); 126 | 127 | const nodeComponent = useMemo( 128 | () => getNodeComponent(name, bgColorsMap, zoomed, handleClick), 129 | [name, bgColorsMap, zoomed, handleClick] 130 | ); 131 | 132 | return ( 133 |
134 |
135 |
139 |
140 | {zoomed === undefined 141 | ? "Fully zoomed out, click to zoom directories (bolded) " 142 | : tree 143 | .descendants() 144 | .find((node) => node.data.name === zoomed) 145 | .ancestors() 146 | .reverse() 147 | .map((node, i) => ( 148 | 149 | {i > 0 ? " - " : ""} 150 | 158 | 159 | ))} 160 |
161 |
162 | {directories.map((d, i) => ( 163 | 164 | 175 | {d}{" "} 176 | 177 | ))} 178 |
179 |
180 | 181 |
190 | 199 |
200 |
201 |
202 | ); 203 | } 204 | -------------------------------------------------------------------------------- /src/bundle/TreemapComponent.js: -------------------------------------------------------------------------------- 1 | //Code by @stil From https://github.com/stil/treemap-multilevel 2 | /* 3 | MIT License 4 | 5 | Copyright (c) 2019 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | /* eslint-disable no-param-reassign */ 27 | import React, { Fragment, useMemo } from "react"; 28 | import PropTypes from "prop-types"; // eslint-disable-line import/no-extraneous-dependencies 29 | import { treemap, treemapBinary } from "d3-hierarchy"; 30 | import {} from "d3-hierarchy"; 31 | 32 | function canDisplay(node) { 33 | const width = node.x1 - node.x0; 34 | const height = node.y1 - node.y0; 35 | if (width <= 5 || height <= 3) { 36 | return false; 37 | } 38 | 39 | return true; 40 | } 41 | 42 | function calculatePos(node) { 43 | return { 44 | transform: `translate(${node.x0}px, ${node.y0}px)`, 45 | width: node.x1 - node.x0, 46 | height: node.y1 - node.y0, 47 | }; 48 | } 49 | 50 | function reposition(root, width, height, zoomed, padding) { 51 | treemap().tile(treemapBinary).size([width, height])(root); 52 | 53 | const zoomedEl = zoomed 54 | ? root.descendants().find((node) => node.data.name === zoomed) 55 | : root; 56 | 57 | if (zoomedEl.depth > 0) { 58 | const scaleX = (root.x1 - root.x0) / (zoomedEl.x1 - zoomedEl.x0); 59 | const scaleY = (root.y1 - root.y0) / (zoomedEl.y1 - zoomedEl.y0); 60 | const refPointY = zoomedEl.y0; 61 | const refPointX = zoomedEl.x0; 62 | root.descendants().forEach((node) => { 63 | node.y0 = scaleY * (node.y0 - refPointY); 64 | node.y1 = scaleY * (node.y1 - refPointY); 65 | node.x0 = scaleX * (node.x0 - refPointX); 66 | node.x1 = scaleX * (node.x1 - refPointX); 67 | }); 68 | } 69 | 70 | zoomedEl.descendants().forEach((relativeRoot) => { 71 | relativeRoot 72 | .descendants() 73 | .slice(1) 74 | .forEach((node) => { 75 | let refPoint; 76 | let scale; 77 | 78 | // Scale vertically to bottom. 79 | scale = 1 - padding[0] / (relativeRoot.y1 - relativeRoot.y0); 80 | 81 | if (scale > 0) { 82 | refPoint = relativeRoot.y1; 83 | node.y0 = refPoint + scale * (node.y0 - refPoint); 84 | node.y1 = refPoint + scale * (node.y1 - refPoint); 85 | } 86 | 87 | // Scale vertically to top. 88 | scale = 1 - padding[2] / (relativeRoot.y1 - relativeRoot.y0); 89 | 90 | if (scale > 0) { 91 | refPoint = relativeRoot.y0; 92 | node.y0 = refPoint + scale * (node.y0 - refPoint); 93 | node.y1 = refPoint + scale * (node.y1 - refPoint); 94 | } 95 | // Scale horizontally to left. 96 | scale = 1 - padding[1] / (relativeRoot.x1 - relativeRoot.x0); 97 | if (scale > 0) { 98 | refPoint = relativeRoot.x0; 99 | node.x0 = refPoint + scale * (node.x0 - refPoint); 100 | node.x1 = refPoint + scale * (node.x1 - refPoint); 101 | } 102 | // Scale horizontally to right. 103 | scale = 1 - padding[3] / (relativeRoot.x1 - relativeRoot.x0); 104 | if (scale > 0) { 105 | refPoint = relativeRoot.x1; 106 | node.x0 = refPoint + scale * (node.x0 - refPoint); 107 | node.x1 = refPoint + scale * (node.x1 - refPoint); 108 | } 109 | }); 110 | }); 111 | 112 | return zoomedEl; 113 | } 114 | 115 | export default function TreemapComponent({ 116 | root, 117 | zoomed, 118 | width, 119 | height, 120 | padding, 121 | nodeComponent, 122 | }) { 123 | const zoomedEl = useMemo( 124 | () => reposition(root, width, height, zoomed, padding), 125 | [root, width, height, zoomed, padding] 126 | ); 127 | 128 | return zoomedEl 129 | .descendants() 130 | .filter((node) => canDisplay(node)) 131 | .map((node, i) => ( 132 | 133 | {nodeComponent(node, i, calculatePos(node))} 134 | 135 | )); 136 | } 137 | 138 | TreemapComponent.propTypes = { 139 | root: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types 140 | width: PropTypes.number.isRequired, 141 | height: PropTypes.number.isRequired, 142 | padding: PropTypes.arrayOf(PropTypes.number), 143 | nodeComponent: PropTypes.func, 144 | zoomed: PropTypes.string, 145 | tile: PropTypes.func.isRequired, 146 | }; 147 | 148 | TreemapComponent.defaultProps = { 149 | zoomed: null, 150 | nodeComponent: (node, i, posStyle) => ( 151 |
158 | {node.data.key} 159 |
160 | ), 161 | padding: [20, 2, 2, 2], 162 | }; 163 | -------------------------------------------------------------------------------- /src/bundle/bundle.css: -------------------------------------------------------------------------------- 1 | #bundle .download { 2 | width: 100%; 3 | border: none; 4 | /* height: 20px; */ 5 | background: var(--primary-color); 6 | color: white; 7 | padding: 10px; 8 | } 9 | 10 | #bundle .left-panel { 11 | padding: 30px 60px 0px 30px; 12 | } 13 | 14 | #bundle-header, 15 | #bundle-header .sticky { 16 | background: var(--grey200); 17 | } 18 | 19 | #bundle-header .total-size { 20 | background: white; 21 | padding: 12px 24px; 22 | } 23 | 24 | #bundle-header .total-size p { 25 | text-align: center; 26 | } 27 | 28 | #bundle-header .total-size .value { 29 | font-size: 64px; 30 | font-weight: 100; 31 | } 32 | 33 | #bundle .Report { 34 | background: var(--primary-color); 35 | } 36 | 37 | #bundle .Analyze .header { 38 | background: var(--grey900); 39 | color: white; 40 | } 41 | 42 | #bundle .Report h1, 43 | .Analyze h1 { 44 | color: white; 45 | } 46 | 47 | #bundle .Report .report-panel { 48 | background: white; 49 | padding: 12px 24px; 50 | margin-right: 24px; 51 | } 52 | 53 | #bundle .Report table th { 54 | text-align: left; 55 | 56 | /* background: blue; */ 57 | } 58 | 59 | #bundle .Report table th { 60 | border-bottom: 1px solid var(--grey200); 61 | } 62 | #bundle .Report table td:first-of-type { 63 | border-right: 1px solid var(--grey200); 64 | } 65 | 66 | #bundle .Report table td { 67 | font-size: 14px; 68 | padding: 4px; 69 | border-bottom: 1px solid var(--grey200); 70 | } 71 | 72 | #bundle .subheader { 73 | letter-spacing: 2px; 74 | text-transform: uppercase; 75 | font-weight: 700; 76 | font-size: 14px; 77 | } 78 | -------------------------------------------------------------------------------- /src/bundle/stringFormats.js: -------------------------------------------------------------------------------- 1 | const mb = 1024 * 1024; 2 | const kb = 1024; 3 | 4 | export function getFileSizeSplit(size) { 5 | if (!size || size === 0) return { size: 0, type: "KB" }; 6 | let value = size && size >= mb ? size / mb : size / kb; 7 | if (value < 1 || size >= mb) value = value.toFixed(2); 8 | else value = value.toFixed(0); 9 | return { value, type: size >= mb ? "MB" : "KB" }; 10 | } 11 | 12 | export function getFileSize(size) { 13 | const { value, type } = getFileSizeSplit(size); 14 | return `${value} ${type}`; 15 | } 16 | 17 | export const getPercent = (size, total) => { 18 | if (size === 1) return "100%"; 19 | let rounded = size <= 1 ? size * 100 : (size / total) * 100; 20 | 21 | if (rounded < 0.1) rounded = "<.1"; 22 | else if (rounded >= 99.5) rounded = rounded.toFixed(1); 23 | else if (rounded < 1) rounded = rounded.toFixed(1); 24 | else rounded = rounded.toFixed(0); 25 | 26 | rounded = rounded + "%"; 27 | 28 | if (rounded === "1.0%") rounded = "1%"; 29 | if (rounded.slice(0, 2) === "0.") rounded = rounded.slice(1); 30 | 31 | return rounded; 32 | }; 33 | 34 | export const getCSSPercent = (size, total) => { 35 | if (size === 1) return "100%"; 36 | 37 | let rounded = size <= 1 ? size * 100 : (size / total) * 100; 38 | 39 | if (rounded < 0.1) rounded = ".1"; 40 | else if (rounded >= 99.5) rounded = rounded.toFixed(1); 41 | else if (rounded < 1) rounded = rounded.toFixed(1); 42 | else rounded = rounded.toFixed(0); 43 | 44 | rounded = rounded + "%"; 45 | 46 | if (rounded === "1.0%") rounded = "1%"; 47 | if (rounded.slice(0, 2) === "0.") rounded = rounded.slice(1); 48 | 49 | return rounded; 50 | }; 51 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "viz-annotation/*"; 2 | -------------------------------------------------------------------------------- /src/graph/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | requiredBy, 3 | directRequires, 4 | calculateTransitiveRequires, 5 | edgesToGraph, 6 | } from "."; 7 | 8 | it("generates a flattend graph", () => { 9 | const flat = edgesToGraph([ 10 | { 11 | fileName: "susie", 12 | imported: "randy", 13 | }, 14 | { 15 | fileName: "susie", 16 | imported: "sam", 17 | }, 18 | { 19 | fileName: "sam", 20 | imported: "randy", 21 | }, 22 | ]); 23 | 24 | expect(Array.from(flat["randy"].requires).sort()).toEqual([]); 25 | expect(Array.from(flat["randy"].requiredBy).sort()).toEqual(["sam", "susie"]); 26 | 27 | expect(Array.from(flat["susie"].requires).sort()).toEqual(["randy", "sam"]); 28 | expect(Array.from(flat["susie"].requiredBy).sort()).toEqual([]); 29 | 30 | expect(Array.from(flat["sam"].requires).sort()).toEqual(["randy"]); 31 | expect(Array.from(flat["sam"].requiredBy).sort()).toEqual(["susie"]); 32 | }); 33 | 34 | it("calculate transitive requires cycles", () => { 35 | const input = { 36 | a: { requiredBy: ["b"] }, 37 | c: { requiredBy: ["a"] }, 38 | b: { requiredBy: ["c", "a"] }, 39 | }; 40 | 41 | expect( 42 | Array.from(calculateTransitiveRequires("a", directRequires(input))).sort() 43 | ).toEqual(["b", "c"]); 44 | 45 | expect( 46 | Array.from(calculateTransitiveRequires("b", directRequires(input))).sort() 47 | ).toEqual(["a", "c"]); 48 | 49 | expect( 50 | Array.from(calculateTransitiveRequires("c", directRequires(input))).sort() 51 | ).toEqual(["a", "b"]); 52 | }); 53 | 54 | it("calculate transitive requires", () => { 55 | const input = { 56 | a: { requiredBy: ["b"] }, 57 | c: { requiredBy: ["a"] }, 58 | z: { requiredBy: ["a"] }, 59 | q: { requiredBy: ["c"] }, 60 | }; 61 | 62 | // handles when a node is not in the graph 63 | expect( 64 | Array.from(calculateTransitiveRequires("foo", directRequires(input))).sort() 65 | ).toEqual([]); 66 | 67 | expect( 68 | Array.from(calculateTransitiveRequires("a", directRequires(input))).sort() 69 | ).toEqual(["c", "q", "z"]); 70 | }); 71 | 72 | it("builds direct requires (depends on) graph", () => { 73 | const input = { 74 | a: { requiredBy: ["b"] }, 75 | c: { requiredBy: ["a"] }, 76 | z: { requiredBy: ["a", "t"] }, 77 | q: { requiredBy: ["c"] }, 78 | }; 79 | 80 | const ret = directRequires(input); 81 | 82 | expect(Array.from(ret["a"].requires).sort()).toEqual(["c", "z"]); 83 | 84 | expect(Array.from(ret["b"].requires)).toEqual(["a"]); 85 | 86 | expect(Array.from(ret["c"].requires).sort()).toEqual(["q"]); 87 | 88 | expect(Array.from(ret["t"].requires).sort()).toEqual(["z"]); 89 | }); 90 | 91 | it("calulates transitive required by (required by)", () => { 92 | const input = { 93 | a: { requiredBy: ["b"] }, 94 | b: { requiredBy: [] }, 95 | c: { requiredBy: ["a", "b"] }, 96 | d: { requiredBy: ["a"] }, 97 | }; 98 | 99 | const ret = requiredBy(input); 100 | 101 | expect(ret["a"].transitiveRequiredBy).toEqual(["b"]); 102 | expect(ret["b"].transitiveRequiredBy).toEqual([]); 103 | expect(ret["c"].transitiveRequiredBy.sort()).toEqual(["a", "b"].sort()); 104 | expect(ret["d"].transitiveRequiredBy.sort()).toEqual(["a", "b"].sort()); 105 | }); 106 | 107 | it("does not cycle", () => { 108 | const input = { 109 | a: { requiredBy: ["b"] }, 110 | b: { requiredBy: ["a"] }, 111 | }; 112 | 113 | const ret = requiredBy(input); 114 | 115 | expect(ret["a"].transitiveRequiredBy).toEqual(["b"]); 116 | expect(ret["b"].transitiveRequiredBy).toEqual(["a"]); 117 | }); 118 | -------------------------------------------------------------------------------- /src/graph/index.ts: -------------------------------------------------------------------------------- 1 | import { Imported, FlattendGraph } from "../types"; 2 | 3 | export interface RequireGraph { 4 | [target: string]: { 5 | requires: Set; 6 | }; 7 | } 8 | 9 | export function edgesToGraph(edges: Imported[]): FlattendGraph { 10 | const ret: FlattendGraph = {}; 11 | 12 | // materialze the graph with all nodes. 13 | for (const edge of edges) { 14 | if (ret[edge.fileName] == null) { 15 | ret[edge.fileName] = { 16 | requires: new Set(), 17 | requiredBy: new Set(), 18 | }; 19 | } 20 | 21 | if (ret[edge.imported] == null) { 22 | ret[edge.imported] = { 23 | requires: new Set(), 24 | requiredBy: new Set(), 25 | }; 26 | } 27 | } 28 | 29 | for (const key of Object.keys(ret)) { 30 | for (const edge of Object.values(edges)) { 31 | if (edge.fileName === key) { 32 | ret[key].requires.add(edge.imported); 33 | } 34 | 35 | if (edge.imported === key) { 36 | ret[key].requiredBy.add(edge.fileName); 37 | } 38 | } 39 | } 40 | 41 | return ret; 42 | } 43 | 44 | function getModules( 45 | graph: { [target: string]: { requiredBy: string[] | Set } }, 46 | node: string, 47 | { isRoot }: { isRoot: boolean }, 48 | seen: Set = new Set() 49 | ): string[] { 50 | seen.add(node); 51 | 52 | const newNodes: string[] = []; 53 | 54 | // Add all new nodes to the seen list 55 | for (const n of graph[node].requiredBy) { 56 | const prevL = seen.size; 57 | seen.add(n); 58 | if (seen.size > prevL) { 59 | newNodes.push(n); 60 | } 61 | } 62 | 63 | // Walk graph gathering all new nodes 64 | const allNames = newNodes.map((v) => { 65 | return getModules(graph, v, { isRoot: false }, seen); 66 | }); 67 | 68 | // Add the current node that we are looking at. 69 | if (!isRoot) { 70 | allNames.push([node]); 71 | } 72 | 73 | // Flatten the list now and return. 74 | const ret: string[] = []; 75 | for (const nameList of allNames) { 76 | ret.push(...nameList); 77 | } 78 | 79 | return ret; 80 | } 81 | 82 | /** 83 | * Determines the transitive requires for a given node in the require graph. 84 | * This function is useful for answering the question, what are *all* of requires for this node. 85 | * 86 | * Example: 87 | * a.js 88 | * import 'foo' 89 | * 90 | * foo.js 91 | * import 'zap' 92 | * 93 | * 94 | * transitive requires of 'a.js' would be ['foo', 'zap'] 95 | * 96 | * 97 | * @param nodeId the node you would like to calculate the transitive requires for 98 | * @param graph the dependency graph 99 | */ 100 | export function calculateTransitiveRequires( 101 | nodeId: string, 102 | graph: RequireGraph 103 | ): Set { 104 | const ret = new Set(); 105 | 106 | if (graph[nodeId] == null) { 107 | return ret; 108 | } 109 | 110 | const toScan = graph[nodeId].requires; 111 | 112 | while (toScan.size) { 113 | for (const n of toScan) { 114 | // add dep 115 | ret.add(n); 116 | 117 | // Add the dependent nodes now 118 | if (graph[n]?.requires) { 119 | for (const nested of graph[n]?.requires) { 120 | // skip adding if we have already seen this node 121 | if (ret.has(nested)) { 122 | continue; 123 | } 124 | toScan.add(nested); 125 | } 126 | } 127 | 128 | // remove the node now that we have scanned it 129 | toScan.delete(n); 130 | } 131 | } 132 | 133 | if (ret.has(nodeId)) { 134 | ret.delete(nodeId); 135 | } 136 | 137 | return ret; 138 | } 139 | 140 | /** 141 | * Determines the number of nodes that a given node directly depends on 142 | * @param d network 143 | */ 144 | export function directRequires(d: { 145 | [target: string]: { requiredBy: string[] | Set }; 146 | }): RequireGraph { 147 | const ret: RequireGraph = {}; 148 | 149 | for (const [target, { requiredBy }] of Object.entries(d)) { 150 | for (const v of Array.from(requiredBy)) { 151 | if (ret[v] == null) { 152 | ret[v] = { requires: new Set() }; 153 | } 154 | 155 | ret[v].requires.add(target); 156 | } 157 | } 158 | 159 | return ret; 160 | } 161 | 162 | /** 163 | * Determines the number of nodes that something is transitivly required by 164 | * @param d network 165 | */ 166 | export function requiredBy(d: { 167 | [target: string]: { requiredBy: string[] | Set }; 168 | }): { 169 | [target: string]: { 170 | transitiveRequiredBy: string[]; 171 | }; 172 | } { 173 | const ret: { 174 | [target: string]: { 175 | transitiveRequiredBy: string[]; 176 | }; 177 | } = {}; 178 | 179 | for (const rootK of Object.keys(d)) { 180 | const moduleDeps = getModules(d, rootK, { isRoot: true }); 181 | ret[rootK] = { 182 | transitiveRequiredBy: moduleDeps, 183 | }; 184 | } 185 | 186 | return ret; 187 | } 188 | -------------------------------------------------------------------------------- /src/home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Route, Switch } from "react-router-dom"; 2 | import React from "react"; 3 | import Resolve from "../resolve/Resolve"; 4 | import { ImportResolveState, ImportHistory, ImportTypes } from "../types"; 5 | import Importer from "../import/Importer"; 6 | import ImportSelector from "../import/ImportSelector"; 7 | 8 | import "./home.css"; 9 | 10 | interface Props extends ImportResolveState { 11 | history: ImportHistory; 12 | } 13 | 14 | export default function Home(props: Props) { 15 | const { 16 | graphEdges, 17 | processedSourceMap, 18 | bundledFilesTransform: sourceMapFileTransform, 19 | graphFileTransform, 20 | history, 21 | } = props; 22 | 23 | return ( 24 |
25 |
26 |
27 |
31 |

Bundle Buddy

32 |
33 |
34 | 35 |
36 |

37 | 38 | Visualizing what code is in your web bundle, and how it got there. 39 | 40 |

41 |
42 |
43 | 44 |
45 |
46 |
50 |

Step 1

51 |
52 |
53 |
54 |

Select the bundler you are using:

55 | 56 |
57 |
58 |
61 | 62 | 63 |
64 |
68 |

Step 2

69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | 78 | { 81 | return ( 82 | 87 | ); 88 | }} 89 | /> 90 | { 93 | return ( 94 | 99 | ); 100 | }} 101 | /> 102 | { 105 | return ( 106 | 111 | ); 112 | }} 113 | /> 114 | { 117 | return ( 118 | 123 | ); 124 | }} 125 | /> 126 | { 129 | return ( 130 | 135 | ); 136 | }} 137 | /> 138 | { 141 | return ( 142 | 147 | ); 148 | }} 149 | /> 150 | 151 |
152 |
153 |
154 |
155 | {graphEdges && ( 156 |
157 |
158 |
162 |

Step 3

163 |
164 |
165 |
166 | 173 |
174 |
175 | )} 176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /src/home/home.css: -------------------------------------------------------------------------------- 1 | #home header h1 { 2 | font-size: 48px; 3 | font-weight: bold; 4 | } 5 | 6 | #home section { 7 | display: flex; 8 | align-items: baseline; 9 | padding-bottom: 12px; 10 | } 11 | 12 | #home .left-panel { 13 | width: 30vw; 14 | min-width: 30vw; 15 | height: inherit; 16 | } 17 | #home .right-panel { 18 | width: 70vw; 19 | padding-right: 48px; 20 | } 21 | 22 | #home .inner-border { 23 | /* min-height: 200px; */ 24 | height: 100%; 25 | margin: 36px; 26 | justify-content: flex-end; 27 | padding-right: 36px; 28 | padding-left: 36px; 29 | text-align: right; 30 | } 31 | 32 | #home h2 { 33 | font-size: 36px; 34 | } 35 | 36 | #home button span { 37 | flex-grow: 1; 38 | text-align: center; 39 | } 40 | 41 | #home pre { 42 | background: white; 43 | } 44 | 45 | #home .upload { 46 | /* min-height: 500px; */ 47 | } 48 | -------------------------------------------------------------------------------- /src/import/ImportSelector.tsx: -------------------------------------------------------------------------------- 1 | import * as pako from "pako"; 2 | import React, { Component } from "react"; 3 | import { readFileAsText, readFileAsBinary } from "./file_reader"; 4 | import { NavLink as Link } from "react-router-dom"; 5 | import { ImportHistory } from "../types"; 6 | import { storeProcessedState } from "../routes"; 7 | // noopener noreferrer 8 | 9 | class ImportSelector extends Component<{ history: ImportHistory }> { 10 | existingImportInput: React.RefObject; 11 | 12 | constructor(props: { history: ImportHistory }) { 13 | super(props); 14 | this.existingImportInput = React.createRef(); 15 | } 16 | 17 | async onExistingImportInput() { 18 | const file = this.existingImportInput.current?.files[0]; 19 | if (file == null) { 20 | return; 21 | } 22 | 23 | const contents = await readFileAsBinary(file); 24 | const inflated = pako.inflate(contents); 25 | const previousState = JSON.parse(new TextDecoder().decode(inflated)); 26 | this.props.history.push("/bundle", storeProcessedState(previousState)); 27 | } 28 | 29 | state: never; 30 | 31 | render() { 32 | return ( 33 |
34 |
35 |
36 | 41 | 55 | 56 | 61 | 75 | 76 | 81 | 95 | 96 | 101 | 115 | 116 | 121 | 135 | 136 | 141 | 148 | 149 |
150 | 151 | 163 |
164 |
165 | ); 166 | } 167 | } 168 | 169 | export default ImportSelector; 170 | -------------------------------------------------------------------------------- /src/import/builtins.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | "assert", 3 | "async_hooks", 4 | "buffer", 5 | "child_process", 6 | "cluster", 7 | "console", 8 | "constants", 9 | "crypto", 10 | "dgram", 11 | "dns", 12 | "domain", 13 | "events", 14 | "fs", 15 | "http", 16 | "http2", 17 | "https", 18 | "inspector", 19 | "module", 20 | "net", 21 | "os", 22 | "path", 23 | "perf_hooks", 24 | "process", 25 | "punycode", 26 | "querystring", 27 | "readline", 28 | "repl", 29 | "stream", 30 | "string_decoder", 31 | "timers", 32 | "tls", 33 | "tty", 34 | "url", 35 | "util", 36 | "v8", 37 | "vm", 38 | "zlib" 39 | ]; 40 | -------------------------------------------------------------------------------- /src/import/clipboard.ts: -------------------------------------------------------------------------------- 1 | export async function toClipboard(text: string) { 2 | await (navigator as Navigator & { 3 | clipboard: { writeText: (t: string) => Promise }; 4 | }).clipboard.writeText(text); 5 | } 6 | -------------------------------------------------------------------------------- /src/import/esbuild/index.test.ts: -------------------------------------------------------------------------------- 1 | import { toEdges, toProcessedBundles } from "."; 2 | 3 | it("handles the empty case for toEdges", () => { 4 | expect( 5 | toEdges({ 6 | outputs: {}, 7 | inputs: {}, 8 | }) 9 | ).toEqual([]); 10 | }); 11 | 12 | it("handles generating edges", () => { 13 | expect( 14 | toEdges({ 15 | outputs: {}, 16 | inputs: { 17 | "foo.js": { 18 | bytes: 0, 19 | imports: [ 20 | { 21 | path: "tap.js", 22 | }, 23 | { 24 | path: "lap.js", 25 | }, 26 | ], 27 | }, 28 | "wow.js": { 29 | bytes: 0, 30 | imports: [ 31 | { 32 | path: "tap.js", 33 | }, 34 | ], 35 | }, 36 | }, 37 | }) 38 | ).toEqual([ 39 | { 40 | source: "foo.js", 41 | target: "tap.js", 42 | }, 43 | { 44 | source: "foo.js", 45 | target: "lap.js", 46 | }, 47 | { 48 | source: "wow.js", 49 | target: "tap.js", 50 | }, 51 | ]); 52 | }); 53 | 54 | it("handles the empty case for toProcessedBundles", () => { 55 | expect( 56 | toProcessedBundles({ 57 | outputs: {}, 58 | inputs: {}, 59 | }) 60 | ).toEqual({}); 61 | }); 62 | 63 | it("converts into a processed bundle format", () => { 64 | expect( 65 | toProcessedBundles({ 66 | outputs: { 67 | "foo.min.js": { 68 | bytes: 0, 69 | inputs: { 70 | "foo.js": { 71 | bytesInOutput: 0, 72 | }, 73 | "tap.js": { 74 | bytesInOutput: 0, 75 | }, 76 | }, 77 | }, 78 | "zap.min.js": { 79 | bytes: 0, 80 | inputs: { 81 | "zap.js": { 82 | bytesInOutput: 0, 83 | }, 84 | }, 85 | }, 86 | }, 87 | inputs: {}, 88 | }) 89 | ).toEqual({ 90 | "foo.min.js": { 91 | files: { 92 | "foo.js": { 93 | totalBytes: 0, 94 | }, 95 | "tap.js": { 96 | totalBytes: 0, 97 | }, 98 | }, 99 | totalBytes: 0, 100 | }, 101 | "zap.min.js": { 102 | files: { 103 | "zap.js": { 104 | totalBytes: 0, 105 | }, 106 | }, 107 | totalBytes: 0, 108 | }, 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/import/esbuild/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EsBuildMetadata, 3 | GraphEdges, 4 | ProcessedBundle, 5 | BundledFiles, 6 | } from "../../types"; 7 | 8 | /** 9 | * Given an esbuild metafile file convert it into edges. 10 | * @param metadata esbuild metafile info. 11 | */ 12 | export function toEdges(metadata: EsBuildMetadata): GraphEdges { 13 | const ret: GraphEdges = []; 14 | 15 | for (const [file, val] of Object.entries(metadata.inputs)) { 16 | for (const { path } of val.imports) { 17 | ret.push({ 18 | source: file, 19 | target: path, 20 | }); 21 | } 22 | } 23 | 24 | return ret; 25 | } 26 | 27 | /** 28 | * Given an esbuild metafile convert it into a list of processed bundles. 29 | * @param metadata esbuild metafile info. 30 | */ 31 | export function toProcessedBundles( 32 | metadata: EsBuildMetadata 33 | ): { [bundleName: string]: ProcessedBundle } { 34 | const ret: { [bundleName: string]: ProcessedBundle } = {}; 35 | 36 | for (const [bundleName, stats] of Object.entries(metadata.outputs)) { 37 | const files: BundledFiles = {}; 38 | 39 | for (const [fileName, fileStats] of Object.entries(stats.inputs)) { 40 | files[fileName] = { 41 | totalBytes: fileStats.bytesInOutput, 42 | }; 43 | } 44 | 45 | ret[bundleName] = { 46 | totalBytes: stats.bytes, 47 | files, 48 | }; 49 | } 50 | 51 | return ret; 52 | } 53 | -------------------------------------------------------------------------------- /src/import/file_reader.ts: -------------------------------------------------------------------------------- 1 | export async function readFilesAsText( 2 | files: File[] 3 | ): Promise<{ [filename: string]: string }> { 4 | const ret: { [filename: string]: string } = {}; 5 | for (const f of files) { 6 | ret[f.name] = await readFileAsText(f); 7 | } 8 | 9 | return ret; 10 | } 11 | 12 | export function readFileAsText(file: File): Promise { 13 | return new Promise((res, rej) => { 14 | const reader = new FileReader(); 15 | reader.onload = (e) => { 16 | const target = e.target as EventTarget & { result: string }; 17 | res(target.result); 18 | }; 19 | 20 | reader.onabort = reader.onerror = (e) => Promise.reject(e); 21 | reader.readAsText(file); 22 | }); 23 | } 24 | 25 | export function readFileAsBinary(file: File): Promise { 26 | return new Promise((res, rej) => { 27 | const reader = new FileReader(); 28 | reader.onload = (e) => { 29 | const target = e.target as EventTarget & { result: string }; 30 | res(target.result); 31 | }; 32 | 33 | reader.onabort = reader.onerror = (e) => Promise.reject(e); 34 | reader.readAsBinaryString(file); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/import/graph_process.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphEdges } from "../types"; 2 | import { cleanGraph } from "./graph_process"; 3 | 4 | it("strips magic prefixes", () => { 5 | const nodes: GraphEdges = [ 6 | { source: "commonjs-proxy:/foo.js", target: "zap.ts" } 7 | ]; 8 | const ret = cleanGraph(nodes); 9 | 10 | expect(ret[0].source).toBe("foo.js"); 11 | }); 12 | 13 | it("strips common prefix", () => { 14 | const nodes: GraphEdges = [{ source: "wow/foo.js", target: "wow/zap.ts" }]; 15 | const ret = cleanGraph(nodes); 16 | 17 | expect(ret[0].source).toBe("foo.js"); 18 | expect(ret[0].target).toBe("zap.ts"); 19 | }); 20 | 21 | it("strips common prefix ignoring ignored nodes", () => { 22 | const nodes: GraphEdges = [ 23 | { 24 | source: "wow/foo.js", 25 | target: "wow/zap.ts" 26 | }, 27 | { 28 | source: "fs", 29 | target: "wow/zap.ts" 30 | } 31 | ]; 32 | const ret = cleanGraph(nodes); 33 | 34 | expect(ret[0].source).toBe("foo.js"); 35 | expect(ret[0].target).toBe("zap.ts"); 36 | expect(ret[1].target).toBe("zap.ts"); 37 | }); 38 | 39 | it("strips no matching prefix but common /", () => { 40 | const nodes: GraphEdges = [ 41 | { 42 | source: "(foo) ./wow.js", 43 | target: "./zap.ts" 44 | }, 45 | { 46 | source: "./client.js", 47 | target: "./zap.ts" 48 | }, 49 | { 50 | source: "./more.js", 51 | target: "./no.ts" 52 | } 53 | ]; 54 | const ret = cleanGraph(nodes); 55 | 56 | expect(ret[0].source).toBe("(foo) ./wow.js"); 57 | expect(ret[2].target).toBe("no.ts"); 58 | }); 59 | -------------------------------------------------------------------------------- /src/import/graph_process.ts: -------------------------------------------------------------------------------- 1 | import builtins from "./builtins"; 2 | import { findFirstIndex, findCommonPrefix } from "../import/prefix_cleaner"; 3 | import { GraphEdges } from "../types"; 4 | 5 | const prefixStrips = [ 6 | // Rollup specific prefix added to add commonjs proxy nodes. 7 | "\u0000commonjs-proxy:", 8 | "commonjs-proxy:/", 9 | // Rollup specific prefix added to add commonjs external nodes. 10 | "\u0000commonjs-external:", 11 | // More rollup magic prefixing. 12 | "\u0000" 13 | ]; 14 | 15 | const ignoreNodes = new Set( 16 | [ 17 | // Rollup specific magic module. 18 | "\u0000commonjsHelpers", 19 | "commonjsHelpers", 20 | "babelHelpers" 21 | ].concat(builtins) 22 | ); 23 | 24 | function removedIgnoredFiles(nodes: string[]) { 25 | return nodes.filter(v => !ignoreNodes.has(v)); 26 | } 27 | 28 | function getAllGraphFiles(graph: GraphEdges): string[] { 29 | const ret = new Set(); 30 | for (const { target, source } of graph) { 31 | if (target != null) { 32 | ret.add(target); 33 | } 34 | ret.add(source); 35 | } 36 | 37 | return Array.from(ret); 38 | } 39 | 40 | export function cleanGraph(graph: GraphEdges): GraphEdges { 41 | // Strip all magic prefixes 42 | for (const node of graph) { 43 | for (const key of Object.keys(node) as Array<"target" | "source">) { 44 | if (node[key] == null) { 45 | continue; 46 | } 47 | 48 | for (const magicPrefix of prefixStrips) { 49 | if (node[key]!.startsWith(magicPrefix)) { 50 | if (node[key]!.length !== magicPrefix.length) { 51 | node[key] = node[key]!.slice(magicPrefix.length); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | // Strip common prefixes 59 | const graphFiles = removedIgnoredFiles(getAllGraphFiles(graph)); 60 | const prefix = findCommonPrefix(removedIgnoredFiles(graphFiles)) || ""; 61 | 62 | if (prefix.length) { 63 | for (const node of graph) { 64 | for (const key of Object.keys(node) as Array<"target" | "source">) { 65 | if (node[key] != null) { 66 | if (node[key]!.startsWith(prefix)) { 67 | node[key] = node[key]!.slice(prefix.length); 68 | } 69 | } 70 | } 71 | } 72 | } else { 73 | // fallback to Strip up to first / 74 | const firstIndex = findFirstIndex(removedIgnoredFiles(graphFiles)); 75 | if (firstIndex > 0) { 76 | for (const node of graph) { 77 | for (const key of Object.keys(node) as Array<"target" | "source">) { 78 | if (node[key] != null) { 79 | if (node[key]![firstIndex] === "/") { 80 | node[key] = node[key]!.slice(firstIndex + 1); 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | // Remove null nodes in graph 89 | const ret: GraphEdges = []; 90 | for (const node of graph) { 91 | if ( 92 | node.target !== node.source && 93 | node.target !== null && 94 | node.source !== null 95 | ) { 96 | ret.push(node); 97 | } 98 | } 99 | 100 | return ret; 101 | } 102 | -------------------------------------------------------------------------------- /src/import/prefix_cleaner.ts: -------------------------------------------------------------------------------- 1 | export function findCommonPrefix(strings: string[]): string | null { 2 | let commonPrefix = null; 3 | 4 | for (const k of strings) { 5 | if (commonPrefix == null) { 6 | commonPrefix = k; 7 | } else { 8 | let matched = false; 9 | for (let splitPoint = k.length; splitPoint > 0; splitPoint--) { 10 | const split = k.slice(0, splitPoint); 11 | if (commonPrefix.indexOf(split) !== -1) { 12 | commonPrefix = split; 13 | matched = true; 14 | break; 15 | } 16 | 17 | if (split.length === 1) { 18 | break; 19 | } 20 | } 21 | 22 | if (!matched) { 23 | commonPrefix = ""; 24 | break; 25 | } 26 | } 27 | } 28 | 29 | if (commonPrefix) { 30 | const nodeModulesIndex = commonPrefix.indexOf("node_modules"); 31 | if (nodeModulesIndex !== -1) { 32 | return commonPrefix.slice(0, nodeModulesIndex); 33 | } 34 | } 35 | return commonPrefix; 36 | } 37 | 38 | export function findFirstIndex(strings: Array) { 39 | let indexes: { [backslashIndex: number]: number } = {}; 40 | let total: number = 0; 41 | 42 | for (const k of strings) { 43 | if (k == null) { 44 | continue; 45 | } 46 | 47 | const index = k.indexOf("/"); 48 | if (!indexes[index]) indexes[index] = 0; 49 | 50 | indexes[index]++; 51 | total++; 52 | } 53 | 54 | let validIndex = 0; 55 | 56 | Array.from(Object.keys(indexes)).find(k => { 57 | const v = indexes[parseInt(k)]; 58 | 59 | if (v / total >= 0.8) { 60 | validIndex = parseInt(k); 61 | return true; 62 | } 63 | return false; 64 | }); 65 | 66 | return validIndex; 67 | } 68 | -------------------------------------------------------------------------------- /src/import/process_imports.ts: -------------------------------------------------------------------------------- 1 | import { 2 | calculateSourcemapFileContents, 3 | mergeProcessedBundles, 4 | } from "./process_sourcemaps"; 5 | import { GraphEdges, ProcessedBundle, ImportProcess } from "../types"; 6 | import { ReportErrorUri } from "../report_error"; 7 | 8 | // TODO(samccone) we will want to handle more error types. 9 | function humanizeSourceMapImportError(e: Error) { 10 | return `importing source map: \n${e.toString()}`; 11 | } 12 | 13 | function humanizeGraphProcessError(e: Error) { 14 | return `importing graph contents: \n${e.toString()}`; 15 | } 16 | 17 | export async function processImports(opts: { 18 | sourceMapContents: { [filename: string]: string }; 19 | graphEdges: GraphEdges | string; 20 | graphPreProcessFn?: (contents: any) => GraphEdges; 21 | }): Promise { 22 | const ret: ImportProcess = { 23 | bundleSizes: {}, 24 | processedSourcemap: { 25 | files: {}, 26 | totalBytes: 0, 27 | }, 28 | }; 29 | 30 | const processed: { 31 | [bundleName: string]: ProcessedBundle; 32 | } = {}; 33 | 34 | for (const bundleName of Object.keys(opts.sourceMapContents)) { 35 | if (ret.sourceMapProcessError != null) { 36 | continue; 37 | } 38 | 39 | try { 40 | processed[bundleName] = await calculateSourcemapFileContents( 41 | opts.sourceMapContents[bundleName] 42 | ); 43 | } catch (e) { 44 | ret.sourceMapProcessError = new Error(humanizeSourceMapImportError(e)); 45 | } 46 | } 47 | 48 | for (const bundle of Object.keys(processed)) { 49 | ret.bundleSizes[bundle] = { 50 | totalBytes: processed[bundle].totalBytes, 51 | }; 52 | } 53 | 54 | ret.processedSourcemap = mergeProcessedBundles(processed); 55 | 56 | try { 57 | if (typeof opts.graphEdges === "string") { 58 | let parsedNodes = JSON.parse(opts.graphEdges); 59 | 60 | if (opts.graphPreProcessFn != null) { 61 | parsedNodes = opts.graphPreProcessFn(parsedNodes); 62 | } 63 | 64 | ret.processedGraph = parsedNodes as GraphEdges; 65 | } else { 66 | ret.processedGraph = opts.graphEdges; 67 | } 68 | } catch (e) { 69 | ret.graphProcessError = new Error(humanizeGraphProcessError(e)); 70 | } 71 | 72 | return ret; 73 | } 74 | 75 | export function buildImportErrorReport( 76 | processed: ImportProcess, 77 | files: { graphFile: { name: string }; sourceMapFiles: File[] } 78 | ) { 79 | let importError = null; 80 | const reportUri = new ReportErrorUri(); 81 | 82 | if (processed.graphProcessError != null) { 83 | importError = `${files.graphFile.name} ${processed.graphProcessError}\n`; 84 | reportUri.addError(files.graphFile.name, processed.graphProcessError); 85 | } 86 | 87 | if (processed.sourceMapProcessError != null) { 88 | if (importError == null) { 89 | importError = ""; 90 | } 91 | 92 | reportUri.addError( 93 | Object.keys(files.sourceMapFiles.map((f) => f.name)).join(","), 94 | processed.sourceMapProcessError 95 | ); 96 | importError += `${Object.keys(files.sourceMapFiles.map((f) => f.name)).join( 97 | "," 98 | )}: ${processed.sourceMapProcessError}`; 99 | } 100 | 101 | return { 102 | importError, 103 | importErrorUri: reportUri.toUri(), 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /src/import/process_sourcemaps.ts: -------------------------------------------------------------------------------- 1 | import { ProcessedBundle, BundledFiles } from "../types"; 2 | 3 | import * as sourceMap from "source-map"; 4 | 5 | (sourceMap.SourceMapConsumer as any).initialize({ 6 | "lib/mappings.wasm": "/mappings.wasm", 7 | }); 8 | 9 | /** 10 | * Calculates the total size of the processed sourcemap's contents. 11 | * @param processedSourceMap 12 | */ 13 | export function getTotalSize(processedSourceMap: BundledFiles): number { 14 | return Object.values(processedSourceMap).reduce( 15 | (a, b) => { 16 | return { totalBytes: a.totalBytes + b.totalBytes }; 17 | }, 18 | { totalBytes: 0 } 19 | ).totalBytes; 20 | } 21 | 22 | /** 23 | * Calculate the size of the sourcemap file contents. 24 | * @param contents The string sourcemap contents. 25 | */ 26 | export function calculateSourcemapFileContents( 27 | contents: string 28 | ): Promise { 29 | // TODO(samccone) fix typing when https://github.com/mozilla/source-map/pull/374 lands. 30 | function onMapping( 31 | cursor: { line: number; column: number }, 32 | processed: ProcessedBundle, 33 | m: sourceMap.MappingItem & { lastGeneratedColumn?: number } 34 | ) { 35 | if (m.source == null) { 36 | return; 37 | } 38 | 39 | if (processed.files[m.source] == null) { 40 | processed.files[m.source] = { 41 | totalBytes: 0, 42 | }; 43 | } 44 | 45 | if (m.generatedLine == null) { 46 | return; 47 | } 48 | 49 | // On newline reset cursor info 50 | if (cursor.line !== m.generatedLine && m.generatedLine != null) { 51 | cursor.line = m.generatedLine; 52 | cursor.column = m.lastGeneratedColumn || 1; 53 | } else { 54 | // On non-newline update column cursor 55 | cursor.column = m.lastGeneratedColumn || 1; 56 | } 57 | 58 | if (m.lastGeneratedColumn != null && m.lastGeneratedColumn !== -1) { 59 | processed.files[m.source].totalBytes += 60 | m.lastGeneratedColumn - m.generatedColumn + 1; 61 | } else { 62 | // this seems to only happen when we encounter the last char on the line so add 1 char. 63 | processed.files[m.source].totalBytes += 1; 64 | } 65 | } 66 | 67 | return new Promise((res, rej) => { 68 | sourceMap.SourceMapConsumer.with(contents, null, (consumer) => { 69 | const processed: ProcessedBundle = { 70 | totalBytes: 0, 71 | files: {}, 72 | }; 73 | const cursor = { line: 1, column: 1 }; 74 | try { 75 | consumer.computeColumnSpans(); 76 | } catch (e) { 77 | rej(e); 78 | return; 79 | } 80 | 81 | consumer.eachMapping((m) => onMapping(cursor, processed, m)); 82 | // Now sum up the total sizes for the graph. 83 | processed.totalBytes = getTotalSize(processed.files); 84 | res(processed); 85 | }).catch((e) => rej(e)); 86 | }); 87 | } 88 | 89 | export function mergeProcessedBundles(processed: { 90 | [bundlename: string]: ProcessedBundle; 91 | }): ProcessedBundle { 92 | const ret: ProcessedBundle = { 93 | files: {}, 94 | totalBytes: Object.values(processed).reduce( 95 | (a, b) => { 96 | return { totalBytes: a.totalBytes + b.totalBytes }; 97 | }, 98 | { totalBytes: 0 } 99 | ).totalBytes, 100 | }; 101 | 102 | for (const bundleName of Object.keys(processed)) { 103 | for (const filename of Object.keys(processed[bundleName].files)) { 104 | if ( 105 | ret.files[filename] == null || 106 | ret.files[filename].totalBytes < 107 | processed[bundleName].files[filename].totalBytes 108 | ) { 109 | ret.files[filename] = processed[bundleName].files[filename]; 110 | } 111 | } 112 | } 113 | 114 | return ret; 115 | } 116 | -------------------------------------------------------------------------------- /src/import/stats_to_graph.ts: -------------------------------------------------------------------------------- 1 | import { Edge, GraphEdges } from "../types"; 2 | 3 | interface Module { 4 | name: string; 5 | reasons: Array<{ moduleName: string }>; 6 | modules?: Array; 7 | } 8 | 9 | function cleanWebpackMagicFiles(f: string): string { 10 | const matches = [ 11 | { 12 | replace: "node_modules/webpack/buildin", 13 | matcher: /\(webpack\)\/buildin/ 14 | } 15 | ]; 16 | 17 | for (const m of matches) { 18 | if (m.matcher.exec(f)) { 19 | return f.replace(m.matcher.exec(f)![0], m.replace); 20 | } 21 | } 22 | 23 | return f; 24 | } 25 | 26 | function cleanEdges( 27 | edges: Edge[], 28 | plusMap: Map> 29 | ): GraphEdges { 30 | const exploded: GraphEdges = []; 31 | 32 | let pushedMore = false; 33 | for (const uncleanEdge of edges) { 34 | const foundNestedDeps = plusMap.get(uncleanEdge.target); 35 | if (foundNestedDeps) { 36 | pushedMore = true; 37 | for (const toExplodeTarget of foundNestedDeps.values()) { 38 | exploded.push({ 39 | target: toExplodeTarget, 40 | source: uncleanEdge.source 41 | }); 42 | } 43 | } else { 44 | exploded.push(uncleanEdge); 45 | } 46 | } 47 | 48 | if (pushedMore) { 49 | return cleanEdges(exploded, plusMap); 50 | } else { 51 | return exploded; 52 | } 53 | } 54 | 55 | export function gatherEdges( 56 | stats: { modules: Module[] }, 57 | edges: Edge[] = [], 58 | lookupMap: Set = new Set(), 59 | plusMap: Map> = new Map() 60 | ): { edges: Edge[]; plusMap: Map> } { 61 | for (const module of stats.modules || []) { 62 | if (module.modules != null) { 63 | if (!plusMap.has(module.name)) { 64 | plusMap.set(module.name, new Set()); 65 | for (const subModule of module.modules) { 66 | plusMap.get(module.name)!.add(subModule.name); 67 | } 68 | } 69 | 70 | edges = gatherEdges( 71 | { modules: module.modules }, 72 | edges, 73 | lookupMap, 74 | plusMap 75 | ).edges; 76 | } else { 77 | const moduleName = cleanWebpackMagicFiles(module.name); 78 | for (const reason of module.reasons || []) { 79 | const reasonModuleName = cleanWebpackMagicFiles(reason.moduleName); 80 | if (!lookupMap.has(`${moduleName}|${reasonModuleName}`)) { 81 | edges.push({ 82 | source: reasonModuleName, 83 | target: moduleName 84 | }); 85 | lookupMap.add(`${moduleName}|${reasonModuleName}`); 86 | } 87 | } 88 | } 89 | } 90 | 91 | return { edges, plusMap }; 92 | } 93 | 94 | /** 95 | * Converts a webpack stats.json object to a dot formatted graph list. 96 | * @param stats A webpack stats.json object 97 | */ 98 | export function statsToGraph(stats: { modules: Module[] }): Edge[] { 99 | const { edges, plusMap } = gatherEdges(stats); 100 | return cleanEdges(edges, plusMap); 101 | } 102 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --primary-color: #33bd5e; 7 | --link-color: darkblue; 8 | --margin-left: 90px; 9 | --side-panel-width: 100px; 10 | --total-width: 580px; 11 | --default-text: #111; 12 | /* --grey100: #f1f2f4; 13 | --grey200: #dfe1e5; 14 | --grey300: #ccd1d7; 15 | --grey400: #a8b1bc; 16 | --grey500: #8892a0; 17 | --grey600: #6a7585; 18 | --grey700: #4e5a6a; 19 | --grey800: #36404e; 20 | --grey900: #212833; */ 21 | 22 | --grey100: #e8f8ed; 23 | --grey200: #d6ebdd; 24 | --grey300: #b6d6c3; 25 | --grey400: #9ec0ab; 26 | --grey500: #87ab95; 27 | --grey600: #6e9079; 28 | --grey700: #506c5b; 29 | --grey800: #395243; 30 | --grey900: #293930; 31 | } 32 | 33 | .grey100 { 34 | color: var(--grey100); 35 | } 36 | .grey200 { 37 | color: var(--grey200); 38 | } 39 | .grey300 { 40 | color: var(--grey300); 41 | } 42 | .grey400 { 43 | color: var(--grey400); 44 | } 45 | .grey500 { 46 | color: var(--grey500); 47 | } 48 | .grey600 { 49 | color: var(--grey600); 50 | } 51 | .grey700 { 52 | color: var(--grey700); 53 | } 54 | .grey800 { 55 | color: var(--grey800); 56 | } 57 | .grey900 { 58 | color: var(--grey900); 59 | } 60 | 61 | .ft-10 { 62 | font-size: 10px; 63 | } 64 | 65 | .ft-11 { 66 | font-size: 11px; 67 | } 68 | 69 | .ft-12 { 70 | font-size: 12px; 71 | } 72 | 73 | .ft-14 { 74 | font-size: 14px; 75 | } 76 | 77 | .ft-16 { 78 | font-size: 16px; 79 | } 80 | 81 | .ft-18 { 82 | font-size: 18px; 83 | } 84 | 85 | .ft-20 { 86 | font-size: 20px; 87 | } 88 | 89 | .ft-24 { 90 | font-size: 24px; 91 | } 92 | 93 | .ft-30 { 94 | font-size: 30px; 95 | } 96 | 97 | .ft-36 { 98 | font-size: 36px; 99 | } 100 | 101 | .ft-48 { 102 | font-size: 48px; 103 | } 104 | 105 | .vertical-center { 106 | display: flex; 107 | align-items: center; 108 | } 109 | 110 | body, 111 | button { 112 | font-family: "Josefin Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 113 | Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 114 | } 115 | 116 | body { 117 | margin: 0; 118 | padding: 0; 119 | height: 100vh; 120 | overflow-x: hidden; 121 | color: var(--default-text); 122 | } 123 | 124 | h1 { 125 | font-weight: 300; 126 | font-size: 23px; 127 | } 128 | 129 | .primary { 130 | color: var(--primary-color); 131 | } 132 | 133 | .pointer, 134 | a { 135 | cursor: pointer; 136 | } 137 | 138 | .uppercase { 139 | letter-spacing: 0.5px; 140 | text-transform: uppercase; 141 | } 142 | 143 | .margin-bottom { 144 | margin-bottom: 40px; 145 | } 146 | 147 | .padding-right { 148 | padding-right: 15px; 149 | } 150 | 151 | .right-padding { 152 | padding-right: 24px; 153 | } 154 | .left-padding { 155 | padding-left: 24px; 156 | } 157 | .left-spacing { 158 | padding-left: 12px; 159 | } 160 | 161 | .right-spacing { 162 | padding-right: 12px; 163 | } 164 | 165 | .inline-block { 166 | display: inline-block; 167 | } 168 | 169 | .capitalize { 170 | text-transform: capitalize; 171 | } 172 | 173 | .inline { 174 | display: inline-block; 175 | } 176 | 177 | .relative { 178 | position: relative; 179 | } 180 | 181 | .absolute { 182 | position: absolute; 183 | } 184 | 185 | .normal-weight { 186 | font-weight: 300; 187 | } 188 | 189 | svg { 190 | overflow: hidden; 191 | } 192 | 193 | .overflow-hidden .visualization-layer { 194 | position: inherit !important; 195 | } 196 | 197 | .overflow-visible svg, 198 | .overflow-visible { 199 | overflow: visible; 200 | } 201 | 202 | .flex { 203 | display: flex; 204 | } 205 | 206 | .App { 207 | width: 100vw; 208 | } 209 | 210 | .App-header { 211 | height: 50px; 212 | width: 100%; 213 | border-bottom: 1px solid #ddd; 214 | background: var(--grey600); 215 | text-transform: uppercase; 216 | } 217 | 218 | .bottom-panel { 219 | bottom: 0px; 220 | left: 0px; 221 | padding-left: 24px; 222 | width: 100%; 223 | background: white; 224 | height: 30vh; 225 | padding-bottom: 10px; 226 | position: sticky; 227 | border-top: solid 1px var(--grey800); 228 | } 229 | .bottom-panel.paper { 230 | box-shadow: none; 231 | } 232 | 233 | .App-header h1 { 234 | margin: 0; 235 | font-size: 16px; 236 | 237 | color: white; 238 | font-weight: 500; 239 | letter-spacing: 3px; 240 | } 241 | 242 | .App-header .nav { 243 | font-size: 15px; 244 | text-decoration: none; 245 | } 246 | 247 | .App-header .nav a { 248 | text-decoration: none; 249 | padding-right: 10px; 250 | font-weight: bold; 251 | color: black; 252 | } 253 | 254 | /* .flex.nav { 255 | align-content: center; 256 | } */ 257 | 258 | .page { 259 | width: 100vw; 260 | height: 100vh; 261 | overflow: hidden; 262 | } 263 | 264 | img.icon { 265 | margin-bottom: -3px; 266 | margin-right: 10px; 267 | height: 1em; 268 | } 269 | 270 | .baseline { 271 | align-items: center; 272 | } 273 | 274 | .logo { 275 | /* margin-top: 10px; */ 276 | margin-right: 10px; 277 | } 278 | 279 | .left-col { 280 | border-right: 1px solid #ddd; 281 | width: 30vw; 282 | } 283 | 284 | .margin-left { 285 | margin-left: 10px; 286 | } 287 | 288 | .panel { 289 | /* width: var(--panel-width); */ 290 | /* min-width: 260px; */ 291 | /* width: 30%; */ 292 | width: 25vw; 293 | border-right: 1px solid #ddd; 294 | padding-right: 20px; 295 | padding-left: 25px; 296 | padding-top: 20px; 297 | display: flex; 298 | flex-direction: column; 299 | overflow-y: auto; 300 | } 301 | 302 | .top-panel { 303 | border-bottom: 1px solid #ddd; 304 | max-height: 350px; 305 | overflow: hidden; 306 | } 307 | 308 | .panel.large { 309 | width: 75vw; 310 | } 311 | 312 | .panel.left-side { 313 | padding-left: var(--margin-left); 314 | } 315 | 316 | .no-border { 317 | border: none; 318 | } 319 | 320 | .no-margin { 321 | margin: 0px; 322 | } 323 | 324 | .top-border { 325 | border-top: 1px solid #ddd; 326 | } 327 | 328 | .padding { 329 | padding: 20px; 330 | } 331 | 332 | .scroll-y { 333 | overflow-y: auto; 334 | } 335 | 336 | .sticky { 337 | position: sticky; 338 | top: 0; 339 | background: white; 340 | z-index: 300; 341 | } 342 | 343 | .bottom { 344 | justify-content: flex-end; 345 | } 346 | 347 | .space-between { 348 | justify-content: space-between; 349 | } 350 | 351 | a { 352 | color: var(--link-color); 353 | } 354 | 355 | button { 356 | background: #ffffff; 357 | border-radius: 0px; 358 | border: 2px solid black; 359 | cursor: pointer; 360 | 361 | letter-spacing: 0.5px; 362 | text-transform: uppercase; 363 | font-size: 12px; 364 | line-height: 1.3em; 365 | 366 | /* font-weight: bold; */ 367 | position: relative; 368 | } 369 | 370 | button.import { 371 | padding: 20px 18px; 372 | } 373 | 374 | button:disabled { 375 | border: 2px solid var(--grey300); 376 | color: var(--grey300); 377 | } 378 | 379 | .upload-files-container button.copy-button, 380 | button.copy-button { 381 | background-image: url(/img/copy_icon.svg); 382 | background-size: cover; 383 | background-repeat: no-repeat; 384 | box-shadow: none; 385 | opacity: 0.2; 386 | width: 20px; 387 | float: right; 388 | height: 20px; 389 | min-width: 20px; 390 | } 391 | 392 | button.copy-button:hover { 393 | opacity: 0.4; 394 | } 395 | 396 | button.clear { 397 | box-shadow: none; 398 | } 399 | 400 | button.good { 401 | background: var(--primary-color); 402 | padding: 10px; 403 | color: white; 404 | } 405 | 406 | button.alert { 407 | color: #ff3733; 408 | } 409 | 410 | .resolved-message { 411 | margin: 10px; 412 | } 413 | 414 | .resolved-message, 415 | .resolved-message button { 416 | font-size: 1.2em; 417 | font-weight: 300; 418 | } 419 | 420 | .col-container { 421 | display: flex; 422 | } 423 | 424 | .col-container div { 425 | /* flex: 1; */ 426 | /* border-right: solid 2px transparent; 427 | border-left: solid 2px transparent; */ 428 | } 429 | 430 | .col-container .col-narrow { 431 | flex: 0.25; 432 | } 433 | 434 | .type-button { 435 | min-width: 140px; 436 | display: block; 437 | /* margin-bottom: 10px; */ 438 | overflow: hidden; 439 | } 440 | 441 | .webpack-logo { 442 | width: 150px; 443 | margin: -20px -14px; 444 | } 445 | 446 | img.rollup-logo { 447 | margin: -10px 0; 448 | } 449 | 450 | img.rome-logo { 451 | margin-right: 10px; 452 | margin-top: -10px; 453 | margin-bottom: -10px; 454 | zoom: 0.9; 455 | } 456 | 457 | img.parcel-logo { 458 | margin: -10px 0 -10px -2px; 459 | } 460 | 461 | button.project-import { 462 | text-align: left; 463 | width: 200px; 464 | height: 70px; 465 | margin-right: 12px; 466 | margin-bottom: 12px; 467 | /* font-size: 17px; */ 468 | display: flex; 469 | align-items: center; 470 | } 471 | 472 | .no-link-underline, 473 | .no-link-underline:hover { 474 | text-decoration: none; 475 | } 476 | 477 | pre { 478 | margin: 0; 479 | padding: 0; 480 | display: block; 481 | padding: 15px; 482 | border: solid 1px #ddd; 483 | position: relative; 484 | } 485 | 486 | code { 487 | font-size: 1em; 488 | background: #f5f5f5; 489 | } 490 | 491 | .add-diff { 492 | font-weight: bold; 493 | background: #bcffcf; 494 | } 495 | 496 | input[type="file"] { 497 | position: absolute; 498 | top: 0; 499 | left: 0; 500 | width: 100%; 501 | height: 100%; 502 | cursor: pointer; 503 | color: transparent; 504 | opacity: 0; 505 | } 506 | 507 | input[type="file"]::-webkit-file-upload-button { 508 | visibility: hidden; 509 | } 510 | 511 | input.search, 512 | button.clear { 513 | font-size: 15px; 514 | } 515 | 516 | .button-import-container { 517 | display: flex; 518 | margin-bottom: 13px; 519 | margin-right: 30px; 520 | } 521 | 522 | /* .import-project button { 523 | margin-top: 10px; 524 | width: 140px; 525 | } */ 526 | 527 | .button-import-container button:focus-within, 528 | .project-import:focus-within, 529 | a.active button, 530 | button:focus { 531 | box-shadow: 4px 4px 0px rgba(0, 0, 0, 1); 532 | outline: none; 533 | } 534 | 535 | .upload-files-container button { 536 | min-width: 140px; 537 | height: 40px; 538 | } 539 | 540 | .attach-icon { 541 | opacity: 0.4; 542 | } 543 | 544 | .status-icon { 545 | height: 24px; 546 | position: relative; 547 | top: 7px; 548 | margin-left: 15px; 549 | } 550 | 551 | .error { 552 | background: #ff988b; 553 | padding: 10px; 554 | border: solid 2px #c55c5c; 555 | white-space: pre-wrap; 556 | font-size: 12px; 557 | } 558 | 559 | .resolve-conflicts li { 560 | font-size: 11px; 561 | } 562 | 563 | .resolve-conflicts ul { 564 | /* height: 600px; */ 565 | overflow: auto; 566 | } 567 | 568 | .code-editor { 569 | width: 400px; 570 | height: 150px; 571 | } 572 | 573 | .opacity-filter.off { 574 | transition: opacity 0.3s; 575 | transition-delay: 0.2s; 576 | opacity: 0; 577 | } 578 | 579 | .opacity-filter.on { 580 | opacity: 0.6; 581 | } 582 | 583 | .treemap__node { 584 | position: absolute; 585 | border-radius: 2px; 586 | box-sizing: border-box; 587 | border-width: 1px; 588 | border-style: solid; 589 | border-color: rgb(105, 105, 105); 590 | border-radius: 2px; 591 | overflow: hidden; 592 | box-shadow: inset 1px 1px 0px 0px rgba(255, 255, 255, 0.5); 593 | } 594 | 595 | .treemap__label { 596 | box-sizing: border-box; 597 | font-size: 12px; 598 | white-space: nowrap; 599 | padding-left: 4px; 600 | transform: translate(1px, 1px); 601 | } 602 | 603 | .treemap__label--children { 604 | background-color: rgba(255, 255, 255, 0.15); 605 | } 606 | 607 | .treemap__label--children:hover { 608 | background-color: rgba(255, 255, 255, 0.3); 609 | cursor: pointer; 610 | } 611 | 612 | a { 613 | color: #007bff; 614 | text-decoration: none; 615 | background-color: transparent; 616 | } 617 | 618 | a:hover { 619 | color: #0056b3; 620 | text-decoration: underline; 621 | } 622 | /* 623 | table { 624 | border-spacing: 0px; 625 | } 626 | 627 | thead { 628 | text-align: left; 629 | } 630 | 631 | th { 632 | border-bottom: 1px solid #ccc; 633 | } 634 | 635 | th, 636 | td { 637 | padding: 3px; 638 | } */ 639 | 640 | .Table { 641 | width: calc(100% - 16px); 642 | border: 1px solid rgba(0, 0, 0, 0.1); 643 | border-collapse: collapse; 644 | position: relative; 645 | } 646 | 647 | .Table tr:hover { 648 | background: var(--grey100); 649 | } 650 | 651 | .Table tr.selected { 652 | background: var(--grey200); 653 | } 654 | 655 | .Table tr td { 656 | padding: 4px; 657 | font-size: 13px; 658 | padding-bottom: 10px; 659 | } 660 | 661 | .Table thead { 662 | border-bottom: 1px solid rgba(0, 0, 0, 0.05); 663 | box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.15); 664 | } 665 | 666 | .Table thead th.top { 667 | top: -4px; 668 | position: sticky; 669 | z-index: 100; 670 | } 671 | 672 | .Table thead th.bottom { 673 | font-weight: 500; 674 | text-transform: uppercase; 675 | font-size: 12px; 676 | letter-spacing: 0.5px; 677 | color: var(--grey700); 678 | position: sticky; 679 | top: calc(30vh + 40px); 680 | z-index: 100; 681 | padding: 5px 10px; 682 | } 683 | 684 | .Table th, 685 | .Table td { 686 | border-right: 1px solid rgba(0, 0, 0, 0.1); 687 | } 688 | 689 | .Table tbody td { 690 | border-bottom: 1px solid hsla(0, 0%, 0%, 0.1); 691 | } 692 | 693 | .Table tr.selected .name { 694 | font-weight: 700; 695 | } 696 | 697 | .Table th { 698 | font-size: 14px; 699 | background: var(--grey100); 700 | } 701 | 702 | .bar-chart .fixed-label { 703 | font-size: 12px; 704 | position: absolute; 705 | transform: translateX(-100%); 706 | white-space: nowrap; 707 | } 708 | 709 | .header-link, 710 | .header-link:visited, 711 | .header-link:hover { 712 | align-items: center; 713 | color: var(--default-text); 714 | text-decoration: none; 715 | } 716 | 717 | .import-asset { 718 | display: flex; 719 | align-items: center; 720 | } 721 | .github-corner:hover .octo-arm { 722 | animation: octocat-wave 560ms ease-in-out; 723 | } 724 | @keyframes octocat-wave { 725 | 0 %, 726 | 100 % { 727 | transform: rotate(0); 728 | } 729 | 20%, 730 | 60% { 731 | transform: rotate(-25deg); 732 | } 733 | 40%, 734 | 80% { 735 | transform: rotate(10deg); 736 | } 737 | } 738 | @media (max-width: 500px) { 739 | .github - corner:hover .octo-arm { 740 | animation: none; 741 | } 742 | .github-corner .octo-arm { 743 | animation: octocat-wave 560ms ease-in-out; 744 | } 745 | } 746 | 747 | .alpha-warning { 748 | margin-left: 20px; 749 | background: var(--grey400); 750 | padding: 4px; 751 | color: var(--grey800); 752 | } 753 | 754 | .button-wrap { 755 | display: inline-block; 756 | } 757 | 758 | .button-wrap button { 759 | display: inline-block; 760 | } 761 | 762 | .import-instruction { 763 | flex-direction: column; 764 | } 765 | 766 | .uppercase-header { 767 | letter-spacing: 2px; 768 | text-transform: uppercase; 769 | font-weight: 700; 770 | font-size: 18px; 771 | } 772 | 773 | .paper { 774 | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2); 775 | } 776 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | import * as serviceWorker from "./serviceWorker"; 6 | import "./index.css"; 7 | 8 | const rootEl = document.getElementById("root"); 9 | 10 | ReactDOM.render(, rootEl); 11 | if (module.hot) { 12 | module.hot.accept("./App", () => { 13 | const NextApp = require("./App").default; 14 | ReactDOM.render(, rootEl); 15 | }); 16 | } 17 | 18 | serviceWorker.register(); 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/report_error.ts: -------------------------------------------------------------------------------- 1 | export class ReportErrorUri { 2 | erroredFiles: string[] = []; 3 | errorBodies: { [file: string]: string } = {}; 4 | 5 | addError(fileName: string, error: Error) { 6 | this.erroredFiles.push(fileName); 7 | this.errorBodies[fileName] = `${error.message} 8 | ---- 9 | ${error.stack}`; 10 | } 11 | 12 | toUri() { 13 | const base = "https://github.com/samccone/bundle-buddy/issues/new"; 14 | const params = new URLSearchParams(); 15 | 16 | params.append("title", `Error from ${this.erroredFiles.join(" & ")}`); 17 | 18 | let body = ""; 19 | 20 | for (const filename of Object.keys(this.errorBodies)) { 21 | body += `\`${filename}\`:\n\`\`\`${this.errorBodies[filename]}\`\`\`\n`; 22 | } 23 | 24 | params.append("body", body); 25 | 26 | return `${base}?${params}`; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/resolve/Resolve.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { transform } from "./process"; 3 | import { ResolveProps, GraphEdges, ProcessedBundle } from "../types"; 4 | import { findTrims } from "./trim"; 5 | import { storeProcessedState, storeResolveState } from "../routes"; 6 | 7 | // noopener noreferrer 8 | 9 | export interface ResolveState { 10 | sourceMapFiles: string[]; 11 | transforms: { 12 | sourceMapFileTransform: (v: string) => string; 13 | graphFileTransform: (v: string) => string; 14 | }; 15 | graphFiles: string[]; 16 | resolveError?: string; 17 | } 18 | 19 | function toFunctionRef(func: string) { 20 | let ref: any; 21 | try { 22 | /* eslint-disable-next-line no-eval */ 23 | ref = eval(`(${func})`); 24 | } catch (e) { 25 | alert(`unable to compile transform due to ${e}`); 26 | } 27 | 28 | return ref; 29 | } 30 | 31 | function transformGraphNames( 32 | nodes: GraphEdges, 33 | graphTransform: (v: string) => string, 34 | trims: string[] 35 | ): GraphEdges { 36 | return nodes.map((n) => { 37 | n.source = graphTransform(trimClean(trims, n.source)); 38 | if (n.target != null) { 39 | n.target = graphTransform(trimClean(trims, n.target)); 40 | } 41 | return n; 42 | }); 43 | } 44 | 45 | function transformSourceMapNames( 46 | sourcemap: ProcessedBundle, 47 | sourcemapTransform: (v: string) => string, 48 | trims: string[] 49 | ): ProcessedBundle { 50 | const ret: ProcessedBundle = { 51 | files: {}, 52 | totalBytes: sourcemap.totalBytes, 53 | }; 54 | 55 | for (const fileName of Object.keys(sourcemap.files)) { 56 | ret.files[sourcemapTransform(trimClean(trims, fileName))] = 57 | sourcemap.files[fileName]; 58 | } 59 | 60 | return ret; 61 | } 62 | 63 | function getGraphFiles(graphEdges: GraphEdges) { 64 | const ret = new Set(); 65 | 66 | for (const edge of graphEdges) { 67 | ret.add(edge.source); 68 | if (edge.target) { 69 | ret.add(edge.target); 70 | } 71 | } 72 | 73 | return Array.from(ret); 74 | } 75 | 76 | function trimClean(trims: string[], word: string) { 77 | for (const t of trims) { 78 | if (word.startsWith(t)) { 79 | return word.slice(t.length); 80 | } 81 | } 82 | return word; 83 | } 84 | 85 | function autoclean(opts: { 86 | processedSourceMap: ProcessedBundle; 87 | graphEdges: GraphEdges; 88 | }): { sourceMapFiles: string[]; graphFiles: string[]; trims: string[] } { 89 | const sourceMapFiles = Object.keys(opts.processedSourceMap.files); 90 | const graphFiles = getGraphFiles(opts.graphEdges); 91 | const trims = Object.keys(findTrims(sourceMapFiles, graphFiles)); 92 | 93 | return { 94 | sourceMapFiles: sourceMapFiles.map((v) => trimClean(trims, v)), 95 | graphFiles: graphFiles.map((v) => trimClean(trims, v)), 96 | trims, 97 | }; 98 | } 99 | 100 | class Resolve extends Component { 101 | sourceMapTransformRef?: React.RefObject; 102 | sourceGraphTransformRef?: React.RefObject; 103 | 104 | state: ResolveState; 105 | trims: string[]; 106 | 107 | constructor(props: ResolveProps) { 108 | super(props); 109 | 110 | this.sourceMapTransformRef = React.createRef(); 111 | this.sourceGraphTransformRef = React.createRef(); 112 | const { sourceMapFiles, graphFiles, trims } = autoclean({ 113 | processedSourceMap: this.props.processedBundle, 114 | graphEdges: this.props.graphEdges, 115 | }); 116 | this.trims = trims; 117 | this.state = { 118 | sourceMapFiles, 119 | graphFiles, 120 | transforms: { 121 | sourceMapFileTransform: 122 | (props.sourceMapFileTransform && 123 | toFunctionRef(props.sourceMapFileTransform)) || 124 | ((fileName) => fileName), 125 | graphFileTransform: 126 | (props.graphFileTransform && 127 | toFunctionRef(props.graphFileTransform)) || 128 | ((fileName) => fileName), 129 | }, 130 | }; 131 | } 132 | 133 | static sorted(arr: Array) { 134 | const ret = Array.from(arr); 135 | ret.sort(); 136 | return ret; 137 | } 138 | 139 | transformFiles( 140 | a: Array, 141 | b: Array, 142 | aTransform: (v: T) => T, 143 | bTransform: (v: T) => T 144 | ): { files: T[]; lastError: undefined | Error } { 145 | let lastError: Error | undefined = undefined; 146 | const setA = new Set( 147 | a.map((v) => { 148 | try { 149 | return aTransform(v); 150 | } catch (e) { 151 | lastError = e; 152 | return v; 153 | } 154 | }) 155 | ); 156 | const setB = new Set( 157 | b.map((v) => { 158 | try { 159 | return bTransform(v); 160 | } catch (e) { 161 | lastError = e; 162 | return v; 163 | } 164 | }) 165 | ); 166 | 167 | const ret: Array = []; 168 | for (const v of setA) { 169 | if (!setB.has(v)) { 170 | ret.push(v); 171 | } 172 | } 173 | 174 | return { 175 | files: ret, 176 | lastError, 177 | }; 178 | } 179 | 180 | updateSourceMapTransform() { 181 | if ( 182 | this.sourceMapTransformRef != null && 183 | this.sourceMapTransformRef.current != null 184 | ) { 185 | const transformRef = toFunctionRef( 186 | this.sourceMapTransformRef.current.value 187 | ); 188 | if (transformRef == null) { 189 | return; 190 | } 191 | 192 | const k = storeResolveState({ 193 | graphEdges: this.props.graphEdges, 194 | processedSourceMap: this.props.processedBundle, 195 | graphFileTransform: this.state.transforms.graphFileTransform.toString(), 196 | bundledFilesTransform: transformRef.toString(), 197 | }); 198 | 199 | this.props.history.replace(window.location.pathname, k); 200 | 201 | this.setState({ 202 | transforms: { 203 | graphFileTransform: this.state.transforms.graphFileTransform, 204 | sourceMapFileTransform: transformRef, 205 | }, 206 | }); 207 | } 208 | } 209 | 210 | updateGraphSourceTransform() { 211 | if ( 212 | this.sourceGraphTransformRef != null && 213 | this.sourceGraphTransformRef.current != null 214 | ) { 215 | const transformRef = toFunctionRef( 216 | this.sourceGraphTransformRef.current.value 217 | ); 218 | if (transformRef == null) { 219 | return; 220 | } 221 | 222 | const k = storeResolveState({ 223 | graphEdges: this.props.graphEdges, 224 | processedSourceMap: this.props.processedBundle, 225 | graphFileTransform: transformRef.toString(), 226 | bundledFilesTransform: this.state.transforms.sourceMapFileTransform.toString(), 227 | }); 228 | 229 | this.props.history.replace(window.location.pathname, k); 230 | 231 | this.setState({ 232 | transforms: { 233 | graphFileTransform: transformRef, 234 | sourceMapFileTransform: this.state.transforms.sourceMapFileTransform, 235 | }, 236 | }); 237 | } 238 | } 239 | 240 | import() { 241 | if (this.props.graphEdges == null || this.props.processedBundle == null) { 242 | throw new Error("Unable to find graph edges or sourcemap data"); 243 | } 244 | 245 | const processed = transform( 246 | transformGraphNames( 247 | this.props.graphEdges, 248 | this.state.transforms.graphFileTransform, 249 | this.trims 250 | ), 251 | transformSourceMapNames( 252 | this.props.processedBundle, 253 | this.state.transforms.sourceMapFileTransform, 254 | this.trims 255 | ), 256 | this.state.sourceMapFiles 257 | ); 258 | 259 | this.props.history.push("/bundle", storeProcessedState(processed)); 260 | } 261 | 262 | formatError(e: Error) { 263 | return ` 264 | ${e.message} 265 | \n----------------\n 266 | ${e.stack}`; 267 | } 268 | 269 | render() { 270 | const sourceMapTransformed = this.transformFiles( 271 | this.state.sourceMapFiles, 272 | this.state.graphFiles, 273 | this.state.transforms.sourceMapFileTransform, 274 | this.state.transforms.graphFileTransform 275 | ); 276 | 277 | const graphTransformed = this.transformFiles( 278 | this.state.graphFiles, 279 | this.state.sourceMapFiles, 280 | this.state.transforms.graphFileTransform, 281 | this.state.transforms.sourceMapFileTransform 282 | ); 283 | return ( 284 |
285 |
286 |
287 |

Resolve source map files:

288 | {sourceMapTransformed.lastError != null ? ( 289 |
290 | {this.formatError(sourceMapTransformed.lastError)} 291 |
292 | ) : null} 293 | 294 | 299 | {sourceMapTransformed.files.length} 300 | {" "} 301 | source map files of {this.state.sourceMapFiles.length} total need 302 | resolving 303 | 304 |