feat(ui): client addons, ipc, shared data, plugin actions

This commit is contained in:
Guillaume Chau
2018-04-09 01:53:41 +02:00
parent fd7868c052
commit 3c59d6f4f4
69 changed files with 2506 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"plugins": {
"autoprefixer": {}
}
}
+1 -1
View File
@@ -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')
}
@@ -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) {
@@ -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,
@@ -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
+4
View File
@@ -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",
+201
View File
@@ -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'
})
}
@@ -0,0 +1,5 @@
{
"presets": [
"@vue/app"
]
}
@@ -0,0 +1,11 @@
{
"root": true,
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"globals": {
"ClientAddonApi": false,
"Vue": false
}
}
@@ -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
@@ -0,0 +1,5 @@
{
"plugins": {
"autoprefixer": {}
}
}
@@ -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"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="shortcut icon" href="<%= webpackConfig.output.publicPath %>favicon.ico">
<title>cli-ui-addon-build</title>
</head>
<body>
<noscript>
<strong>We're sorry but cli-ui-addon-build doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -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 }
}
@@ -0,0 +1,65 @@
<template>
<div class="asset-list list-block">
<div class="content">
<div class="title">Assets</div>
<VueIcon
v-if="!assetsSorted.length"
icon="more_horiz"
class="blank-icon"
/>
<template v-else>
<div class="headers">
<div class="header"></div>
<div class="header">{{ useGzip ? 'Gzip' : 'Disk' }}</div>
<div class="header">Global</div>
<div class="header">3G Slow</div>
<div class="header">3G Fast</div>
<div class="header"></div>
</div>
<div class="list">
<AssetListItem
v-for="asset of assetsSorted"
:key="asset.name"
:asset="asset"
/>
</div>
</template>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import AssetListItem from './AssetListItem'
export default {
components: {
AssetListItem
},
computed: {
...mapGetters([
'assetsSorted',
'useGzip'
])
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.headers
display grid
grid-template-columns 5fr repeat(4, 1fr) 16px
grid-gap $padding-item
font-family $font-mono
font-size 12px
text-align right
margin-bottom $padding-item
</style>
@@ -0,0 +1,77 @@
<template>
<div
:class="{
big: asset.big,
secondary: asset.secondary
}"
class="asset-list-item"
>
<div class="content">
<div class="info name">{{ asset.name }}</div>
<div class="info size">{{ asset.size | size('B') }}</div>
<div class="info speed global">{{ asset.speeds.global.totalDownloadTime | round(100) }}s</div>
<div class="info speed 3gs">{{ asset.speeds['3gs'].totalDownloadTime | round(100) }}s</div>
<div class="info speed 3gf">{{ asset.speeds['3gf'].totalDownloadTime | round(100) }}s</div>
<div class="info warning">
<VueIcon
v-if="!asset.secondary && asset.big"
icon="warning"
class="icon"
v-tooltip="'This asset is big, consider using Code splitting to create smaller assets.'"
/>
</div>
</div>
</div>
</template>
<script>
import { size, round } from '../filters'
export default {
filters: {
size,
round
},
props: {
asset: {
type: Object,
required: true
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.asset-list-item
.content
display grid
grid-template-columns 5fr repeat(4, 1fr) 16px
grid-gap $padding-item
font-family $font-mono
font-size 12px
.info
display flex
align-items center
.name
word-break break-all
.size,
.speed
color $vue-ui-color-primary
justify-content flex-end
.icon
>>> svg
fill $vue-ui-color-primary
&.big
font-weight bold
&.secondary
opacity .5
</style>
@@ -0,0 +1,80 @@
<template>
<div class="build-progress">
<div class="content">
<loading-progress
:progress="progress"
size="128"
counter-clockwise
/>
<div class="progress">
{{ Math.round(progress * 100) }}
</div>
<div class="operations">
<span v-if="operations">{{ operations }}</span>
<VueIcon
v-else
icon="more_horiz"
class="blank-icon"
/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters([
'mode',
])
},
sharedData () {
return mapSharedData('webpack-dashboard-', {
progress: `${this.mode}-progress`,
operations: `${this.mode}-operations`
})
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.build-progress
.content
v-box()
box-center()
position relative
height 100%
>>> > *
space-between-y($padding-item)
.vue-progress-path
>>> .background
stroke rgba($vue-ui-color-dark, .1)
.operations
color $vue-ui-color-dark
padding-bottom 12px
&:first-letter
text-transform uppercase
.progress
h-box()
box-center()
flex none
position absolute
top 0
left 0
width 100%
height 178px
color lighten($vue-ui-color-dark, 60%)
font-weight lighter
font-size 42px
</style>
@@ -0,0 +1,89 @@
<template>
<div class="build-status">
<div class="content">
<div class="info-block status">
<div class="label">Status</div>
<div class="value">{{ status || 'Idle' }}</div>
</div>
<div class="info-block errors">
<div class="label">Errors</div>
<div class="value">{{ errors.length }}</div>
</div>
<div class="info-block warnings">
<div class="label">Warnings</div>
<div class="value">{{ warnings.length }}</div>
</div>
<div class="info-block assets">
<div class="label">Assets</div>
<div class="value">
{{ assetsTotalSize | size('B') }}
<span class="secondary">
{{ useGzip ? '(gzip)' : '(disk)' }}
</span>
</div>
</div>
<div class="info-block modules">
<div class="label">Modules</div>
<div class="value">{{ modulesTotalSize | size('B') }}</div>
</div>
<div class="info-block dep-modules">
<div class="label">Dependencies</div>
<div class="value">
{{ depModulesTotalSize | size('B') }}
<span class="secondary">
{{ depSizeRatio | round(100) }}%
</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { size, round } from '../filters'
export default {
filters: {
size,
round
},
computed: {
...mapGetters([
'useGzip',
'mode',
'errors',
'warnings',
'assetsTotalSize',
'modulesTotalSize',
'depModulesTotalSize'
]),
depSizeRatio () {
if (this.modulesTotalSize) {
return this.depModulesTotalSize / this.modulesTotalSize * 100
} else {
return 0
}
}
},
sharedData () {
return {
status: `webpack-dashboard-${this.mode}-status`
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.build-status
.content
height 100%
display grid
grid-template-columns repeat(3, 1fr)
grid-gap $padding-item
</style>
@@ -0,0 +1,39 @@
<template>
<div class="module-list list-block">
<div class="content">
<div class="title">Dependencies</div>
<VueIcon
v-if="!depModules.length"
icon="more_horiz"
class="blank-icon"
/>
<div class="list">
<ModuleListItem
v-for="m of depModules"
:key="m.id"
:module="m"
/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import ModuleListItem from './ModuleListItem'
export default {
components: {
ModuleListItem
},
computed: {
...mapGetters([
'depModules'
])
}
}
</script>
@@ -0,0 +1,51 @@
<template>
<div class="module-list-item">
<div class="content">
<div class="info name">{{ module.name }}</div>
<div class="info size">{{ module.size | size('B') }}</div>
<div class="info">
<VueLoadingBar :value="module.ratio" class="primary"/>
</div>
</div>
</div>
</template>
<script>
import { size } from '../filters'
export default {
filters: {
size
},
props: {
module: {
type: Object,
required: true
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.module-list-item
.content
display grid
grid-template-columns 3fr 1fr 1fr
grid-gap $padding-item
font-family $font-mono
font-size 12px
.info
display flex
align-items center
.size
color $vue-ui-color-primary
justify-content flex-end
.vue-ui-loading-bar
width 100%
</style>
@@ -0,0 +1,55 @@
<template>
<div class="speed-stats">
<div class="content">
<div class="title">Speed stats</div>
<VueIcon
v-if="!assetsTotalSize"
icon="more_horiz"
class="blank-icon"
/>
<div v-else class="items">
<SpeedStatsItem
v-for="(stats, key) of speeds"
:key="key"
:stats="stats"
/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { getSpeeds } from '../util/assets'
import SpeedStatsItem from './SpeedStatsItem.vue'
export default {
components: {
SpeedStatsItem
},
computed: {
...mapGetters([
'assetsTotalSize'
]),
speeds () {
return getSpeeds(this.assetsTotalSize)
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.items
display grid
grid-template-columns repeat(auto-fill, 200px)
grid-gap 16px
justify-content space-between
padding 16px
</style>
@@ -0,0 +1,54 @@
<template>
<div
class="speed-stats-item"
v-tooltip="{
content: tooltip,
offset: 16
}"
>
<div class="content">
<div class="label">{{ stats.title }}</div>
<div class="value">{{ stats.totalDownloadTime | round(100) }}s</div>
</div>
</div>
</template>
<script>
import { round } from '../filters'
export default {
filters: {
round
},
props: {
stats: {
type: Object,
required: true
}
},
computed: {
tooltip () {
return `${this.stats.mbps}Mbps + ${this.stats.rtt}ms RTT`
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.content
font-family $font-mono
font-size 12px
display grid
grid-template-columns 3fr 2fr
grid-gap $padding-item
.label
text-align right
.value
color $vue-ui-color-primary
</style>
@@ -0,0 +1,117 @@
<template>
<div class="vue-webpack-dashboard">
<div class="pane-toolbar">
<VueIcon icon="dashboard"/>
<div class="title">Dashboard</div>
<VueSwitch
v-model="useGzip"
>
Gzip assets
</VueSwitch>
</div>
<div class="content vue-ui-grid default-gap">
<BuildStatus />
<BuildProgress />
<SpeedStats class="span-2"/>
<AssetList />
<ModuleList />
</div>
</div>
</template>
<script>
import store from '../store'
import BuildStatus from './BuildStatus'
import BuildProgress from './BuildProgress'
import SpeedStats from './SpeedStats'
import AssetList from './AssetList'
import ModuleList from './ModuleList'
export default {
store,
components: {
BuildStatus,
BuildProgress,
SpeedStats,
AssetList,
ModuleList
},
inject: [
'TaskDetails'
],
computed: {
useGzip: {
get () { return this.$store.getters.useGzip },
set (value) { this.$store.commit('useGzip', value) }
}
},
created () {
const 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
})
})
}
}
</script>
<style>
@import '~vue-progress-path/dist/vue-progress-path.css';
</style>
<style lang="stylus" scoped>
@import "~@vue/cli-ui/src/style/imports"
.vue-webpack-dashboard
.content
grid-template-columns 9fr 4fr
>>>
.title
color lighten($vue-ui-color-dark, 60%)
font-size 20px
font-weight lighter
text-align center
margin-bottom $padding-item
.list
display grid
grid-template-columns 1fr
grid-gap 16px
.info-block
v-box()
box-center()
font-weight lighter
text-align center
.label
color lighten($vue-ui-color-dark, 60%)
font-size 20px
.value
color $vue-ui-color-dark
font-size 24px
.secondary
opacity .75
font-size 16px
.pane-toolbar,
.content >>> > div
padding $padding-item
background $vue-ui-color-light-neutral
border-radius $br
.pane-toolbar
margin-bottom $padding-item
</style>
@@ -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
}
@@ -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)
@@ -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
@@ -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
}
@@ -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
}
@@ -0,0 +1,12 @@
module.exports = {
configureWebpack: {
output: {
publicPath: 'http://localhost:8081/'
}
},
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
}
}
}
+5 -2
View File
@@ -3,5 +3,8 @@
"extends": [
"plugin:vue/essential",
"@vue/standard"
]
}
],
"globals": {
"ClientAddonApi": false
}
}
+1
View File
@@ -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",
+1 -10
View File
@@ -5,19 +5,10 @@
</div>
<StatusBar/>
<ClientAddonLoader/>
</div>
</template>
<script>
import StatusBar from './components/StatusBar'
export default {
components: {
StatusBar
}
}
</script>
<style lang="stylus">
@import "~@vue/ui/dist/vue-ui.css"
@import "~file-icons-js/css/style.css"
@@ -0,0 +1,49 @@
<template>
<component
v-if="component"
:is="component"
/>
<div v-else class="loading">
<VueLoadingIndicator />
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
default: null
}
},
data () {
return {
component: null
}
},
watch: {
name: {
handler: 'updateComponent',
immediate: true
}
},
methods: {
async updateComponent () {
this.component = await ClientAddonApi.awaitComponent(this.name)
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.loading
v-box()
box-center()
padding 100px
</style>
@@ -0,0 +1,43 @@
<script>
import CLIENT_ADDONS from '../graphql/clientAddons.gql'
import CLIENT_ADDON_ADDED from '../graphql/clientAddonAdded.gql'
export default {
apollo: {
clientAddons: {
query: CLIENT_ADDONS,
manual: true,
result ({ data: { clientAddons } }) {
clientAddons.forEach(this.loadAddon)
}
},
$subscribe: {
clientAddonAdded: {
query: CLIENT_ADDON_ADDED,
result ({ data }) {
this.loadAddon(data.clientAddonAdded)
}
}
}
},
created () {
this.$_scripts = new Map()
},
methods: {
loadAddon (addon) {
console.log(`Loading addon ${addon.id}...`)
const script = document.createElement('script')
this.$_scripts.set(addon.id, script)
script.setAttribute('src', addon.url)
document.body.appendChild(script)
}
},
render () {
return null
}
}
</script>
@@ -71,6 +71,7 @@ export default {
.list-item-info
flex 100% 1 1
width 0
overflow hidden
>>> .description
white-space nowrap
@@ -1,6 +1,6 @@
<template>
<div class="terminal-view">
<div v-if="toolbar" class="toolbar">
<div v-if="toolbar" class="pane-toolbar">
<VueIcon
icon="dvr"
/>
@@ -97,6 +97,11 @@ export default {
title: {
type: String,
default: null
},
openLinks: {
type: Boolean,
default: false
}
},
@@ -166,6 +171,9 @@ export default {
},
handleLink (event, uri) {
if (this.openLinks) {
window.open(uri, '_blank')
}
this.$emit('link', uri)
},
@@ -205,19 +213,6 @@ export default {
align-items stretch
background $vue-ui-color-light-neutral
.toolbar
h-box()
align-items center
padding 6px 6px 6px $padding-item
> :not(.separator)
space-between-x(6px)
> * + .separator
margin-left 6px
.title
flex 100% 1 1
width 0
ellipsis()
.view
flex 100% 1 1
height 0
@@ -1,7 +1,14 @@
const plugins = require('../connectors/plugins')
const sharedData = require('../connectors/shared-data')
const ipc = require('../utils/ipc')
class PluginApi {
constructor () {
constructor (context) {
this.context = context
this.configurations = []
this.tasks = []
this.clientAddons = []
this.actions = new Map()
}
describeConfig (options) {
@@ -17,6 +24,54 @@ class PluginApi {
options => options.match.test(command)
)
}
addClientAddon (options) {
this.clientAddons.push(options)
}
ipcOn (cb) {
return ipc.on(cb)
}
ipcOff (cb) {
ipc.off(cb)
}
ipcSend (data) {
ipc.send(data)
}
/* Namespaced */
getSharedData (id) {
return sharedData.get(id, this.context)
}
setSharedData (id, value) {
sharedData.set({ id, value }, this.context)
}
onAction (id, cb) {
let list = this.actions.get(id)
if (!list) {
list = []
this.actions.set(id, list)
}
list.push(cb)
}
callAction (id, params) {
return plugins.callAction({ id, params }, this.context)
}
namespace (namespace) {
return {
getSharedData: (id) => this.getSharedData(namespace + id),
setSharedData: (id, value) => this.setSharedData(namespace + id, value),
onAction: (id, cb) => this.onAction(namespace + id, cb),
callAction: (id, params) => this.callAction(namespace + id, params)
}
}
}
module.exports = PluginApi
@@ -7,5 +7,9 @@ module.exports = {
TASK_LOG_ADDED: 'task_log_added',
ROUTE_ADDED: 'route_added',
ROUTE_REMOVED: 'route_removed',
ROUTE_CHANGED: 'route_changed'
ROUTE_CHANGED: 'route_changed',
CLIENT_ADDON_ADDED: 'client_addon_added',
SHARED_DATA_UPDATED: 'shared_data_updated',
PLUGIN_ACTION_CALLED: 'plugin_action_called',
PLUGIN_ACTION_RESOLVED: 'plugin_action_resolved'
}
@@ -0,0 +1,36 @@
const channels = require('../channels')
let addons = []
function list (context) {
return addons
}
function add (options, context) {
if (findOne(options.id)) return
addons.push(options)
context.pubsub.publish(channels.CLIENT_ADDON_ADDED, {
clientAddonAdded: options
})
}
function findOne (id, context) {
return addons.find(
addon => addon.id === id
)
}
function remove (id, context) {
const index = addons.findIndex(
addon => addon.id === id
)
if (index !== -1) addons.splice(index, 1)
}
module.exports = {
list,
add,
remove,
findOne
}
@@ -16,12 +16,15 @@ const {
updatePackage
} = require('@vue/cli/lib/util/installDeps')
const invoke = require('@vue/cli/lib/invoke')
// Subs
const channels = require('../channels')
// Connectors
const cwd = require('./cwd')
const folders = require('./folders')
const prompts = require('./prompts')
const progress = require('./progress')
const logs = require('./logs')
const clientAddons = require('./client-addons')
// Api
const PluginApi = require('../api/PluginApi')
// Utils
@@ -73,12 +76,17 @@ function list (file, context) {
}
function resetPluginApi (context) {
pluginApi = new PluginApi()
pluginApi = new PluginApi(context)
// Run Plugin API
runPluginApi('@vue/cli-service', context)
plugins.forEach(plugin => runPluginApi(plugin.id, context))
runPluginApi('.', context, 'vue-cli-ui')
// Add client addons
pluginApi.clientAddons.forEach(options => clientAddons.add(options, context))
}
function runPluginApi (id, context) {
const module = loadModule(`${id}/ui`, cwd.get(), true)
function runPluginApi (id, context, fileName = 'ui') {
const module = loadModule(`${id}/${fileName}`, cwd.get(), true)
if (module) {
module(pluginApi)
}
@@ -278,6 +286,32 @@ function getApi () {
return pluginApi
}
async function callAction ({ id, params }, context) {
context.pubsub.publish(channels.PLUGIN_ACTION_CALLED, {
pluginActionCalled: { id, params }
})
const results = []
const errors = []
const list = pluginApi.actions.get(id)
if (list) {
for (const cb of list) {
let result = null
let error = null
try {
result = await cb(params)
} catch (e) {
error = e
}
results.push(result)
errors.push(error)
}
}
context.pubsub.publish(channels.PLUGIN_ACTION_RESOLVED, {
pluginActionResolved: { id, params, results, errors }
})
return { id, params, results, errors }
}
module.exports = {
list,
findOne,
@@ -291,5 +325,6 @@ module.exports = {
runInvoke,
resetPluginApi,
getApi,
finishInstall
finishInstall,
callAction
}
@@ -85,7 +85,7 @@ function setAnswer (id, value) {
}
function getAnswer (id) {
ObjectUtil.get(answers, id)
return ObjectUtil.get(answers, id)
}
function removeAnswer (id) {
@@ -0,0 +1,28 @@
// Subscriptions channels
const channels = require('../channels')
let sharedData = new Map()
function get (id, context) {
const value = sharedData.get(id)
if (typeof value === 'undefined') return null
return {
id,
value
}
}
function set ({ id, value }, context) {
sharedData.set(id, value)
context.pubsub.publish(channels.SHARED_DATA_UPDATED, {
sharedDataUpdated: { id, value }
})
return { id, value }
}
module.exports = {
get,
set
}
@@ -44,6 +44,7 @@ function list (context) {
command,
index: list.findIndex(t => t.id === id),
prompts: [],
views: [],
...moreData
}
}
@@ -141,9 +142,9 @@ function run (id, context) {
answers
}, context)
// Plugin api
if (task.onRun) {
task.onRun({
// Plugin API
if (task.onBeforeRun) {
task.onBeforeRun({
answers,
args
})
@@ -151,9 +152,18 @@ function run (id, context) {
const child = execa(getCommand(), args, {
cwd: cwd.get(),
stdio: ['inherit', 'pipe', 'pipe']
stdio: ['inherit', 'pipe', 'pipe', 'ipc']
})
// Plugin API
if (task.onRun) {
task.onRun({
args,
child,
cwd: cwd.get()
})
}
updateOne({
id: task.id,
status: 'running',
@@ -180,7 +190,18 @@ function run (id, context) {
}, context)
})
child.on('close', (code, signal) => {
const onExit = (code, signal) => {
// Plugin API
if (task.onExit) {
task.onExit({
args,
child,
cwd: cwd.get(),
code,
signal
})
}
if (code === null) {
updateOne({
id: task.id,
@@ -212,7 +233,9 @@ function run (id, context) {
type: 'done'
}, context)
}
})
}
child.on('exit', onExit)
}
return task
}
@@ -1,8 +1,8 @@
const { withFilter } = require('graphql-subscriptions')
const exit = require('@vue/cli-shared-utils/lib/exit')
const GraphQLJSON = require('graphql-type-json')
// Channels for subscriptions
const channels = require('./channels')
// Connectors
const cwd = require('./connectors/cwd')
const folders = require('./connectors/folders')
@@ -15,6 +15,10 @@ const configurations = require('./connectors/configurations')
const git = require('./connectors/git')
const files = require('./connectors/files')
const routes = require('./connectors/routes')
const clientAddons = require('./connectors/client-addons')
const sharedData = require('./connectors/shared-data')
// Start ipc server
require('./utils/ipc')
// Prevent code from exiting server process
exit.exitProcess = false
@@ -22,6 +26,8 @@ exit.exitProcess = false
process.env.VUE_CLI_API_MODE = true
module.exports = {
JSON: GraphQLJSON,
Folder: {
children: (folder, args, context) => folders.list(folder.path, context),
isPackage: (folder, args, context) => folders.isPackage(folder.path, context),
@@ -63,7 +69,9 @@ module.exports = {
configurations: (root, args, context) => configurations.list(context),
configuration: (root, { id }, context) => configurations.findOne(id, context),
fileDiffs: (root, args, context) => git.getDiffs(context),
routes: (root, args, context) => routes.list(context)
routes: (root, args, context) => routes.list(context),
clientAddons: (root, args, context) => clientAddons.list(context),
sharedData: (root, { id }, context) => sharedData.get(id, context)
},
Mutation: {
@@ -88,13 +96,15 @@ module.exports = {
pluginInvoke: (root, { id }, context) => plugins.runInvoke(id, context),
pluginFinishInstall: (root, args, context) => plugins.finishInstall(context),
pluginUpdate: (root, { id }, context) => plugins.update(id, context),
pluginActionCall: (root, args, context) => plugins.callAction(args, context),
taskRun: (root, { id }, context) => tasks.run(id, context),
taskStop: (root, { id }, context) => tasks.stop(id, context),
taskLogsClear: (root, { id }, context) => tasks.clearLogs(id, context),
configurationSave: (root, { id }, context) => configurations.save(id, context),
configurationCancel: (root, { id }, context) => configurations.cancel(id, context),
gitCommit: (root, { message }, context) => git.commit(message, context),
fileOpenInEditor: (root, { input }, context) => files.openInEditor(input, context)
fileOpenInEditor: (root, { input }, context) => files.openInEditor(input, context),
sharedDataUpdate: (root, args, context) => sharedData.set(args, context)
},
Subscription: {
@@ -131,6 +141,21 @@ module.exports = {
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.TASK_LOG_ADDED),
(payload, vars) => payload.taskLogAdded.taskId === vars.id
)
},
clientAddonAdded: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.CLIENT_ADDON_ADDED)
},
sharedDataUpdated: {
subscribe: withFilter(
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.SHARED_DATA_UPDATED),
(payload, vars) => payload.sharedDataUpdated.id === vars.id
)
},
pluginActionCalled: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.PLUGIN_ACTION_CALLED)
},
pluginActionResolved: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.PLUGIN_ACTION_RESOLVED)
}
}
}
@@ -1,4 +1,5 @@
module.exports = `
scalar JSON
type ConsoleLog {
id: ID!
@@ -108,6 +109,18 @@ enum PluginInstallationStep {
diff
}
type PluginActionCall {
id: ID!
params: JSON
}
type PluginActionResult {
id: ID!
params: JSON
results: [JSON]
errors: [JSON]
}
type Feature implements DescribedEntity {
id: ID!
name: String
@@ -179,6 +192,8 @@ type Task implements DescribedEntity {
link: String
logs: [TaskLog]
prompts: [Prompt]
views: [TaskView]
defaultView: String
}
enum TaskStatus {
@@ -200,6 +215,13 @@ enum TaskLogType {
stderr
}
type TaskView {
id: ID!
label: String!
component: String!
icon: String
}
type Configuration implements DescribedEntity {
id: ID!
name: String
@@ -256,6 +278,16 @@ type Route {
tooltip: String
}
type ClientAddon {
id: ID!
url: String!
}
type SharedData {
id: ID!
value: JSON
}
type Query {
progress (id: ID!): Progress
cwd: String!
@@ -274,6 +306,8 @@ type Query {
configuration (id: ID!): Configuration
fileDiffs: [FileDiff]
routes: [Route]
clientAddons: [ClientAddon]
sharedData (id: ID!): SharedData
}
type Mutation {
@@ -295,6 +329,7 @@ type Mutation {
pluginInvoke (id: ID!): PluginInstallation
pluginFinishInstall: PluginInstallation
pluginUpdate (id: ID!): Plugin
pluginActionCall (id: ID!, params: JSON): PluginActionResult
taskRun (id: ID!): Task
taskStop (id: ID!): Task
taskLogsClear (id: ID!): Task
@@ -302,6 +337,7 @@ type Mutation {
configurationCancel (id: ID!): Configuration
gitCommit (message: String!): Boolean
fileOpenInEditor (input: OpenInEditorInput!): Boolean
sharedDataUpdate (id: ID!, value: JSON!): SharedData
}
type Subscription {
@@ -314,5 +350,9 @@ type Subscription {
routeAdded: Route
routeRemoved: Route
routeChanged: Route
clientAddonAdded: ClientAddon
sharedDataUpdated (id: ID!): SharedData
pluginActionCalled: PluginActionCall
pluginActionResolved: PluginActionResult
}
`
@@ -0,0 +1,42 @@
const ipc = require('node-ipc')
ipc.config.id = 'vue-cli'
ipc.config.retry = 1500
ipc.config.silent = true
const listeners = []
ipc.serve(() => {
ipc.server.on('message', (data, socket) => {
for (const listener of listeners) {
listener({
data,
emit: data => {
ipc.server.emit(socket, 'message', data)
}
})
}
})
})
ipc.server.start()
function on (cb) {
listeners.push(cb)
return () => off(cb)
}
function off (cb) {
const index = listeners.indexOf(cb)
if (index !== -1) listeners.splice(index, 1)
}
function send (data) {
ipc.server.emit('message', data)
}
module.exports = {
on,
off,
send
}
@@ -0,0 +1,7 @@
#import "./clientAddonFragment.gql"
subscription clientAddonAdded {
clientAddonAdded {
...clientAddon
}
}
@@ -0,0 +1,4 @@
fragment clientAddon on ClientAddon {
id
url
}
@@ -0,0 +1,7 @@
#import "./clientAddonFragment.gql"
query clientAddons {
clientAddons {
...clientAddon
}
}
@@ -0,0 +1,7 @@
#import "./pluginActionResultFragment.gql"
mutation pluginActionCall ($id: ID!, $params: JSON) {
pluginActionCall (id: $id, params: $params) {
...pluginActionResult
}
}
@@ -0,0 +1,4 @@
fragment pluginActionCall on PluginActionCall {
id
params
}
@@ -0,0 +1,7 @@
#import "./pluginActionCallFragment.gql"
subscription pluginActionCalled {
pluginActionCalled {
...pluginActionCall
}
}
@@ -0,0 +1,7 @@
#import "./pluginActionResultFragment.gql"
subscription pluginActionResolved {
pluginActionResolved {
...pluginActionResult
}
}
@@ -0,0 +1,6 @@
fragment pluginActionResult on PluginActionResult {
id
params
results
errors
}
@@ -0,0 +1,7 @@
#import "./sharedDataFragment.gql"
query sharedData ($id: ID!) {
sharedData (id: $id) {
...sharedData
}
}
@@ -0,0 +1,4 @@
fragment sharedData on SharedData {
id
value
}
@@ -0,0 +1,7 @@
#import "./sharedDataFragment.gql"
mutation sharedDataUpdate ($id: ID!, $value: JSON!) {
sharedDataUpdate (id: $id, value: $value) {
...sharedData
}
}
@@ -0,0 +1,7 @@
#import "./sharedDataFragment.gql"
subscription sharedDataUpdated ($id: ID!) {
sharedDataUpdated (id: $id) {
...sharedData
}
}
+8 -1
View File
@@ -4,9 +4,16 @@
query task ($id: ID!) {
task (id: $id) {
...task
link
prompts {
...prompt
}
link
views {
id
label
icon
component
}
defaultView
}
}
+26
View File
@@ -7,9 +7,31 @@ import VueUi from '@vue/ui'
import InstantSearch from 'vue-instantsearch'
import * as Filters from './filters'
import './register-components'
import ClientAddonApi from './util/ClientAddonApi'
import Responsive from './util/responsive'
import SharedData from './util/SharedData'
import PluginAction from './util/PluginAction'
import gql from 'graphql-tag'
window.gql = gql
Vue.use(VueUi)
Vue.use(InstantSearch)
Vue.use(Responsive, {
computed: {
mobile () {
return this.width <= 768
},
tablet () {
return this.width <= 900
},
desktop () {
return !this.tablet
}
}
})
Vue.use(SharedData)
Vue.use(PluginAction)
for (const key in Filters) {
Vue.filter(key, Filters[key])
@@ -17,6 +39,10 @@ for (const key in Filters) {
Vue.config.productionTip = false
// For client addons
window.Vue = Vue
window.ClientAddonApi = new ClientAddonApi()
const app = new Vue({
provide: apolloProvider.provide(),
router,
+26
View File
@@ -101,3 +101,29 @@ ansi-colors('white', $vue-ui-color-light)
.fill-height
height 100%
.blank-icon
width 24px
height @width
display block
margin auto
svg
fill rgba($vue-ui-color-dark, .3)
.pane-toolbar
h-box()
align-items center
padding 6px 6px 6px $padding-item
> :not(.separator)
space-between-x(6px)
> * + .separator
margin-left 6px
.title
flex 100% 1 1
width 0
ellipsis()
> .vue-ui-switch
&:not(.selected)
.wrapper
background rgba($vue-ui-color-dark, .2)
@@ -0,0 +1,49 @@
import Vue from 'vue'
export default class ClientAddonApi {
constructor (addonId) {
this.components = new Map()
this.componentListeners = new Map()
}
component (id, definition) {
this.components.set(id, definition)
const componentId = toComponentId(id)
Vue.component(componentId, definition)
console.log(`Registered ${componentId} component`)
// Call listeners
const listeners = this.componentListeners.get(id)
if (listeners) {
listeners.forEach(l => l(definition))
this.componentListeners.delete(id)
}
}
getComponent (id) {
return this.components.get(id)
}
listenForComponent (id, cb) {
let listeners = this.componentListeners.get(id)
if (!listeners) {
listeners = []
this.componentListeners.set(id, listeners)
}
listeners.push(cb)
}
awaitComponent (id) {
return new Promise((resolve, reject) => {
const result = this.getComponent(id)
if (result) {
resolve(result)
} else {
this.listenForComponent(id, resolve)
}
})
}
}
export function toComponentId (id) {
return `client-addon--${id}`
}
@@ -0,0 +1,35 @@
import PLUGIN_ACTION_CALL from '../graphql/pluginActionCall.gql'
import PLUGIN_ACTION_CALLED from '../graphql/pluginActionCalled.gql'
import PLUGIN_ACTION_RESOLVED from '../graphql/pluginActionResolved.gql'
let uid = 0
export default {
install (Vue) {
Vue.mixin({
methods: {
$callAction (id, params) {
return this.$apollo.mutate({
mutation: PLUGIN_ACTION_CALL,
variables: {
id,
params
}
})
},
$onActionCalled (cb) {
return this.$apollo.addSmartSubscription(`plugin-action-called-${uid++}`, {
query: PLUGIN_ACTION_CALLED,
result: cb
})
},
$onActionResolved (cb) {
return this.$apollo.addSmartSubscription(`plugin-action-resolved-${uid++}`, {
query: PLUGIN_ACTION_RESOLVED,
result: cb
})
}
}
})
}
}
+111
View File
@@ -0,0 +1,111 @@
import SHARED_DATA from '../graphql/sharedData.gql'
import SHARED_DATA_UPDATE from '../graphql/sharedDataUpdate.gql'
import SHARED_DATA_UPDATED from '../graphql/sharedDataUpdated.gql'
function genQuery (id) {
return {
query: SHARED_DATA,
variables: {
id
},
fetchPolicy: 'cache-and-network',
update: ({ sharedData }) => sharedData.value,
subscribeToMore: {
document: SHARED_DATA_UPDATED,
variables: {
id
},
updateQuery: (previousResult, { subscriptionData }) => {
return {
sharedData: subscriptionData.data.sharedDataUpdated
}
}
}
}
}
export default {
install (Vue) {
Vue.mixin({
data () {
return {
$sharedData: {}
}
},
created () {
const options = this.$options.sharedData
if (options) {
if (typeof options === 'function') {
let smartQueries
this.$watch(options.bind(this), result => {
if (smartQueries) {
smartQueries.forEach(s => s.destroy())
}
smartQueries = this.$syncSharedData(result)
}, {
immediate: true
})
} else {
this.$syncSharedData(options)
}
}
},
methods: {
async $getSharedData (id) {
const result = await this.$apollo.query({
query: SHARED_DATA,
variables: {
id
}
})
return result.sharedData.value
},
$watchSharedData (id, cb) {
return this.$apollo.addSmartQuery(id, {
...genQuery(id),
manual: true,
result: ({ data }) => {
data && cb(data.sharedData.value)
}
})
},
$setSharedData (id, value) {
return this.$apollo.mutate({
mutation: SHARED_DATA_UPDATE,
variables: {
id,
value
}
})
},
$syncSharedData (options) {
const smartQueries = []
for (const key in options) {
const id = options[key]
this.$set(this.$data.$sharedData, key, null)
// Proxy
Object.defineProperty(this, key, {
get: () => this.$data.$sharedData[key],
set: value => this.$set(this.$data.$sharedData, key, value),
enumerable: true,
configurable: true
})
const smartQuery = this.$apollo.addSmartQuery(key, genQuery(id))
smartQueries.push(smartQuery)
}
return smartQueries
}
}
})
window.mapSharedData = (namespace, options) => {
const result = {}
for (const key in options) {
result[key] = namespace + options[key]
}
return result
}
}
}
@@ -0,0 +1,28 @@
export let responsive
export default {
install (Vue, options) {
const finalOptions = Object.assign({}, {
computed: {}
}, options)
responsive = new Vue({
data () {
return {
width: window.innerWidth,
height: window.innerHeight
}
},
computed: finalOptions.computed
})
Object.defineProperty(Vue.prototype, '$responsive', {
get: () => responsive
})
window.addEventListener('resize', () => {
responsive.width = window.innerWidth
responsive.height = window.innerHeight
})
}
}
@@ -2,6 +2,7 @@
<div class="project-task-details">
<template v-if="task">
<div class="header">
<VueIcon icon="assignment" class="task-icon big"/>
<div class="name">{{ task.name }}</div>
<div class="description">{{ task.description }}</div>
</div>
@@ -48,11 +49,33 @@
/>
<div class="vue-ui-spacer"/>
<VueGroup
v-if="task.views.length"
v-model="currentView"
>
<VueGroupButton
:label="$t('views.project-task-details.output')"
icon-left="dvr"
value="_output"
/>
<VueGroupButton
v-for="view of task.views"
:key="view.id"
:value="view.id"
:icon-left="view.icon"
:label="view.label"
/>
</VueGroup>
</div>
<div class="content">
<TerminalView
ref="terminal"
:class="{
ghost: currentView !== '_output'
}"
:key="id"
:cols="100"
:rows="24"
@@ -64,8 +87,16 @@
}"
:title="$t('views.project-task-details.output')"
toolbar
open-links
@clear="clearLogs()"
/>
<ClientAddonComponent
v-if="currentView !== '_output'"
:name="currentViewComponent"
:key="currentView"
class="view"
/>
</div>
</template>
@@ -106,6 +137,12 @@ import TASK_LOG_ADDED from '../graphql/taskLogAdded.gql'
export default {
name: 'ProjectTaskDetails',
provide () {
return {
TaskDetails: this
}
},
mixins: [
Prompts({
field: 'task',
@@ -123,7 +160,8 @@ export default {
data () {
return {
task: null,
showParameters: false
showParameters: false,
currentView: '_output'
}
},
@@ -135,7 +173,14 @@ export default {
id: this.id
}
},
fetchPolicy: 'cache-and-network'
fetchPolicy: 'cache-and-network',
async result ({ data, loading }) {
if (!this.$_init && !loading && data && data.task.defaultView) {
this.$_init = true
await this.$nextTick()
this.currentView = data.task.defaultView
}
}
},
taskLogs: {
@@ -147,10 +192,13 @@ export default {
},
fetchPolicy: 'network-only',
manual: true,
result ({ data, loading }) {
async result ({ data, loading }) {
if (!loading) {
await this.$nextTick()
const terminal = this.$refs.terminal
data.taskLogs.logs.forEach(terminal.addLog)
if (terminal) {
data.taskLogs.logs.forEach(terminal.addLog)
}
}
}
},
@@ -163,8 +211,9 @@ export default {
id: this.id
}
},
result ({ data }) {
async result ({ data }) {
if (data.taskLogAdded.taskId === this.id) {
await this.$nextTick()
const terminal = this.$refs.terminal
terminal.addLog(data.taskLogAdded)
}
@@ -173,9 +222,22 @@ export default {
}
},
computed: {
currentViewComponent () {
if (this.currentView !== '_output') {
const id = this.task.views.find(
view => view.id === this.currentView
).component
return id
}
},
},
watch: {
id () {
this.showParameters = false
this.currentView = '_output'
this.$_init = false
}
},
@@ -232,19 +294,40 @@ export default {
.content
flex 100% 1 1
height 0
padding 0 $padding-item $padding-item
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 18px
font-size 22px
color $vue-ui-color-dark
position relative
top -1px
.description
color $color-text-light
+115 -6
View File
@@ -575,6 +575,16 @@
dependencies:
lodash.once "^4.1.1"
"@most/multicast@^1.2.5":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@most/multicast/-/multicast-1.3.0.tgz#e01574840df634478ac3fabd164c6e830fb3b966"
dependencies:
"@most/prelude" "^1.4.0"
"@most/prelude@^1.4.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@most/prelude/-/prelude-1.7.0.tgz#0956ed464ad03e7fc95143eac0c6dd028498d975"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -1486,7 +1496,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.26.0:
invariant "^2.2.2"
lodash "^4.17.4"
babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0:
babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0, babel-types@^6.7.2:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies:
@@ -1499,7 +1509,7 @@ babylon@7.0.0-beta.40, babylon@^7.0.0-beta.40:
version "7.0.0-beta.40"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.40.tgz#91fc8cd56d5eb98b28e6fde41045f2957779940a"
babylon@^6.18.0:
babylon@^6.18.0, babylon@^6.7.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
@@ -1545,6 +1555,14 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
better-sqlite3@^4.0.2:
version "4.1.0"
resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-4.1.0.tgz#1dbb0b13b280ce1eebebad431502deb08b78941e"
dependencies:
bindings "^1.3.0"
integer "^1.0.3"
lzz-gyp "^0.4.2"
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -1553,6 +1571,10 @@ binary-extensions@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
bindings@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
bl@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e"
@@ -3400,6 +3422,10 @@ duplexify@^3.4.2, duplexify@^3.5.3:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
easy-stack@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -3857,6 +3883,10 @@ event-emitter@~0.3.5:
d "1"
es5-ext "~0.10.14"
event-pubsub@4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e"
event-stream@~3.3.0:
version "3.3.4"
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
@@ -4090,6 +4120,12 @@ extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
farmhash@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/farmhash/-/farmhash-1.2.1.tgz#2dbf12604ef5ca1f1420fb6600fccb30d5474dfd"
dependencies:
nan "^2.4.0"
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -4842,6 +4878,10 @@ graphql-tools@^2.21.0:
iterall "^1.1.3"
uuid "^3.1.0"
graphql-type-json@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.2.0.tgz#34c1a5fc2b7707a367c436482b253dbeebf1130e"
graphql@^0.13.0, graphql@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.1.tgz#9b3db3d8e40d1827e4172404bfdd2e4e17a58b55"
@@ -5331,6 +5371,27 @@ inquirer@^4.0.1:
strip-ansi "^4.0.0"
through "^2.3.6"
inspectpack@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/inspectpack/-/inspectpack-2.2.4.tgz#db783646d89f59133b3087c83f201d44546d0651"
dependencies:
babel-traverse "^6.26.0"
babel-types "^6.7.2"
babylon "^6.7.0"
lodash "^4.6.1"
uglify-es "^3.0.28"
workerpool "^2.2.1"
yargs "^4.3.1"
optionalDependencies:
better-sqlite3 "^4.0.2"
farmhash "^1.2.1"
integer@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/integer/-/integer-1.0.3.tgz#6e6403896de6781c50974c5adf9eea9bb0ce6acb"
dependencies:
bindings "^1.3.0"
internal-ip@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
@@ -6128,6 +6189,16 @@ js-beautify@^1.6.12, js-beautify@^1.6.14:
mkdirp "~0.5.0"
nopt "~3.0.1"
js-message@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.5.tgz#2300d24b1af08e89dd095bc1a4c9c9cfcb892d15"
js-queue@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/js-queue/-/js-queue-2.0.0.tgz#362213cf860f468f0125fc6c96abc1742531f948"
dependencies:
easy-stack "^1.0.0"
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@@ -6712,6 +6783,10 @@ lodash.endswith@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
lodash.get@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -6812,7 +6887,7 @@ lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@4, lodash@4.x, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
lodash@4, lodash@4.x, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@@ -6902,6 +6977,10 @@ lru-cache@~2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
lzz-gyp@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/lzz-gyp/-/lzz-gyp-0.4.2.tgz#495c44034a1f50c48a60f0ef9c28d42763a5b297"
macaddress@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
@@ -7271,6 +7350,14 @@ moment@^2.6.0:
version "2.21.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"
most@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/most/-/most-1.7.3.tgz#406c31a66d73aa16957816fdf96965e27df84f1a"
dependencies:
"@most/multicast" "^1.2.5"
"@most/prelude" "^1.4.0"
symbol-observable "^1.0.2"
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -7318,6 +7405,10 @@ nan@^2.3.0:
version "2.9.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866"
nan@^2.4.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
nanomatch@^1.2.9:
version "1.2.9"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
@@ -7397,6 +7488,14 @@ node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
node-ipc@^9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.1.1.tgz#4e245ed6938e65100e595ebc5dc34b16e8dd5d69"
dependencies:
event-pubsub "4.3.0"
js-message "1.0.5"
js-queue "2.0.0"
"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
@@ -7578,7 +7677,7 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
object-assign@4.1.1, object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -10233,7 +10332,7 @@ typescript@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836"
uglify-es@^3.3.4:
uglify-es@^3.0.28, uglify-es@^3.3.4:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
dependencies:
@@ -10688,6 +10787,10 @@ vue-parser@^1.1.5:
dependencies:
parse5 "^3.0.3"
vue-progress-path@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/vue-progress-path/-/vue-progress-path-0.0.2.tgz#d780fc7a96dbc7f784eb52895aeef7a5f0af6325"
vue-property-decorator@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-6.0.0.tgz#bb651b293542e31db0d24f36f4b0250ef08d8515"
@@ -11052,6 +11155,12 @@ worker-farm@^1.5.2:
errno "~0.1.7"
xtend "~4.0.1"
workerpool@^2.2.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-2.3.0.tgz#86c5cbe946b55e7dc9d12b1936c8801a6e2d744d"
dependencies:
object-assign "4.1.1"
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -11246,7 +11355,7 @@ yargs@^11.0.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
yargs@^4.8.0:
yargs@^4.3.1, yargs@^4.8.0:
version "4.8.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0"
dependencies: