first commit

This commit is contained in:
DanielRamirezGe
2020-01-14 20:43:08 -06:00
parent 3557894f7f
commit 5eecfbf6cd
26326 changed files with 2434803 additions and 0 deletions
+362
View File
@@ -0,0 +1,362 @@
# Changelog
> **Tags:**
> - [Breaking Change]
> - [New Feature]
> - [Improvement]
> - [Bug Fix]
> - [Internal]
> - [Documentation]
_Note: Gaps between patch versions are faulty, broken or test releases._
## UNRELEASED
<!-- Add changelog entries for new changes under this section -->
* **Improvement**
* Support webpack builds where `output.globalObject` is set to `'self'` ([#323](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/323) by [@lemonmade](https://github.com/lemonmade))
* Improve readability of tooltips ([#320](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/320) by [@lorenzos](https://github.com/lorenzos))
## 3.5.2
* **Bug Fix**
* Fix sidebar not showing visibility status of chunks hidden via popup menu (issue [#316](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/316) by [@gaokun](https://github.com/gaokun), fixed in [#317](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/317) by [@bregenspan](https://github.com/bregenspan))
## 3.5.1
* **Bug Fix**
* Fix regression in support of webpack dev server and `webpack --watch` (issue [#312](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/312), fixed in [#313](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/313) by [@gaokun](https://github.com/gaokun))
## 3.5.0
* **Improvements**
* Improved report title and added favicon ([#310](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/310), [@gaokun](https://github.com/gaokun))
## 3.4.1
* **Bug Fix**
* Fix regression of requiring an object to be passed to `new BundleAnalyzerPlugin()` (issue [#300](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/300), fixed in [#302](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/302) by [@jerryOnlyZRJ](https://github.com/jerryOnlyZRJ))
## 3.4.0
* **Improvements**
* Add `port: 'auto'` option ([#290](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/290), [@avin-kavish](https://github.com/avin-kavish))
* **Bug Fix**
* Avoid mutation of the generated `stats.json` ([#293](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/293), [@wood1986](https://github.com/wood1986))
* **Internal**
* Use Autoprefixer ([#266](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/266), [@bregenspan](https://github.com/bregenspan))
* Detect `AsyncMFS` to support dev-server of Nuxt 2.5 and above ([#275](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/275), [@amoshydra](https://github.com/amoshydra))
## 3.3.2
* **Bug Fix**
* Fix regression with escaping internal assets ([#264](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/264), fixes [#263](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/263))
## 3.3.1
* **Improvements**
* Use relative links for serving internal assets ([#261](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/261), fixes [#254](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/254))
* Properly escape embedded JS/JSON ([#262](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/262))
* **Bug Fix**
* Fix showing help message on `-h` flag ([#260](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/260), fixes [#239](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/239))
## 3.3.0
* **New Feature**
* Show/hide chunks using context menu ([#246](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/246), [@bregenspan](https://github.com/bregenspan))
* **Internal**
* Updated dev dependencies
## 3.2.0
* **Improvements**
* Add support for .mjs output files ([#252](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/252), [@jlopezxs](https://github.com/jlopezxs))
## 3.1.0
* **Bug Fix**
* Properly determine the size of the modules containing special characters ([#223](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/223), [@hulkish](https://github.com/hulkish))
* Update acorn to v6 ([#248](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/248), [@realityking](https://github.com/realityking))
## 3.0.4
* **Bug Fix**
* Make webpack's done hook wait until analyzer writes report or stat file ([#247](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/247), [@mareolan](https://github.com/mareolan))
## 3.0.3
* **Bug Fix**
* Disable viewer websocket connection when report is generated in `static` mode ([#215](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/215), [@sebastianhaeni](https://github.com/sebastianhaeni))
## 3.0.2
* **Improvements**
* Drop `@babel/runtime` dependency ([#209](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/209), [@realityking](https://github.com/realityking))
* Properly specify minimal Node.js version in `.babelrc` ([#209](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/209), [@realityking](https://github.com/realityking))
* **Bug Fix**
* Move some "dependencies" to "devDependencies" ([#209](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/209), [@realityking](https://github.com/realityking))
## 3.0.1
* **Bug Fix**
* Small UI fixes
## 3.0.0
* **Breaking change**
* Dropped support for Node.js v4. Minimal required version now is v6.14.4
* Contents of concatenated modules are now hidden by default because of a number of related issues ([details](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/188)), but can be shown using a new checkbox in the sidebar.
* **New Feature**
* Added modules search
* Added ability to pin and resize the sidebar
* Added button to toggle the sidebar
* Added checkbox to show/hide contents of concatenated modules
* **Improvements**
* Nested folders that contain only one child folder are now visually merged i.e. `folder1 => folder2 => file1` is now shown like `folder1/folder2 => file1` (thanks to [@varun-singh-1](https://github.com/varun-singh-1) for the idea)
* **Internal**
* Dropped support for Node.js v4
* Using MobX for state management
* Updated dependencies
## 2.13.1
* **Improvement**
* Pretty-format the generated stats.json ([#180](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/180)) [@edmorley](https://github.com/edmorley))
* **Bug Fix**
* Properly parse Webpack 4 async chunk with `Array.concat` optimization ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184), fixes [#183](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/183))
* **Internal**
* Refactor bundle parsing logic ([#184](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/184))
## 2.13.0
* **Improvement**
* Loosen bundle parsing logic ([#181](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/181)). Now analyzer will still show parsed sizes even if:
* It can't parse some bundle chunks. Those chunks just won't have content in the report. Fixes issues like [#160](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/160).
* Some bundle chunks are missing (it couldn't find files to parse). Those chunks just won't be visible in the report for parsed/gzipped sizes.
## 2.12.0
* **New Feature**
* Add option that allows to exclude assets from the report ([#178](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/178))
## 2.11.3
* **Bug Fix**
* Filter out modules that weren't found during bundles parsing ([#177](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/177))
## 2.11.2
* **Bug Fix**
* Properly process stat files that contain modules inside of `chunks` array ([#175](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/175))
* Fix parsing of async chunks that push to `this.webpackJsonp` array ([#176](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/176))
## 2.11.1
* **Improvement**
* Add support for parsing Webpack 4's chunked modules ([#159](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/159), [@jdelStrother](https://github.com/jdelStrother))
## 2.11.0
* **Improvement**
* Show contents of concatenated module (requires Webpack 4) ([#158](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/158), closes [#157](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/157))
## 2.10.1
* **Improvement**
* Support webpack 4 without deprecation warnings. @ai in [#156](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/156), fixes [#154](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/154)
## 2.10.0
* **Bug Fix**
* Fix "out of memory" crash when dealing with huge stats objects ([#129](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/129), [@ryan953](https://github.com/ryan953))
* **Internal**
* Update dependencies ([#146](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/146))
* Update gulp to v4 and simplify gulpfile ([#146](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/146), [#149](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/149))
* Simplify ESLint configs ([#148](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/148))
## 2.9.2
* **Bug Fix**
* Add a listener for the 'error' event on the WebSocket server client (#140)
* **Internal**
* Clean up .travis.yml (#140)
* Update ws to version 4.0.0 (#140)
## 2.9.1
* **Bug Fix**
* Bump `ws` dependency to fix DoS vulnerability (closes [#130](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/130))
## 2.9.0
* **New Feature**
* Show chunk sizes in sidebar (closes #91)
* **Bug Fix**
* Properly parse webpack bundles that use arrow functions as module wrappers (#108, @regiontog)
## 2.8.3
* **Bug Fix**
* Correctly advertise port when using a random one (#89, @yannickcr)
* Add proper support for `multi` entries (fixes #92, #87)
* Support parsing of ESNext features (fixes #94)
## 2.8.2
* **Improvement**
* Greatly improved accuracy of gzip sizes
* **Bug Fix**
* Generate report file in the bundle output directory when used with Webpack Dev Server (fixes #75)
## 2.8.1
* **Improvement**
* Improve warning message when analyzer client couldn't connect to WebSocket server
## 2.8.0
* **Improvement**
* Analyzer now supports `webpack --watch` and Webpack Dev Server!
It will automatically update modules treemap according to changes in the sources via WebSockets!
* **Internal**
* Use `babel-preset-env` and two different Babel configs to compile node and browser code
* Update deps
## 2.7.0
* **New Feature**
* Add control to sidebar that allows to choose shown chunks (closes #71 and partially addresses #38)
## 2.6.0
* **New Feature**
* Add `defaultSizes` option (closes #52)
## 2.5.0
* **New Feature**
* Added `--host` CLI option (@difelice)
## 2.4.1
* **Improvement**
* Support `NamedChunksPlugin` (@valscion)
## 2.4.0
* **Bug Fix**
* Fix `TypeError: currentFolder.addModule is not a function`
* **Internal**
* Update deps
## 2.3.1
* **Improvement**
* Improve compatibility with Webpack 2 (@valscion)
## 2.3.0
* **Improvement**
* Add `analyzerHost` option (@freaz)
* **Internal**
* Update deps
## 2.2.3
* **Bug Fix**
* Support bundles that uses `Array.concat` expression in modules definition (@valscion)
## 2.2.1
* **Bug Fix**
* Fix regression in analyzing stats files with non-empty `children` property (@gbakernet)
## 2.2.0
* **Improvement**
* Improve treemap sharpness on hi-res displays (fixes #33)
* Add support for stats files with all the information under `children` property (fixes #10)
* **Internal**
* Update deps
## 2.1.1
* **Improvement**
* Add support for `output.jsonpFunction` webpack config option (fixes #16)
## 2.1.0
* **New Feature**
* Add `logLevel` option (closes #19)
## 2.0.1
* **Bug Fix**
* Support query in bundle filenames (fixes #22)
* **Internal**
* Minimize CSS for report UI
## 2.0.0
* **New Feature**
* Analyzer now also shows gzipped sizes (closes #6)
* Added switcher that allows to choose what sizes will be used to generate tree map.
Just move your mouse to the left corner of the browser and settings sidebar will appear.
* **Bug Fix**
* Properly show sizes for some asset modules (e.g. CSS files loaded with `css-loader`)
* **Internal**
* Completely rewritten analyzer UI. Now uses Preact and Webpack 2.
## 1.5.4
* **Bug Fix**
* Fix bug when Webpack build is being controlled by some wrapper like `grunt-webpack` (see #21)
## 1.5.3
* **Bug Fix**
* Workaround `Express` bug that caused wrong `ejs` version to be used as view engine (fixes #17)
## 1.5.2
* **Bug Fix**
* Support array module descriptors that can be generated if `DedupePlugin` is used (fixes #4)
## 1.5.1
* **Internal**
* Plug analyzer to Webpack compiler `done` event instead of `emit`. Should fix #15.
## 1.5.0
* **New Feature**
* Add `statsOptions` option for `BundleAnalyzerPlugin`
## 1.4.2
* **Bug Fix**
* Fix "Unable to find bundle asset" error when bundle name starts with `/` (fixes #3)
## 1.4.1
* **Bug Fix**
* Add partial support for `DedupePlugin` (see #4 for more info)
## 1.4.0
* **New Feature**
* Add "static report" mode (closes #2)
## 1.3.0
* **Improvement**
* Add `startAnalyzer` option for `BundleAnalyzerPlugin` (fixes #1)
* **Internal**
* Make module much slimmer - remove/replace bloated dependencies
## 1.2.5
* Initial public release
+20
View File
@@ -0,0 +1,20 @@
Copyright JS Foundation and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+218
View File
@@ -0,0 +1,218 @@
[![npm][npm]][npm-url]
[![node][node]][node-url]
[![deps][deps]][deps-url]
[![tests][tests]][tests-url]
[![downloads][downloads]][downloads-url]
<div align="center">
<a href="https://github.com/webpack/webpack">
<img width="200" height="200"
src="https://webpack.js.org/assets/icon-square-big.svg">
</a>
<h1>Webpack Bundle Analyzer</h1>
<p>Visualize size of webpack output files with an interactive zoomable treemap.</p>
</div>
<h2 align="center">Install</h2>
```bash
# NPM
npm install --save-dev webpack-bundle-analyzer
# Yarn
yarn add -D webpack-bundle-analyzer
```
<h2 align="center">Usage (as a plugin)</h2>
```js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
```
It will create an interactive treemap visualization of the contents of all your bundles.
![webpack bundle analyzer zoomable treemap](https://cloud.githubusercontent.com/assets/302213/20628702/93f72404-b338-11e6-92d4-9a365550a701.gif)
This module will help you:
1. Realize what's *really* inside your bundle
2. Find out what modules make up the most of its size
3. Find modules that got there by mistake
4. Optimize it!
And the best thing is it supports minified bundles! It parses them to get real size of bundled modules.
And it also shows their gzipped sizes!
<h2 align="center">Options (for plugin)</h2>
```js
new BundleAnalyzerPlugin(options?: object)
```
|Name|Type|Description|
|:--:|:--:|:----------|
|**`analyzerMode`**|One of: `server`, `static`, `disabled`|Default: `server`. In `server` mode analyzer will start HTTP server to show bundle report. In `static` mode single HTML file with bundle report will be generated. In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`. |
|**`analyzerHost`**|`{String}`|Default: `127.0.0.1`. Host that will be used in `server` mode to start HTTP server.|
|**`analyzerPort`**|`{Number}` or `auto`|Default: `8888`. Port that will be used in `server` mode to start HTTP server.|
|**`reportFilename`**|`{String}`|Default: `report.html`. Path to bundle report file that will be generated in `static` mode. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|**`defaultSizes`**|One of: `stat`, `parsed`, `gzip`|Default: `parsed`. Module sizes to show in report by default. [Size definitions](#size-definitions) section describes what these values mean.|
|**`openAnalyzer`**|`{Boolean}`|Default: `true`. Automatically open report in default browser.|
|**`generateStatsFile`**|`{Boolean}`|Default: `false`. If `true`, webpack stats JSON file will be generated in bundle output directory|
|**`statsFilename`**|`{String}`|Default: `stats.json`. Name of webpack stats JSON file that will be generated if `generateStatsFile` is `true`. It can be either an absolute path or a path relative to a bundle output directory (which is output.path in webpack config).|
|**`statsOptions`**|`null` or `{Object}`|Default: `null`. Options for `stats.toJson()` method. For example you can exclude sources of your modules from stats file with `source: false` option. [See more options here](https://webpack.js.org/configuration/stats/). |
|**`excludeAssets`**|`{null\|pattern\|pattern[]}` where `pattern` equals to `{String\|RegExp\|function}`|Default: `null`. Patterns that will be used to match against asset names to exclude them from the report. If pattern is a string it will be converted to RegExp via `new RegExp(str)`. If pattern is a function it should have the following signature `(assetName: string) => boolean` and should return `true` to *exclude* matching asset. If multiple patterns are provided asset should match at least one of them to be excluded. |
|**`logLevel`**|One of: `info`, `warn`, `error`, `silent`|Default: `info`. Used to control how much details the plugin outputs.|
<h2 align="center">Usage (as a CLI utility)</h2>
You can analyze an existing bundle if you have a webpack stats JSON file.
You can generate it using `BundleAnalyzerPlugin` with `generateStatsFile` option set to `true` or with this simple
command:
```bash
webpack --profile --json > stats.json
```
If you're on Windows and using PowerShell, you can generate the stats file with this command to [avoid BOM issues](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/47):
```
webpack --profile --json | Out-file 'stats.json' -Encoding OEM
```
Then you can run the CLI tool.
```
webpack-bundle-analyzer bundle/output/path/stats.json
```
<h2 align="center">Options (for CLI)</h2>
```bash
webpack-bundle-analyzer <bundleStatsFile> [bundleDir] [options]
```
Arguments are documented below:
### `bundleStatsFile`
Path to webpack stats JSON file
### `bundleDir`
Directory containing all generated bundles.
### `options`
```
-V, --version output the version number
-m, --mode <mode> Analyzer mode. Should be `server` or `static`.
In `server` mode analyzer will start HTTP server to show bundle report.
In `static` mode single HTML file with bundle report will be generated. (default: server)
-h, --host <host> Host that will be used in `server` mode to start HTTP server. (default: 127.0.0.1)
-p, --port <n> Port that will be used in `server` mode to start HTTP server. Should be a number or `auto` (default: 8888)
-r, --report <file> Path to bundle report file that will be generated in `static` mode. (default: report.html)
-s, --default-sizes <type> Module sizes to show in treemap by default.
Possible values: stat, parsed, gzip (default: parsed)
-O, --no-open Don't open report in default browser automatically.
-e, --exclude <regexp> Assets that should be excluded from the report.
Can be specified multiple times.
-l, --log-level <level> Log level.
Possible values: debug, info, warn, error, silent (default: info)
-h, --help output usage information
```
<h2 align="center" id="size-definitions">Size definitions</h2>
webpack-bundle-analyzer reports three values for sizes. `defaultSizes` can be used to control which of these is shown by default. The different reported sizes are:
### `stat`
This is the "input" size of your files, before any transformations like
minification.
It is called "stat size" because it's obtained from Webpack's
[stats object](https://webpack.js.org/configuration/stats/).
### `parsed`
This is the "output" size of your files. If you're using a Webpack plugin such
as Uglify, then this value will reflect the minified size of your code.
### `gzip`
This is the size of running the parsed bundles/modules through gzip compression.
<h2 align="center">Selecting Which Chunks to Display</h2>
When opened, the report displays all of the Webpack chunks for your project. It's possible to filter to a more specific list of chunks by using the sidebar or the chunk context menu.
### Sidebar
The Sidebar Menu can be opened by clicking the `>` button at the top left of the report. You can select or deselect chunks to display under the "Show chunks" heading there.
### Chunk Context Menu
The Chunk Context Menu can be opened by right-clicking or `Ctrl`-clicking on a specific chunk in the report. It provides the following options:
* **Hide chunk:** Hides the selected chunk
* **Hide all other chunks:** Hides all chunks besides the selected one
* **Show all chunks:** Un-hides any hidden chunks, returning the report to its initial, unfiltered view
<h2 align="center">Troubleshooting</h2>
### I don't see `gzip` or `parsed` sizes, it only shows `stat` size
It happens when `webpack-bundle-analyzer` analyzes files that don't actually exist in your file system, for example when you work with `webpack-dev-server` that keeps all the files in RAM. If you use `webpack-bundle-analyzer` as a plugin you won't get any errors, however if you run it via CLI you get the error message in terminal:
```
Couldn't parse bundle asset "your_bundle_name.bundle.js".
Analyzer will use module sizes from stats file.
```
To get more information about it you can read [issue #147](https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/147).
<h2 align="center">Maintainers</h2>
<table>
<tbody>
<tr>
<td align="center">
<img width="150" height="150"
src="https://avatars3.githubusercontent.com/u/302213?v=4&s=150">
</br>
<a href="https://github.com/th0r">Yuriy Grunin</a>
</td>
<td align="center">
<img width="150" height="150"
src="https://avatars3.githubusercontent.com/u/482561?v=4&s=150">
</br>
<a href="https://github.com/valscion">Vesa Laakso</a>
</td>
</tr>
<tbody>
</table>
[npm]: https://img.shields.io/npm/v/webpack-bundle-analyzer.svg
[npm-url]: https://npmjs.com/package/webpack-bundle-analyzer
[node]: https://img.shields.io/node/v/webpack-bundle-analyzer.svg
[node-url]: https://nodejs.org
[deps]: https://david-dm.org/webpack-contrib/webpack-bundle-analyzer.svg
[deps-url]: https://david-dm.org/webpack-contrib/webpack-bundle-analyzer
[tests]: http://img.shields.io/travis/webpack-contrib/webpack-bundle-analyzer.svg
[tests-url]: https://travis-ci.org/webpack-contrib/webpack-bundle-analyzer
[downloads]: https://img.shields.io/npm/dt/webpack-bundle-analyzer.svg
[downloads-url]: https://npmjs.com/package/webpack-bundle-analyzer
<h2 align="center">Contributing</h2>
Check out [CONTRIBUTING.md](./CONTRIBUTING.md) for instructions on contributing :tada:
+172
View File
@@ -0,0 +1,172 @@
"use strict";
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
const bfj = require('bfj');
const path = require('path');
const mkdir = require('mkdirp');
const {
bold
} = require('chalk');
const Logger = require('./Logger');
const viewer = require('./viewer');
class BundleAnalyzerPlugin {
constructor(opts = {}) {
this.opts = _objectSpread({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
excludeAssets: null,
logLevel: 'info',
// deprecated
startAnalyzer: true
}, opts, {
analyzerPort: 'analyzerPort' in opts ? opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort : 8888
});
this.server = null;
this.logger = new Logger(this.opts.logLevel);
}
apply(compiler) {
this.compiler = compiler;
const done = (stats, callback) => {
callback = callback || (() => {});
const actions = [];
if (this.opts.generateStatsFile) {
actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions)));
} // Handling deprecated `startAnalyzer` flag
if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) {
this.opts.analyzerMode = 'disabled';
}
if (this.opts.analyzerMode === 'server') {
actions.push(() => this.startAnalyzerServer(stats.toJson()));
} else if (this.opts.analyzerMode === 'static') {
actions.push(() => this.generateStaticReport(stats.toJson()));
}
if (actions.length) {
// Making analyzer logs to be after all webpack logs in the console
setImmediate(
/*#__PURE__*/
_asyncToGenerator(function* () {
try {
yield Promise.all(actions.map(action => action()));
callback();
} catch (e) {
callback(e);
}
}));
} else {
callback();
}
};
if (compiler.hooks) {
compiler.hooks.done.tapAsync('webpack-bundle-analyzer', done);
} else {
compiler.plugin('done', done);
}
}
generateStatsFile(stats) {
var _this = this;
return _asyncToGenerator(function* () {
const statsFilepath = path.resolve(_this.compiler.outputPath, _this.opts.statsFilename);
mkdir.sync(path.dirname(statsFilepath));
try {
yield bfj.write(statsFilepath, stats, {
space: 2,
promises: 'ignore',
buffers: 'ignore',
maps: 'ignore',
iterables: 'ignore',
circular: 'ignore'
});
_this.logger.info(`${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`);
} catch (error) {
_this.logger.error(`${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`);
}
})();
}
startAnalyzerServer(stats) {
var _this2 = this;
return _asyncToGenerator(function* () {
if (_this2.server) {
(yield _this2.server).updateChartData(stats);
} else {
_this2.server = viewer.startServer(stats, {
openBrowser: _this2.opts.openAnalyzer,
host: _this2.opts.analyzerHost,
port: _this2.opts.analyzerPort,
bundleDir: _this2.getBundleDirFromCompiler(),
logger: _this2.logger,
defaultSizes: _this2.opts.defaultSizes,
excludeAssets: _this2.opts.excludeAssets
});
}
})();
}
generateStaticReport(stats) {
var _this3 = this;
return _asyncToGenerator(function* () {
yield viewer.generateReport(stats, {
openBrowser: _this3.opts.openAnalyzer,
reportFilename: path.resolve(_this3.compiler.outputPath, _this3.opts.reportFilename),
bundleDir: _this3.getBundleDirFromCompiler(),
logger: _this3.logger,
defaultSizes: _this3.opts.defaultSizes,
excludeAssets: _this3.opts.excludeAssets
});
})();
}
getBundleDirFromCompiler() {
switch (this.compiler.outputFileSystem.constructor.name) {
case 'MemoryFileSystem':
return null;
// Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development
// Related: #274
case 'AsyncMFS':
return null;
default:
return this.compiler.outputPath;
}
}
}
module.exports = BundleAnalyzerPlugin;
+38
View File
@@ -0,0 +1,38 @@
"use strict";
const LEVELS = ['debug', 'info', 'warn', 'error', 'silent'];
const LEVEL_TO_CONSOLE_METHOD = new Map([['debug', 'log'], ['info', 'log'], ['warn', 'log']]);
class Logger {
constructor(level = Logger.defaultLevel) {
this.activeLevels = new Set();
this.setLogLevel(level);
}
setLogLevel(level) {
const levelIndex = LEVELS.indexOf(level);
if (levelIndex === -1) throw new Error(`Invalid log level "${level}". Use one of these: ${LEVELS.join(', ')}`);
this.activeLevels.clear();
for (const [i, level] of LEVELS.entries()) {
if (i >= levelIndex) this.activeLevels.add(level);
}
}
_log(level, ...args) {
console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args);
}
}
Logger.levels = LEVELS;
Logger.defaultLevel = 'info';
;
LEVELS.forEach(level => {
if (level === 'silent') return;
Logger.prototype[level] = function (...args) {
if (this.activeLevels.has(level)) this._log(level, ...args);
};
});
module.exports = Logger;
+135
View File
@@ -0,0 +1,135 @@
"use strict";
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const gzipSize = require('gzip-size');
const Logger = require('./Logger');
const Folder = require('./tree/Folder').default;
const {
parseBundle
} = require('./parseUtils');
const {
createAssetsFilter
} = require('./utils');
const FILENAME_QUERY_REGEXP = /\?[\0-\t\x0B\f\x0E-\u2027\u202A-\u{10FFFF}]*$/u;
const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
module.exports = {
getViewerData,
readStatsFromFile
};
function getViewerData(bundleStats, bundleDir, opts) {
const {
logger = new Logger(),
excludeAssets = null
} = opts || {};
const isAssetIncluded = createAssetsFilter(excludeAssets); // Sometimes all the information is located in `children` array (e.g. problem in #10)
if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
bundleStats = bundleStats.children[0];
} // Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
bundleStats.assets = _.filter(bundleStats.assets, asset => {
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
// See #22
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
}); // Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
let bundlesSources = null;
let parsedModules = null;
if (bundleDir) {
bundlesSources = {};
parsedModules = {};
for (const statAsset of bundleStats.assets) {
const assetFile = path.join(bundleDir, statAsset.name);
let bundleInfo;
try {
bundleInfo = parseBundle(assetFile);
} catch (err) {
const msg = err.code === 'ENOENT' ? 'no such file' : err.message;
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`);
continue;
}
bundlesSources[statAsset.name] = bundleInfo.src;
_.assign(parsedModules, bundleInfo.modules);
}
if (_.isEmpty(bundlesSources)) {
bundlesSources = null;
parsedModules = null;
logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
}
}
const modules = getBundleModules(bundleStats);
const assets = _.transform(bundleStats.assets, (result, statAsset) => {
const asset = result[statAsset.name] = _.pick(statAsset, 'size');
if (bundlesSources && _.has(bundlesSources, statAsset.name)) {
asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name]);
asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]);
} // Picking modules from current bundle script
asset.modules = _(modules).filter(statModule => assetHasModule(statAsset, statModule)).each(statModule => {
if (parsedModules) {
statModule.parsedSrc = parsedModules[statModule.id];
}
});
asset.tree = createModulesTree(asset.modules);
}, {});
return _.transform(assets, (result, asset, filename) => {
result.push({
label: filename,
isAsset: true,
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
// be the size of minified bundle.
// Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
groups: _.invokeMap(asset.tree.children, 'toChartData')
});
}, []);
}
function readStatsFromFile(filename) {
return JSON.parse(fs.readFileSync(filename, 'utf8'));
}
function getBundleModules(bundleStats) {
return _(bundleStats.chunks).map('modules').concat(bundleStats.modules).compact().flatten().uniqBy('id').value();
}
function assetHasModule(statAsset, statModule) {
// Checking if this module is the part of asset chunks
return _.some(statModule.chunks, moduleChunk => _.includes(statAsset.chunks, moduleChunk));
}
function createModulesTree(modules) {
const root = new Folder('.');
_.each(modules, module => root.addModule(module));
root.mergeNestedFolders();
return root;
}
+105
View File
@@ -0,0 +1,105 @@
#! /usr/bin/env node
"use strict";
const {
resolve,
dirname
} = require('path');
const _ = require('lodash');
const commander = require('commander');
const {
magenta
} = require('chalk');
const analyzer = require('../analyzer');
const viewer = require('../viewer');
const Logger = require('../Logger');
const SIZES = new Set(['stat', 'parsed', 'gzip']);
const program = commander.version(require('../../package.json').version).usage(`<bundleStatsFile> [bundleDir] [options]
Arguments:
bundleStatsFile Path to Webpack Stats JSON file.
bundleDir Directory containing all generated bundles.
You should provided it if you want analyzer to show you the real parsed module sizes.
By default a directory of stats file is used.`).option('-m, --mode <mode>', 'Analyzer mode. Should be `server` or `static`.' + br('In `server` mode analyzer will start HTTP server to show bundle report.') + br('In `static` mode single HTML file with bundle report will be generated.'), 'server').option( // Had to make `host` parameter optional in order to let `-h` flag output help message
// Fixes https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/239
'-h, --host [host]', 'Host that will be used in `server` mode to start HTTP server.', '127.0.0.1').option('-p, --port <n>', 'Port that will be used in `server` mode to start HTTP server.', 8888).option('-r, --report <file>', 'Path to bundle report file that will be generated in `static` mode.', 'report.html').option('-s, --default-sizes <type>', 'Module sizes to show in treemap by default.' + br(`Possible values: ${[...SIZES].join(', ')}`), 'parsed').option('-O, --no-open', "Don't open report in default browser automatically.").option('-e, --exclude <regexp>', 'Assets that should be excluded from the report.' + br('Can be specified multiple times.'), array()).option('-l, --log-level <level>', 'Log level.' + br(`Possible values: ${[...Logger.levels].join(', ')}`), Logger.defaultLevel).parse(process.argv);
let {
mode,
host,
port,
report: reportFilename,
defaultSizes,
logLevel,
open: openBrowser,
exclude: excludeAssets,
args: [bundleStatsFile, bundleDir]
} = program;
const logger = new Logger(logLevel);
if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument');
if (mode !== 'server' && mode !== 'static') showHelp('Invalid mode. Should be either `server` or `static`.');
if (mode === 'server') {
if (!host) showHelp('Invalid host name');
port = port === 'auto' ? 0 : Number(port);
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
}
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
bundleStatsFile = resolve(bundleStatsFile);
if (!bundleDir) bundleDir = dirname(bundleStatsFile);
let bundleStats;
try {
bundleStats = analyzer.readStatsFromFile(bundleStatsFile);
} catch (err) {
logger.error(`Couldn't read webpack bundle stats from "${bundleStatsFile}":\n${err}`);
logger.debug(err.stack);
process.exit(1);
}
if (mode === 'server') {
viewer.startServer(bundleStats, {
openBrowser,
port,
host,
defaultSizes,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
});
} else {
viewer.generateReport(bundleStats, {
openBrowser,
reportFilename: resolve(reportFilename),
defaultSizes,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
});
}
function showHelp(error) {
if (error) console.log(`\n ${magenta(error)}\n`);
program.outputHelp();
process.exit(1);
}
function br(str) {
return `\n${_.repeat(' ', 28)}${str}`;
}
function array() {
const arr = [];
return val => {
arr.push(val);
return arr;
};
}
+10
View File
@@ -0,0 +1,10 @@
"use strict";
const {
start
} = require('./viewer');
module.exports = {
start,
BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin')
};
+174
View File
@@ -0,0 +1,174 @@
"use strict";
const fs = require('fs');
const _ = require('lodash');
const acorn = require('acorn');
const walk = require('acorn-walk');
module.exports = {
parseBundle
};
function parseBundle(bundlePath) {
const content = fs.readFileSync(bundlePath, 'utf8');
const ast = acorn.parse(content, {
sourceType: 'script',
// I believe in a bright future of ECMAScript!
// Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
// Seems like `acorn` supports such weird option value.
ecmaVersion: 2050
});
const walkState = {
locations: null
};
walk.recursive(ast, walkState, {
CallExpression(node, state, c) {
if (state.locations) return;
const args = node.arguments; // Main chunk with webpack loader.
// Modules are stored in first argument:
// (function (...) {...})(<modules>)
if (node.callee.type === 'FunctionExpression' && !node.callee.id && args.length === 1 && isSimpleModulesList(args[0])) {
state.locations = getModulesLocations(args[0]);
return;
} // Async Webpack < v4 chunk without webpack loader.
// webpackJsonp([<chunks>], <modules>, ...)
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
if (node.callee.type === 'Identifier' && mayBeAsyncChunkArguments(args) && isModulesList(args[1])) {
state.locations = getModulesLocations(args[1]);
return;
} // Async Webpack v4 chunk without webpack loader.
// (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
if (isAsyncChunkPushExpression(node)) {
state.locations = getModulesLocations(args[0].elements[1]);
return;
} // Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
// features (e.g. `umd` library output) can wrap modules list into additional IIFE.
_.each(args, arg => c(arg, state));
}
});
let modules;
if (walkState.locations) {
modules = _.mapValues(walkState.locations, loc => content.slice(loc.start, loc.end));
} else {
modules = {};
}
return {
src: content,
modules
};
}
function isModulesList(node) {
return isSimpleModulesList(node) || // Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
isOptimizedModulesArray(node);
}
function isSimpleModulesList(node) {
return (// Modules are contained in hash. Keys are module ids.
isModulesHash(node) || // Modules are contained in array. Indexes are module ids.
isModulesArray(node)
);
}
function isModulesHash(node) {
return node.type === 'ObjectExpression' && _(node.properties).map('value').every(isModuleWrapper);
}
function isModulesArray(node) {
return node.type === 'ArrayExpression' && _.every(node.elements, elem => // Some of array items may be skipped because there is no module with such id
!elem || isModuleWrapper(elem));
}
function isOptimizedModulesArray(node) {
// Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
// https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
// The `<minimum ID>` + array indexes are module ids
return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && // Make sure the object called is `Array(<some number>)`
node.callee.object.type === 'CallExpression' && node.callee.object.callee.type === 'Identifier' && node.callee.object.callee.name === 'Array' && node.callee.object.arguments.length === 1 && isNumericId(node.callee.object.arguments[0]) && // Make sure the property X called for `Array(<some number>).X` is `concat`
node.callee.property.type === 'Identifier' && node.callee.property.name === 'concat' && // Make sure exactly one array is passed in to `concat`
node.arguments.length === 1 && isModulesArray(node.arguments[0]);
}
function isModuleWrapper(node) {
return (// It's an anonymous function expression that wraps module
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id || // If `DedupePlugin` is used it can be an ID of duplicated module...
isModuleId(node) || // or an array of shape [<module_id>, ...args]
node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0])
);
}
function isModuleId(node) {
return node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string');
}
function isNumericId(node) {
return node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0;
}
function isChunkIds(node) {
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
return node.type === 'ArrayExpression' && _.every(node.elements, isModuleId);
}
function isAsyncChunkPushExpression(node) {
const {
callee,
arguments: args
} = node;
return callee.type === 'MemberExpression' && callee.property.name === 'push' && callee.object.type === 'AssignmentExpression' && callee.object.left.object && (callee.object.left.object.name === 'window' || // `self` is a common output.globalObject value used to support both workers and browsers
callee.object.left.object.name === 'self' || // Webpack 4 uses `this` instead of `window`
callee.object.left.object.type === 'ThisExpression') && args.length === 1 && args[0].type === 'ArrayExpression' && mayBeAsyncChunkArguments(args[0].elements) && isModulesList(args[0].elements[1]);
}
function mayBeAsyncChunkArguments(args) {
return args.length >= 2 && isChunkIds(args[0]);
}
function getModulesLocations(node) {
if (node.type === 'ObjectExpression') {
// Modules hash
const modulesNodes = node.properties;
return _.transform(modulesNodes, (result, moduleNode) => {
const moduleId = moduleNode.key.name || moduleNode.key.value;
result[moduleId] = getModuleLocation(moduleNode.value);
}, {});
}
const isOptimizedArray = node.type === 'CallExpression';
if (node.type === 'ArrayExpression' || isOptimizedArray) {
// Modules array or optimized array
const minId = isOptimizedArray ? // Get the [minId] value from the Array() call first argument literal value
node.callee.object.arguments[0].value : // `0` for simple array
0;
const modulesNodes = isOptimizedArray ? // The modules reside in the `concat()` function call arguments
node.arguments[0].elements : node.elements;
return _.transform(modulesNodes, (result, moduleNode, i) => {
if (!moduleNode) return;
result[i + minId] = getModuleLocation(moduleNode);
}, {});
}
return {};
}
function getModuleLocation(node) {
return {
start: node.start,
end: node.end
};
}
+129
View File
@@ -0,0 +1,129 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = _interopRequireDefault(require("lodash"));
var _Node = _interopRequireDefault(require("./Node"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class BaseFolder extends _Node.default {
constructor(name, parent) {
super(name, parent);
this.children = Object.create(null);
}
get src() {
if (!_lodash.default.has(this, '_src')) {
this._src = this.walk((node, src) => src += node.src || '', '', false);
}
return this._src;
}
get size() {
if (!_lodash.default.has(this, '_size')) {
this._size = this.walk((node, size) => size + node.size, 0, false);
}
return this._size;
}
getChild(name) {
return this.children[name];
}
addChildModule(module) {
const {
name
} = module;
const currentChild = this.children[name]; // For some reason we already have this node in children and it's a folder.
if (currentChild && currentChild instanceof BaseFolder) return;
if (currentChild) {
// We already have this node in children and it's a module.
// Merging it's data.
currentChild.mergeData(module.data);
} else {
// Pushing new module
module.parent = this;
this.children[name] = module;
}
delete this._size;
delete this._src;
}
addChildFolder(folder) {
folder.parent = this;
this.children[folder.name] = folder;
delete this._size;
delete this._src;
return folder;
}
walk(walker, state = {}, deep = true) {
let stopped = false;
_lodash.default.each(this.children, child => {
if (deep && child.walk) {
state = child.walk(walker, state, stop);
} else {
state = walker(child, state, stop);
}
if (stopped) return false;
});
return state;
function stop(finalState) {
stopped = true;
return finalState;
}
}
mergeNestedFolders() {
if (!this.isRoot) {
let childNames;
while ((childNames = Object.keys(this.children)).length === 1) {
const childName = childNames[0];
const onlyChild = this.children[childName];
if (onlyChild instanceof this.constructor) {
this.name += `/${onlyChild.name}`;
this.children = onlyChild.children;
} else {
break;
}
}
}
this.walk(child => {
child.parent = this;
if (child.mergeNestedFolders) {
child.mergeNestedFolders();
}
}, null, false);
}
toChartData() {
return {
label: this.name,
path: this.path,
statSize: this.size,
groups: _lodash.default.invokeMap(this.children, 'toChartData')
};
}
}
exports.default = BaseFolder;
;
+91
View File
@@ -0,0 +1,91 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = _interopRequireDefault(require("lodash"));
var _Module = _interopRequireDefault(require("./Module"));
var _ContentModule = _interopRequireDefault(require("./ContentModule"));
var _ContentFolder = _interopRequireDefault(require("./ContentFolder"));
var _utils = require("./utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class ConcatenatedModule extends _Module.default {
constructor(name, data, parent) {
super(name, data, parent);
this.name += ' (concatenated)';
this.children = Object.create(null);
this.fillContentModules();
}
fillContentModules() {
_lodash.default.each(this.data.modules, moduleData => this.addContentModule(moduleData));
}
addContentModule(moduleData) {
const pathParts = (0, _utils.getModulePathParts)(moduleData);
if (!pathParts) {
return;
}
const [folders, fileName] = [pathParts.slice(0, -1), _lodash.default.last(pathParts)];
let currentFolder = this;
_lodash.default.each(folders, folderName => {
let childFolder = currentFolder.getChild(folderName);
if (!childFolder) {
childFolder = currentFolder.addChildFolder(new _ContentFolder.default(folderName, this));
}
currentFolder = childFolder;
});
const module = new _ContentModule.default(fileName, moduleData, this);
currentFolder.addChildModule(module);
}
getChild(name) {
return this.children[name];
}
addChildModule(module) {
module.parent = this;
this.children[module.name] = module;
}
addChildFolder(folder) {
folder.parent = this;
this.children[folder.name] = folder;
return folder;
}
mergeNestedFolders() {
_lodash.default.invokeMap(this.children, 'mergeNestedFolders');
}
toChartData() {
return _objectSpread({}, super.toChartData(), {
concatenated: true,
groups: _lodash.default.invokeMap(this.children, 'toChartData')
});
}
}
exports.default = ConcatenatedModule;
;
+51
View File
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _BaseFolder = _interopRequireDefault(require("./BaseFolder"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class ContentFolder extends _BaseFolder.default {
constructor(name, ownerModule, parent) {
super(name, parent);
this.ownerModule = ownerModule;
}
get parsedSize() {
return this.getSize('parsedSize');
}
get gzipSize() {
return this.getSize('gzipSize');
}
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
if (ownerModuleSize !== undefined) {
return Math.floor(this.size / this.ownerModule.size * ownerModuleSize);
}
}
toChartData() {
return _objectSpread({}, super.toChartData(), {
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
inaccurateSizes: true
});
}
}
exports.default = ContentFolder;
;
+49
View File
@@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _Module = _interopRequireDefault(require("./Module"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class ContentModule extends _Module.default {
constructor(name, data, ownerModule, parent) {
super(name, data, parent);
this.ownerModule = ownerModule;
}
get parsedSize() {
return this.getSize('parsedSize');
}
get gzipSize() {
return this.getSize('gzipSize');
}
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
if (ownerModuleSize !== undefined) {
return Math.floor(this.size / this.ownerModule.size * ownerModuleSize);
}
}
toChartData() {
return _objectSpread({}, super.toChartData(), {
inaccurateSizes: true
});
}
}
exports.default = ContentModule;
;
+81
View File
@@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = _interopRequireDefault(require("lodash"));
var _gzipSize = _interopRequireDefault(require("gzip-size"));
var _Module = _interopRequireDefault(require("./Module"));
var _BaseFolder = _interopRequireDefault(require("./BaseFolder"));
var _ConcatenatedModule = _interopRequireDefault(require("./ConcatenatedModule"));
var _utils = require("./utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class Folder extends _BaseFolder.default {
get parsedSize() {
return this.src ? this.src.length : 0;
}
get gzipSize() {
if (!_lodash.default.has(this, '_gzipSize')) {
this._gzipSize = this.src ? _gzipSize.default.sync(this.src) : 0;
}
return this._gzipSize;
}
addModule(moduleData) {
const pathParts = (0, _utils.getModulePathParts)(moduleData);
if (!pathParts) {
return;
}
const [folders, fileName] = [pathParts.slice(0, -1), _lodash.default.last(pathParts)];
let currentFolder = this;
_lodash.default.each(folders, folderName => {
let childNode = currentFolder.getChild(folderName);
if ( // Folder is not created yet
!childNode || // In some situations (invalid usage of dynamic `require()`) webpack generates a module with empty require
// context, but it's moduleId points to a directory in filesystem.
// In this case we replace this `File` node with `Folder`.
// See `test/stats/with-invalid-dynamic-require.json` as an example.
!(childNode instanceof Folder)) {
childNode = currentFolder.addChildFolder(new Folder(folderName));
}
currentFolder = childNode;
});
const ModuleConstructor = moduleData.modules ? _ConcatenatedModule.default : _Module.default;
const module = new ModuleConstructor(fileName, moduleData, this);
currentFolder.addChildModule(module);
}
toChartData() {
return _objectSpread({}, super.toChartData(), {
parsedSize: this.parsedSize,
gzipSize: this.gzipSize
});
}
}
exports.default = Folder;
;
+75
View File
@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = _interopRequireDefault(require("lodash"));
var _gzipSize = _interopRequireDefault(require("gzip-size"));
var _Node = _interopRequireDefault(require("./Node"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class Module extends _Node.default {
constructor(name, data, parent) {
super(name, parent);
this.data = data;
}
get src() {
return this.data.parsedSrc;
}
set src(value) {
this.data.parsedSrc = value;
delete this._gzipSize;
}
get size() {
return this.data.size;
}
set size(value) {
this.data.size = value;
}
get parsedSize() {
return this.src ? this.src.length : undefined;
}
get gzipSize() {
if (!_lodash.default.has(this, '_gzipSize')) {
this._gzipSize = this.src ? _gzipSize.default.sync(this.src) : undefined;
}
return this._gzipSize;
}
mergeData(data) {
if (data.size) {
this.size += data.size;
}
if (data.parsedSrc) {
this.src = (this.src || '') + data.parsedSrc;
}
}
toChartData() {
return {
id: this.data.id,
label: this.name,
path: this.path,
statSize: this.size,
parsedSize: this.parsedSize,
gzipSize: this.gzipSize
};
}
}
exports.default = Module;
;
+33
View File
@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
class Node {
constructor(name, parent) {
this.name = name;
this.parent = parent;
}
get path() {
const path = [];
let node = this;
while (node) {
path.push(node.name);
node = node.parent;
}
return path.reverse().join('/');
}
get isRoot() {
return !this.parent;
}
}
exports.default = Node;
;
+26
View File
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getModulePathParts = getModulePathParts;
var _lodash = _interopRequireDefault(require("lodash"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const MULTI_MODULE_REGEXP = /^multi /u;
function getModulePathParts(moduleData) {
if (MULTI_MODULE_REGEXP.test(moduleData.identifier)) {
return [moduleData.identifier];
}
const parsedPath = _lodash.default // Removing loaders from module path: they're joined by `!` and the last part is a raw module path
.last(moduleData.name.split('!')) // Splitting module path into parts
.split('/') // Removing first `.`
.slice(1) // Replacing `~` with `node_modules`
.map(part => part === '~' ? 'node_modules' : part);
return parsedPath.length ? parsedPath : null;
}
+51
View File
@@ -0,0 +1,51 @@
"use strict";
const {
inspect
} = require('util');
const _ = require('lodash');
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
exports.createAssetsFilter = createAssetsFilter;
function createAssetsFilter(excludePatterns) {
const excludeFunctions = _(excludePatterns).castArray().compact().map(pattern => {
if (typeof pattern === 'string') {
pattern = new RegExp(pattern, 'u');
}
if (_.isRegExp(pattern)) {
return asset => pattern.test(asset);
}
if (!_.isFunction(pattern)) {
throw new TypeError(`Pattern should be either string, RegExp or a function, but "${inspect(pattern, {
depth: 0
})}" got.`);
}
return pattern;
}).value();
if (excludeFunctions.length) {
return asset => _.every(excludeFunctions, fn => fn(asset) !== true);
} else {
return () => true;
}
}
/**
* @desc get string of current time
* format: dd/MMM HH:mm
* */
exports.getCurrentTime = function () {
const time = new Date();
const year = time.getFullYear();
const month = MONTHS[time.getMonth()];
const day = time.getDate();
const hour = time.getHours();
const minute = time.getMinutes();
return `${day} ${month} ${year} at ${hour}:${minute}`;
};
+227
View File
@@ -0,0 +1,227 @@
"use strict";
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
const path = require('path');
const fs = require('fs');
const http = require('http');
const WebSocket = require('ws');
const _ = require('lodash');
const express = require('express');
const ejs = require('ejs');
const opener = require('opener');
const mkdir = require('mkdirp');
const {
bold
} = require('chalk');
const utils = require('./utils');
const Logger = require('./Logger');
const analyzer = require('./analyzer');
const projectRoot = path.resolve(__dirname, '..');
const assetsRoot = path.join(projectRoot, 'public');
module.exports = {
startServer,
generateReport,
// deprecated
start: startServer
};
const title = `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${utils.getCurrentTime()}]`;
function startServer(_x, _x2) {
return _startServer.apply(this, arguments);
}
function _startServer() {
_startServer = _asyncToGenerator(function* (bundleStats, opts) {
const {
port = 8888,
host = '127.0.0.1',
openBrowser = true,
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const analyzerOpts = {
logger,
excludeAssets
};
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
if (!chartData) return;
const app = express(); // Explicitly using our `ejs` dependency to render templates
// Fixes #17
app.engine('ejs', require('ejs').renderFile);
app.set('view engine', 'ejs');
app.set('views', `${projectRoot}/views`);
app.use(express.static(`${projectRoot}/public`));
app.use('/', (req, res) => {
res.render('viewer', {
mode: 'server',
title,
get chartData() {
return chartData;
},
defaultSizes,
enableWebSocket: true,
// Helpers
escapeJson
});
});
const server = http.createServer(app);
yield new Promise(resolve => {
server.listen(port, host, () => {
resolve();
const url = `http://${host}:${server.address().port}`;
logger.info(`${bold('Webpack Bundle Analyzer')} is started at ${bold(url)}\n` + `Use ${bold('Ctrl+C')} to close it`);
if (openBrowser) {
opener(url);
}
});
});
const wss = new WebSocket.Server({
server
});
wss.on('connection', ws => {
ws.on('error', err => {
// Ignore network errors like `ECONNRESET`, `EPIPE`, etc.
if (err.errno) return;
logger.info(err.message);
});
});
return {
ws: wss,
http: server,
updateChartData
};
function updateChartData(bundleStats) {
const newChartData = getChartData(analyzerOpts, bundleStats, bundleDir);
if (!newChartData) return;
chartData = newChartData;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
event: 'chartDataUpdated',
data: newChartData
}));
}
});
}
});
return _startServer.apply(this, arguments);
}
function generateReport(_x3, _x4) {
return _generateReport.apply(this, arguments);
}
function _generateReport() {
_generateReport = _asyncToGenerator(function* (bundleStats, opts) {
const {
openBrowser = true,
reportFilename = 'report.html',
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const chartData = getChartData({
logger,
excludeAssets
}, bundleStats, bundleDir);
if (!chartData) return;
yield new Promise((resolve, reject) => {
ejs.renderFile(`${projectRoot}/views/viewer.ejs`, {
mode: 'static',
title,
chartData,
defaultSizes,
enableWebSocket: false,
// Helpers
assetContent: getAssetContent,
escapeJson
}, (err, reportHtml) => {
try {
if (err) {
logger.error(err);
reject(err);
return;
}
const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
mkdir.sync(path.dirname(reportFilepath));
fs.writeFileSync(reportFilepath, reportHtml);
logger.info(`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`);
if (openBrowser) {
opener(`file://${reportFilepath}`);
}
resolve();
} catch (e) {
reject(e);
}
});
});
});
return _generateReport.apply(this, arguments);
}
function getAssetContent(filename) {
const assetPath = path.join(assetsRoot, filename);
if (!assetPath.startsWith(assetsRoot)) {
throw new Error(`"${filename}" is outside of the assets root`);
}
return fs.readFileSync(assetPath, 'utf8');
}
/**
* Escapes `<` characters in JSON to safely use it in `<script>` tag.
*/
function escapeJson(json) {
return JSON.stringify(json).replace(/</gu, '\\u003c');
}
function getChartData(analyzerOpts, ...args) {
let chartData;
const {
logger
} = analyzerOpts;
try {
chartData = analyzer.getViewerData(...args, analyzerOpts);
} catch (err) {
logger.error(`Could't analyze webpack bundle:\n${err}`);
logger.debug(err.stack);
chartData = null;
}
if (_.isPlainObject(chartData) && _.isEmpty(chartData)) {
logger.error("Could't find any javascript bundles in provided stats file");
chartData = null;
}
return chartData;
}
+135
View File
@@ -0,0 +1,135 @@
{
"_from": "webpack-bundle-analyzer@^3.6.0",
"_id": "webpack-bundle-analyzer@3.6.0",
"_inBundle": false,
"_integrity": "sha512-orUfvVYEfBMDXgEKAKVvab5iQ2wXneIEorGNsyuOyVYpjYrI7CUOhhXNDd3huMwQ3vNNWWlGP+hzflMFYNzi2g==",
"_location": "/webpack-bundle-analyzer",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "webpack-bundle-analyzer@^3.6.0",
"name": "webpack-bundle-analyzer",
"escapedName": "webpack-bundle-analyzer",
"rawSpec": "^3.6.0",
"saveSpec": null,
"fetchSpec": "^3.6.0"
},
"_requiredBy": [
"/@vue/cli-service"
],
"_resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.0.tgz",
"_shasum": "39b3a8f829ca044682bc6f9e011c95deb554aefd",
"_spec": "webpack-bundle-analyzer@^3.6.0",
"_where": "/home/george/citwa/red_de_investigacion_front/first/node_modules/@vue/cli-service",
"author": {
"name": "Yury Grunin",
"email": "grunin.ya@ya.ru"
},
"bin": {
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
},
"bugs": {
"url": "https://github.com/webpack-contrib/webpack-bundle-analyzer/issues"
},
"bundleDependencies": false,
"changelog": "https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md",
"dependencies": {
"acorn": "^6.0.7",
"acorn-walk": "^6.1.1",
"bfj": "^6.1.1",
"chalk": "^2.4.1",
"commander": "^2.18.0",
"ejs": "^2.6.1",
"express": "^4.16.3",
"filesize": "^3.6.1",
"gzip-size": "^5.0.0",
"lodash": "^4.17.15",
"mkdirp": "^0.5.1",
"opener": "^1.5.1",
"ws": "^6.0.0"
},
"deprecated": false,
"description": "Webpack plugin and CLI utility that represents bundle content as convenient interactive zoomable treemap",
"devDependencies": {
"@babel/core": "7.4.3",
"@babel/plugin-proposal-class-properties": "7.4.0",
"@babel/plugin-proposal-decorators": "7.4.0",
"@babel/plugin-transform-runtime": "7.4.3",
"@babel/polyfill": "7.4.3",
"@babel/preset-env": "7.4.3",
"@babel/preset-react": "7.0.0",
"@babel/register": "7.4.0",
"@babel/runtime": "7.4.3",
"autoprefixer": "9.5.1",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.5",
"babel-plugin-lodash": "3.3.4",
"chai": "4.2.0",
"chai-subset": "1.6.0",
"classnames": "2.2.6",
"core-js": "2.6.5",
"css-loader": "2.1.1",
"cssnano": "4.1.10",
"del": "4.1.0",
"eslint": "5.16.0",
"eslint-config-th0r": "2.0.0",
"eslint-config-th0r-react": "2.0.0",
"eslint-plugin-react": "7.12.4",
"exports-loader": "0.7.0",
"gulp": "4.0.0",
"gulp-babel": "8.0.0",
"mobx": "5.9.4",
"mobx-preact": "3.0.0",
"mocha": "6.1.2",
"nightmare": "3.0.1",
"postcss-icss-values": "2.0.2",
"postcss-loader": "3.0.0",
"preact": "8.4.2",
"stream-combiner2": "1.1.1",
"style-loader": "0.23.1",
"terser-webpack-plugin": "1.2.3",
"url-loader": "1.1.2",
"webpack": "4.29.6",
"webpack-cli": "3.3.0",
"webpack-dev-server": "3.3.1"
},
"engines": {
"node": ">= 6.14.4"
},
"files": [
"public",
"lib",
"src",
"views"
],
"homepage": "https://github.com/webpack-contrib/webpack-bundle-analyzer",
"keywords": [
"webpack",
"bundle",
"analyzer",
"modules",
"size",
"interactive",
"chart",
"treemap",
"zoomable",
"zoom"
],
"license": "MIT",
"main": "lib/index.js",
"name": "webpack-bundle-analyzer",
"repository": {
"type": "git",
"url": "git+https://github.com/webpack-contrib/webpack-bundle-analyzer.git"
},
"scripts": {
"build": "gulp build",
"lint": "eslint --ext js,jsx .",
"npm-publish": "npm run lint && npm run build && npm test && npm publish",
"start": "gulp watch",
"test": "mocha --exit --require @babel/register",
"test-dev": "mocha --watch --require @babel/register"
},
"version": "3.6.0"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+144
View File
@@ -0,0 +1,144 @@
const bfj = require('bfj');
const path = require('path');
const mkdir = require('mkdirp');
const {bold} = require('chalk');
const Logger = require('./Logger');
const viewer = require('./viewer');
class BundleAnalyzerPlugin {
constructor(opts = {}) {
this.opts = {
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
excludeAssets: null,
logLevel: 'info',
// deprecated
startAnalyzer: true,
...opts,
analyzerPort: 'analyzerPort' in opts ? (opts.analyzerPort === 'auto' ? 0 : opts.analyzerPort) : 8888
};
this.server = null;
this.logger = new Logger(this.opts.logLevel);
}
apply(compiler) {
this.compiler = compiler;
const done = (stats, callback) => {
callback = callback || (() => {});
const actions = [];
if (this.opts.generateStatsFile) {
actions.push(() => this.generateStatsFile(stats.toJson(this.opts.statsOptions)));
}
// Handling deprecated `startAnalyzer` flag
if (this.opts.analyzerMode === 'server' && !this.opts.startAnalyzer) {
this.opts.analyzerMode = 'disabled';
}
if (this.opts.analyzerMode === 'server') {
actions.push(() => this.startAnalyzerServer(stats.toJson()));
} else if (this.opts.analyzerMode === 'static') {
actions.push(() => this.generateStaticReport(stats.toJson()));
}
if (actions.length) {
// Making analyzer logs to be after all webpack logs in the console
setImmediate(async () => {
try {
await Promise.all(actions.map(action => action()));
callback();
} catch (e) {
callback(e);
}
});
} else {
callback();
}
};
if (compiler.hooks) {
compiler.hooks.done.tapAsync('webpack-bundle-analyzer', done);
} else {
compiler.plugin('done', done);
}
}
async generateStatsFile(stats) {
const statsFilepath = path.resolve(this.compiler.outputPath, this.opts.statsFilename);
mkdir.sync(path.dirname(statsFilepath));
try {
await bfj.write(statsFilepath, stats, {
space: 2,
promises: 'ignore',
buffers: 'ignore',
maps: 'ignore',
iterables: 'ignore',
circular: 'ignore'
});
this.logger.info(
`${bold('Webpack Bundle Analyzer')} saved stats file to ${bold(statsFilepath)}`
);
} catch (error) {
this.logger.error(
`${bold('Webpack Bundle Analyzer')} error saving stats file to ${bold(statsFilepath)}: ${error}`
);
}
}
async startAnalyzerServer(stats) {
if (this.server) {
(await this.server).updateChartData(stats);
} else {
this.server = viewer.startServer(stats, {
openBrowser: this.opts.openAnalyzer,
host: this.opts.analyzerHost,
port: this.opts.analyzerPort,
bundleDir: this.getBundleDirFromCompiler(),
logger: this.logger,
defaultSizes: this.opts.defaultSizes,
excludeAssets: this.opts.excludeAssets
});
}
}
async generateStaticReport(stats) {
await viewer.generateReport(stats, {
openBrowser: this.opts.openAnalyzer,
reportFilename: path.resolve(this.compiler.outputPath, this.opts.reportFilename),
bundleDir: this.getBundleDirFromCompiler(),
logger: this.logger,
defaultSizes: this.opts.defaultSizes,
excludeAssets: this.opts.excludeAssets
});
}
getBundleDirFromCompiler() {
switch (this.compiler.outputFileSystem.constructor.name) {
case 'MemoryFileSystem':
return null;
// Detect AsyncMFS used by Nuxt 2.5 that replaces webpack's MFS during development
// Related: #274
case 'AsyncMFS':
return null;
default:
return this.compiler.outputPath;
}
}
}
module.exports = BundleAnalyzerPlugin;
+51
View File
@@ -0,0 +1,51 @@
const LEVELS = [
'debug',
'info',
'warn',
'error',
'silent'
];
const LEVEL_TO_CONSOLE_METHOD = new Map([
['debug', 'log'],
['info', 'log'],
['warn', 'log']
]);
class Logger {
static levels = LEVELS;
static defaultLevel = 'info';
constructor(level = Logger.defaultLevel) {
this.activeLevels = new Set();
this.setLogLevel(level);
}
setLogLevel(level) {
const levelIndex = LEVELS.indexOf(level);
if (levelIndex === -1) throw new Error(`Invalid log level "${level}". Use one of these: ${LEVELS.join(', ')}`);
this.activeLevels.clear();
for (const [i, level] of LEVELS.entries()) {
if (i >= levelIndex) this.activeLevels.add(level);
}
}
_log(level, ...args) {
console[LEVEL_TO_CONSOLE_METHOD.get(level) || level](...args);
}
};
LEVELS.forEach(level => {
if (level === 'silent') return;
Logger.prototype[level] = function (...args) {
if (this.activeLevels.has(level)) this._log(level, ...args);
};
});
module.exports = Logger;
+140
View File
@@ -0,0 +1,140 @@
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const gzipSize = require('gzip-size');
const Logger = require('./Logger');
const Folder = require('./tree/Folder').default;
const {parseBundle} = require('./parseUtils');
const {createAssetsFilter} = require('./utils');
const FILENAME_QUERY_REGEXP = /\?.*$/u;
const FILENAME_EXTENSIONS = /\.(js|mjs)$/iu;
module.exports = {
getViewerData,
readStatsFromFile
};
function getViewerData(bundleStats, bundleDir, opts) {
const {
logger = new Logger(),
excludeAssets = null
} = opts || {};
const isAssetIncluded = createAssetsFilter(excludeAssets);
// Sometimes all the information is located in `children` array (e.g. problem in #10)
if (_.isEmpty(bundleStats.assets) && !_.isEmpty(bundleStats.children)) {
bundleStats = bundleStats.children[0];
}
// Picking only `*.js or *.mjs` assets from bundle that has non-empty `chunks` array
bundleStats.assets = _.filter(bundleStats.assets, asset => {
// Removing query part from filename (yes, somebody uses it for some reason and Webpack supports it)
// See #22
asset.name = asset.name.replace(FILENAME_QUERY_REGEXP, '');
return FILENAME_EXTENSIONS.test(asset.name) && !_.isEmpty(asset.chunks) && isAssetIncluded(asset.name);
});
// Trying to parse bundle assets and get real module sizes if `bundleDir` is provided
let bundlesSources = null;
let parsedModules = null;
if (bundleDir) {
bundlesSources = {};
parsedModules = {};
for (const statAsset of bundleStats.assets) {
const assetFile = path.join(bundleDir, statAsset.name);
let bundleInfo;
try {
bundleInfo = parseBundle(assetFile);
} catch (err) {
const msg = (err.code === 'ENOENT') ? 'no such file' : err.message;
logger.warn(`Error parsing bundle asset "${assetFile}": ${msg}`);
continue;
}
bundlesSources[statAsset.name] = bundleInfo.src;
_.assign(parsedModules, bundleInfo.modules);
}
if (_.isEmpty(bundlesSources)) {
bundlesSources = null;
parsedModules = null;
logger.warn('\nNo bundles were parsed. Analyzer will show only original module sizes from stats file.\n');
}
}
const modules = getBundleModules(bundleStats);
const assets = _.transform(bundleStats.assets, (result, statAsset) => {
const asset = result[statAsset.name] = _.pick(statAsset, 'size');
if (bundlesSources && _.has(bundlesSources, statAsset.name)) {
asset.parsedSize = Buffer.byteLength(bundlesSources[statAsset.name]);
asset.gzipSize = gzipSize.sync(bundlesSources[statAsset.name]);
}
// Picking modules from current bundle script
asset.modules = _(modules)
.filter(statModule => assetHasModule(statAsset, statModule))
.each(statModule => {
if (parsedModules) {
statModule.parsedSrc = parsedModules[statModule.id];
}
});
asset.tree = createModulesTree(asset.modules);
}, {});
return _.transform(assets, (result, asset, filename) => {
result.push({
label: filename,
isAsset: true,
// Not using `asset.size` here provided by Webpack because it can be very confusing when `UglifyJsPlugin` is used.
// In this case all module sizes from stats file will represent unminified module sizes, but `asset.size` will
// be the size of minified bundle.
// Using `asset.size` only if current asset doesn't contain any modules (resulting size equals 0)
statSize: asset.tree.size || asset.size,
parsedSize: asset.parsedSize,
gzipSize: asset.gzipSize,
groups: _.invokeMap(asset.tree.children, 'toChartData')
});
}, []);
}
function readStatsFromFile(filename) {
return JSON.parse(
fs.readFileSync(filename, 'utf8')
);
}
function getBundleModules(bundleStats) {
return _(bundleStats.chunks)
.map('modules')
.concat(bundleStats.modules)
.compact()
.flatten()
.uniqBy('id')
.value();
}
function assetHasModule(statAsset, statModule) {
// Checking if this module is the part of asset chunks
return _.some(statModule.chunks, moduleChunk =>
_.includes(statAsset.chunks, moduleChunk)
);
}
function createModulesTree(modules) {
const root = new Folder('.');
_.each(modules, module => root.addModule(module));
root.mergeNestedFolders();
return root;
}
+148
View File
@@ -0,0 +1,148 @@
#! /usr/bin/env node
const {resolve, dirname} = require('path');
const _ = require('lodash');
const commander = require('commander');
const {magenta} = require('chalk');
const analyzer = require('../analyzer');
const viewer = require('../viewer');
const Logger = require('../Logger');
const SIZES = new Set(['stat', 'parsed', 'gzip']);
const program = commander
.version(require('../../package.json').version)
.usage(
`<bundleStatsFile> [bundleDir] [options]
Arguments:
bundleStatsFile Path to Webpack Stats JSON file.
bundleDir Directory containing all generated bundles.
You should provided it if you want analyzer to show you the real parsed module sizes.
By default a directory of stats file is used.`
)
.option(
'-m, --mode <mode>',
'Analyzer mode. Should be `server` or `static`.' +
br('In `server` mode analyzer will start HTTP server to show bundle report.') +
br('In `static` mode single HTML file with bundle report will be generated.'),
'server'
)
.option(
// Had to make `host` parameter optional in order to let `-h` flag output help message
// Fixes https://github.com/webpack-contrib/webpack-bundle-analyzer/issues/239
'-h, --host [host]',
'Host that will be used in `server` mode to start HTTP server.',
'127.0.0.1'
)
.option(
'-p, --port <n>',
'Port that will be used in `server` mode to start HTTP server.',
8888
)
.option(
'-r, --report <file>',
'Path to bundle report file that will be generated in `static` mode.',
'report.html'
)
.option(
'-s, --default-sizes <type>',
'Module sizes to show in treemap by default.' +
br(`Possible values: ${[...SIZES].join(', ')}`),
'parsed'
)
.option(
'-O, --no-open',
"Don't open report in default browser automatically."
)
.option(
'-e, --exclude <regexp>',
'Assets that should be excluded from the report.' +
br('Can be specified multiple times.'),
array()
)
.option(
'-l, --log-level <level>',
'Log level.' +
br(`Possible values: ${[...Logger.levels].join(', ')}`),
Logger.defaultLevel
)
.parse(process.argv);
let {
mode,
host,
port,
report: reportFilename,
defaultSizes,
logLevel,
open: openBrowser,
exclude: excludeAssets,
args: [bundleStatsFile, bundleDir]
} = program;
const logger = new Logger(logLevel);
if (!bundleStatsFile) showHelp('Provide path to Webpack Stats file as first argument');
if (mode !== 'server' && mode !== 'static') showHelp('Invalid mode. Should be either `server` or `static`.');
if (mode === 'server') {
if (!host) showHelp('Invalid host name');
port = port === 'auto' ? 0 : Number(port);
if (isNaN(port)) showHelp('Invalid port. Should be a number or `auto`');
}
if (!SIZES.has(defaultSizes)) showHelp(`Invalid default sizes option. Possible values are: ${[...SIZES].join(', ')}`);
bundleStatsFile = resolve(bundleStatsFile);
if (!bundleDir) bundleDir = dirname(bundleStatsFile);
let bundleStats;
try {
bundleStats = analyzer.readStatsFromFile(bundleStatsFile);
} catch (err) {
logger.error(`Couldn't read webpack bundle stats from "${bundleStatsFile}":\n${err}`);
logger.debug(err.stack);
process.exit(1);
}
if (mode === 'server') {
viewer.startServer(bundleStats, {
openBrowser,
port,
host,
defaultSizes,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
});
} else {
viewer.generateReport(bundleStats, {
openBrowser,
reportFilename: resolve(reportFilename),
defaultSizes,
bundleDir,
excludeAssets,
logger: new Logger(logLevel)
});
}
function showHelp(error) {
if (error) console.log(`\n ${magenta(error)}\n`);
program.outputHelp();
process.exit(1);
}
function br(str) {
return `\n${_.repeat(' ', 28)}${str}`;
}
function array() {
const arr = [];
return (val) => {
arr.push(val);
return arr;
};
}
+6
View File
@@ -0,0 +1,6 @@
const {start} = require('./viewer');
module.exports = {
start,
BundleAnalyzerPlugin: require('./BundleAnalyzerPlugin')
};
+247
View File
@@ -0,0 +1,247 @@
const fs = require('fs');
const _ = require('lodash');
const acorn = require('acorn');
const walk = require('acorn-walk');
module.exports = {
parseBundle
};
function parseBundle(bundlePath) {
const content = fs.readFileSync(bundlePath, 'utf8');
const ast = acorn.parse(content, {
sourceType: 'script',
// I believe in a bright future of ECMAScript!
// Actually, it's set to `2050` to support the latest ECMAScript version that currently exists.
// Seems like `acorn` supports such weird option value.
ecmaVersion: 2050
});
const walkState = {
locations: null
};
walk.recursive(
ast,
walkState,
{
CallExpression(node, state, c) {
if (state.locations) return;
const args = node.arguments;
// Main chunk with webpack loader.
// Modules are stored in first argument:
// (function (...) {...})(<modules>)
if (
node.callee.type === 'FunctionExpression' &&
!node.callee.id &&
args.length === 1 &&
isSimpleModulesList(args[0])
) {
state.locations = getModulesLocations(args[0]);
return;
}
// Async Webpack < v4 chunk without webpack loader.
// webpackJsonp([<chunks>], <modules>, ...)
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
if (
node.callee.type === 'Identifier' &&
mayBeAsyncChunkArguments(args) &&
isModulesList(args[1])
) {
state.locations = getModulesLocations(args[1]);
return;
}
// Async Webpack v4 chunk without webpack loader.
// (window.webpackJsonp=window.webpackJsonp||[]).push([[<chunks>], <modules>, ...]);
// As function name may be changed with `output.jsonpFunction` option we can't rely on it's default name.
if (isAsyncChunkPushExpression(node)) {
state.locations = getModulesLocations(args[0].elements[1]);
return;
}
// Walking into arguments because some of plugins (e.g. `DedupePlugin`) or some Webpack
// features (e.g. `umd` library output) can wrap modules list into additional IIFE.
_.each(args, arg => c(arg, state));
}
}
);
let modules;
if (walkState.locations) {
modules = _.mapValues(walkState.locations,
loc => content.slice(loc.start, loc.end)
);
} else {
modules = {};
}
return {
src: content,
modules
};
}
function isModulesList(node) {
return (
isSimpleModulesList(node) ||
// Modules are contained in expression `Array([minimum ID]).concat([<module>, <module>, ...])`
isOptimizedModulesArray(node)
);
}
function isSimpleModulesList(node) {
return (
// Modules are contained in hash. Keys are module ids.
isModulesHash(node) ||
// Modules are contained in array. Indexes are module ids.
isModulesArray(node)
);
}
function isModulesHash(node) {
return (
node.type === 'ObjectExpression' &&
_(node.properties)
.map('value')
.every(isModuleWrapper)
);
}
function isModulesArray(node) {
return (
node.type === 'ArrayExpression' &&
_.every(node.elements, elem =>
// Some of array items may be skipped because there is no module with such id
!elem ||
isModuleWrapper(elem)
)
);
}
function isOptimizedModulesArray(node) {
// Checking whether modules are contained in `Array(<minimum ID>).concat(...modules)` array:
// https://github.com/webpack/webpack/blob/v1.14.0/lib/Template.js#L91
// The `<minimum ID>` + array indexes are module ids
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
// Make sure the object called is `Array(<some number>)`
node.callee.object.type === 'CallExpression' &&
node.callee.object.callee.type === 'Identifier' &&
node.callee.object.callee.name === 'Array' &&
node.callee.object.arguments.length === 1 &&
isNumericId(node.callee.object.arguments[0]) &&
// Make sure the property X called for `Array(<some number>).X` is `concat`
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'concat' &&
// Make sure exactly one array is passed in to `concat`
node.arguments.length === 1 &&
isModulesArray(node.arguments[0])
);
}
function isModuleWrapper(node) {
return (
// It's an anonymous function expression that wraps module
((node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && !node.id) ||
// If `DedupePlugin` is used it can be an ID of duplicated module...
isModuleId(node) ||
// or an array of shape [<module_id>, ...args]
(node.type === 'ArrayExpression' && node.elements.length > 1 && isModuleId(node.elements[0]))
);
}
function isModuleId(node) {
return (node.type === 'Literal' && (isNumericId(node) || typeof node.value === 'string'));
}
function isNumericId(node) {
return (node.type === 'Literal' && Number.isInteger(node.value) && node.value >= 0);
}
function isChunkIds(node) {
// Array of numeric or string ids. Chunk IDs are strings when NamedChunksPlugin is used
return (
node.type === 'ArrayExpression' &&
_.every(node.elements, isModuleId)
);
}
function isAsyncChunkPushExpression(node) {
const {
callee,
arguments: args
} = node;
return (
callee.type === 'MemberExpression' &&
callee.property.name === 'push' &&
callee.object.type === 'AssignmentExpression' &&
callee.object.left.object &&
(
callee.object.left.object.name === 'window' ||
// `self` is a common output.globalObject value used to support both workers and browsers
callee.object.left.object.name === 'self' ||
// Webpack 4 uses `this` instead of `window`
callee.object.left.object.type === 'ThisExpression'
) &&
args.length === 1 &&
args[0].type === 'ArrayExpression' &&
mayBeAsyncChunkArguments(args[0].elements) &&
isModulesList(args[0].elements[1])
);
}
function mayBeAsyncChunkArguments(args) {
return (
args.length >= 2 &&
isChunkIds(args[0])
);
}
function getModulesLocations(node) {
if (node.type === 'ObjectExpression') {
// Modules hash
const modulesNodes = node.properties;
return _.transform(modulesNodes, (result, moduleNode) => {
const moduleId = moduleNode.key.name || moduleNode.key.value;
result[moduleId] = getModuleLocation(moduleNode.value);
}, {});
}
const isOptimizedArray = (node.type === 'CallExpression');
if (node.type === 'ArrayExpression' || isOptimizedArray) {
// Modules array or optimized array
const minId = isOptimizedArray ?
// Get the [minId] value from the Array() call first argument literal value
node.callee.object.arguments[0].value :
// `0` for simple array
0;
const modulesNodes = isOptimizedArray ?
// The modules reside in the `concat()` function call arguments
node.arguments[0].elements :
node.elements;
return _.transform(modulesNodes, (result, moduleNode, i) => {
if (!moduleNode) return;
result[i + minId] = getModuleLocation(moduleNode);
}, {});
}
return {};
}
function getModuleLocation(node) {
return {
start: node.start,
end: node.end
};
}
+118
View File
@@ -0,0 +1,118 @@
import _ from 'lodash';
import Node from './Node';
export default class BaseFolder extends Node {
constructor(name, parent) {
super(name, parent);
this.children = Object.create(null);
}
get src() {
if (!_.has(this, '_src')) {
this._src = this.walk((node, src) => (src += node.src || ''), '', false);
}
return this._src;
}
get size() {
if (!_.has(this, '_size')) {
this._size = this.walk((node, size) => (size + node.size), 0, false);
}
return this._size;
}
getChild(name) {
return this.children[name];
}
addChildModule(module) {
const {name} = module;
const currentChild = this.children[name];
// For some reason we already have this node in children and it's a folder.
if (currentChild && currentChild instanceof BaseFolder) return;
if (currentChild) {
// We already have this node in children and it's a module.
// Merging it's data.
currentChild.mergeData(module.data);
} else {
// Pushing new module
module.parent = this;
this.children[name] = module;
}
delete this._size;
delete this._src;
}
addChildFolder(folder) {
folder.parent = this;
this.children[folder.name] = folder;
delete this._size;
delete this._src;
return folder;
}
walk(walker, state = {}, deep = true) {
let stopped = false;
_.each(this.children, child => {
if (deep && child.walk) {
state = child.walk(walker, state, stop);
} else {
state = walker(child, state, stop);
}
if (stopped) return false;
});
return state;
function stop(finalState) {
stopped = true;
return finalState;
}
}
mergeNestedFolders() {
if (!this.isRoot) {
let childNames;
while ((childNames = Object.keys(this.children)).length === 1) {
const childName = childNames[0];
const onlyChild = this.children[childName];
if (onlyChild instanceof this.constructor) {
this.name += `/${onlyChild.name}`;
this.children = onlyChild.children;
} else {
break;
}
}
}
this.walk(child => {
child.parent = this;
if (child.mergeNestedFolders) {
child.mergeNestedFolders();
}
}, null, false);
}
toChartData() {
return {
label: this.name,
path: this.path,
statSize: this.size,
groups: _.invokeMap(this.children, 'toChartData')
};
}
};
+72
View File
@@ -0,0 +1,72 @@
import _ from 'lodash';
import Module from './Module';
import ContentModule from './ContentModule';
import ContentFolder from './ContentFolder';
import {getModulePathParts} from './utils';
export default class ConcatenatedModule extends Module {
constructor(name, data, parent) {
super(name, data, parent);
this.name += ' (concatenated)';
this.children = Object.create(null);
this.fillContentModules();
}
fillContentModules() {
_.each(this.data.modules, moduleData => this.addContentModule(moduleData));
}
addContentModule(moduleData) {
const pathParts = getModulePathParts(moduleData);
if (!pathParts) {
return;
}
const [folders, fileName] = [pathParts.slice(0, -1), _.last(pathParts)];
let currentFolder = this;
_.each(folders, folderName => {
let childFolder = currentFolder.getChild(folderName);
if (!childFolder) {
childFolder = currentFolder.addChildFolder(new ContentFolder(folderName, this));
}
currentFolder = childFolder;
});
const module = new ContentModule(fileName, moduleData, this);
currentFolder.addChildModule(module);
}
getChild(name) {
return this.children[name];
}
addChildModule(module) {
module.parent = this;
this.children[module.name] = module;
}
addChildFolder(folder) {
folder.parent = this;
this.children[folder.name] = folder;
return folder;
}
mergeNestedFolders() {
_.invokeMap(this.children, 'mergeNestedFolders');
}
toChartData() {
return {
...super.toChartData(),
concatenated: true,
groups: _.invokeMap(this.children, 'toChartData')
};
}
};
+35
View File
@@ -0,0 +1,35 @@
import BaseFolder from './BaseFolder';
export default class ContentFolder extends BaseFolder {
constructor(name, ownerModule, parent) {
super(name, parent);
this.ownerModule = ownerModule;
}
get parsedSize() {
return this.getSize('parsedSize');
}
get gzipSize() {
return this.getSize('gzipSize');
}
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
if (ownerModuleSize !== undefined) {
return Math.floor((this.size / this.ownerModule.size) * ownerModuleSize);
}
}
toChartData() {
return {
...super.toChartData(),
parsedSize: this.parsedSize,
gzipSize: this.gzipSize,
inaccurateSizes: true
};
}
};
+33
View File
@@ -0,0 +1,33 @@
import Module from './Module';
export default class ContentModule extends Module {
constructor(name, data, ownerModule, parent) {
super(name, data, parent);
this.ownerModule = ownerModule;
}
get parsedSize() {
return this.getSize('parsedSize');
}
get gzipSize() {
return this.getSize('gzipSize');
}
getSize(sizeType) {
const ownerModuleSize = this.ownerModule[sizeType];
if (ownerModuleSize !== undefined) {
return Math.floor((this.size / this.ownerModule.size) * ownerModuleSize);
}
}
toChartData() {
return {
...super.toChartData(),
inaccurateSizes: true
};
}
};
+64
View File
@@ -0,0 +1,64 @@
import _ from 'lodash';
import gzipSize from 'gzip-size';
import Module from './Module';
import BaseFolder from './BaseFolder';
import ConcatenatedModule from './ConcatenatedModule';
import {getModulePathParts} from './utils';
export default class Folder extends BaseFolder {
get parsedSize() {
return this.src ? this.src.length : 0;
}
get gzipSize() {
if (!_.has(this, '_gzipSize')) {
this._gzipSize = this.src ? gzipSize.sync(this.src) : 0;
}
return this._gzipSize;
}
addModule(moduleData) {
const pathParts = getModulePathParts(moduleData);
if (!pathParts) {
return;
}
const [folders, fileName] = [pathParts.slice(0, -1), _.last(pathParts)];
let currentFolder = this;
_.each(folders, folderName => {
let childNode = currentFolder.getChild(folderName);
if (
// Folder is not created yet
!childNode ||
// In some situations (invalid usage of dynamic `require()`) webpack generates a module with empty require
// context, but it's moduleId points to a directory in filesystem.
// In this case we replace this `File` node with `Folder`.
// See `test/stats/with-invalid-dynamic-require.json` as an example.
!(childNode instanceof Folder)
) {
childNode = currentFolder.addChildFolder(new Folder(folderName));
}
currentFolder = childNode;
});
const ModuleConstructor = moduleData.modules ? ConcatenatedModule : Module;
const module = new ModuleConstructor(fileName, moduleData, this);
currentFolder.addChildModule(module);
}
toChartData() {
return {
...super.toChartData(),
parsedSize: this.parsedSize,
gzipSize: this.gzipSize
};
}
};
+63
View File
@@ -0,0 +1,63 @@
import _ from 'lodash';
import gzipSize from 'gzip-size';
import Node from './Node';
export default class Module extends Node {
constructor(name, data, parent) {
super(name, parent);
this.data = data;
}
get src() {
return this.data.parsedSrc;
}
set src(value) {
this.data.parsedSrc = value;
delete this._gzipSize;
}
get size() {
return this.data.size;
}
set size(value) {
this.data.size = value;
}
get parsedSize() {
return this.src ? this.src.length : undefined;
}
get gzipSize() {
if (!_.has(this, '_gzipSize')) {
this._gzipSize = this.src ? gzipSize.sync(this.src) : undefined;
}
return this._gzipSize;
}
mergeData(data) {
if (data.size) {
this.size += data.size;
}
if (data.parsedSrc) {
this.src = (this.src || '') + data.parsedSrc;
}
}
toChartData() {
return {
id: this.data.id,
label: this.name,
path: this.path,
statSize: this.size,
parsedSize: this.parsedSize,
gzipSize: this.gzipSize
};
}
};
+24
View File
@@ -0,0 +1,24 @@
export default class Node {
constructor(name, parent) {
this.name = name;
this.parent = parent;
}
get path() {
const path = [];
let node = this;
while (node) {
path.push(node.name);
node = node.parent;
}
return path.reverse().join('/');
}
get isRoot() {
return !this.parent;
}
};
+21
View File
@@ -0,0 +1,21 @@
import _ from 'lodash';
const MULTI_MODULE_REGEXP = /^multi /u;
export function getModulePathParts(moduleData) {
if (MULTI_MODULE_REGEXP.test(moduleData.identifier)) {
return [moduleData.identifier];
}
const parsedPath = _
// Removing loaders from module path: they're joined by `!` and the last part is a raw module path
.last(moduleData.name.split('!'))
// Splitting module path into parts
.split('/')
// Removing first `.`
.slice(1)
// Replacing `~` with `node_modules`
.map(part => (part === '~' ? 'node_modules' : part));
return parsedPath.length ? parsedPath : null;
}
+50
View File
@@ -0,0 +1,50 @@
const {inspect} = require('util');
const _ = require('lodash');
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
exports.createAssetsFilter = createAssetsFilter;
function createAssetsFilter(excludePatterns) {
const excludeFunctions = _(excludePatterns)
.castArray()
.compact()
.map(pattern => {
if (typeof pattern === 'string') {
pattern = new RegExp(pattern, 'u');
}
if (_.isRegExp(pattern)) {
return (asset) => pattern.test(asset);
}
if (!_.isFunction(pattern)) {
throw new TypeError(
`Pattern should be either string, RegExp or a function, but "${inspect(pattern, {depth: 0})}" got.`
);
}
return pattern;
})
.value();
if (excludeFunctions.length) {
return (asset) => _.every(excludeFunctions, fn => fn(asset) !== true);
} else {
return () => true;
}
}
/**
* @desc get string of current time
* format: dd/MMM HH:mm
* */
exports.getCurrentTime = function () {
const time = new Date();
const year = time.getFullYear();
const month = MONTHS[time.getMonth()];
const day = time.getDate();
const hour = time.getHours();
const minute = time.getMinutes();
return `${day} ${month} ${year} at ${hour}:${minute}`;
};
+212
View File
@@ -0,0 +1,212 @@
const path = require('path');
const fs = require('fs');
const http = require('http');
const WebSocket = require('ws');
const _ = require('lodash');
const express = require('express');
const ejs = require('ejs');
const opener = require('opener');
const mkdir = require('mkdirp');
const {bold} = require('chalk');
const utils = require('./utils');
const Logger = require('./Logger');
const analyzer = require('./analyzer');
const projectRoot = path.resolve(__dirname, '..');
const assetsRoot = path.join(projectRoot, 'public');
module.exports = {
startServer,
generateReport,
// deprecated
start: startServer
};
const title = `${process.env.npm_package_name || 'Webpack Bundle Analyzer'} [${utils.getCurrentTime()}]`;
async function startServer(bundleStats, opts) {
const {
port = 8888,
host = '127.0.0.1',
openBrowser = true,
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const analyzerOpts = {logger, excludeAssets};
let chartData = getChartData(analyzerOpts, bundleStats, bundleDir);
if (!chartData) return;
const app = express();
// Explicitly using our `ejs` dependency to render templates
// Fixes #17
app.engine('ejs', require('ejs').renderFile);
app.set('view engine', 'ejs');
app.set('views', `${projectRoot}/views`);
app.use(express.static(`${projectRoot}/public`));
app.use('/', (req, res) => {
res.render('viewer', {
mode: 'server',
title,
get chartData() { return chartData },
defaultSizes,
enableWebSocket: true,
// Helpers
escapeJson
});
});
const server = http.createServer(app);
await new Promise(resolve => {
server.listen(port, host, () => {
resolve();
const url = `http://${host}:${server.address().port}`;
logger.info(
`${bold('Webpack Bundle Analyzer')} is started at ${bold(url)}\n` +
`Use ${bold('Ctrl+C')} to close it`
);
if (openBrowser) {
opener(url);
}
});
});
const wss = new WebSocket.Server({server});
wss.on('connection', ws => {
ws.on('error', err => {
// Ignore network errors like `ECONNRESET`, `EPIPE`, etc.
if (err.errno) return;
logger.info(err.message);
});
});
return {
ws: wss,
http: server,
updateChartData
};
function updateChartData(bundleStats) {
const newChartData = getChartData(analyzerOpts, bundleStats, bundleDir);
if (!newChartData) return;
chartData = newChartData;
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
event: 'chartDataUpdated',
data: newChartData
}));
}
});
}
}
async function generateReport(bundleStats, opts) {
const {
openBrowser = true,
reportFilename = 'report.html',
bundleDir = null,
logger = new Logger(),
defaultSizes = 'parsed',
excludeAssets = null
} = opts || {};
const chartData = getChartData({logger, excludeAssets}, bundleStats, bundleDir);
if (!chartData) return;
await new Promise((resolve, reject) => {
ejs.renderFile(
`${projectRoot}/views/viewer.ejs`,
{
mode: 'static',
title,
chartData,
defaultSizes,
enableWebSocket: false,
// Helpers
assetContent: getAssetContent,
escapeJson
},
(err, reportHtml) => {
try {
if (err) {
logger.error(err);
reject(err);
return;
}
const reportFilepath = path.resolve(bundleDir || process.cwd(), reportFilename);
mkdir.sync(path.dirname(reportFilepath));
fs.writeFileSync(reportFilepath, reportHtml);
logger.info(
`${bold('Webpack Bundle Analyzer')} saved report to ${bold(reportFilepath)}`
);
if (openBrowser) {
opener(`file://${reportFilepath}`);
}
resolve();
} catch (e) {
reject(e);
}
}
);
});
}
function getAssetContent(filename) {
const assetPath = path.join(assetsRoot, filename);
if (!assetPath.startsWith(assetsRoot)) {
throw new Error(`"${filename}" is outside of the assets root`);
}
return fs.readFileSync(assetPath, 'utf8');
}
/**
* Escapes `<` characters in JSON to safely use it in `<script>` tag.
*/
function escapeJson(json) {
return JSON.stringify(json).replace(/</gu, '\\u003c');
}
function getChartData(analyzerOpts, ...args) {
let chartData;
const {logger} = analyzerOpts;
try {
chartData = analyzer.getViewerData(...args, analyzerOpts);
} catch (err) {
logger.error(`Could't analyze webpack bundle:\n${err}`);
logger.debug(err.stack);
chartData = null;
}
if (_.isPlainObject(chartData) && _.isEmpty(chartData)) {
logger.error("Could't find any javascript bundles in provided stats file");
chartData = null;
}
return chartData;
}
+8
View File
@@ -0,0 +1,8 @@
<% if (mode === 'static') { %>
<!-- <%= filename %> -->
<script>
<%- assetContent(filename) %>
</script>
<% } else { %>
<script src="<%= filename %>"></script>
<% } %>
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><%= title %></title>
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABrVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+O1foceMD///+J0/qK1Pr7/v8Xdr/9///W8P4UdL7L7P0Scr2r4Pyj3vwad8D5/f/2/f+55f3E6f34+/2H0/ojfMKpzOd0rNgQcb3F3O/j9f7c8v6g3Pz0/P/w+v/q+P7n9v6T1/uQ1vuE0vqLut/y+v+Z2fvt+f+15Pzv9fuc2/vR7v2V2Pvd6/bg9P7I6/285/2y4/yp3/zp8vk8i8kqgMT7/P31+fyv4vxGkcz6/P6/6P3j7vfS5PNnpNUxhcbO7f7F6v3O4vHK3/DA2u631Ouy0eqXweKJud5wqthfoNMMbLvY8f73+v2dxeR8sNtTmdDx9/zX6PSjyeaCtd1YnNGX2PuQveCGt95Nls42h8dLlM3F4vBtAAAAM3RSTlMAAyOx0/sKBvik8opWGBMOAe3l1snDm2E9LSb06eHcu5JpHbarfHZCN9CBb08zzkdNS0kYaptYAAAFV0lEQVRYw92X51/aYBDHHS2O2qqttVbrqNq9m+TJIAYIShBkWwqIiCgoWvfeq7Z2/s29hyQNyUcR7LveGwVyXy6XH8/9rqxglLfUPLxVduUor3h0rfp2TYvpivk37929TkG037hffoX0+peVtZQc1589rigVUdXS/ABSAyEmGIO/1XfvldSK8vs3OqB6u3m0nxmIrvgB0dj7rr7Y9IbuF68hnfFaiHA/sxqm0wciIG43P60qKv9WXWc1RXGh/mFESFABTSBi0sNAKzqet17eCtOb3kZIDwxEEU0oAIJGYxNBDhBND29e0rtXXbcpuPmED9IhEAAQ/AXEaF8EPmnrrKsv0LvWR3fg5sWDNAFZOgAgaKvZDogHNU9MFwnnYROkc56RD5CjAbQX9Ow4g7upCsvYu55aSI/Nj0H1akgKQEUM94dwK65hYRmFU9MIcH/fqJYOZYcnuJSU/waKDgTOEVaVKhwrTRP5XzgSpAITYzom7UvkhFX5VutmxeNnWDjjswTKTyfgluNDGbUpWissXhF3s7mlSml+czWkg3D0l1nNjGNjz3myOQOa1KM/jOS6ebdbAVTCi4gljHSFrviza7tOgRWcS0MOUX9zdNgag5w7rRqA44Lzw0hr1WqES36dFliSJFlh2rXIae3FFcDDgKdxrUIDePr8jGcSClV1u7A9xeN0ModY/pHMxmR1EzRh8TJiwqsHmKW0l4FCEZI+jHio+JdPPE9qwQtTRxku2D8sIeRL2LnxWSllANCQGOIiqVHAz2ye2JR0DcH+HoxDkaADLjgxjKQ+AwCX/g0+DNgdG0ukYCONAe+dbc2IAc6fwt1ARoDSezNHxV2Cmzwv3O6lDMV55edBGwGK9n1+x2F8EDfAGCxug8MhpsMEcTEAWf3rx2vZhe/LAmtIn/6apE6PN0ULKgywD9mmdxbmFl3OvD5AS5fW5zLbv/YHmcsBTjf/afDz3MaZTVCfAP9z6/Bw6ycv8EUBWJIn9zYcoAWWlW9+OzO3vkTy8H+RANLmdrpOuYWdZYEXpo+TlCJrW5EARb7fF+bWdqf3hhyZI1nWJQHgznErZhbjoEsWqi8dQNoE294aldzFurwSABL2XXMf9+H1VQGke9exw5P/AnA5Pv5ngMul7LOvO922iwACu8WkCwLCafvM4CeWPxfA8lNHcWZSoi8EwMAIciKX2Z4SWCMAa3snCZ/G4EA8D6CMLNFsGQhkkz/gQNEBbPCbWsxGUpYVu3z8IyNAknwJkfPMEhLyrdi5RTyUVACkw4GSFRNWJNEW+fgPGwHD8/JxnRuLabN4CGNRkAE23na2+VmEAUmrYymSGjMAYqH84YUIyzgzs3XC7gNgH36Vcc4zKY9o9fgPBXUAiHHwVboBHGLiX6Zcjp1f2wu4tvzZKo0ecPnDtQYDQvJXaBeNzce45Fp28ZQLrEZVuFqgBwOalArKXnW1UzlnSusQKJqKYNuz4tOnI6sZG4zanpemv+7ySU2jbA9h6uhcgpfy6G2PahirDZ6zvq6zDduMVFTKvzw8wgyEdelwY9in3XkEPs3osJuwRQ4qTkfzifndg9Gfc4pdsu82+tTnHZTBa2EAMrqr2t43pguc8tNm7JQVQ2S0ukj2d22dhXYP0/veWtwKrCkNoNimAN5+Xr/oLrxswKbVJjteWrX7eR63o4j9q0GxnaBdWgGA5VStpanIjQmEhV0/nVt5VOFUvix6awJhPcAaTEShgrG+iGyvb5a0Ndb1YGHFPEwoqAinoaykaID1o1pdPNu7XsnCKQ3R+hwWIIhGvORcJUBYXe3Xa3vq/mF/N9V13ugufMkfXn+KHsRD0B8AAAAASUVORK5CYII=" type="image/x-icon" />
<script>
window.enableWebSocket = <%- escapeJson(enableWebSocket) %>;
</script>
<%- include('script', { filename: 'viewer.js' }) %>
</head>
<body>
<div id="app"></div>
<script>
window.chartData = <%- escapeJson(chartData) %>;
window.defaultSizes = <%- escapeJson(defaultSizes) %>;
</script>
</body>
</html>