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 @@