feat(ui): Progress and Logs systems

This commit is contained in:
Guillaume Chau
2018-03-09 20:35:37 +01:00
parent 61655b14df
commit 9f0eece1d1
37 changed files with 1134 additions and 261 deletions

View File

@@ -1,6 +1,19 @@
const chalk = require('chalk')
const readline = require('readline')
const padStart = require('string.prototype.padstart')
const EventEmitter = require('events')
exports.events = new EventEmitter()
function _log (type, tag, message) {
if (process.env.VUE_CLI_API_MODE && message) {
exports.events.emit('log', {
message,
type,
tag
})
}
}
const format = (label, msg) => {
return msg.split('\n').map((line, i) => {
@@ -12,24 +25,32 @@ const format = (label, msg) => {
const chalkTag = msg => chalk.bgBlackBright.white.dim(` ${msg} `)
exports.log = (msg = '', tag = null) => tag ? console.log(format(chalkTag(tag), msg)) : console.log(msg)
exports.log = (msg = '', tag = null) => {
tag ? console.log(format(chalkTag(tag), msg)) : console.log(msg)
_log('log', tag, msg)
}
exports.info = (msg, tag = null) => {
console.log(format(chalk.bgBlue.black(' INFO ') + (tag ? chalkTag(tag) : ''), msg))
_log('info', tag, msg)
}
exports.done = (msg, tag = null) => {
console.log(format(chalk.bgGreen.black(' DONE ') + (tag ? chalkTag(tag) : ''), msg))
_log('done', tag, msg)
}
exports.warn = (msg, tag = null) => {
console.warn(format(chalk.bgYellow.black(' WARN ') + (tag ? chalkTag(tag) : ''), chalk.yellow(msg)))
_log('warn', tag, msg)
}
exports.error = (msg, tag = null) => {
console.error(format(chalk.bgRed(' ERROR ') + (tag ? chalkTag(tag) : ''), chalk.red(msg)))
_log('error', tag, msg)
if (msg instanceof Error) {
console.error(msg.stack)
_log('error', tag, msg.stack)
}
}

View File

@@ -21,6 +21,7 @@
"@vue/cli-service": "^3.0.0-beta.3",
"@vue/eslint-config-standard": "^3.0.0-beta.3",
"@vue/ui": "^0.1.9",
"ansi_up": "^2.0.2",
"apollo-cache-inmemory": "^1.1.10",
"apollo-client": "^2.2.6",
"apollo-link": "^1.2.1",
@@ -38,7 +39,8 @@
"vue-apollo": "^3.0.0-alpha.1",
"vue-cli-plugin-apollo": "^0.4.1",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13"
"vue-template-compiler": "^2.5.13",
"xterm": "^3.2.0"
},
"browserslist": [
"> 1%",

View File

@@ -31,7 +31,7 @@ export default {
.app
display grid
grid-template-columns 1fr
grid-template-rows auto 28px
grid-template-rows 1fr auto
grid-template-areas "content" "status"
.content

View File

@@ -55,7 +55,7 @@ export default {
justify-content center
.description
color lighten($vue-color-dark, 40%)
color $color-text-light
&.selected
.name

View File

@@ -1,64 +0,0 @@
<template>
<div
class="loading-screen"
:class="{
loading
}"
>
<VueLoadingIndicator
class="primary big overlay fixed"
>
<div class="content">
<slot/>
</div>
</VueLoadingIndicator>
</div>
</template>
<script>
import { DisableScroll } from '@vue/ui'
export default {
mixins: [
DisableScroll
],
props: {
loading: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.loading-screen
position absolute
.content
display grid
grid-template-columns 1fr
grid-gap $padding-item
>>> .error
color $vue-color-danger
v-box()
box-center()
> .vue-icon
margin-bottom $padding-item
svg
fill @color
.actions
margin-top $padding-item
&:not(.loading)
.vue-loading-indicator
>>> .animation
display none
&.loading
.content
margin-top $padding-item
</style>

View File

@@ -0,0 +1,105 @@
<template>
<div
class="logger-message"
:class="[
`type-${message.type}`,
{
'has-type': message.type !== 'log',
'has-tag': message.tag,
pre
}
]"
>
<div v-if="message.type !== 'log'" class="type">{{ message.type }}</div>
<div v-if="message.tag" class="tag">{{ message.tag }}</div>
<div class="message" v-html="formattedMessage"/>
</div>
</template>
<script>
import AU from 'ansi_up'
const ansiUp = new AU()
ansiUp.use_classes = true
export default {
props: {
message: {
type: Object,
required: true
},
pre: {
type: Boolean,
default: false
}
},
computed: {
formattedMessage () {
return ansiUp.ansi_to_html(this.message.message)
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.logger-message
h-box()
align-items baseline
font-family 'Roboto Mono', monospace
box-sizing border-box
padding 2px 4px
.type,
.tag
padding 2px 6px
border-radius $br
.type
text-transform uppercase
&.type-warn
.type
background $vue-color-warning
color $vue-color-light
&.type-error
.type
background $vue-color-danger
color $vue-color-light
&.type-info
.type
background $vue-color-info
color $vue-color-light
&.type-done
.type
background $vue-color-success
color $vue-color-light
.tag
background lighten($vue-color-dark, 60%)
&.has-type.has-tag
.type
border-top-right-radius 0
border-bottom-right-radius 0
.tag
border-top-left-radius 0
border-bottom-left-radius 0
.message
flex 100% 1 1
width 0
ellipsis()
&.has-type,
&.has-tag
.message
margin-left 12px
&.pre
.message
white-space pre-wrap
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="logger-view">
<ApolloQuery
:query="require('../graphql/consoleLogs.gql')"
fetch-policy="cache-and-network"
class="logs"
>
<ApolloSubscribeToMore
ref="logs"
:document="require('../graphql/consoleLogAdded.gql')"
:update-query="onConsoleLogAdded"
/>
<template slot-scope="{ result: { data } }">
<template v-if="data && data.consoleLogs">
<LoggerMessage
v-for="log of data.consoleLogs"
:key="log.id"
:message="log"
pre
/>
</template>
</template>
</ApolloQuery>
</div>
</template>
<script>
import LoggerMessage from './LoggerMessage'
export default {
components: {
LoggerMessage
},
methods: {
onConsoleLogAdded (previousResult, { subscriptionData }) {
this.scrollToBottom()
return {
consoleLogs: [
...previousResult.consoleLogs,
subscriptionData.data.consoleLogAdded
]
}
},
async scrollToBottom () {
await this.$nextTick()
const list = this.$refs.logs.$el
list.scrollTop = list.scrollHeight
console.log(list.scrollHeight)
},
clearLogs () {
// TODO
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.logger-view
background $vue-color-light-neutral
padding $padding-item 0
height 160px
display grid
grid-template-columns 1fr
grid-template-rows 1fr
grid-template-areas "logs"
.logs
grid-area logs
padding 0 $padding-item
overflow-x hidden
overflow-y auto
font-size 12px
.logger-message
&:hover
background lighten(@background, 40%)
</style>

View File

@@ -0,0 +1,117 @@
<template>
<transition name="vue-fade">
<div
v-if="progress"
class="loading-screen"
:class="{
loading
}"
>
<VueLoadingIndicator
class="primary big overlay fixed"
>
<div class="content">
<div v-if="progress.error" class="error">
<VueIcon
icon="error"
class="huge"
/>
<div>{{ progress.error }}</div>
<div class="actions">
<VueButton
icon-left="close"
label="Close"
@click="close()"
/>
</div>
</div>
<template v-else>
<div v-if="statusMessage" class="status">
{{ statusMessage }}
</div>
<div class="secondary-info">
<div v-if="progress.info" class="info">
{{ progress.info }}
</div>
<VueLoadingBar
v-if="progress.progress !== -1"
:value="progress.progress"
/>
</div>
</template>
</div>
</VueLoadingIndicator>
</div>
</transition>
</template>
<script>
import { DisableScroll } from '@vue/ui'
import Progress from '../mixins/Progress'
export default {
mixins: [
DisableScroll,
Progress
],
methods: {
close () {
this.progress = null
}
}
}
</script>
<style lang="stylus" scoped>
@import "~@/style/imports"
.loading-screen
position absolute
.content
display grid
grid-template-columns 1fr
grid-gap $padding-item
text-align center
.error
color $vue-color-danger
v-box()
box-center()
> .vue-icon
margin-bottom $padding-item
>>> svg
fill @color
.actions
margin-top $padding-item
.secondary-info
position absolute
bottom 42px
left 0
right 0
v-box()
box-center()
.info
color $color-text-light
.vue-loading-bar
width 50vw
max-width 400px
margin-top 24px
&:not(.loading)
.vue-loading-indicator
>>> .animation
display none
&.loading
.content
margin-top $padding-item
</style>

View File

@@ -52,7 +52,8 @@ export default {
button-colors(rgba($vue-color-light, .7), transparent)
border-radius 0
&:hover
button-colors($vue-color-light, lighten($vue-color-dark, 10%))
$bg = darken($vue-color-dark, 70%)
button-colors($vue-color-light, $bg)
&.selected
button-colors($vue-color-primary, lighten($vue-color-dark, 10%))
button-colors(lighten($vue-color-primary, 40%), $bg)
</style>

View File

@@ -1,58 +1,112 @@
<template>
<div class="status-bar">
<div
class="section current-project"
v-tooltip="'Current project'"
@click="$emit('project')"
>
<VueIcon icon="work"/>
<span v-if="projectCurrent">{{ projectCurrent.name }}</span>
<span v-else class="label">No project</span>
</div>
<LoggerView
v-if="showLogs"
/>
<ApolloQuery
:query="require('@/graphql/cwd.gql')"
class="section current-path"
v-tooltip="'Current Working Directory'"
@click.native="$emit('cwd')"
>
<ApolloSubscribeToMore
:document="require('@/graphql/cwdChanged.gql')"
:update-query="(previousResult, { subscriptionData }) => ({
cwd: subscriptionData.data.cwd
})"
/>
<div class="content">
<div
class="section current-project"
v-tooltip="'Current project'"
@click="$emit('project')"
>
<VueIcon icon="work"/>
<span v-if="projectCurrent">{{ projectCurrent.name }}</span>
<span v-else class="label">No project</span>
</div>
<template slot-scope="{ result: { data } }">
<VueIcon icon="folder"/>
<span v-if="data">{{ data.cwd }}</span>
</template>
</ApolloQuery>
<ApolloQuery
:query="require('@/graphql/cwd.gql')"
class="section current-path"
v-tooltip="'Current Working Directory'"
@click.native="$emit('cwd')"
>
<ApolloSubscribeToMore
:document="require('@/graphql/cwdChanged.gql')"
:update-query="(previousResult, { subscriptionData }) => ({
cwd: subscriptionData.data.cwd
})"
/>
<div
class="section console-log"
v-tooltip="'Console'"
@click="$emit('console')"
>
<VueIcon icon="subtitles"/>
<span v-if="consoleLog">{{ consoleLog }}</span>
<template slot-scope="{ result: { data } }">
<VueIcon icon="folder"/>
<span v-if="data">{{ data.cwd }}</span>
</template>
</ApolloQuery>
<div
class="section console-log"
v-tooltip="'Logs'"
@click="onConsoleClick()"
>
<VueIcon icon="subtitles"/>
<LoggerMessage class="last-message"
v-if="consoleLogLast"
:message="consoleLogLast"
/>
<!-- <TerminalView
:cols="100"
:rows="1"
:content="consoleLogLast"
auto-size
:options="{
scorllback: 0,
disableStdin: true
}"
/> -->
</div>
</div>
</div>
</template>
<script>
import LoggerMessage from '../components/LoggerMessage'
import LoggerView from '../components/LoggerView'
import PROJECT_CURRENT from '../graphql/projectCurrent.gql'
import CONSOLE_LOG_LAST from '../graphql/consoleLogLast.gql'
import CONSOLE_LOG_ADDED from '../graphql/consoleLogAdded.gql'
export default {
components: {
LoggerMessage,
LoggerView
},
data () {
return {
consoleLog: '',
consoleLogLast: null,
showLogs: false,
projectCurrent: null
}
},
apollo: {
projectCurrent: PROJECT_CURRENT
projectCurrent: {
query: PROJECT_CURRENT,
fetchPolicy: 'cache-and-network'
},
consoleLogLast: {
query: CONSOLE_LOG_LAST,
fetchPolicy: 'cache-and-network'
},
$subscribe: {
consoleLogAdded: {
query: CONSOLE_LOG_ADDED,
result ({ data }) {
this.consoleLogLast = data.consoleLogAdded
}
}
}
},
methods: {
onConsoleClick () {
this.$emit('console')
this.showLogs = !this.showLogs
}
}
}
</script>
@@ -62,10 +116,14 @@ export default {
@import "~@/style/imports"
.status-bar
h-box()
align-items center
background $vue-color-light-neutral
font-size $padding-item
$bg = $vue-color-light-neutral
.content
h-box()
align-items center
background $bg
font-size $padding-item
height 28px
.section
h-box()
@@ -77,11 +135,20 @@ export default {
&:hover
opacity 1
background lighten(@background, 40%)
background lighten($bg, 40%)
> .vue-icon + *
margin-left 4px
.label
color lighten($vue-color-dark, 20%)
.console-log
&,
.last-message
flex 100% 1 1
width 0
.last-message
font-size .9em
</style>

View File

@@ -0,0 +1,159 @@
<template>
<div class="terminal-view">
<div ref="render" class="xterm-render"></div>
<resize-observer v-if="autoSize" @notify="fit"/>
</div>
</template>
<script>
import { Terminal } from 'xterm'
import * as fit from 'xterm/dist/addons/fit/fit'
import * as webLinks from 'xterm/dist/addons/webLinks/webLinks'
Terminal.applyAddon(fit)
Terminal.applyAddon(webLinks)
const defaultTheme = {
foreground: '#000',
background: '#dbebec',
cursor: 'rgba(0, 0, 0, .4)',
selection: 'rgba(0, 0, 0, 0.3)',
black: '#000000',
red: '#e06c75',
brightRed: '#e06c75',
green: '#A4EFA1',
brightGreen: '#A4EFA1',
brightYellow: '#EDDC96',
yellow: '#EDDC96',
magenta: '#e39ef7',
brightMagenta: '#e39ef7',
cyan: '#5fcbd8',
brightBlue: '#5fcbd8',
brightCyan: '#5fcbd8',
blue: '#5fcbd8',
white: '#d0d0d0',
brightBlack: '#808080',
brightWhite: '#ffffff'
}
export default {
props: {
cols: {
type: Number,
required: true
},
rows: {
type: Number,
required: true
},
content: {
type: String,
default: undefined
},
autoSize: {
type: Boolean,
default: false
},
options: {
type: Object,
default: () => ({})
}
},
watch: {
cols (c) {
this.$_terminal.resize(c, this.rows)
},
rows (r) {
this.$_terminal.resize(this.cols, r)
},
content: 'setContent'
},
mounted () {
this.initTerminal()
},
beforeDestroy () {
this.$_terminal.destroy()
},
methods: {
initTerminal () {
let term = this.$_terminal = new Terminal({
cols: this.cols,
rows: this.rows,
theme: defaultTheme,
...this.options
})
webLinks.webLinksInit(term, this.handleLink)
term.open(this.$refs.render)
term.on('blur', () => this.$emit('blur'))
term.on('focus', () => this.$emit('focus'))
},
setContent (value) {
if (typeof value === 'string') {
this.$_terminal.write(value)
} else {
this.$_terminal.writeln('')
}
},
clear () {
this.$_terminal.clear()
},
handleLink (event, uri) {
this.$emit('link', uri)
},
async fit () {
let parent = this.$el
let el = this.$refs.render
let term = this.$_terminal
term.element.style.display = 'none'
await this.$nextTick()
term.fit()
term.element.style.display = ''
term.refresh(0, term.rows - 1)
},
focus () {
this.$_terminal.focus()
},
blur () {
this.$_terminal.blur()
}
}
}
</script>
<style lang="stylus">
@import "~xterm/dist/xterm.css"
</style>
<style lang="stylus" scoped>
@import "~@/style/imports"
.terminal-view
position relative
.xterm-render
width 100%
height 100%
>>> .xterm
.xterm-cursor-layer
display none
</style>

View File

@@ -1,4 +1,5 @@
module.exports = {
CWD_CHANGED: 'cwd_changed',
CREATE_STATUS: 'create_status'
PROGRESS_CHANGED: 'progress_changed',
CONSOLE_LOG_ADDED: 'console_log_added'
}

View File

@@ -1,5 +1,6 @@
const path = require('path')
const fs = require('fs')
const rimraf = require('rimraf')
const cwd = require('./cwd')
@@ -77,6 +78,18 @@ function setFavorite ({ file, favorite }, context) {
return generateFolder(file, context)
}
function deleteFolder (file) {
return new Promise((resolve, reject) => {
rimraf(file, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
module.exports = {
getCurrent,
list,
@@ -86,5 +99,6 @@ module.exports = {
readPackage,
isVueProject,
listFavorite,
setFavorite
setFavorite,
delete: deleteFolder
}

View File

@@ -0,0 +1,53 @@
const shortId = require('shortid')
const { events } = require('@vue/cli-shared-utils/lib/logger')
const { generateTitle } = require('@vue/cli/lib/util/clearConsole')
const channels = require('../channels')
let init = false
let logs = []
exports.add = function (log, context) {
const item = {
id: shortId.generate(),
date: new Date().toISOString(),
tag: null,
...log
}
logs.push(item)
context.pubsub.publish(channels.CONSOLE_LOG_ADDED, {
consoleLogAdded: item
})
return item
}
exports.init = function (context) {
if (!init) {
init = true
events.on('log', log => {
exports.add(log, context)
})
exports.add({
type: 'info',
tag: null,
message: generateTitle(true)
}, context)
}
}
exports.list = function (context) {
return logs
}
exports.last = function (context) {
if (logs.length) {
return logs[logs.length - 1]
}
return null
}
exports.clear = function (context) {
logs = []
return logs
}

View File

@@ -0,0 +1,59 @@
const channels = require('../channels')
let map = new Map()
function get (id, context) {
return map.get(id)
}
function set (data, context) {
const { id } = data
let progress = get(id, context)
if (!progress) {
progress = data
map.set(id, Object.assign({}, {
status: null,
error: null,
info: null,
progress: -1
}, progress))
} else {
Object.assign(progress, data)
}
context.pubsub.publish(channels.PROGRESS_CHANGED, { progressChanged: progress })
return progress
}
function remove (id, context) {
return map.delete(id)
}
async function wrap (id, context, operation) {
set({ id }, context)
let result
let error = null
try {
result = await operation(data => {
set(Object.assign({ id }, data), context)
})
} catch (e) {
error = e
set({ id, error: error.message }, context)
}
remove(id, context)
if (error) {
throw error
}
return result
}
module.exports = {
get,
set,
remove,
wrap
}

View File

@@ -1,24 +1,27 @@
const path = require('path')
const fs = require('fs')
const shortId = require('shortid')
const rimraf = require('rimraf')
const Creator = require('@vue/cli/lib/Creator')
const { getPromptModules } = require('@vue/cli/lib/util/createTools')
const { getFeatures } = require('@vue/cli/lib/util/features')
const { defaults } = require('@vue/cli/lib/options')
const { toShortPluginId } = require('@vue/cli-shared-utils')
const { progress: installProgress } = require('@vue/cli/lib/util/installDeps')
const channels = require('../channels')
const progress = require('./progress')
const cwd = require('./cwd')
const prompts = require('./prompts')
const folders = require('./folders')
const PROGRESS_ID = 'project-create'
let currentProject = null
let creator = null
let presets = []
let features = []
let onCreationEvent = null
let onInstallProgress = null
let onInstallLog = null
function list (context) {
return context.db.get('projects').value()
@@ -50,9 +53,23 @@ function initCreator (context) {
const creator = new Creator('', cwd.get(), getPromptModules())
onCreationEvent = ({ event }) => {
context.pubsub.publish(channels.CREATE_STATUS, { createStatus: event })
progress.set({ id: PROGRESS_ID, status: event, info: null }, context)
}
creator.addListener('creation', onCreationEvent)
creator.on('creation', onCreationEvent)
onInstallProgress = value => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, progress: value }, context)
}
}
installProgress.on('progress', onInstallProgress)
onInstallLog = message => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, info: message }, context)
}
}
installProgress.on('log', onInstallLog)
// Presets
const presetsData = creator.getPresets()
@@ -116,6 +133,8 @@ function initCreator (context) {
function removeCreator (context) {
if (creator) {
creator.removeListener('creation', onCreationEvent)
installProgress.removeListener('progress', onInstallProgress)
installProgress.removeListener('log', onInstallLog)
creator = null
}
}
@@ -185,59 +204,77 @@ function answerPrompt ({ id, value }, context) {
}
async function create (input, context) {
const targetDir = path.join(cwd.get(), input.folder)
creator.context = targetDir
return progress.wrap(PROGRESS_ID, context, async setProgress => {
setProgress({
status: 'creating'
})
const inCurrent = input.folder === '.'
const name = inCurrent ? path.relative('../', process.cwd()) : input.folder
creator.name = name
const targetDir = path.join(cwd.get(), input.folder)
creator.context = targetDir
if (fs.existsSync(targetDir)) {
if (input.force) {
rimraf.sync(targetDir)
} else {
throw new Error(`Folder ${targetDir} already exists`)
const inCurrent = input.folder === '.'
const name = inCurrent ? path.relative('../', process.cwd()) : input.folder
creator.name = name
if (fs.existsSync(targetDir)) {
if (input.force) {
setProgress({
info: 'Cleaning folder...'
})
await folders.delete(targetDir)
setProgress({
info: null
})
} else {
throw new Error(`Folder ${targetDir} already exists`)
}
}
}
const answers = prompts.getAnswers()
prompts.reset()
let index
const answers = prompts.getAnswers()
prompts.reset()
let index
// Package Manager
answers.packageManager = input.packageManager
// Package Manager
answers.packageManager = input.packageManager
// Config files
if ((index = answers.features.includes('use-config-files')) !== -1) {
answers.features.splice(index, 1)
answers.useConfigFiles = 'files'
}
// Config files
if ((index = answers.features.includes('use-config-files')) !== -1) {
answers.features.splice(index, 1)
answers.useConfigFiles = 'files'
}
// Preset
answers.preset = input.preset
if (input.save) {
answers.save = true
answers.saveName = input.save
}
// Preset
answers.preset = input.preset
if (input.save) {
answers.save = true
answers.saveName = input.save
}
let preset
if (input.remote) {
// vue create foo --preset bar
preset = await creator.resolvePreset(input.preset, input.clone)
} else if (input.preset === 'default') {
// vue create foo --default
preset = defaults.presets.default
} else {
preset = await creator.promptAndResolvePreset(answers)
}
setProgress({
info: 'Resolving preset...'
})
let preset
if (input.remote) {
// vue create foo --preset bar
preset = await creator.resolvePreset(input.preset, input.clone)
} else if (input.preset === 'default') {
// vue create foo --default
preset = defaults.presets.default
} else {
preset = await creator.promptAndResolvePreset(answers)
}
setProgress({
info: null
})
await creator.create({}, preset)
await creator.create({}, preset)
removeCreator()
removeCreator()
return importProject({
path: targetDir
}, context)
return importProject({
path: targetDir
}, context)
})
}
async function importProject (input, context) {

View File

@@ -1,12 +1,20 @@
const { withFilter } = require('graphql-subscriptions')
const exit = require('@vue/cli-shared-utils/lib/exit')
const channels = require('./channels')
// Connectors
const cwd = require('./connectors/cwd')
const folders = require('./connectors/folders')
const projects = require('./connectors/projects')
const progress = require('./connectors/progress')
const logs = require('./connectors/logs')
// Prevent code from exiting server process
exit.exitProcess = false
process.env.VUE_CLI_API_MODE = true
module.exports = {
Folder: {
children: (folder, args, context) => folders.list(folder.path, context),
@@ -20,6 +28,9 @@ module.exports = {
Query: {
cwd: () => cwd.get(),
consoleLogs: (root, args, context) => logs.list(context),
consoleLogLast: (root, args, context) => logs.last(context),
progress: (root, { id }, context) => progress.get(id, context),
folderCurrent: (root, args, context) => folders.getCurrent(args, context),
foldersFavorite: (root, args, context) => folders.listFavorite(context),
projects: (root, args, context) => projects.list(context),
@@ -28,6 +39,7 @@ module.exports = {
},
Mutation: {
consoleLogsClear: (root, args, context) => logs.clear(context),
folderOpen: (root, { path }, context) => folders.open(path, context),
folderOpenParent: (root, args, context) => folders.openParent(cwd.get(), context),
folderSetFavorite: (root, args, context) => folders.setFavorite({
@@ -47,8 +59,19 @@ module.exports = {
cwdChanged: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.CWD_CHANGED)
},
createStatus: {
subscribe: (parent, args, { pubsub }) => pubsub.asyncIterator(channels.CREATE_STATUS)
progressChanged: {
subscribe: withFilter(
// Iterator
(parent, args, { pubsub }) => pubsub.asyncIterator(channels.PROGRESS_CHANGED),
// Filter
(payload, variables) => payload.progressChanged.id === variables.id
)
},
consoleLogAdded: {
subscribe: (parent, args, context) => {
logs.init(context)
return context.pubsub.asyncIterator(channels.CONSOLE_LOG_ADDED)
}
}
}
}

View File

@@ -3,9 +3,9 @@ module.exports = `
type ConsoleLog {
id: ID!
message: String!
label: String
tag: String
type: ConsoleLogType!
date: String
}
enum ConsoleLogType {
@@ -140,8 +140,20 @@ input PromptInput {
value: String!
}
type Progress {
id: ID!
status: String
info: String
error: String
# Progress from 0 to 1 (-1 means disabled)
progress: Float
}
type Query {
progress (id: ID!): Progress
cwd: String!
consoleLogs: [ConsoleLog]
consoleLogLast: ConsoleLog
folderCurrent: Folder
foldersFavorite: [Folder]
projects: [Project]
@@ -151,6 +163,7 @@ type Query {
}
type Mutation {
consoleLogsClear: [ConsoleLog]
folderOpen (path: String!): Folder
folderOpenParent: Folder
folderSetFavorite (path: String!, favorite: Boolean!): Folder
@@ -166,8 +179,8 @@ type Mutation {
}
type Subscription {
progressChanged (id: ID!): Progress
consoleLogAdded: ConsoleLog!
cwdChanged: String!
createStatus: String!
}
`

View File

@@ -0,0 +1,7 @@
#import "./consoleLogFragment.gql"
subscription consoleLogAdded {
consoleLogAdded {
...consoleLog
}
}

View File

@@ -0,0 +1,7 @@
fragment consoleLog on ConsoleLog {
id
type
message
tag
date
}

View File

@@ -0,0 +1,7 @@
#import "./consoleLogFragment.gql"
query consoleLogLast {
consoleLogLast {
...consoleLog
}
}

View File

@@ -0,0 +1,7 @@
#import "./consoleLogFragment.gql"
query consoleLogs {
consoleLogs {
...consoleLog
}
}

View File

@@ -0,0 +1,7 @@
#import "./consoleLogFragment.gql"
mutation consoleLogsClear {
consoleLogsClear {
...consoleLog
}
}

View File

@@ -1,3 +0,0 @@
subscription createStatus {
createStatus
}

View File

@@ -0,0 +1,7 @@
#import "./progressFragment.gql"
query progress ($id: ID!) {
progress (id: $id) {
...progress
}
}

View File

@@ -0,0 +1,7 @@
#import "./progressFragment.gql"
subscription progressChanged ($id: ID!) {
progressChanged (id: $id) {
...progress
}
}

View File

@@ -0,0 +1,7 @@
fragment progress on Progress {
id
status
info
error
progress
}

View File

@@ -0,0 +1,68 @@
import PROGRESS from '../graphql/progress.gql'
import PROGRESS_CHANGED from '../graphql/progressChanged.gql'
const messages = {
'creating': 'Creating project...',
'git-init': 'Initializing git repository...',
'plugins-install': 'Installing CLI plugins. This might take a while...',
'invoking-generators': 'Invoking generators...',
'deps-install': 'Installing additional dependencies...',
'completion-hooks': 'Running completion hooks...',
'fetch-remote-preset': `Fetching remote preset...`,
'done': 'Successfully created project'
}
// @vue/component
export default {
props: {
progressId: {
type: String,
required: true
}
},
data () {
return {
progress: null
}
},
apollo: {
progress: {
query: PROGRESS,
variables () {
return {
id: this.progressId
}
},
fetchPolicy: 'network-only',
subscribeToMore: {
document: PROGRESS_CHANGED,
variables () {
return {
id: this.progressId
}
},
updateQuery: (previousResult, { subscriptionData }) => {
return {
progress: subscriptionData.data.progressChanged
}
}
}
}
},
computed: {
loading () {
return this.progress && !this.progress.error
},
statusMessage () {
if (!this.progress) return null
const { status } = this.progress
const message = messages[status]
return message || status || ''
}
}
}

View File

@@ -1 +1,2 @@
$color-light-background = lighten($vue-color-light-neutral, 80%)
$color-text-light = lighten($vue-color-dark, 40%)

View File

@@ -32,8 +32,17 @@ body,
.cta-text
margin $padding-item
color lighten($vue-color-dark, 40%)
color $color-text-light
font-size 18px
.list-item
list-item()
ansi-colors('black', $vue-color-dark)
ansi-colors('red', $vue-color-danger)
ansi-colors('green', $vue-color-primary)
ansi-colors('yellow', $vue-color-warning)
ansi-colors('blue', $md-blue)
ansi-colors('magenta', $vue-color-accent)
ansi-colors('cyan', $vue-color-info)
ansi-colors('white', $vue-color-light)

View File

@@ -7,3 +7,14 @@ list-item()
&:hover
background rgba($vue-color-primary, .1)
ansi-colors($name, $color)
.ansi-{$name}-fg
color $color
.ansi-{$name}-bg
background $color
.ansi-bright-{$name}-fg
color lighten($color, 10%)
.ansi-bright-{$name}-bg
background lighten($color, 10%)

View File

@@ -297,7 +297,7 @@
@close="showCancel = false"
>
<div class="default-body">
Are you sure you want to cancel the project creation?
Are you sure you want to reset the project creation?
</div>
<div slot="footer" class="actions space-between">
@@ -312,7 +312,7 @@
label="Clear project"
icon-left="delete_forever"
class="danger"
@click="cancel()"
@click="reset()"
/>
</div>
</VueModal>
@@ -358,36 +358,14 @@
</div>
</VueModal>
<transition name="vue-fade">
<LoadingScreen
v-if="showLoading"
:loading="loading"
>
<div v-if="error" class="error">
<VueIcon
icon="error"
class="huge"
/>
<div>{{ error }}</div>
<div class="actions">
<VueButton
icon-left="close"
label="close"
@click="showLoading = false"
/>
</div>
</div>
<template v-else>
<div class="status">{{ statusMessage }}</div>
</template>
</LoadingScreen>
</transition>
<ProgressScreen
progress-id="project-create"
/>
</div>
</template>
<script>
import LoadingScreen from '../components/LoadingScreen'
import ProgressScreen from '../components/ProgressScreen'
import ProjectFeatureItem from '../components/ProjectFeatureItem'
import ProjectPresetItem from '../components/ProjectPresetItem'
import PrompsList from '../components/PromptsList'
@@ -399,12 +377,11 @@ import FEATURE_SET_ENABLED from '../graphql/featureSetEnabled.gql'
import PRESET_APPLY from '../graphql/presetApply.gql'
import PROMPT_ANSWER from '../graphql/promptAnswer.gql'
import PROJECT_CREATE from '../graphql/projectCreate.gql'
import CREATE_STATUS from '../graphql/createStatus.gql'
function formDataFactory () {
return {
folder: '',
force: false,
folder: 'test-app',
force: true,
packageManager: undefined,
selectedPreset: null,
remotePreset: {
@@ -419,7 +396,7 @@ let formData = formDataFactory()
export default {
components: {
LoadingScreen,
ProgressScreen,
ProjectFeatureItem,
ProjectPresetItem,
PrompsList,
@@ -434,10 +411,6 @@ export default {
showCancel: false,
showRemotePreset: false,
showSavePreset: false,
showLoading: false,
loading: false,
createStatus: '',
error: null
}
},
@@ -450,15 +423,6 @@ export default {
projectCreation: {
query: PROJECT_CREATION,
fetchPolicy: 'network-only',
},
$subscribe: {
createStatus: {
query: CREATE_STATUS,
result ({ data }) {
this.createStatus = data.createStatus
}
}
}
},
@@ -495,29 +459,11 @@ export default {
name: 'Remote preset',
description: 'Fetch a preset from a git repository'
}
},
statusMessage () {
const messages = {
'creating': 'Creating project...',
'git-init': 'Initializing git repository...',
'plugins-install': 'Installing CLI plugins. This might take a while...',
'invoking-generators': 'Invoking generators...',
'deps-install': 'Installing additional dependencies...',
'completion-hooks': 'Running completion hooks...',
'fetch-remote-preset': `Fetching remote preset ${this.formData.remotePreset.url}...`,
'done': 'Successfully created project'
}
const message = messages[this.createStatus]
if (!message) {
console.warn(`Message not found for '${this.createStatus}'`)
}
return message || ''
}
},
methods: {
cancel () {
reset () {
formData = formDataFactory()
},
@@ -576,10 +522,6 @@ export default {
async createProject () {
this.showSavePreset = false
this.error = null
this.createStatus = 'creating'
this.showLoading = true
this.loading = true
try {
await this.$apollo.mutate({
@@ -597,15 +539,10 @@ export default {
}
})
this.$router.push({ name: 'home' })
this.reset()
} catch(e) {
if (e.graphQLErrors && e.graphQLErrors.length) {
this.error = e.graphQLErrors[0].message
} else {
this.error = e.message
}
console.error(e)
}
this.loading = false
},
}
}

View File

@@ -9,7 +9,7 @@ const cloneDeep = require('lodash.clonedeep')
const sortObject = require('./util/sortObject')
const getVersions = require('./util/getVersions')
const { installDeps } = require('./util/installDeps')
const clearConsole = require('./util/clearConsole')
const { clearConsole } = require('./util/clearConsole')
const PromptModuleAPI = require('./PromptModuleAPI')
const writeFileTree = require('./util/writeFileTree')
const { formatFeatures } = require('./util/features')

View File

@@ -4,7 +4,7 @@ const chalk = require('chalk')
const rimraf = require('rimraf')
const inquirer = require('inquirer')
const Creator = require('./Creator')
const clearConsole = require('./util/clearConsole')
const { clearConsole } = require('./util/clearConsole')
const { getPromptModules } = require('./util/createTools')
const { error, stopSpinner } = require('@vue/cli-shared-utils')

View File

@@ -3,7 +3,7 @@ const semver = require('semver')
const getVersions = require('./getVersions')
const { clearConsole } = require('@vue/cli-shared-utils')
module.exports = async function clearConsoleWithTitle (checkUpdate) {
exports.generateTitle = async function (checkUpdate) {
const { current, latest } = await getVersions()
let title = chalk.bold.blue(`Vue CLI v${current}`)
@@ -21,5 +21,10 @@ module.exports = async function clearConsoleWithTitle (checkUpdate) {
└─────────────────────────${``.repeat(latest.length)}─┘`)
}
return title
}
exports.clearConsole = async function clearConsoleWithTitle (checkUpdate) {
const title = await exports.generateTitle(checkUpdate)
clearConsole(title)
}

View File

@@ -1,3 +1,4 @@
const EventEmitter = require('events')
const request = require('./request')
const chalk = require('chalk')
const execa = require('execa')
@@ -15,6 +16,37 @@ const registries = {
}
const taobaoDistURL = 'https://npm.taobao.org/dist'
class InstallProgress extends EventEmitter {
constructor () {
super()
this._progress = -1
}
get progress () {
return this._progress
}
set progress (value) {
this._progress = value
this.emit('progress', value)
}
get enabled () {
return this._progress !== -1
}
set enabled (value) {
this.progress = value ? 0 : -1
}
log (value) {
this.emit('log', value)
}
}
const progress = exports.progress = new InstallProgress()
async function ping (registry) {
await request.get(`${registry}/vue-cli-version-marker/latest`)
return registry
@@ -119,28 +151,86 @@ async function addRegistryToArgs (command, args, cliRegistry) {
function executeCommand (command, args, targetDir) {
return new Promise((resolve, reject) => {
const apiMode = process.env.VUE_CLI_API_MODE
progress.enabled = false
// const lines = []
// let count = 0
// const unhook = intercept(buffer => {
// count++
// lines.push(buffer)
// const str = buffer === 'string' ? buffer : buffer.toString()
// // Steps
// const stepMatch = str.match(/\[\d+\/\d+]\s+(.*)/)
// if (stepMatch) {
// progress.log(stepMatch[1])
// }
// })
if (apiMode) {
if (command === 'npm') {
// TODO when this is supported
} else if (command === 'yarn') {
args.push('--json')
}
}
const child = execa(command, args, {
cwd: targetDir,
stdio: ['inherit', 'inherit', command === 'yarn' ? 'pipe' : 'inherit']
stdio: ['inherit', apiMode ? 'pipe' : 'inherit', !apiMode && command === 'yarn' ? 'pipe' : 'inherit']
})
// filter out unwanted yarn output
if (command === 'yarn') {
child.stderr.on('data', buf => {
const str = buf.toString()
if (/warning/.test(str)) {
return
if (apiMode) {
let progressTotal = 0
let progressTime = Date.now()
child.stdout.on('data', buffer => {
let str = buffer.toString().trim()
if (str && command === 'yarn' && str.indexOf('"type":') !== -1) {
const newLineIndex = str.lastIndexOf('\n')
if (newLineIndex !== -1) {
str = str.substr(newLineIndex)
}
const data = JSON.parse(str)
if (data.type === 'step') {
progress.enabled = false
progress.log(data.data.message)
} else if (data.type === 'progressStart') {
progressTotal = data.data.total
} else if (data.type === 'progressTick') {
const time = Date.now()
if (time - progressTime > 20) {
progressTime = time
progress.progress = data.data.current / progressTotal
}
} else {
progress.enabled = false
}
} else {
process.stdout.write(buffer)
}
// progress bar
const progressBarMatch = str.match(/\[.*\] (\d+)\/(\d+)/)
if (progressBarMatch) {
// since yarn is in a child process, it's unable to get the width of
// the terminal. reimplement the progress bar ourselves!
renderProgressBar(progressBarMatch[1], progressBarMatch[2])
return
}
process.stderr.write(buf)
})
} else {
// filter out unwanted yarn output
if (command === 'yarn') {
child.stderr.on('data', buf => {
const str = buf.toString()
if (/warning/.test(str)) {
return
}
// progress bar
const progressBarMatch = str.match(/\[.*\] (\d+)\/(\d+)/)
if (progressBarMatch) {
// since yarn is in a child process, it's unable to get the width of
// the terminal. reimplement the progress bar ourselves!
renderProgressBar(progressBarMatch[1], progressBarMatch[2])
return
}
process.stderr.write(buf)
})
}
}
child.on('close', code => {

View File

@@ -905,6 +905,10 @@ ansi-wrap@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
ansi_up@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-2.0.2.tgz#9b54de508c5c579f5b6968e65c1b863e0680ab92"
any-observable@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
@@ -11167,6 +11171,10 @@ xtend@^4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
xterm@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.2.0.tgz#da50f54a83d81463c0a2c9f2e0a5d14d3867df02"
y18n@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"