├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
├── plugin
│ ├── main.jsx
│ ├── plugin-app.jsx
│ └── plugin.js
├── shared
│ ├── buildHierarchy.js
│ ├── colors.js
│ ├── components
│ │ ├── breadcrumbs.jsx
│ │ ├── chart-details.jsx
│ │ ├── chart-with-details.jsx
│ │ ├── chart.jsx
│ │ └── footer.jsx
│ ├── createVisualization.js
│ ├── partitionedDataUtils.js
│ ├── style.css
│ └── util
│ │ ├── dragdrop.js
│ │ ├── formatSize.js
│ │ ├── readFile.js
│ │ └── stat-utils.js
└── site
│ ├── app.jsx
│ ├── index.html.js
│ ├── main.jsx
│ └── serverRender.js
├── test
├── stats-demo.json
├── stats-multiple-without-chunkModules.json
├── stats-multiple.json
├── stats-simple.json
└── stats-single.json
├── webpack.base.js
├── webpack.dev.js
└── webpack.prod.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "plugins": [
4 | "react"
5 | ],
6 | "env": {
7 | "browser": true,
8 | "es6": true,
9 | "node": true
10 | },
11 | "ecmaFeatures": {
12 | "experimentalObjectRestSpread": true,
13 | "jsx": true,
14 | "modules": true
15 | },
16 | "rules": {
17 | "no-alert": 2,
18 | "no-array-constructor": 2,
19 | "no-bitwise": 1,
20 | "no-catch-shadow": 2,
21 | "no-empty": 1,
22 | "no-eval": 2,
23 | "no-extend-native": 2,
24 | "no-extra-bind": 1,
25 | "no-implied-eval": 2,
26 | "no-iterator": 2,
27 | "no-label-var": 2,
28 | "no-labels": 2,
29 | "no-lone-blocks": 2,
30 | "no-loop-func": 2,
31 | "no-multi-spaces": 1,
32 | "no-native-reassign": 2,
33 | "no-new-func": 2,
34 | "no-new-wrappers": 2,
35 | "no-octal-escape": 2,
36 | "no-proto": 2,
37 | "no-return-assign": 2,
38 | "no-sequences": 2,
39 | "no-shadow": 2,
40 | "no-shadow-restricted-names": 2,
41 | "no-spaced-func": 2,
42 | "no-undef-init": 2,
43 | "no-unused-vars": [2, {"vars": "all", "args": "none"}],
44 | "no-use-before-define": [2, "nofunc"],
45 | "no-with": 2,
46 |
47 | "arrow-spacing": 1,
48 | "brace-style": [2, "1tbs"],
49 | "camelcase": 1,
50 | "comma-dangle": 1,
51 | "comma-spacing": 1,
52 | "curly": [2, "all"],
53 | "dot-notation": [1, {"allowKeywords": true}],
54 | "eqeqeq": [2, "smart"],
55 | "indent": 2,
56 | "jsx-quotes": 1,
57 | "key-spacing": 1,
58 | "new-cap": 1,
59 | "new-parens": 2,
60 | "quotes": [2, "single"],
61 | "semi": 2,
62 | "semi-spacing": 1,
63 | "space-infix-ops": 1,
64 | "space-return-throw-case": 1,
65 | "space-unary-ops": 1,
66 | "strict": [0, "function"],
67 | "wrap-iife": [2, "any"],
68 | "yoda": [1, "never"],
69 |
70 | "react/jsx-closing-bracket-location": 1,
71 | "react/jsx-curly-spacing": 1,
72 | "react/jsx-indent-props": 1,
73 | "react/jsx-max-props-per-line": [1, {"maximum": 3}],
74 | "react/jsx-no-duplicate-props": 2,
75 | "react/jsx-no-undef": 2,
76 | "react/jsx-sort-prop-types": 1,
77 | "react/jsx-uses-react": 2,
78 | "react/jsx-uses-vars": 2,
79 | "react/no-did-mount-set-state": [2, "allow-in-func"],
80 | "react/no-did-update-set-state": 2,
81 | "react/no-direct-mutation-state": 2,
82 | "react/no-multi-comp": 1,
83 | "react/no-unknown-property": 2,
84 | "react/prop-types": [2, {ignore: "children"}],
85 | "react/react-in-jsx-scope": 2,
86 | "react/self-closing-comp": 1,
87 | "react/sort-comp": 2,
88 | "react/wrap-multilines": 2
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist-site
2 | lib
3 | node_modules
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Chris Bateman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webpack Visualizer
2 | Visualize and analyze your Webpack bundle to see which modules are taking up space and which might be duplicates.
3 |
4 | This tool is still pretty new, so please submit issues or feature requests!
5 |
6 |
7 | ## Site Usage
8 |
9 | Upload your stats JSON file to the site: [chrisbateman.github.io/webpack-visualizer/](http://chrisbateman.github.io/webpack-visualizer/)
10 |
11 | ## Plugin Usage
12 |
13 | ```
14 | npm install webpack-visualizer-plugin
15 | ```
16 | ```javascript
17 | var Visualizer = require('webpack-visualizer-plugin');
18 |
19 | //...
20 | plugins: [new Visualizer()],
21 | //...
22 | ```
23 | This will output a file named `stats.html` in your output directory. You can modify the name/location by passing a `filename` parameter into the constructor.
24 |
25 | ```javascript
26 | var Visualizer = require('webpack-visualizer-plugin');
27 |
28 | //...
29 | plugins: [new Visualizer({
30 | filename: './statistics.html'
31 | })],
32 | //...
33 | ```
34 |
35 | ---
36 |
37 | 
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack-visualizer-plugin",
3 | "version": "0.1.11",
4 | "main": "lib/plugin.js",
5 | "author": "Chris Bateman (http://cbateman.com/)",
6 | "license": "MIT",
7 | "files": [
8 | "lib",
9 | "README.md"
10 | ],
11 | "repository": {
12 | "type": "git",
13 | "url": "git@github.com:chrisbateman/webpack-visualizer.git"
14 | },
15 | "scripts": {
16 | "build": "npm run buildsite && npm run buildplugin",
17 | "prebuildplugin": "rm -rf lib && mkdir lib",
18 | "buildplugin": "webpack src/plugin/main.jsx lib/pluginmain.js --config webpack.prod.js",
19 | "postbuildplugin": "babel src/plugin/plugin.js --out-file lib/plugin.js && cp src/shared/style.css lib",
20 | "prebuildsite": "rm -rf dist-site && mkdir dist-site",
21 | "buildsite": "webpack src/site/main.jsx dist-site/build.js --config webpack.prod.js && babel-node src/site/serverRender.js",
22 | "postbuildsite": "cp src/shared/style.css test/stats-demo.json dist-site",
23 | "dev": "webpack-dev-server --config webpack.dev.js",
24 | "lint": "eslint src --ext .js,.jsx",
25 | "preversion": "npm run lint && npm run build",
26 | "publishSite": "git checkout gh-pages && cp dist-site/* . && git add . && git commit -m 'release' && git push origin gh-pages && git checkout master"
27 | },
28 | "dependencies": {
29 | "d3": "^3.5.6",
30 | "mkdirp": "^0.5.1",
31 | "react": "^0.14.0",
32 | "react-dom": "^0.14.0"
33 | },
34 | "devDependencies": {
35 | "babel": "^5.8.23",
36 | "babel-core": "^5.8.25",
37 | "babel-loader": "^5.3.2",
38 | "eslint": "^1.6.0",
39 | "eslint-plugin-react": "^3.5.1",
40 | "merge": "^1.2.0",
41 | "webpack": "^1.12.2",
42 | "webpack-dev-server": "^1.12.0"
43 | },
44 | "engines": {
45 | "npm": ">=2.13.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/plugin/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './plugin-app';
4 |
5 |
6 | ReactDOM.render(, document.getElementById('App'));
7 |
--------------------------------------------------------------------------------
/src/plugin/plugin-app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChartWithDetails from '../shared/components/chart-with-details';
3 | import Footer from '../shared/components/footer';
4 | import buildHierarchy from '../shared/buildHierarchy';
5 | import {getAssetsData, getBundleDetails, ERROR_CHUNK_MODULES} from '../shared/util/stat-utils';
6 |
7 |
8 | export default React.createClass({
9 | propTypes: {
10 | stats: React.PropTypes.object
11 | },
12 |
13 | getInitialState() {
14 | return {
15 | assets: [],
16 | chartData: null,
17 | selectedAssetIndex: 0
18 | };
19 | },
20 |
21 | componentWillMount() {
22 | let stats = this.props.stats;
23 | let assets = getAssetsData(stats.assets, stats.chunks);
24 |
25 | this.setState({
26 | assets,
27 | chartData: buildHierarchy(stats.modules),
28 | selectedAssetIndex: 0,
29 | stats
30 | });
31 | },
32 |
33 | onAssetChange(ev) {
34 | let selectedAssetIndex = Number(ev.target.value);
35 | let modules, chartData, error;
36 |
37 | if (selectedAssetIndex === 0) {
38 | modules = this.state.stats.modules;
39 | } else {
40 | let asset = this.state.assets[selectedAssetIndex - 1];
41 | modules = asset.chunk.modules;
42 | }
43 |
44 | if (modules) {
45 | chartData = buildHierarchy(modules);
46 | } else {
47 | error = ERROR_CHUNK_MODULES;
48 | }
49 |
50 | this.setState({
51 | chartData,
52 | error,
53 | selectedAssetIndex
54 | });
55 | },
56 |
57 | render() {
58 | let assetList;
59 | let bundleDetails = {};
60 |
61 | if (this.state.stats){
62 | bundleDetails = getBundleDetails({
63 | assets: this.state.assets,
64 | selectedAssetIndex: this.state.selectedAssetIndex
65 | });
66 | }
67 |
68 | if (this.state.assets.length > 1) {
69 | assetList = (
70 |
71 |
75 |
76 | );
77 | }
78 |
79 | return (
80 |
81 |
Webpack Visualizer
82 |
83 | {assetList}
84 |
85 |
86 |
87 | {this.state.error &&
{this.state.error}
}
88 |
89 |
90 |
91 | );
92 | }
93 | });
94 |
--------------------------------------------------------------------------------
/src/plugin/plugin.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 |
3 | import path from 'path';
4 | import fs from 'fs';
5 | import mkdirp from 'mkdirp';
6 | let cssString = fs.readFileSync(path.join(__dirname, './style.css'), 'utf8');
7 | let jsString = fs.readFileSync(path.join(__dirname, './pluginmain.js'), 'utf8');
8 |
9 |
10 | export default class VisualizerPlugin {
11 | constructor(opts = {filename: 'stats.html'}) {
12 | this.opts = opts;
13 | }
14 |
15 | apply(compiler) {
16 | compiler.plugin('emit', (compilation, callback) => {
17 | let stats = compilation.getStats().toJson({chunkModules: true});
18 | let stringifiedStats = JSON.stringify(stats);
19 | stringifiedStats = stringifiedStats.replace(/
22 |
23 | Webpack Visualizer
24 |
25 |
26 |
27 |
28 | `;
29 |
30 | let outputFile = path.join(compilation.outputOptions.path, this.opts.filename);
31 |
32 | mkdirp(path.dirname(outputFile), (mkdirpErr) => {
33 | if (mkdirpErr) {
34 | console.log('webpack-visualizer-plugin: error writing stats file');
35 | }
36 |
37 | fs.writeFile(outputFile, html, (err) => {
38 | if (err) {
39 | console.log('webpack-visualizer-plugin: error writing stats file');
40 | }
41 |
42 | callback();
43 | });
44 | });
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/shared/buildHierarchy.js:
--------------------------------------------------------------------------------
1 | export default function buildHierarchy(modules) {
2 | let maxDepth = 1;
3 |
4 | let root = {
5 | children: [],
6 | name: 'root'
7 | };
8 |
9 | modules.forEach(function addToTree(module) {
10 | // remove this module if either:
11 | // - index is null
12 | // - issued by extract-text-plugin
13 | let extractInIdentifier = module.identifier.indexOf('extract-text-webpack-plugin') !== -1;
14 | let extractInIssuer = module.issuer && module.issuer.indexOf('extract-text-webpack-plugin') !== -1;
15 | if (extractInIdentifier || extractInIssuer || module.index === null) {
16 | return;
17 | }
18 |
19 | let mod = {
20 | id: module.id,
21 | fullName: module.name,
22 | size: module.size,
23 | reasons: module.reasons
24 | };
25 |
26 | let depth = mod.fullName.split('/').length - 1;
27 | if (depth > maxDepth) {
28 | maxDepth = depth;
29 | }
30 |
31 | let fileName = mod.fullName;
32 |
33 | let beginning = mod.fullName.slice(0, 2);
34 | if (beginning === './') {
35 | fileName = fileName.slice(2);
36 | }
37 |
38 | getFile(mod, fileName, root);
39 | });
40 |
41 | root.maxDepth = maxDepth;
42 |
43 | return root;
44 | }
45 |
46 |
47 | function getFile(module, fileName, parentTree) {
48 | let charIndex = fileName.indexOf('/');
49 |
50 | if (charIndex !== -1) {
51 | let folder = fileName.slice(0, charIndex);
52 | if (folder === '~') {
53 | folder = 'node_modules';
54 | }
55 |
56 | let childFolder = getChild(parentTree.children, folder);
57 | if (!childFolder) {
58 | childFolder = {
59 | name: folder,
60 | children: []
61 | };
62 | parentTree.children.push(childFolder);
63 | }
64 |
65 | getFile(module, fileName.slice(charIndex + 1), childFolder);
66 | } else {
67 | module.name = fileName;
68 | parentTree.children.push(module);
69 | }
70 | }
71 |
72 |
73 | function getChild(arr, name) {
74 | for (let i = 0; i < arr.length; i++) {
75 | if (arr[i].name === name) {
76 | return arr[i];
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/shared/colors.js:
--------------------------------------------------------------------------------
1 | let colors = {
2 | '__file__': '#db7100',
3 | //'node_modules': '#599e59',
4 | //'node_modules': '#215E21',
5 | //'node_modules': '#326589', //#26587A',
6 | '__default__': '#487ea4'
7 | };
8 |
9 |
10 | export function getColor(obj) {
11 | let name = obj.name;
12 | let dotIndex = name.indexOf('.');
13 |
14 | if (dotIndex !== -1 && dotIndex !== 0 && dotIndex !== name.length - 1) {
15 | return colors.__file__;
16 | } else if (obj.parent && obj.parent.name === 'node_modules') {
17 | return '#599e59';
18 | }
19 |
20 | return colors[name] || colors.__default__;
21 | }
22 |
--------------------------------------------------------------------------------
/src/shared/components/breadcrumbs.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 |
3 |
4 | let Breadcrumbs = props => (
5 |
6 | {props.nodes.map((node, i) => {
7 | let result = ' > ';
8 | if (i === 0) {
9 | result = '';
10 | }
11 | return result + node.name;
12 | })}
13 |
14 | );
15 |
16 | Breadcrumbs.propTypes = {
17 | nodes: PropTypes.array
18 | };
19 |
20 | export default Breadcrumbs;
21 |
--------------------------------------------------------------------------------
/src/shared/components/chart-details.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 | import formatSize from '../util/formatSize';
3 |
4 |
5 | export default function ChartDetails(props) {
6 | let title, bigText, sizeText;
7 | let {bundleDetails, details} = props;
8 |
9 | if (details) {
10 | let rawSize = formatSize(details.size);
11 |
12 | if (bundleDetails.actual) {
13 | let actualSize = formatSize(bundleDetails.actual * details.percentage.replace('%', '') * .01, 0);
14 | sizeText = `${actualSize} actual | ${rawSize} raw`;
15 | } else {
16 | sizeText = `${rawSize} raw`;
17 | }
18 |
19 | title = details.name;
20 | bigText = details.percentage;
21 |
22 | } else if (bundleDetails.assetName) {
23 | title = bundleDetails.assetName;
24 | if (bundleDetails.type === 'collection') {
25 | bigText = ;
26 | sizeText = '';
27 | } else {
28 | let rawSize = formatSize(bundleDetails.raw);
29 | let actualSize = formatSize(bundleDetails.actual);
30 |
31 | bigText = ;
32 | sizeText = `${actualSize} actual | ${rawSize} raw`;
33 | }
34 | } else {
35 | return ;
36 | }
37 |
38 | return (
39 |
40 |
{title}
41 |
{bigText}
42 | {sizeText &&
{sizeText}
}
43 |
44 | );
45 | }
46 |
47 | ChartDetails.propTypes = {
48 | bundleDetails: PropTypes.object,
49 | details: PropTypes.object,
50 | topMargin: PropTypes.number
51 | };
52 |
--------------------------------------------------------------------------------
/src/shared/components/chart-with-details.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 | import Chart from './chart';
3 | import ChartDetails from './chart-details';
4 | import Breadcrumbs from './breadcrumbs';
5 |
6 |
7 | export default class ChartWithDetails extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | breadcrumbNodes: [],
13 | hoverDetails: null,
14 | paddingDiff: 0
15 | };
16 |
17 | this.onChartHover = this.onChartHover.bind(this);
18 | this.onChartUnhover = this.onChartUnhover.bind(this);
19 | this.onChartRender = this.onChartRender.bind(this);
20 | }
21 |
22 | onChartHover(details) {
23 | this.setState({
24 | hoverDetails: details,
25 | breadcrumbNodes: details.ancestorArray
26 | });
27 | }
28 |
29 | onChartUnhover() {
30 | this.setState({
31 | hoverDetails: null,
32 | breadcrumbNodes: []
33 | });
34 | }
35 |
36 | onChartRender(details) {
37 | this.setState({
38 | paddingDiff: details.removedTopPadding
39 | });
40 | }
41 |
42 | render() {
43 | let chartAreaClass = 'chart';
44 |
45 | if (this.props.chartData && this.props.chartData.maxDepth > 9) {
46 | chartAreaClass += ' chart--large';
47 | }
48 |
49 | if (!this.props.bundleDetails || Object.keys(this.props.bundleDetails).length === 0) {
50 | return null;
51 | }
52 |
53 | return (
54 |
55 |
56 |
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 | ChartWithDetails.propTypes = {
69 | breadcrumbNodes: PropTypes.array,
70 | bundleDetails: PropTypes.object,
71 | chartData: PropTypes.object
72 | };
73 |
--------------------------------------------------------------------------------
/src/shared/components/chart.jsx:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 | import createVisualization from '../createVisualization';
3 |
4 |
5 | export default React.createClass({
6 | propTypes: {
7 | data: PropTypes.object,
8 | onHover: PropTypes.func,
9 | onRender: PropTypes.func,
10 | onUnhover: PropTypes.func
11 | },
12 |
13 | componentDidMount() {
14 | if (this.props.data) {
15 | this.createChart(this.props.data);
16 | }
17 | },
18 |
19 | componentDidUpdate(prevProps) {
20 | if (this.props.data && this.props.data !== prevProps.data) {
21 | this.createChart(this.props.data);
22 | }
23 | },
24 |
25 | createChart(root) {
26 | let details = createVisualization({
27 | svgElement: this.refs.svg,
28 | root,
29 | onHover: this.props.onHover,
30 | onUnhover: this.props.onUnhover
31 | });
32 |
33 | if (this.props.onRender) {
34 | this.props.onRender(details);
35 | }
36 | },
37 |
38 | render() {
39 | if (!this.props.data) {
40 | return null;
41 | }
42 |
43 | return ;
44 | }
45 | });
46 |
--------------------------------------------------------------------------------
/src/shared/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 |
4 | export default props => (
5 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/shared/createVisualization.js:
--------------------------------------------------------------------------------
1 | import d3 from 'd3';
2 | import {getColor} from './colors';
3 | import {markDuplicates, getAllChildren, getAncestors} from './partitionedDataUtils';
4 |
5 |
6 | const FADE_OPACITY = 0.5;
7 | let paths, vis, totalSize;
8 |
9 |
10 | export default function createVisualization({svgElement, root, onHover, onUnhover}) {
11 | let chartSize = (root.maxDepth > 9) ? 950 : 750;
12 | let radius = Math.min(chartSize, chartSize) / 2;
13 |
14 |
15 | let partition = d3.layout.partition()
16 | .size([2 * Math.PI, radius * radius])
17 | .value(d => d.size);
18 |
19 |
20 | let arc = d3.svg.arc()
21 | .startAngle(d => d.x)
22 | .endAngle(d => d.x + d.dx)
23 | //.innerRadius(d => d.y / 400 + 60)
24 | //.outerRadius(d => (d.y + d.dy) / 400 + 60);
25 | .innerRadius(d => Math.sqrt(d.y))
26 | .outerRadius(d => Math.sqrt(d.y + d.dy));
27 |
28 |
29 | if (vis) {
30 | svgElement.innerHTML = '';
31 | }
32 |
33 |
34 | // Filter out very small nodes
35 | let nodes = partition.nodes(root).filter(d => d.dx > 0.005); // 0.005 radians
36 |
37 | markDuplicates(nodes);
38 |
39 |
40 | vis = d3.select(svgElement)
41 | .attr('width', chartSize)
42 | .attr('height', chartSize)
43 | .append('svg:g')
44 | .attr('id', 'svgWrapper')
45 | .attr('transform', `translate(${chartSize / 2}, ${chartSize / 2})`);
46 |
47 |
48 | paths = vis.data([root]).selectAll('path')
49 | .data(nodes)
50 | .enter()
51 | .append('svg:path')
52 | .attr('display', d => (d.depth ? null : 'none'))
53 | .attr('d', arc)
54 | .attr('fill-rule', 'evenodd')
55 | .style('stroke', d => (d.duplicate) ? '#000' : '')
56 | .style('fill', d => getColor(d))
57 | .style('opacity', 1)
58 | .on('mouseover', object => {
59 | mouseover(object, onHover);
60 | });
61 |
62 | totalSize = paths.node().__data__.value;
63 |
64 |
65 | let svgWrapper = vis[0][0];
66 | let chart = svgElement.parentNode;
67 |
68 | let visHeight = svgWrapper.getBoundingClientRect().height;
69 |
70 | let topPadding = (svgWrapper.getBoundingClientRect().top + window.scrollY) - (d3.select(chart)[0][0].getBoundingClientRect().top + window.scrollY);
71 |
72 | d3.select(svgElement).attr('height', visHeight);
73 | vis.attr('transform', `translate(${chartSize / 2}, ${(chartSize / 2) - topPadding})`);
74 | d3.select(chart.querySelector('.details')).style('margin-top', `${-topPadding}px`);
75 |
76 |
77 | d3.select(svgWrapper).on('mouseleave', object => {
78 | mouseleave(object, onUnhover);
79 | });
80 |
81 | return {
82 | removedTopPadding: topPadding,
83 | vis
84 | };
85 | }
86 |
87 |
88 | function mouseover(object, callback) {
89 | let childrenArray = getAllChildren(object);
90 | let ancestorArray = getAncestors(object);
91 |
92 | // Fade all the segments.
93 | paths.style({
94 | 'opacity': FADE_OPACITY,
95 | 'stroke-width': FADE_OPACITY
96 | });
97 |
98 | // Highlight only those that are children of the current segment.
99 | paths.filter(node => childrenArray.indexOf(node) >= 0)
100 | .style({
101 | 'stroke-width': 2,
102 | 'opacity': 1
103 | });
104 |
105 | let percentage = (100 * object.value / totalSize).toFixed(1);
106 | let percentageString = percentage + '%';
107 | if (percentage < 0.1) {
108 | percentageString = '< 0.1%';
109 | }
110 |
111 | callback({
112 | ancestorArray,
113 | name: object.name,
114 | size: object.value,
115 | percentage: percentageString
116 | });
117 | }
118 |
119 | function mouseleave(object, callback) {
120 | paths.style({
121 | 'opacity': 1,
122 | 'stroke-width': 1
123 | });
124 |
125 | callback();
126 | }
127 |
--------------------------------------------------------------------------------
/src/shared/partitionedDataUtils.js:
--------------------------------------------------------------------------------
1 |
2 | export function getAncestors(node) {
3 | let ancestors = [];
4 | let current = node;
5 |
6 | while (current.parent) {
7 | ancestors.unshift(current);
8 | current = current.parent;
9 | }
10 |
11 | return ancestors;
12 | }
13 |
14 |
15 | export function getAllChildren(rootNode) {
16 | let allChildren = [];
17 |
18 | let getChildren = function(node) {
19 | allChildren.push(node);
20 |
21 | if (node.children) {
22 | node.children.forEach(child => {
23 | getChildren(child);
24 | });
25 | }
26 | };
27 |
28 | getChildren(rootNode);
29 |
30 | return allChildren;
31 | }
32 |
33 |
34 | export function markDuplicates(nodes) {
35 | let fullNameList = {};
36 |
37 | nodes.forEach(item => {
38 | if (!item.fullName) {
39 | return;
40 | }
41 |
42 | let lastIndex = item.fullName.lastIndexOf('~');
43 | if (lastIndex !== -1) {
44 | let fullName = item.fullName.substring(lastIndex);
45 |
46 | if (fullName in fullNameList) {
47 | item.duplicate = true;
48 | fullNameList[fullName].duplicate = true;
49 | } else {
50 | fullNameList[fullName] = item;
51 | }
52 | }
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/src/shared/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | background-color: #f7eedf;
3 | color: #333;
4 | }
5 |
6 | body {
7 | font-family: sans-serif;
8 | margin: 10px auto 0;
9 | width: 750px;
10 | padding: 0 10px;
11 | }
12 |
13 | a,
14 | .destyledButton {
15 | color: #347AB7;
16 | }
17 |
18 | p {
19 | margin-top: 0.5em;
20 | }
21 |
22 | svg {
23 | vertical-align: middle;
24 | }
25 |
26 | h1 {
27 | font-family: "Oswald", "HelveticaNeue-CondensedBold", "Arial Narrow", sans-serif;
28 | font-weight: bold;
29 | font-size: 70px;
30 | text-transform: uppercase;
31 | text-align: center;
32 | }
33 |
34 | hr {
35 | border: 0 none;
36 | border-top: 1px solid #aaa;
37 | }
38 |
39 | code {
40 | font-size: 16px;
41 | }
42 |
43 |
44 |
45 | .breadcrumbs {
46 | height: 1em;
47 | margin: 1em 0;
48 | }
49 |
50 | .uploadArea {
51 | position: relative;
52 | margin: 0 auto;
53 | min-height: 350px;
54 | }
55 | .uploadArea--needsUpload {
56 | border: 2px dashed #AC9062;
57 | border-radius: 10px;
58 | cursor: pointer;
59 | }
60 |
61 | .uploadArea--dragging {
62 | border-style: solid;
63 | background-color: #E6D4B6;
64 | }
65 |
66 | .uploadArea-uploadMessage {
67 | display: none;
68 | font-size: 1.9em;
69 | text-align: center;
70 | margin-top: 100px;
71 | pointer-events: none;
72 | }
73 | .uploadArea--needsUpload .uploadArea-uploadMessage {
74 | display: block;
75 | }
76 |
77 | .uploadArea-uploadMessage small {
78 | font-size: 0.5em;
79 | }
80 |
81 |
82 | .chart {
83 | position: relative;
84 | margin: 0 auto;
85 | min-height: 350px;
86 | }
87 | .chart--large {
88 | width: 950px;
89 | margin-left: -100px;
90 | }
91 |
92 |
93 |
94 | .hiddenFileInput {
95 | width: 0px;
96 | height: 0px;
97 | visibility: hidden;
98 | }
99 |
100 | .chart path {
101 | stroke: #fff;
102 | }
103 |
104 | .details {
105 | position: absolute;
106 | top: 325px;
107 | left: 50%;
108 | width: 170px;
109 | margin-left: -85px;
110 | font-size: 14px;
111 | text-align: center;
112 | color: #666;
113 | z-index: -1;
114 | overflow: hidden;
115 | text-overflow: ellipsis;
116 | }
117 |
118 | .chart--large .details {
119 | top: 425px;
120 | }
121 |
122 | .details-size {
123 | font-size: 0.9em;
124 | margin-top: 1em;
125 | }
126 |
127 | .details-name {
128 | font-weight: bold;
129 | }
130 |
131 | .details-subText {
132 | min-height: 1.2em;
133 | }
134 |
135 | .details-percentage {
136 | margin: 0.4em 0 0em;
137 | font-size: 2.4em;
138 | line-height: 1em;
139 | }
140 |
141 |
142 |
143 | .errorMessage {
144 | margin-top: 2em;
145 | padding: 0.8em 1em;
146 | border: 1px solid #ab2222;
147 | color: #ab2222;
148 | }
149 |
150 |
151 | footer {
152 | margin-top: 4em;
153 | }
154 |
155 | footer h2 {
156 | margin: 1.5em 0 0.5em;
157 | font-size: 1.3em;
158 | }
159 |
160 |
161 |
162 | .destyledButton {
163 | background: none;
164 | border: 0 none;
165 | cursor: pointer;
166 | font-size: inherit;
167 | padding: 0;
168 | text-decoration: underline;
169 | }
170 |
171 |
172 |
173 | @font-face {
174 | font-family: 'Oswald';
175 | src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff');
176 | font-weight: bold;
177 | font-style: normal;
178 | }
179 |
--------------------------------------------------------------------------------
/src/shared/util/dragdrop.js:
--------------------------------------------------------------------------------
1 |
2 | export default function addDragDrop({el, onDragStart, onDragEnd, callback}) {
3 | el.addEventListener('dragenter', onDragStart);
4 | el.addEventListener('dragleave', onDragEnd);
5 | el.addEventListener('dragover', onDragOver);
6 | el.addEventListener('drop', onDrop);
7 |
8 |
9 | function onDragOver(ev) {
10 | ev.preventDefault();
11 | }
12 |
13 | function onDrop(ev) {
14 | ev.preventDefault();
15 |
16 | let file = ev.dataTransfer.files[0];
17 |
18 | onDragEnd();
19 | callback(file);
20 | }
21 | }
--------------------------------------------------------------------------------
/src/shared/util/formatSize.js:
--------------------------------------------------------------------------------
1 |
2 | export default function formatSize(size, precision = 1) {
3 | let kb = {
4 | label: 'k',
5 | value: 1024
6 | };
7 | let mb = {
8 | label: 'M',
9 | value: 1024 * 1024
10 | };
11 | let denominator;
12 |
13 | if (size >= mb.value) {
14 | denominator = mb;
15 | } else {
16 | denominator = kb;
17 | if (size < (kb.value * 0.92) && precision === 0) {
18 | precision = 1;
19 | }
20 | }
21 | return (size / denominator.value).toFixed(precision) + denominator.label;
22 | }
23 |
--------------------------------------------------------------------------------
/src/shared/util/readFile.js:
--------------------------------------------------------------------------------
1 |
2 | export default function readFile(file, callback) {
3 | let reader = new FileReader();
4 |
5 | reader.onloadend = ev => {
6 | if (ev.target.readyState === FileReader.DONE) {
7 | callback(reader.result);
8 | }
9 | };
10 |
11 | reader.readAsText(file);
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/util/stat-utils.js:
--------------------------------------------------------------------------------
1 |
2 | export const ERROR_CHUNK_MODULES = `Unfortunately, it looks like your stats don't include chunk-specific module data. See below for details.`;
3 |
4 |
5 | export function getAssetsData(assets, chunks) {
6 | let chunksMap = {};
7 | chunks.forEach(chunk => {
8 | chunksMap[chunk.id] = chunk;
9 | });
10 |
11 | return assets
12 | .filter(asset => asset.name.indexOf('.js') === asset.name.length - 3)
13 | .map(asset => {
14 | let chunkIndex = asset.chunks[0];
15 |
16 | return {
17 | ...asset,
18 | chunk: chunksMap[chunkIndex]
19 | };
20 | });
21 | }
22 |
23 |
24 | export function getBundleDetails({assets, chunks, selectedAssetIndex}) {
25 |
26 | if (selectedAssetIndex === 0) {
27 | if (assets.length === 1) {
28 | return {
29 | type: 'normal',
30 | assetName: assets[0].name,
31 | actual: assets[0].size,
32 | raw: assets.reduce((total, thisAsset) => total + thisAsset.chunk.size, 0)
33 | };
34 | } else {
35 | return {
36 | type: 'collection',
37 | assetName: 'All Modules',
38 | actual: '',
39 | raw: ''
40 | };
41 | }
42 | } else {
43 | let asset = assets[selectedAssetIndex - 1];
44 |
45 | return {
46 | type: 'normal',
47 | assetName: asset.name,
48 | actual: asset.size,
49 | raw: asset.chunk.size
50 | };
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/site/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ChartWithDetails from '../shared/components/chart-with-details';
3 | import Footer from '../shared/components/footer';
4 | import addDragDrop from '../shared/util/dragdrop';
5 | import readFile from '../shared/util/readFile';
6 | import formatSize from '../shared/util/formatSize';
7 | import {getAssetsData, getBundleDetails, ERROR_CHUNK_MODULES} from '../shared/util/stat-utils';
8 | import buildHierarchy from '../shared/buildHierarchy';
9 |
10 |
11 | export default React.createClass({
12 | getInitialState() {
13 | return {
14 | assets: [],
15 | needsUpload: true,
16 | dragging: false,
17 | chartData: null,
18 | selectedAssetIndex: 0
19 | };
20 | },
21 |
22 | componentDidMount() {
23 | addDragDrop({
24 | el: this.refs.UploadArea,
25 | callback: file => {
26 | readFile(file, this.handleFileUpload);
27 | },
28 | onDragStart: () => {
29 | this.setState({
30 | dragging: true
31 | });
32 | },
33 | onDragEnd: () => {
34 | this.setState({
35 | dragging: false
36 | });
37 | }
38 | });
39 | },
40 |
41 | uploadAreaClick() {
42 | if (this.state.needsUpload) {
43 | this.refs.FileInput.click();
44 | }
45 | },
46 |
47 | onFileChange(ev) {
48 | readFile(ev.target.files[0], this.handleFileUpload);
49 | },
50 |
51 | handleFileUpload(jsonText) {
52 | let stats = JSON.parse(jsonText);
53 | let assets = getAssetsData(stats.assets, stats.chunks);
54 |
55 | this.setState({
56 | assets,
57 | chartData: buildHierarchy(stats.modules),
58 | needsUpload: false,
59 | selectedAssetIndex: 0,
60 | stats
61 | });
62 | },
63 |
64 | loadDemo() {
65 | this.setState({
66 | demoLoading: true
67 | });
68 |
69 | let request = new XMLHttpRequest();
70 | request.open('GET', 'stats-demo.json', true);
71 |
72 | request.onload = () => {
73 | this.setState({
74 | demoLoading: false
75 | });
76 |
77 | if (request.status >= 200 && request.status < 400) {
78 | this.handleFileUpload(request.response);
79 | }
80 | };
81 |
82 | request.send();
83 | },
84 |
85 | onAssetChange(ev) {
86 | let selectedAssetIndex = Number(ev.target.value);
87 | let modules, chartData, error;
88 |
89 | if (selectedAssetIndex === 0) {
90 | modules = this.state.stats.modules;
91 | } else {
92 | let asset = this.state.assets[selectedAssetIndex - 1];
93 | modules = asset.chunk.modules;
94 | }
95 |
96 | if (modules) {
97 | chartData = buildHierarchy(modules);
98 | } else {
99 | error = ERROR_CHUNK_MODULES;
100 | }
101 |
102 | this.setState({
103 | chartData,
104 | error,
105 | selectedAssetIndex
106 | });
107 | },
108 |
109 | renderUploadArea(uploadAreaClass) {
110 | if (this.state.needsUpload) {
111 | return (
112 |
113 |
114 |
Drop JSON file here or click to choose.
115 |
Files won't be uploaded — your data stays in your browser.
116 |
117 |
123 |
124 | );
125 | }
126 | },
127 |
128 | render() {
129 | let demoButton, assetList;
130 | let uploadAreaClass = 'uploadArea';
131 | let bundleDetails = {};
132 |
133 | if (this.state.dragging) {
134 | uploadAreaClass += ' uploadArea--dragging';
135 | }
136 |
137 | if (this.state.needsUpload) {
138 | uploadAreaClass += ' uploadArea--needsUpload';
139 |
140 | let demoClass = 'destyledButton';
141 | if (this.state.demoLoading) {
142 | demoClass += ' demoLoading';
143 | }
144 |
145 | demoButton = ;
146 | }
147 |
148 | if (this.state.stats){
149 | bundleDetails = getBundleDetails({
150 | assets: this.state.assets,
151 | selectedAssetIndex: this.state.selectedAssetIndex
152 | });
153 | }
154 |
155 | if (this.state.assets.length > 1) {
156 | assetList = (
157 |
158 |
162 |
163 | );
164 | }
165 |
166 | return (
167 |
168 |
Webpack Visualizer
169 |
170 | {assetList}
171 | {this.renderUploadArea(uploadAreaClass)}
172 | {demoButton}
173 |
174 |
175 |
176 | {this.state.error &&
{this.state.error}
}
177 |
178 |
187 |
188 | );
189 | }
190 | });
191 |
--------------------------------------------------------------------------------
/src/site/index.html.js:
--------------------------------------------------------------------------------
1 |
2 | export default function(cfg) {
3 | return (
4 | `
5 |
6 |
7 | Webpack Visualizer
8 |
9 |
10 |
11 |
12 |
13 | ${cfg.appHTML}
14 |
15 |
23 | `
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/site/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './app';
4 |
5 |
6 | ReactDOM.render(, document.getElementById('App'));
7 |
--------------------------------------------------------------------------------
/src/site/serverRender.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/server';
4 | import App from './app';
5 | import createHTMLString from './index.html.js';
6 |
7 |
8 | let pageHTML = createHTMLString({
9 | appHTML: ReactDOM.renderToString()
10 | });
11 |
12 | fs.writeFile('dist-site/index.html', pageHTML);
13 |
--------------------------------------------------------------------------------
/test/stats-simple.json:
--------------------------------------------------------------------------------
1 | {
2 | "assets": [],
3 | "modules": [
4 | {
5 | "id": 0,
6 | "name": "./src/main.jsx",
7 | "identifier": "/dev/app/src/main.jsx",
8 | "size": 4,
9 | "reasons": []
10 | },
11 |
12 | {
13 | "id": 1,
14 | "name": "./~/someLib/someLib.js",
15 | "identifier": "/dev/app/node_modules/someLib/someLib.js",
16 | "size": 10,
17 | "reasons": [{"moduleId": 0}, {"moduleId": 3}, {"moduleId": 3}, {"moduleId": 4}]
18 | },
19 |
20 | {
21 | "id": 2,
22 | "name": "./~/someLib/~/sub/index.js",
23 | "identifier": "/dev/app/node_modules/someLib/~/sub/index.js",
24 | "size": 5,
25 | "reasons": [{
26 | "moduleId": 1
27 | }]
28 | },
29 |
30 | {
31 | "id": 3,
32 | "name": "./src/routes.jsx",
33 | "identifier": "/dev/app/src/routes.jsx",
34 | "size": 3,
35 | "reasons": [{
36 | "moduleId": 0
37 | }]
38 | },
39 |
40 | {
41 | "id": 4,
42 | "name": "./src/routes/a.jsx",
43 | "identifier": "/dev/app/src/routes/a.jsx",
44 | "size": 1,
45 | "reasons": [{
46 | "moduleId": 3
47 | }]
48 | },
49 |
50 | {
51 | "id": 5,
52 | "name": "./src/routes/b.jsx",
53 | "identifier": "/dev/app/src/routes/b.jsx",
54 | "size": 1,
55 | "reasons": [{
56 | "moduleId": 3
57 | }]
58 | },
59 |
60 | {
61 | "id": 6,
62 | "name": "./src/util/thing.js",
63 | "identifier": "/dev/app/src/util/thing.js",
64 | "size": 2,
65 | "reasons": [{
66 | "moduleId": 5
67 | }]
68 | },
69 |
70 | {
71 | "id": 7,
72 | "name": "./~/someLib/~/sub/wow/wow.js",
73 | "identifier": "/dev/app/node_modules/someLib/~/sub/wow/wow.js",
74 | "size": 5,
75 | "reasons": [{
76 | "moduleId": 6
77 | }]
78 | },
79 |
80 | {
81 | "id": 8,
82 | "name": "./~/someLib/~/sub/wow/such/such.js",
83 | "identifier": "/dev/app/node_modules/someLib/~/sub/wow/such/such.js",
84 | "size": 5,
85 | "reasons": [{
86 | "moduleId": 7
87 | }]
88 | },
89 |
90 | {
91 | "id": 9,
92 | "name": "./~/someLib/~/sub/wow/such/very/very.js",
93 | "identifier": "/dev/app/node_modules/someLib/~/sub/wow/such/very/very.js",
94 | "size": 5,
95 | "reasons": [{
96 | "moduleId": 8
97 | }]
98 | },
99 |
100 | {
101 | "id": 10,
102 | "name": "./~/someLib/~/sub/wow/such/very/nesting/nesting.js",
103 | "identifier": "/dev/app/node_modules/someLib/~/sub/wow/such/very/nesting/nesting.js",
104 | "size": 5,
105 | "reasons": [{
106 | "moduleId": 9
107 | }]
108 | },
109 |
110 | {
111 | "id": 101,
112 | "name": "./~/someLib/~/sub/wow/such/very/nesting/much/so/nesting.js",
113 | "identifier": "/dev/app/node_modules/someLib/~/sub/wow/such/very/nesting/much/so/nesting.js",
114 | "size": 5,
115 | "reasons": [{
116 | "moduleId": 9
117 | }]
118 | },
119 |
120 | {
121 | "id": 11,
122 | "name": "./~/testlib/lib.js",
123 | "identifier": "/dev/app/node_modules/testlib/lib.js",
124 | "size": 10,
125 | "reasons": [{
126 | "moduleId": 0
127 | }]
128 | },
129 |
130 | {
131 | "id": 12,
132 | "name": "./~/someLib/~/testlib/lib.js",
133 | "identifier": "/dev/app/node_modules/someLib/~/testlib/lib.js",
134 | "size": 10,
135 | "reasons": [{
136 | "moduleId": 2
137 | }]
138 | },
139 |
140 | {
141 | "id": 13,
142 | "name": "../symlinklib/slib.js",
143 | "identifier": "/dev/symlinklib/slib.js",
144 | "size": 3,
145 | "reasons": [{
146 | "moduleId": 0
147 | }]
148 | },
149 |
150 | {
151 | "id": 14,
152 | "name": "C:/aliasedlib/alib.js",
153 | "identifier": "/dev/aliasedlib/alib.js",
154 | "size": 3,
155 | "reasons": [{
156 | "moduleId": 0
157 | }]
158 | }
159 |
160 | ]
161 | }
--------------------------------------------------------------------------------
/webpack.base.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var webpack = require('webpack');
5 |
6 |
7 | module.exports = {
8 | context: __dirname,
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.(js|jsx)$/,
13 | loaders: ['babel'],
14 | exclude: /node_modules/
15 | }
16 | ]
17 | },
18 | resolve: {
19 | extensions: ['', '.js', '.jsx']
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var webpack = require('webpack');
5 | var merge = require('merge');
6 |
7 | var baseConfig = require('./webpack.base.js');
8 |
9 | var devConfig = {
10 | entry: './src/site/main',
11 | output: {
12 | filename: 'build.js'
13 | },
14 | devtool: 'source-map', // @see http://webpack.github.io/docs/configuration.html#devtool
15 | devServer: {
16 | inline: true,
17 | contentBase: 'dist-site',
18 | host: process.env.IP,
19 | port: process.env.PORT
20 | }
21 | };
22 |
23 |
24 | module.exports = merge({}, baseConfig, devConfig);
25 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var path = require('path');
4 | var webpack = require('webpack');
5 | var merge = require('merge');
6 |
7 | var baseConfig = require('./webpack.base.js');
8 |
9 |
10 | var config = merge(true, baseConfig);
11 |
12 | if (!config.plugins) config.plugins = [];
13 |
14 | config.plugins = config.plugins.concat([
15 | new webpack.DefinePlugin({
16 | 'process.env': {
17 | NODE_ENV: JSON.stringify('production')
18 | }
19 | }),
20 | new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}),
21 | new webpack.NoErrorsPlugin()
22 | ]);
23 |
24 | module.exports = config;
25 |
--------------------------------------------------------------------------------