feat(ui): PluginApi -> describeTask initial impl.

This commit is contained in:
Guillaume Chau
2018-03-25 19:53:58 +02:00
parent fbc9a090af
commit e07abbbfa6
19 changed files with 379 additions and 97 deletions

View File

@@ -0,0 +1,74 @@
module.exports = api => {
// Config file
api.describeConfig({
name: 'ESLint configuration',
description: 'Error checking & Code quality',
link: 'https://eslint.org',
files: {
json: ['eslintrc', 'eslintrc.json'],
js: ['eslintrc.js'],
package: 'eslintConfig'
},
onRead: ({ data }) => {
return {
prompts: [
{
name: 'rules.commaDangle',
type: 'list',
message: 'Trailing commas',
description: 'Enforce or disallow trailing commas at the end of the lines',
link: 'https://eslint.org/docs/rules/comma-dangle',
choices: [
{
name: 'Off',
value: 'off'
},
{
name: 'Never',
value: JSON.stringify(['error', 'never'])
},
{
name: 'Always',
value: JSON.stringify(['error', 'always'])
},
{
name: 'Always on multiline',
value: JSON.stringify(['error', 'always-multiline'])
},
{
name: 'Only on multiline',
value: JSON.stringify(['error', 'only-multiline'])
}
],
value: JSON.stringify(data.rules['comma-dangle'] || ['error', 'never'])
}
]
}
},
onWrite: ({ file, answers }) => {
file.assignData({
'rules.comma-dangle': answers.rules.commaDangle
})
}
})
// Tasks
api.describeTask({
match: /vue-cli-service lint/,
description: 'Lints and fixes files',
link: 'https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint#injected-commands',
prompts: [
{
name: 'noFix',
type: 'confirm',
default: false,
description: 'Do not fix errors'
}
],
onRun: ({ answers, args }) => {
if (answers.noFix) {
args.push('--no-fix')
}
}
})
}

View File

