diff --git a/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js index 16810f78d9..8c37c56d25 100644 --- a/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js +++ b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js @@ -190,6 +190,9 @@ class DashboardPlugin { asset.fullPath = path.join(outputPath, asset.name) asset.gzipSize = assetSources && getGzipSize(assetSources.get(asset.name)) }) + statsData.modules.forEach(module => { + module.gzipSize = module.source && getGzipSize(module.source) + }) const hasErrors = stats.hasErrors() diff --git a/packages/@vue/cli-service/ui.js b/packages/@vue/cli-service/ui.js index 349d5823d3..10c9e760f3 100644 --- a/packages/@vue/cli-service/ui.js +++ b/packages/@vue/cli-service/ui.js @@ -30,6 +30,23 @@ module.exports = api => { } // Tasks + const views = { + views: [ + { + id: 'vue-webpack-dashboard', + label: 'Dashboard', + icon: 'dashboard', + component: 'vue-webpack-dashboard' + }, + { + id: 'vue-webpack-analyzer', + label: 'Analyzer', + icon: 'donut_large', + component: 'vue-webpack-analyzer' + } + ], + defaultView: 'vue-webpack-dashboard' + } api.describeTask({ match: /vue-cli-service serve/, description: 'Compiles and hot-reloads for development', @@ -100,15 +117,7 @@ module.exports = api => { api.ipcOff(onWebpackMessage) removeSharedData('serve-url') }, - views: [ - { - id: 'vue-webpack-dashboard', - label: 'Dashboard', - icon: 'dashboard', - component: 'vue-webpack-dashboard' - } - ], - defaultView: 'vue-webpack-dashboard' + ...views }) api.describeTask({ match: /vue-cli-service build/, @@ -189,15 +198,7 @@ module.exports = api => { onExit: () => { api.ipcOff(onWebpackMessage) }, - views: [ - { - id: 'vue-webpack-dashboard', - label: 'Dashboard', - icon: 'dashboard', - component: 'vue-webpack-dashboard' - } - ], - defaultView: 'vue-webpack-dashboard' + ...views }) // Webpack dashboard diff --git a/packages/@vue/cli-ui-addon-webpack/src/components/DonutModule.vue b/packages/@vue/cli-ui-addon-webpack/src/components/DonutModule.vue new file mode 100644 index 0000000000..1d9325053a --- /dev/null +++ b/packages/@vue/cli-ui-addon-webpack/src/components/DonutModule.vue @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + diff --git a/packages/@vue/cli-ui-addon-webpack/src/components/WebpackAnalyzer.vue b/packages/@vue/cli-ui-addon-webpack/src/components/WebpackAnalyzer.vue new file mode 100644 index 0000000000..f28d5a56af --- /dev/null +++ b/packages/@vue/cli-ui-addon-webpack/src/components/WebpackAnalyzer.vue @@ -0,0 +1,325 @@ + + + + + Analyzer + + + + + + + + + + + Gzip size + + + + + + + + + + + + + + + + + Disk: {{ describedModule.size.disk | size('B')}} + + + Gzip: {{ describedModule.size.gzip | size('B')}} + + + + + + + + + + diff --git a/packages/@vue/cli-ui-addon-webpack/src/components/WebpackDashboard.vue b/packages/@vue/cli-ui-addon-webpack/src/components/WebpackDashboard.vue index 7ae3c31a3a..4a401276f5 100644 --- a/packages/@vue/cli-ui-addon-webpack/src/components/WebpackDashboard.vue +++ b/packages/@vue/cli-ui-addon-webpack/src/components/WebpackDashboard.vue @@ -42,7 +42,7 @@ diff --git a/packages/@vue/cli-ui-addon-webpack/src/main.js b/packages/@vue/cli-ui-addon-webpack/src/main.js index 001bb2f0ba..688f204ae4 100644 --- a/packages/@vue/cli-ui-addon-webpack/src/main.js +++ b/packages/@vue/cli-ui-addon-webpack/src/main.js @@ -1,5 +1,6 @@ import VueProgress from 'vue-progress-path' import WebpackDashboard from './components/WebpackDashboard.vue' +import WebpackAnalyzer from './components/WebpackAnalyzer.vue' import TestView from './components/TestView.vue' Vue.use(VueProgress, { @@ -7,6 +8,7 @@ Vue.use(VueProgress, { }) ClientAddonApi.component('vue-webpack-dashboard', WebpackDashboard) +ClientAddonApi.component('vue-webpack-analyzer', WebpackAnalyzer) ClientAddonApi.addRoutes('vue-webpack', [ { path: '', name: 'test-webpack-route', component: TestView } diff --git a/packages/@vue/cli-ui-addon-webpack/src/mixins/Dashboard.js b/packages/@vue/cli-ui-addon-webpack/src/mixins/Dashboard.js new file mode 100644 index 0000000000..f2cc871d8f --- /dev/null +++ b/packages/@vue/cli-ui-addon-webpack/src/mixins/Dashboard.js @@ -0,0 +1,34 @@ +import store from '../store' + +// @vue/component +export default { + store, + + inject: [ + 'TaskDetails' + ], + + data () { + return { + mode: null + } + }, + + computed: { + useGzip: { + get () { return this.$store.getters.useGzip }, + set (value) { this.$store.commit('useGzip', value) } + } + }, + + created () { + const mode = this.mode = this.TaskDetails.task.command.indexOf('vue-cli-service serve') !== -1 ? 'serve' : 'build' + this.$store.commit('mode', mode) + this.$watchSharedData(`webpack-dashboard-${mode}-stats`, value => { + this.$store.commit('stats', { + mode, + value + }) + }) + } +} diff --git a/packages/@vue/cli-ui-addon-webpack/src/store/index.js b/packages/@vue/cli-ui-addon-webpack/src/store/index.js index 78d479681a..f696891b37 100644 --- a/packages/@vue/cli-ui-addon-webpack/src/store/index.js +++ b/packages/@vue/cli-ui-addon-webpack/src/store/index.js @@ -1,7 +1,7 @@ import Vuex from 'vuex' import { buildSortedAssets } from '../util/assets' -import { buildDepModules } from '../util/modules' +import { filterModules, buildDepModules, buildModulesTrees } from '../util/modules' Vue.use(Vuex) @@ -28,8 +28,9 @@ const store = new Vuex.Store({ assets: (state, getters) => (getters.stats && getters.stats.data.assets) || [], assetsSorted: (state, getters) => buildSortedAssets(getters.assets, getters.useGzip), assetsTotalSize: (state, getters) => getters.assetsSorted.filter(a => !a.secondary).reduce((total, asset) => total + asset.size, 0), - modules: (state, getters) => (getters.stats && getters.stats.data.modules) || [], + modules: (state, getters) => (getters.stats && filterModules(getters.stats.data.modules)) || [], modulesTotalSize: (state, getters) => getters.modules.reduce((total, module) => total + module.size, 0), + modulesTrees: (state, getters) => buildModulesTrees(getters.modules), depModules: (state, getters) => buildDepModules(getters.modules), depModulesTotalSize: (state, getters) => getters.depModules.reduce((total, module) => total + module.size, 0), chunks: (state, getters) => (getters.stats && getters.stats.data.chunks) || [] diff --git a/packages/@vue/cli-ui-addon-webpack/src/util/colors.js b/packages/@vue/cli-ui-addon-webpack/src/util/colors.js new file mode 100644 index 0000000000..371f3b5d39 --- /dev/null +++ b/packages/@vue/cli-ui-addon-webpack/src/util/colors.js @@ -0,0 +1,34 @@ +export default [ + [ + '#42b983', + '#5DC395', + '#78CDA7', + '#93D7B9', + '#AEE1CB', + '#C9EBDD' + ], + [ + '#A96FDA', + '#B684DF', + '#C399E4', + '#D0AEE9', + '#DDC3EE', + '#EAD8F3' + ], + [ + '#03C2E6', + '#27CBEA', + '#4BD4EE', + '#6FDDF2', + '#93E6F6', + '#B7EFFA' + ], + [ + '#778F9B', + '#8B9FA9', + '#9FAFB7', + '#B3BFC5', + '#C7CFD3', + '#DBDFE1' + ] +] diff --git a/packages/@vue/cli-ui-addon-webpack/src/util/modules.js b/packages/@vue/cli-ui-addon-webpack/src/util/modules.js index 06db4a7b21..7032d8467b 100644 --- a/packages/@vue/cli-ui-addon-webpack/src/util/modules.js +++ b/packages/@vue/cli-ui-addon-webpack/src/util/modules.js @@ -2,6 +2,10 @@ const getModulePath = function (identifier) { return identifier.replace(/.*!/, '').replace(/\\/g, '/') } +export function filterModules (modules) { + return modules.filter(module => module.name.indexOf('(webpack)') === -1) +} + export function buildDepModules (modules) { const deps = new Map() for (const module of modules) { @@ -36,3 +40,115 @@ export function buildDepModules (modules) { } return list } + +/* +{ + id: './node_modules', + size: { + disk: 1024, + gzip: 400 + } + fullPath: '/node_modules', + parent: undefined, + children: [ + { + id: 'vuex', + identifier: '...', + size: { + disk: 42, + gzip: 12 + }, + // Total size of previous children in list + previousSize: { + disk: 0, + gzip: 0 + }, + fullPath: '/node_modules/vuex', + children: [ + ... + ], + parent: ... + }, + ... + ] +} +*/ + +export function buildModulesTrees (modules) { + const trees = {} + + for (const module of modules) { + const path = getModulePath(module.identifier) + if (path.indexOf('multi ') === 0) continue + const parts = path.split('/') + for (const treeId of module.chunks) { + let subtree = trees[treeId] + if (!subtree) { + subtree = trees[treeId] = { + id: treeId, + size: { + disk: 0, + gzip: 0 + }, + children: {} + } + } + let fullPath = [] + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + let child = subtree.children[part] + if (!child) { + fullPath.push(part) + child = subtree.children[part] = { + id: part, + size: { + disk: 0, + gzip: 0 + }, + fullPath: fullPath.join('/'), + children: {}, + parent: subtree + } + } + child.size.disk += module.size + child.size.gzip += module.gzipSize || 0 + // Leaf + if (i === parts.length - 1) { + child.identifier = module.identifier + } + subtree = child + } + } + } + + for (const n in trees) { + let tree = trees[n] + let keys + while ((keys = Object.keys(tree.children)).length !== 0 && keys.length === 1) { + tree = tree.children[keys[0]] + } + walkTreeToSortChildren(tree) + trees[n] = tree + } + + return trees +} + +function walkTreeToSortChildren (tree) { + let size = { + disk: 0, + gzip: 0 + } + tree.children = Object.keys(tree.children).map( + key => tree.children[key] + ).sort((a, b) => b.size.gzip - a.size.gzip) + for (const child of tree.children) { + child.previousSize = { + disk: size.disk, + gzip: size.gzip + } + size.disk += child.size.disk + size.gzip += child.size.gzip + walkTreeToSortChildren(child) + } +} diff --git a/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue b/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue index 3d1bed84c2..0bc8e78d61 100644 --- a/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue +++ b/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue @@ -287,63 +287,63 @@ export default { align-items stretch height 100% - @media (max-width: 1250px) - .actions-bar - flex-wrap wrap - - .views - margin-top $padding-item - - .command - font-family $font-mono - font-size 12px - background $vue-ui-color-light-neutral +@media (max-width: 1250px) + .actions-bar + flex-wrap wrap + + .views + margin-top $padding-item + +.command + font-family $font-mono + font-size 12px + background $vue-ui-color-light-neutral + color $vue-ui-color-dark + padding 0 16px + height 32px + h-box() + box-center() + border-radius $br + +.content + flex 100% 1 1 + height 0 + margin 0 $padding-item $padding-item + position relative + +.terminal-view + position absolute + top 0 + left 0 + width 100% + height 100% + border-radius $br + &.ghost + opacity 0 + pointer-events none + +.view + max-height 100% + overflow-x hidden + overflow-y auto + +.header + padding $padding-item $padding-item 0 + h-box() + align-items center + + .task-icon + margin-right 4px + >>> svg + fill $vue-ui-color-dark + + .name + font-size 22px color $vue-ui-color-dark - padding 0 16px - height 32px - h-box() - box-center() - border-radius $br - - .content - flex 100% 1 1 - height 0 - margin 0 $padding-item $padding-item position relative + top -1px - .terminal-view - position absolute - top 0 - left 0 - width 100% - height 100% - border-radius $br - &.ghost - opacity 0 - pointer-events none - - .view - max-height 100% - overflow-x hidden - overflow-y auto - - .header - padding $padding-item $padding-item 0 - h-box() - align-items center - - .task-icon - margin-right 4px - >>> svg - fill $vue-ui-color-dark - - .name - font-size 22px - color $vue-ui-color-dark - position relative - top -1px - - .description - color $color-text-light - margin-left $padding-item + .description + color $color-text-light + margin-left $padding-item