diff --git a/packages/@vue/cli-plugin-eslint/ui.js b/packages/@vue/cli-plugin-eslint/ui.js
new file mode 100644
index 000000000..282eb0afa
--- /dev/null
+++ b/packages/@vue/cli-plugin-eslint/ui.js
@@ -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')
+ }
+ }
+ })
+}
diff --git a/packages/@vue/cli-ui/src/components/TaskItem.vue b/packages/@vue/cli-ui/src/components/TaskItem.vue
index 643890c9f..c171bb869 100644
--- a/packages/@vue/cli-ui/src/components/TaskItem.vue
+++ b/packages/@vue/cli-ui/src/components/TaskItem.vue
@@ -17,7 +17,7 @@
@@ -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
diff --git a/packages/@vue/cli-ui/src/graphql-api/api/PluginApi.js b/packages/@vue/cli-ui/src/graphql-api/api/PluginApi.js
new file mode 100644
index 000000000..baac6b691
--- /dev/null
+++ b/packages/@vue/cli-ui/src/graphql-api/api/PluginApi.js
@@ -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
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/logs.js b/packages/@vue/cli-ui/src/graphql-api/connectors/logs.js
index 8041d8af8..6e4cf57ed 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/logs.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/logs.js
@@ -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
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/plugins.js b/packages/@vue/cli-ui/src/graphql-api/connectors/plugins.js
index 646bdd40c..f5fc04f29 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/plugins.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/plugins.js
@@ -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
}
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/progress.js b/packages/@vue/cli-ui/src/graphql-api/connectors/progress.js
index a6a4938ca..b764f7d1a 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/progress.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/progress.js
@@ -1,3 +1,4 @@
+// Subs
const channels = require('../channels')
let map = new Map()
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js b/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js
index 5d874e885..d92c46f99 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/projects.js
@@ -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)
}
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js b/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js
index 11cbece7b..e31b8c3b6 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/prompts.js
@@ -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))
}
diff --git a/packages/@vue/cli-ui/src/graphql-api/connectors/tasks.js b/packages/@vue/cli-ui/src/graphql-api/connectors/tasks.js
index 2506a1774..fe9b0ec3a 100644
--- a/packages/@vue/cli-ui/src/graphql-api/connectors/tasks.js
+++ b/packages/@vue/cli-ui/src/graphql-api/connectors/tasks.js
@@ -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,
diff --git a/packages/@vue/cli-ui/src/graphql-api/resolvers.js b/packages/@vue/cli-ui/src/graphql-api/resolvers.js
index aef5ad5e9..d30198b91 100644
--- a/packages/@vue/cli-ui/src/graphql-api/resolvers.js
+++ b/packages/@vue/cli-ui/src/graphql-api/resolvers.js
@@ -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),
diff --git a/packages/@vue/cli-ui/src/graphql-api/type-defs.js b/packages/@vue/cli-ui/src/graphql-api/type-defs.js
index 04246cc8c..77ba87214 100644
--- a/packages/@vue/cli-ui/src/graphql-api/type-defs.js
+++ b/packages/@vue/cli-ui/src/graphql-api/type-defs.js
@@ -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 {
diff --git a/packages/@vue/cli-ui/src/graphql-api/utils/db.js b/packages/@vue/cli-ui/src/graphql-api/utils/db.js
index 2f2b6954c..226e70f22 100644
--- a/packages/@vue/cli-ui/src/graphql-api/utils/db.js
+++ b/packages/@vue/cli-ui/src/graphql-api/utils/db.js
@@ -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 = {
diff --git a/packages/@vue/cli-ui/src/graphql/task.gql b/packages/@vue/cli-ui/src/graphql/task.gql
index 4a30295b4..2a28170bf 100644
--- a/packages/@vue/cli-ui/src/graphql/task.gql
+++ b/packages/@vue/cli-ui/src/graphql/task.gql
@@ -1,7 +1,12 @@
#import "./taskFragment.gql"
+#import "./promptFragment.gql"
query task ($id: ID!) {
task (id: $id) {
...task
+ prompts {
+ ...prompt
+ }
+ link
}
}
diff --git a/packages/@vue/cli-ui/src/locales/en.json b/packages/@vue/cli-ui/src/locales/en.json
index 5c1fde522..5a608fbf9 100644
--- a/packages/@vue/cli-ui/src/locales/en.json
+++ b/packages/@vue/cli-ui/src/locales/en.json
@@ -248,6 +248,8 @@
"stop": "Stop task"
},
"command": "Script command",
+ "parameters": "Parameters",
+ "more-info": "More Info",
"output": "Output"
}
}
diff --git a/packages/@vue/cli-ui/src/locales/fr.json b/packages/@vue/cli-ui/src/locales/fr.json
index 799f5a118..0fee00277 100644
--- a/packages/@vue/cli-ui/src/locales/fr.json
+++ b/packages/@vue/cli-ui/src/locales/fr.json
@@ -248,6 +248,8 @@
"stop": "Arrêter la tâche"
},
"command": "Commande de script",
+ "parameters": "Paramètres",
+ "more-info": "Plus d'infos",
"output": "Sortie"
}
}
diff --git a/packages/@vue/cli-ui/src/mixins/Prompts.js b/packages/@vue/cli-ui/src/mixins/Prompts.js
index 7bba3e249..5909ee124 100644
--- a/packages/@vue/cli-ui/src/mixins/Prompts.js
+++ b/packages/@vue/cli-ui/src/mixins/Prompts.js
@@ -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 })
}
})
}
diff --git a/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue b/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue
index 1e3794a0a..f167e948d 100644
--- a/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue
+++ b/packages/@vue/cli-ui/src/views/ProjectTaskDetails.vue
@@ -1,52 +1,97 @@
-
-
-
-
-
- {{ task.command }}
+
+
-
-
+
+
+
-
-
-
+
+
+
+ {{ task.command }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Close
+
+