├── .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 | ![](https://cloud.githubusercontent.com/assets/1145857/10471320/5b284d60-71da-11e5-8d35-7d1d4c58843a.png) 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 |
6 | {props.children} 7 | 8 |

Disclaimer

9 |

Due to limitations in Webpack's stats, the "actual" (minified) numbers reported here are approximate, but they should be pretty close.

10 | 11 |

Contribute!

12 |

Check it out on GitHub, and please report issues or request features!

13 | 14 |

Acknowledgements

15 |

Disc for Browserify did this first. Thanks also to this example from the D3 gallery for demonstating how to create sunburst charts.

16 | 17 |

Comments, questions

18 |

Let me know! @batemanchris

19 |
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 |
179 |

How do I get stats JSON from webpack?

180 |

webpack --json > stats.json

181 |

If you're customizing your stats output or using webpack-stats-plugin, be sure to set chunkModules to true (see here for an example).

182 | 183 |

Try the Plugin!

184 |

This tool is also available as a webpack plugin. See here for usage details.

185 |

npm install webpack-visualizer-plugin

186 |
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 | --------------------------------------------------------------------------------