diff --git a/.postcssrc b/.postcssrc new file mode 100644 index 0000000000..ed0149bf8b --- /dev/null +++ b/.postcssrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "autoprefixer": {} + } +} \ No newline at end of file diff --git a/packages/@vue/cli-plugin-eslint/ui.js b/packages/@vue/cli-plugin-eslint/ui.js index bc7cfa30d8..022e5a53ad 100644 --- a/packages/@vue/cli-plugin-eslint/ui.js +++ b/packages/@vue/cli-plugin-eslint/ui.js @@ -68,7 +68,7 @@ module.exports = api => { description: 'Do not fix errors' } ], - onRun: ({ answers, args }) => { + onBeforeRun: ({ answers, args }) => { if (answers.noFix) { args.push('--no-fix') } diff --git a/packages/@vue/cli-service/lib/commands/build/index.js b/packages/@vue/cli-service/lib/commands/build/index.js index 07849ac293..30bab45c8b 100644 --- a/packages/@vue/cli-service/lib/commands/build/index.js +++ b/packages/@vue/cli-service/lib/commands/build/index.js @@ -97,6 +97,14 @@ module.exports = (api, options) => { targetDir ) + // Expose advanced stats + if (args.dashboard) { + const DashboardPlugin = require('../../webpack/DashboardPlugin') + ;(webpackConfig.plugins = webpackConfig.plugins || []).push(new DashboardPlugin({ + type: 'build' + })) + } + return new Promise((resolve, reject) => { rimraf(targetDir, err => { if (err) { diff --git a/packages/@vue/cli-service/lib/commands/serve.js b/packages/@vue/cli-service/lib/commands/serve.js index 7a7b5aa787..2ec5f8a4fa 100644 --- a/packages/@vue/cli-service/lib/commands/serve.js +++ b/packages/@vue/cli-service/lib/commands/serve.js @@ -49,6 +49,16 @@ module.exports = (api, options) => { return portPromise.then(port => new Promise((resolve, reject) => { const webpackConfig = api.resolveWebpackConfig() + // Expose advanced stats + if (args.dashboard) { + const DashboardPlugin = require('../webpack/DashboardPlugin') + ;(webpackConfig.plugins = webpackConfig.plugins || []).push(new DashboardPlugin({ + type: 'serve', + gzip: false, + minified: false + })) + } + const urls = prepareURLs( useHttps ? 'https' : 'http', host, diff --git a/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js new file mode 100644 index 0000000000..58f8d996ec --- /dev/null +++ b/packages/@vue/cli-service/lib/webpack/DashboardPlugin.js @@ -0,0 +1,372 @@ +// From https://github.com/FormidableLabs/webpack-dashboard/blob/master/plugin/index.js +// Modified by Guillaume Chau (Akryum) +/* eslint-disable max-params, max-statements */ +'use strict' + +const os = require('os') +const path = require('path') +const most = require('most') +const webpack = require('webpack') +const InspectpackDaemon = require('inspectpack').daemon +const flatten = require('lodash.flatten') +const zlib = require('zlib') +const ipc = require('node-ipc') + +const ONE_SECOND = 1000 +const INSPECTPACK_PROBLEM_ACTIONS = ['versions', 'duplicates'] +const INSPECTPACK_PROBLEM_TYPE = 'problems' + +ipc.config.id = 'vue-cli' +ipc.config.retry = 1500 +ipc.config.silent = true + +let sendMessage + +ipc.connectTo('vue-cli', () => { + sendMessage = data => ipc.of['vue-cli'].emit('message', data) +}) + +const cacheFilename = path.resolve(os.homedir(), '.webpack-dashboard-cache.db') + +const serializeError = err => ({ + code: err.code, + message: err.message, + stack: err.stack +}) + +function getTimeMessage (timer) { + let time = Date.now() - timer + + if (time >= ONE_SECOND) { + time /= ONE_SECOND + time = Math.round(time) + time += 's' + } else { + time += 'ms' + } + + return ` (${time})` +} + +function getGzipSize (buffer) { + return zlib.gzipSync(buffer).length +} + +class DashboardPlugin { + constructor (options) { + if (typeof options === 'function') { + this.handler = options + } else { + options = options || {} + this.root = options.root + this.gzip = !(options.gzip === false) + // `gzip = true` implies `minified = true`. + this.minified = this.gzip || !(options.minified === false) + this.handler = options.handler || null + this.type = options.type + } + + this.cleanup = this.cleanup.bind(this) + + this.watching = false + } + + cleanup () { + if (!this.watching) { + this.handler = null + if (this.inspectpack) { + this.inspectpack.terminate() + } + } + + setTimeout(() => { + ipc.disconnect('vue-cli') + }, 4000) + } + + apply (compiler) { + // Lazily created so plugin can be configured without starting the daemon + this.inspectpack = InspectpackDaemon.create({ cacheFilename }) + + let handler = this.handler + let timer + + let assetSources + + // Enable pathinfo for inspectpack support + compiler.options.output.pathinfo = true + + const nodeEnv = process.env.NODE_ENV + + if (!handler) { + handler = data => sendMessage && sendMessage({ + webpackDashboardData: { + type: this.type, + value: data + } + }) + } + + compiler.apply( + new webpack.ProgressPlugin((percent, msg) => { + handler([ + { + type: 'status', + value: 'Compiling' + }, + { + type: 'progress', + value: percent + }, + { + type: 'operations', + value: msg + getTimeMessage(timer) + } + ]) + }) + ) + + compiler.plugin('watch-run', (c, done) => { + this.watching = true + done() + }) + + compiler.plugin('run', (c, done) => { + this.watching = false + done() + }) + + compiler.plugin('compile', () => { + timer = Date.now() + handler([ + { + type: 'status', + value: 'Compiling' + } + ]) + }) + + compiler.plugin('invalid', () => { + handler([ + { + type: 'status', + value: 'Invalidated' + }, + { + type: 'progress', + value: 0 + }, + { + type: 'operations', + value: 'idle' + } + ]) + }) + + compiler.plugin('failed', () => { + handler([ + { + type: 'status', + value: 'Failed' + }, + { + type: 'operations', + value: `idle${getTimeMessage(timer)}` + } + ]) + }) + + compiler.plugin('after-emit', (compilation, done) => { + assetSources = new Map() + for (const name in compilation.assets) { + const asset = compilation.assets[name] + assetSources.set(name, asset.source()) + } + done() + }) + + compiler.plugin('done', stats => { + const statsData = stats.toJson() + const outputPath = compiler.options.output.path + statsData.assets.forEach(asset => { + asset.fullPath = path.join(outputPath, asset.name) + asset.gzipSize = getGzipSize(assetSources.get(asset.name)) + }) + + handler([ + { + type: 'status', + value: 'Success' + }, + { + type: 'progress', + value: 0 + }, + { + type: 'operations', + value: `idle${getTimeMessage(timer)}` + }, + { + type: 'stats', + value: { + errors: stats.hasErrors(), + warnings: stats.hasWarnings(), + data: statsData + } + } + ]) + + if (!this.minimal && nodeEnv !== 'production') { + this.observeBundleMetrics(stats, this.inspectpack).subscribe({ + next: message => handler([message]), + error: err => { + console.log('Error from inspectpack:', err) // eslint-disable-line no-console + this.cleanup() + }, + complete: this.cleanup + }) + } else { + this.cleanup() + } + }) + } + + /** + * Infer the root of the project, w/ package.json + node_modules. + * + * Inspectpack's `version` option needs to know where to start resolving + * packages from to translate `~/lodash/index.js` to + * `/ACTUAL/PATH/node_modules/index.js`. + * + * In common practice, this is _usually_ `bundle.context`, but sometimes folks + * will set that to a _different_ directory of assets directly copied in or + * something. + * + * To handle varying scenarios, we resolve the project's root as: + * 1. Plugin `root` option, if set + * 2. `bundle.context`, if `package.json` exists + * 3. `process.cwd()`, if `package.json` exists + * 4. `null` if nothing else matches + * + * @param {Object} bundle Bundle + * @returns {String|null} Project root path or null + */ + getProjectRoot (bundle) { + /*eslint-disable global-require*/ + // Start with plugin option (and don't check it). + // We **will** allow a bad project root to blow up webpack-dashboard. + if (this.root) { + return this.root + } + + // Try bundle context. + try { + if (bundle.context && require(path.join(bundle.context, 'package.json'))) { + return bundle.context + } + } catch (err) { /* passthrough */ } + + // Try CWD. + try { + if (require(path.resolve('package.json'))) { + return process.cwd() + } + } catch (err) { /* passthrough */ } + + // A null will be filtered out, disabling `versions` action. + return null + } + + observeBundleMetrics (stats, inspectpack) { + const bundlesToObserve = Object.keys(stats.compilation.assets) + .filter( + bundlePath => + // Don't include hot reload assets, they break everything + // and the updates are already included in the new assets + bundlePath.indexOf('.hot-update.') === -1 && + // Don't parse sourcemaps! + path.extname(bundlePath) === '.js' + ) + .map(bundlePath => ({ + path: bundlePath, + context: stats.compilation.options.context, + source: stats.compilation.assets[bundlePath].source() + })) + + const getSizes = bundles => + Promise.all( + bundles.map(bundle => + inspectpack + .sizes({ + code: bundle.source, + format: 'object', + minified: this.minified, + gzip: this.gzip + }) + .then(metrics => ({ + path: bundle.path, + metrics + })) + ) + ) + .then(bundle => ({ + type: 'sizes', + value: bundle + })) + .catch(err => ({ + type: 'sizes', + error: true, + value: serializeError(err) + })) + + const getProblems = bundles => + Promise.all( + INSPECTPACK_PROBLEM_ACTIONS.map(action => + Promise.all( + bundles + .map(bundle => { + // Root is only needed for versions and we hit disk to check it. + // So, only check on actual actions and bail out if not found. + let root + if (action === 'versions') { + root = this.getProjectRoot(bundle) + if (!root) { + return null + } + } + + return inspectpack[action]({ + code: bundle.source, + root, + duplicates: true, + format: 'object', + minified: this.minified, + gzip: this.gzip + }) + .then(metrics => ({ + path: bundle.path, + [action]: metrics + })) + }) + .filter(Boolean) // Filter out incorrect actions. + ) + ) + ) + .then(bundle => ({ + type: INSPECTPACK_PROBLEM_TYPE, + value: flatten(bundle) + })) + .catch(err => ({ + type: INSPECTPACK_PROBLEM_TYPE, + error: true, + value: serializeError(err) + })) + + const sizesStream = most.of(bundlesToObserve).map(getSizes) + const problemsStream = most.of(bundlesToObserve).map(getProblems) + + return most.mergeArray([sizesStream, problemsStream]).chain(most.fromPromise) + } +} + +module.exports = DashboardPlugin diff --git a/packages/@vue/cli-service/package.json b/packages/@vue/cli-service/package.json index 68a4f85daa..720891e562 100644 --- a/packages/@vue/cli-service/package.json +++ b/packages/@vue/cli-service/package.json @@ -40,10 +40,14 @@ "get-value": "^3.0.0", "globby": "^8.0.1", "html-webpack-plugin": "^3.0.6", + "inspectpack": "^2.2.4", "javascript-stringify": "^1.6.0", "launch-editor-middleware": "^2.2.1", "lodash.defaultsdeep": "^4.6.0", + "lodash.flatten": "^4.4.0", "minimist": "^1.2.0", + "most": "^1.7.3", + "node-ipc": "^9.1.1", "optimize-css-assets-webpack-plugin": "^3.2.0", "ora": "^2.0.0", "portfinder": "^1.0.13", diff --git a/packages/@vue/cli-service/ui.js b/packages/@vue/cli-service/ui.js new file mode 100644 index 0000000000..41eb8664af --- /dev/null +++ b/packages/@vue/cli-service/ui.js @@ -0,0 +1,201 @@ +module.exports = api => { + const { setSharedData } = api.namespace('webpack-dashboard-') + + function resetSharedData (key) { + setSharedData(`${key}-status`, null) + setSharedData(`${key}-progress`, 0) + setSharedData(`${key}-operations`, null) + setSharedData(`${key}-stats`, null) + setSharedData(`${key}-sizes`, null) + setSharedData(`${key}-problems`, null) + } + + function onWebpackMessage ({ data: message }) { + if (message.webpackDashboardData) { + const type = message.webpackDashboardData.type + for (const data of message.webpackDashboardData.value) { + setSharedData(`${type}-${data.type}`, data.value) + } + } + } + + // Init data + for (const key of ['serve', 'build']) { + resetSharedData(key) + } + + // Tasks + api.describeTask({ + match: /vue-cli-service serve/, + description: 'Compiles and hot-reloads for development', + link: 'https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#serve', + prompts: [ + { + name: 'open', + type: 'confirm', + default: false, + description: 'Open browser on server start' + }, + { + name: 'mode', + type: 'list', + default: 'development', + choices: [ + { + name: 'development', + value: 'development' + }, + { + name: 'production', + value: 'production' + }, + { + name: 'test', + value: 'test' + } + ], + description: 'Specify env mode' + }, + { + name: 'host', + type: 'input', + default: '0.0.0.0', + description: 'Specify host' + }, + { + name: 'port', + type: 'input', + default: 8080, + description: 'Specify port' + }, + { + name: 'https', + type: 'confirm', + default: false, + description: 'Use HTTPS' + } + ], + onBeforeRun: ({ answers, args }) => { + // Args + if (answers.open) args.push('--open') + if (answers.mode) args.push('--mode', answers.mode) + if (answers.host) args.push('--host', answers.host) + if (answers.port) args.push('--port', answers.port) + if (answers.https) args.push('--https') + args.push('--dashboard') + + // Data + resetSharedData('serve') + }, + onRun: () => { + api.ipcOn(onWebpackMessage) + }, + onExit: () => { + api.ipcOff(onWebpackMessage) + }, + views: [ + { + id: 'vue-webpack-dashboard', + label: 'Dashboard', + icon: 'dashboard', + component: 'vue-webpack-dashboard' + } + ], + defaultView: 'vue-webpack-dashboard' + }) + api.describeTask({ + match: /vue-cli-service build/, + description: 'Compiles and minifies for production', + link: 'https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#build', + prompts: [ + { + name: 'mode', + type: 'list', + default: 'production', + choices: [ + { + name: 'development', + value: 'development' + }, + { + name: 'production', + value: 'production' + }, + { + name: 'test', + value: 'test' + } + ], + description: 'Specify env mode' + }, + { + name: 'dest', + type: 'input', + default: 'dist', + description: 'Output directory' + }, + { + name: 'target', + type: 'list', + default: 'app', + choices: [ + { + name: 'Web app', + value: 'app' + }, + { + name: 'Library', + value: 'lib' + }, + { + name: 'Web component', + value: 'wc' + }, + { + name: 'Async web component', + value: 'wc-async' + } + ], + description: 'Build target' + }, + { + name: 'name', + type: 'input', + default: '', + description: 'Name for library or web-component mode (default: "name" in package.json or entry filename)' + } + ], + onBeforeRun: ({ answers, args }) => { + // Args + if (answers.mode) args.push('--mode', answers.mode) + if (answers.dest) args.push('--dest', answers.dest) + if (answers.target) args.push('--target', answers.target) + if (answers.name) args.push('--port', answers.name) + args.push('--dashboard') + + // Data + resetSharedData('build') + }, + onRun: () => { + api.ipcOn(onWebpackMessage) + }, + onExit: () => { + api.ipcOff(onWebpackMessage) + }, + views: [ + { + id: 'vue-webpack-dashboard', + label: 'Dashboard', + icon: 'dashboard', + component: 'vue-webpack-dashboard' + } + ], + defaultView: 'vue-webpack-dashboard' + }) + + // Testing client addon + api.addClientAddon({ + id: 'vue-webpack', + url: 'http://localhost:8081/app.js' + }) +} diff --git a/packages/@vue/cli-ui-addon-build/.babelrc b/packages/@vue/cli-ui-addon-build/.babelrc new file mode 100644 index 0000000000..2a818842cc --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + "@vue/app" + ] +} diff --git a/packages/@vue/cli-ui-addon-build/.eslintrc.json b/packages/@vue/cli-ui-addon-build/.eslintrc.json new file mode 100644 index 0000000000..b12f10152c --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "root": true, + "extends": [ + "plugin:vue/essential", + "@vue/standard" + ], + "globals": { + "ClientAddonApi": false, + "Vue": false + } +} diff --git a/packages/@vue/cli-ui-addon-build/.gitignore b/packages/@vue/cli-ui-addon-build/.gitignore new file mode 100644 index 0000000000..d7efd2016c --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/.gitignore @@ -0,0 +1,20 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/packages/@vue/cli-ui-addon-build/.postcssrc b/packages/@vue/cli-ui-addon-build/.postcssrc new file mode 100644 index 0000000000..ed0149bf8b --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/.postcssrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "autoprefixer": {} + } +} \ No newline at end of file diff --git a/packages/@vue/cli-ui-addon-build/package.json b/packages/@vue/cli-ui-addon-build/package.json new file mode 100644 index 0000000000..abb162f82a --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/package.json @@ -0,0 +1,31 @@ +{ + "name": "cli-ui-addon-build", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "vue-progress-path": "^0.0.2", + "vuex": "^3.0.1" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^3.0.0-beta.6", + "@vue/cli-plugin-eslint": "^3.0.0-beta.6", + "@vue/cli-service": "^3.0.0-beta.6", + "@vue/eslint-config-standard": "^3.0.0-beta.3", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "vue-template-compiler": "^2.5.13" + }, + "peerDependencies": { + "vue": "^2.5.13" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/packages/@vue/cli-ui-addon-build/public/favicon.ico b/packages/@vue/cli-ui-addon-build/public/favicon.ico new file mode 100644 index 0000000000..c7b9a43c8c Binary files /dev/null and b/packages/@vue/cli-ui-addon-build/public/favicon.ico differ diff --git a/packages/@vue/cli-ui-addon-build/public/index.html b/packages/@vue/cli-ui-addon-build/public/index.html new file mode 100644 index 0000000000..ffaf0a08a6 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + cli-ui-addon-build + + + +
+ + + diff --git a/packages/@vue/cli-ui-addon-build/src/assets/logo.png b/packages/@vue/cli-ui-addon-build/src/assets/logo.png new file mode 100644 index 0000000000..f3d2503fc2 Binary files /dev/null and b/packages/@vue/cli-ui-addon-build/src/assets/logo.png differ diff --git a/packages/@vue/cli-ui-addon-build/src/assets/speeds.json b/packages/@vue/cli-ui-addon-build/src/assets/speeds.json new file mode 100644 index 0000000000..99cd1fd2d8 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/assets/speeds.json @@ -0,0 +1,14 @@ +{ + "global": { "title": "Global Average", "mbps": 7, "rtt": 30 }, + "edge": { "title": "Mobile Edge", "mbps": 0.24, "rtt": 840 }, + "2g": { "title": "2G", "mbps": 0.28, "rtt": 800 }, + "3gs": { "title": "3G Slow", "mbps": 0.4, "rtt": 400 }, + "3gb": { "title": "3G Basic", "mbps": 1.6, "rtt": 300 }, + "3gf": { "title": "3G Fast", "mbps": 1.6, "rtt": 150 }, + "4g": { "title": "4G", "mbps": 9, "rtt": 170 }, + "lte": { "title": "LTE", "mbps": 12, "rtt": 70 }, + "dup": { "title": "Dial Up", "mbps": 0.05, "rtt": 120 }, + "dsl": { "title": "DSL", "mbps": 1.5, "rtt": 50 }, + "cable": { "title": "Cable", "mbps": 5, "rtt": 28 }, + "fios": { "title": "FIOS", "mbps": 20, "rtt": 4 } +} diff --git a/packages/@vue/cli-ui-addon-build/src/components/AssetList.vue b/packages/@vue/cli-ui-addon-build/src/components/AssetList.vue new file mode 100644 index 0000000000..1f0cb0fff6 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/AssetList.vue @@ -0,0 +1,65 @@ + + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/AssetListItem.vue b/packages/@vue/cli-ui-addon-build/src/components/AssetListItem.vue new file mode 100644 index 0000000000..a3e6b9e749 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/AssetListItem.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/BuildProgress.vue b/packages/@vue/cli-ui-addon-build/src/components/BuildProgress.vue new file mode 100644 index 0000000000..0fc63dbf28 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/BuildProgress.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/BuildStatus.vue b/packages/@vue/cli-ui-addon-build/src/components/BuildStatus.vue new file mode 100644 index 0000000000..02ff1ee2bf --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/BuildStatus.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/ModuleList.vue b/packages/@vue/cli-ui-addon-build/src/components/ModuleList.vue new file mode 100644 index 0000000000..8a70c58ab6 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/ModuleList.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/ModuleListItem.vue b/packages/@vue/cli-ui-addon-build/src/components/ModuleListItem.vue new file mode 100644 index 0000000000..a99b5fecb4 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/ModuleListItem.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/SpeedStats.vue b/packages/@vue/cli-ui-addon-build/src/components/SpeedStats.vue new file mode 100644 index 0000000000..aa9db31ff1 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/SpeedStats.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/SpeedStatsItem.vue b/packages/@vue/cli-ui-addon-build/src/components/SpeedStatsItem.vue new file mode 100644 index 0000000000..9f0315eb65 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/SpeedStatsItem.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/components/WebpackDashboard.vue b/packages/@vue/cli-ui-addon-build/src/components/WebpackDashboard.vue new file mode 100644 index 0000000000..575184f5eb --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/components/WebpackDashboard.vue @@ -0,0 +1,117 @@ + + + + + + + diff --git a/packages/@vue/cli-ui-addon-build/src/filters.js b/packages/@vue/cli-ui-addon-build/src/filters.js new file mode 100644 index 0000000000..e37492dd2b --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/filters.js @@ -0,0 +1,25 @@ +export function size (size, unit = '', precision = 1) { + const kb = { + label: 'k', + value: 1024 + } + const mb = { + label: 'M', + value: 1024 * 1024 + } + let denominator + + if (size >= mb.value) { + denominator = mb + } else { + denominator = kb + if (size < kb.value * 0.92 && precision === 0) { + precision = 1 + } + } + return (size / denominator.value).toFixed(precision) + denominator.label + unit +} + +export function round (value, precision) { + return Math.round(value * precision) / precision +} diff --git a/packages/@vue/cli-ui-addon-build/src/main.js b/packages/@vue/cli-ui-addon-build/src/main.js new file mode 100644 index 0000000000..d910e179df --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/main.js @@ -0,0 +1,8 @@ +import VueProgress from 'vue-progress-path' +import WebpackDashboard from './components/WebpackDashboard.vue' + +Vue.use(VueProgress, { + defaultShape: 'circle' +}) + +ClientAddonApi.component('vue-webpack-dashboard', WebpackDashboard) diff --git a/packages/@vue/cli-ui-addon-build/src/store/index.js b/packages/@vue/cli-ui-addon-build/src/store/index.js new file mode 100644 index 0000000000..78d479681a --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/store/index.js @@ -0,0 +1,53 @@ +import Vuex from 'vuex' + +import { buildSortedAssets } from '../util/assets' +import { buildDepModules } from '../util/modules' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + state () { + return { + useGzip: true, + mode: 'serve', + serve: { + stats: null + }, + build: { + stats: null + } + } + }, + + getters: { + useGzip: state => state.useGzip, + mode: state => state.mode, + stats: state => state[state.mode].stats, + errors: (state, getters) => (getters.stats && getters.stats.data.errors) || [], + warnings: (state, getters) => (getters.stats && getters.stats.data.warnings) || [], + 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) || [], + modulesTotalSize: (state, getters) => getters.modules.reduce((total, module) => total + module.size, 0), + 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) || [] + }, + + mutations: { + useGzip (state, value) { + state.useGzip = value + }, + + mode (state, value) { + state.mode = value + }, + + stats (state, { mode, value }) { + state[mode].stats = value + } + } +}) + +export default store diff --git a/packages/@vue/cli-ui-addon-build/src/util/assets.js b/packages/@vue/cli-ui-addon-build/src/util/assets.js new file mode 100644 index 0000000000..9ac8544d43 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/util/assets.js @@ -0,0 +1,63 @@ +import speedsData from '../assets/speeds.json' + +const DOWNLOAD_TIME_THRESHOLD_SECONDS = 5 + +export function getSpeedData (datapoint, size) { + const assetsSizeInMB = size / 1024 / 1024 + const bandwidthInMbps = datapoint.mbps + const bandwidthInMBps = bandwidthInMbps / 8 + const rttInSeconds = datapoint.rtt / 1000 + + const totalDownloadTime = assetsSizeInMB / bandwidthInMBps + rttInSeconds + + const isDownloadTimeOverThreshold = + totalDownloadTime > DOWNLOAD_TIME_THRESHOLD_SECONDS + const timeDifferenceToThreshold = + (isDownloadTimeOverThreshold ? '+' : '-') + + Math.abs(totalDownloadTime - DOWNLOAD_TIME_THRESHOLD_SECONDS).toFixed(2) + +'s' + + return { + totalDownloadTime, + isDownloadTimeOverThreshold, + timeDifferenceToThreshold + } +} + +export function getSpeeds (size) { + return Object.keys(speedsData).reduce((obj, key) => { + obj[key] = { + ...getSpeedData(speedsData[key], size), + ...speedsData[key] + } + return obj + }, {}) +} + +export function buildSortedAssets (assets, userGzip) { + let list = assets.slice() + if (list.length) { + const max = list[0].size + list = list.map(asset => { + const size = userGzip ? asset.gzipSize : asset.size + return { + name: asset.name, + size, + big: size > 250000, + ratio: size / max, + secondary: /\.map$/.test(asset.name), + speeds: getSpeeds(size) + } + }) + list = list.sort((a, b) => { + if (a.secondary === b.secondary) { + return b.size - a.size + } else if (a.secondary && !b.secondary) { + return 1 + } else { + return -1 + } + }) + } + return list +} diff --git a/packages/@vue/cli-ui-addon-build/src/util/modules.js b/packages/@vue/cli-ui-addon-build/src/util/modules.js new file mode 100644 index 0000000000..06db4a7b21 --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/src/util/modules.js @@ -0,0 +1,38 @@ +const getModulePath = function (identifier) { + return identifier.replace(/.*!/, '').replace(/\\/g, '/') +} + +export function buildDepModules (modules) { + const deps = new Map() + for (const module of modules) { + const path = getModulePath(module.identifier) + const pathParts = path.split('/node_modules/') + if (pathParts.length === 2) { + let name = pathParts[1] + if (name.charAt(0) === '@') { + // Scoped package + name = name.substr(0, name.indexOf('/', name.indexOf('/') + 1)) + } else { + name = name.substr(0, name.indexOf('/')) + } + let dep = deps.get(name) + if (!dep) { + dep = { + name, + size: 0 + } + deps.set(name, dep) + } + dep.size += module.size + } + } + let list = Array.from(deps.values()) + list = list.sort((a, b) => b.size - a.size) + if (list.length) { + const max = list[0].size + for (const dep of list) { + dep.ratio = dep.size / max + } + } + return list +} diff --git a/packages/@vue/cli-ui-addon-build/vue.config.js b/packages/@vue/cli-ui-addon-build/vue.config.js new file mode 100644 index 0000000000..ef1bb3e32d --- /dev/null +++ b/packages/@vue/cli-ui-addon-build/vue.config.js @@ -0,0 +1,12 @@ +module.exports = { + configureWebpack: { + output: { + publicPath: 'http://localhost:8081/' + } + }, + devServer: { + headers: { + 'Access-Control-Allow-Origin': '*' + } + } +} diff --git a/packages/@vue/cli-ui/.eslintrc b/packages/@vue/cli-ui/.eslintrc index dfc723fafa..71cf2c9e79 100644 --- a/packages/@vue/cli-ui/.eslintrc +++ b/packages/@vue/cli-ui/.eslintrc @@ -3,5 +3,8 @@ "extends": [ "plugin:vue/essential", "@vue/standard" - ] -} \ No newline at end of file + ], + "globals": { + "ClientAddonApi": false + } +} diff --git a/packages/@vue/cli-ui/package.json b/packages/@vue/cli-ui/package.json index 039340f98e..efd81baa97 100644 --- a/packages/@vue/cli-ui/package.json +++ b/packages/@vue/cli-ui/package.json @@ -24,6 +24,7 @@ "clone": "^1.0.4", "file-icons-js": "^1.0.3", "graphql": "^0.13.0", + "graphql-type-json": "^0.2.0", "js-yaml": "^3.11.0", "lowdb": "^1.0.0", "lru-cache": "^4.1.2", diff --git a/packages/@vue/cli-ui/src/App.vue b/packages/@vue/cli-ui/src/App.vue index 6a543dc2ba..d503819e7e 100644 --- a/packages/@vue/cli-ui/src/App.vue +++ b/packages/@vue/cli-ui/src/App.vue @@ -5,19 +5,10 @@ + - - diff --git a/packages/@vue/cli-ui/src/components/ClientAddonLoader.vue b/packages/@vue/cli-ui/src/components/ClientAddonLoader.vue new file mode 100644 index 0000000000..29fcd46d48 --- /dev/null +++ b/packages/@vue/cli-ui/src/components/ClientAddonLoader.vue @@ -0,0 +1,43 @@ + diff --git a/packages/@vue/cli-ui/src/components/TaskItem.vue b/packages/@vue/cli-ui/src/components/TaskItem.vue index c171bb8695..b90f7b9418 100644 --- a/packages/@vue/cli-ui/src/components/TaskItem.vue +++ b/packages/@vue/cli-ui/src/components/TaskItem.vue @@ -71,6 +71,7 @@ export default { .list-item-info flex 100% 1 1 width 0 + overflow hidden >>> .description white-space nowrap diff --git a/packages/@vue/cli-ui/src/components/TerminalView.vue b/packages/@vue/cli-ui/src/components/TerminalView.vue index d0b5541c65..7f066765ba 100644 --- a/packages/@vue/cli-ui/src/components/TerminalView.vue +++ b/packages/@vue/cli-ui/src/components/TerminalView.vue @@ -1,6 +1,6 @@