@@ -17,7 +17,7 @@
<ListItemInfo
:name="task.name"
:description="task.description || status"
:description="(task.status === 'idle' && task.description) || status"
:selected="selected"
/>
</div>
@@ -72,6 +72,11 @@ export default {
flex 100% 1 1
width 0
>>> .description
white-space nowrap
overflow hidden
text-overflow ellipsis
&.selected
&.status-error .list-item-info >>> .name
color $vue-ui-color-danger

View File

@@ -0,0 +1,22 @@
class PluginApi {
constructor () {
this.configurations = []
this.tasks = []
}
describeConfig (options) {
this.configurations.push(options)
}
describeTask (options) {
this.tasks.push(options)
}
getTask (command) {
return this.tasks.find(
options => options.match.test(command)
)
}
}
module.exports = PluginApi

View File

@@ -1,7 +1,7 @@
const shortId = require('shortid')
const { events } = require('@vue/cli-shared-utils/lib/logger')
const { generateTitle } = require('@vue/cli/lib/util/clearConsole')
// Subs
const channels = require('../channels')
let init = false

View File

@@ -8,6 +8,7 @@ const {
getPluginLink
} = require('@vue/cli-shared-utils')
const getPackageVersion = require('@vue/cli/lib/util/getPackageVersion')
const { resolveModule, loadModule } = require('@vue/cli/lib/util/module')
const {
progress: installProgress,
installPackage,
@@ -15,34 +16,36 @@ const {
updatePackage
} = require('@vue/cli/lib/util/installDeps')
const invoke = require('@vue/cli/lib/invoke')
// Connectors
const cwd = require('./cwd')
const folders = require('./folders')
const prompts = require('./prompts')
const progress = require('./progress')
const logs = require('./logs')
// Api
const PluginApi = require('../api/PluginApi')
// Utils
const { getCommand } = require('../utils/command')
const PROGRESS_ID = 'plugin-installation'
// Caches
const metadataCache = new LRU({
max: 200,
maxAge: 1000 * 60 * 30
})
const logoCache = new LRU({
max: 50
})
const PROGRESS_ID = 'plugin-installation'
// Local
let currentPluginId
let eventsInstalled = false
let plugins = []
let pluginApi
function getPath (id) {
return path.dirname(require.resolve(id, {
paths: [cwd.get()]
}))
return path.dirname(resolveModule(id, cwd.get()))
}
function findPlugins (deps) {
@@ -64,9 +67,23 @@ function list (file, context) {
plugins = []
plugins = plugins.concat(findPlugins(pkg.dependencies || {}))
plugins = plugins.concat(findPlugins(pkg.devDependencies || {}))
resetPluginApi(context)
return plugins
}
function resetPluginApi (context) {
pluginApi = new PluginApi()
plugins.forEach(plugin => runPluginApi(plugin.id, context))
}
function runPluginApi (id, context) {
const module = loadModule(`${id}/ui`, cwd.get(), true)
if (module) {
module(pluginApi)
console.log(`PluginApi called for ${id}`)
}
}
function findOne (id, context) {
return plugins.find(
p => p.id === id
@@ -175,13 +192,9 @@ function install (id, context) {
status: 'plugin-install',
args: [id]
})
currentPluginId = id
await installPackage(cwd.get(), getCommand(), null, id)
await initPrompts(id, context)
return getInstallation(context)
})
}
@@ -192,13 +205,9 @@ function uninstall (id, context) {
status: 'plugin-uninstall',
args: [id]
})
currentPluginId = id
await uninstallPackage(cwd.get(), getCommand(), null, id)
currentPluginId = null
return getInstallation(context)
})
}
@@ -209,13 +218,14 @@ function runInvoke (id, context) {
status: 'plugin-invoke',
args: [id]
})
currentPluginId = id
await invoke(id, prompts.getAnswers(), cwd.get())
// Allow plugins that don't have a generator
if (resolveModule(`${id}/generator`, cwd.get())) {
await invoke(id, prompts.getAnswers(), cwd.get())
}
// Run plugin api
runPluginApi(id, context)
currentPluginId = null
return getInstallation(context)
})
}
@@ -240,25 +250,23 @@ function update (id, context) {
status: 'plugin-update',
args: [id]
})
currentPluginId = id
const plugin = findOne(id, context)
const { current, wanted } = await getVersion(plugin, context)
await updatePackage(cwd.get(), getCommand(), null, id)
logs.add({
message: `Plugin ${id} updated from ${current} to ${wanted}`,
type: 'info'
}, context)
currentPluginId = null
return findOne(id)
})
}
function getApi () {
return pluginApi
}
module.exports = {
list,
findOne,
@@ -269,5 +277,7 @@ module.exports = {
install,
uninstall,
update,
runInvoke
runInvoke,
resetPluginApi,
getApi
}

View File

@@ -1,3 +1,4 @@
// Subs
const channels = require('../channels')
let map = new Map()

View File

@@ -7,11 +7,12 @@ 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')
// Connectors
const progress = require('./progress')
const cwd = require('./cwd')
const prompts = require('./prompts')
const folders = require('./folders')
const plugins = require('./plugins')
const PROGRESS_ID = 'project-create'
@@ -33,11 +34,9 @@ function getCurrent (context) {
function generatePresetDescription (preset) {
let description = `Features: ${preset.features.join(', ')}`
if (preset.raw.useConfigFiles) {
description += ` (Use config files)`
}
return description
}
@@ -52,18 +51,20 @@ function generateProjectCreation (creator) {
function initCreator (context) {
const creator = new Creator('', cwd.get(), getPromptModules())
/* Event listeners */
// Creator emits creation events (the project creation steps)
onCreationEvent = ({ event }) => {
progress.set({ id: PROGRESS_ID, status: event, info: null }, context)
}
creator.on('creation', onCreationEvent)
// Progress bar
onInstallProgress = value => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, progress: value }, context)
}
}
installProgress.on('progress', onInstallProgress)
// Package manager steps
onInstallLog = message => {
if (progress.get(PROGRESS_ID)) {
progress.set({ id: PROGRESS_ID, info: message }, context)
@@ -218,6 +219,7 @@ async function create (input, context) {
const name = inCurrent ? path.relative('../', process.cwd()) : input.folder
creator.name = name
// Delete existing folder
if (fs.existsSync(targetDir)) {
if (input.force) {
setProgress({
@@ -232,6 +234,7 @@ async function create (input, context) {
}
}
// Answers
const answers = prompts.getAnswers()
prompts.reset()
let index
@@ -269,8 +272,8 @@ async function create (input, context) {
info: null
})
// Create
await creator.create({}, preset)
removeCreator()
return importProject({
@@ -285,12 +288,9 @@ async function importProject (input, context) {
path: input.path,
favorite: 0
}
const packageData = folders.readPackage(project.path, context)
project.name = packageData.name
context.db.get('projects').push(project).write()
return open(project.id, context)
}
@@ -305,6 +305,8 @@ async function open (id, context) {
currentProject = project
cwd.set(project.path, context)
// Load plugins
plugins.list(project.path, context)
return project
}
@@ -313,9 +315,7 @@ async function remove (id, context) {
if (currentProject && currentProject.id === id) {
currentProject = null
}
context.db.get('projects').remove({ id }).write()
return true
}
@@ -331,7 +331,6 @@ function findOne (id, context) {
function setFavorite ({ id, favorite }, context) {
context.db.get('projects').find({ id }).assign({ favorite }).write()
return findOne(id, context)
}

View File

@@ -20,6 +20,9 @@ function generatePromptError (value) {
}
function getDefaultValue (prompt) {
if (typeof prompt.raw.value !== 'undefined') {
return prompt.raw.value
}
const defaultValue = prompt.raw.default
if (typeof defaultValue === 'function') {
return defaultValue(answers)
@@ -113,6 +116,20 @@ function setAnswer (id, value) {
obj[fields[l - 1]] = value
}
function getAnswer (id) {
const fields = id.split('.')
let obj = answers
const l = fields.length
for (let i = 0; i < l - 1; i++) {
const key = fields[i]
if (!obj[key]) {
return undefined
}
obj = obj[key]
}
return obj[fields[l - 1]]
}
function removeAnswer (id) {
const fields = id.split('.')
let obj = answers
@@ -164,7 +181,13 @@ function updatePrompts () {
removeAnswer(prompt.id)
prompt.valueChanged = false
} else if (prompt.visible && !prompt.valueChanged) {
let value = getDefaultValue(prompt)
let value
const answer = getAnswer(prompt.id)
if (typeof answer !== 'undefined') {
value = answer
} else {
value = getDefaultValue(prompt)
}
prompt.value = getDisplayedValue(prompt, value)
setAnswer(prompt.id, getValue(prompt, value))
}

View File

@@ -5,12 +5,15 @@ const channels = require('../channels')
const cwd = require('./cwd')
const folders = require('./folders')
const logs = require('./logs')
const plugins = require('./plugins')
const prompts = require('./prompts')
const { getCommand } = require('../utils/command')
const MAX_LOGS = 2000
const tasks = new Map()
let promptMemoId
function getTasks () {
const file = cwd.get()
@@ -34,11 +37,15 @@ function list (context) {
name => {
const id = `${file}:${name}`
existing.set(id, true)
const command = pkg.scripts[name]
const moreData = plugins.getApi().getTask(command)
return {
id,
name,
command: pkg.scripts[name],
index: list.findIndex(t => t.id === id)
command,
index: list.findIndex(t => t.id === id),
prompts: [],
...moreData
}
}
)
@@ -84,6 +91,39 @@ function findOne (id, context) {
)
}
function getSavedData (id, context) {
return context.db.get('tasks').find({
id
}).value()
}
function updateSavedData (data, context) {
if (getSavedData(data.id, context)) {
context.db.get('tasks').find({ id: data.id }).assign(data).write()
} else {
context.db.get('tasks').push(data).write()
}
}
function getPrompts (id, context) {
const task = findOne(id, context)
if (task) {
if (promptMemoId !== id) {
promptMemoId = id
prompts.reset()
task.prompts.forEach(prompts.add)
const data = getSavedData(id, context)
if (data) {
prompts.setAnswers(data.answers)
}
}
return prompts.list()
}
}
function updateOne (data, context) {
const task = findOne(data.id)
if (task) {
@@ -99,6 +139,21 @@ function run (id, context) {
const task = findOne(id, context)
if (task && task.status !== 'running') {
const args = ['run', task.name]
const answers = prompts.getAnswers()
// Save parameters
updateSavedData({
id,
answers
}, context)
// Plugin api
if (task.onRun) {
task.onRun({
answers,
args
})
}
const child = execa(getCommand(), args, {
cwd: cwd.get(),
@@ -200,6 +255,7 @@ function clearLogs (id, context) {
module.exports = {
list,
findOne,
getPrompts,
run,
stop,
updateOne,

View File

@@ -34,6 +34,10 @@ module.exports = {
logo: (plugin, args, context) => plugins.getLogo(plugin, context)
},
Task: {
prompts: (task, args, context) => tasks.getPrompts(task.id, context)
},
Query: {
cwd: () => cwd.get(),
consoleLogs: (root, args, context) => logs.list(context),

View File

@@ -164,11 +164,12 @@ type Progress {
type Task implements DescribedEntity {
id: ID!
status: TaskStatus!
name: String
command: String!
name: String
description: String
link: String
logs: [TaskLog]
prompts: [Prompt]
}
enum TaskStatus {

View File

@@ -10,7 +10,8 @@ const db = new Lowdb(new FileSync(resolve(__dirname, '../../../live/db.json')))
// Seed an empty DB
db.defaults({
projects: [],
foldersFavorite: []
foldersFavorite: [],
tasks: []
}).write()
module.exports = {

View File

@@ -1,7 +1,12 @@
#import "./taskFragment.gql"
#import "./promptFragment.gql"
query task ($id: ID!) {
task (id: $id) {
...task
prompts {
...prompt
}
link
}
}

View File

@@ -248,6 +248,8 @@
"stop": "Stop task"
},
"command": "Script command",
"parameters": "Parameters",
"more-info": "More Info",
"output": "Output"
}
}

View File

@@ -248,6 +248,8 @@
"stop": "Arrêter la tâche"
},
"command": "Commande de script",
"parameters": "Paramètres",
"more-info": "Plus d'infos",
"output": "Sortie"
}
}

View File

@@ -37,9 +37,13 @@ export default function ({
}
},
update: (store, { data: { promptAnswer } }) => {
const data = store.readQuery({ query })
let variables = this.$apollo.queries[field].options.variables || undefined
if (typeof variables === 'function') {
variables = variables.call(this)
}
const data = store.readQuery({ query, variables })
data[field].prompts = promptAnswer
store.writeQuery({ query, data })
store.writeQuery({ query, variables, data })
}
})
}

View File

@@ -1,52 +1,97 @@
<template>
<div class="project-task-details">
<div v-if="task" class="actions-bar">
<VueButton
v-if="task.status !== 'running'"
icon-left="play_arrow"
class="primary"
:label="$t('views.project-task-details.actions.play')"
@click="runTask()"
/>
<VueButton
v-else
icon-left="stop"
class="primary"
:label="$t('views.project-task-details.actions.stop')"
@click="stopTask()"
/>
<div
class="command"
v-tooltip="$t('views.project-task-details.command')"
>
{{ task.command }}
<template v-if="task">
<div class="header">
<div class="name">{{ task.name }}</div>
<div class="description">{{ task.description }}</div>
</div>
<div class="vue-ui-spacer"/>
</div>
<div class="actions-bar">
<VueButton
v-if="task.status !== 'running'"
icon-left="play_arrow"
class="primary"
:label="$t('views.project-task-details.actions.play')"
@click="runTask()"
/>
<VueButton
v-else
icon-left="stop"
class="primary"
:label="$t('views.project-task-details.actions.stop')"
@click="stopTask()"
/>
<div class="content">
<TerminalView
ref="terminal"
:key="id"
:cols="100"
:rows="24"
auto-size
:options="{
scrollback: 1000,
disableStdin: true,
useFlowControl: true
}"
:title="$t('views.project-task-details.output')"
toolbar
@clear="clearLogs()"
/>
</div>
<VueButton
v-if="task.prompts.length"
icon-left="settings"
class="icon-button primary"
:disabled="task.status === 'running'"
v-tooltip="$t('views.project-task-details.parameters')"
@click="showParameters = true"
/>
<div
class="command"
v-tooltip="$t('views.project-task-details.command')"
>
{{ task.command }}
</div>
<VueButton
v-if="task.link"
:href="task.link"
target="_blank"
icon-left="open_in_new"
class="icon-button"
v-tooltip="$t('views.project-task-details.more-info')"
/>
<div class="vue-ui-spacer"/>
</div>
<div class="content">
<TerminalView
ref="terminal"
:key="id"
:cols="100"
:rows="24"
auto-size
:options="{
scrollback: 1000,
disableStdin: true,
useFlowControl: true
}"
:title="$t('views.project-task-details.output')"
toolbar
@clear="clearLogs()"
/>
</div>
</template>
<VueModal
v-if="showParameters"
:title="$t('views.project-task-details.parameters')"
class="medium"
@close="showParameters = false"
>
<div class="default-body">
<PromptsList
:prompts="visiblePrompts"
@answer="answerPrompt"
/>
</div>
<div slot="footer" class="actions">
<VueButton class="primary" @click="showParameters = false">Close</VueButton>
</div>
</VueModal>
</div>
</template>
<script>
import Prompts from '../mixins/Prompts'
import TASK from '../graphql/task.gql'
import TASK_LOGS from '../graphql/taskLogs.gql'
import TASK_RUN from '../graphql/taskRun.gql'
@@ -57,6 +102,13 @@ import TASK_LOG_ADDED from '../graphql/taskLogAdded.gql'
export default {
name: 'ProjectTaskDetails',
mixins: [
Prompts({
field: 'task',
query: TASK
})
],
props: {
id: {
type: String,
@@ -67,6 +119,7 @@ export default {
data () {
return {
task: null,
showParameters: false
}
},
@@ -116,6 +169,12 @@ export default {
}
},
watch: {
id () {
this.showParameters = false
}
},
methods: {
runTask () {
this.$apollo.mutate({
@@ -158,8 +217,8 @@ export default {
.command
font-family 'Roboto Mono', monospace
font-size 12px
background lighten($vue-ui-color-dark, 40%)
color $vue-ui-color-light
background $vue-ui-color-light-neutral
color $vue-ui-color-dark
padding 0 16px
height 32px
h-box()
@@ -174,4 +233,16 @@ export default {
.terminal-view
height 100%
border-radius $br
.header
padding $padding-item $padding-item 0
h-box()
align-items center
.name
font-size 18px
.description
color $color-text-light
margin-left $padding-item
</style>

View File

@@ -1,16 +1,19 @@
const resolve = require('resolve')
exports.resolveModule = function resolveModule (request, context) {
exports.resolveModule = function (request, context) {
let resolvedPath
try {
resolvedPath = resolve.sync(request, { basedir: context })
resolvedPath = require.resolve(request, {
paths: [context]
})
} catch (e) {}
return resolvedPath
}
exports.loadModule = function loadModule (request, context) {
exports.loadModule = function (request, context, force = false) {
const resolvedPath = exports.resolveModule(request, context)
if (resolvedPath) {
if (force) {
delete require.cache[resolvedPath]
}
return require(resolvedPath)
}
}

View File

@@ -45,7 +45,6 @@
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"recast": "^0.13.0",
"resolve": "^1.5.0",
"rimraf": "^2.6.2",
"semver": "^5.4.1",
"slash": "^1.0.0",