123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614 |
- const { chalk, execa } = require('@vue/cli-shared-utils')
- // Subs
- const channels = require('../channels')
- // Connectors
- const cwd = require('./cwd')
- const folders = require('./folders')
- const logs = require('./logs')
- const plugins = require('./plugins')
- const prompts = require('./prompts')
- const views = require('./views')
- const projects = require('./projects')
- // Utils
- const { log } = require('../util/logger')
- const { notify } = require('../util/notification')
- const { terminate } = require('../util/terminate')
- const { parseArgs } = require('../util/parse-args')
- const MAX_LOGS = 2000
- const VIEW_ID = 'vue-project-tasks'
- const WIN_ENOENT_THRESHOLD = 500 // ms
- const tasks = new Map()
- function getTasks (file = null) {
- if (!file) file = cwd.get()
- let list = tasks.get(file)
- if (!list) {
- list = []
- tasks.set(file, list)
- }
- return list
- }
- async function list ({ file = null, api = true } = {}, context) {
- if (!file) file = cwd.get()
- let list = getTasks(file)
- const pkg = folders.readPackage(file, context)
- if (pkg.scripts) {
- const existing = new Map()
- if (projects.getType(file, context) === 'vue') {
- await plugins.list(file, context, { resetApi: false, lightApi: true })
- }
- const pluginApi = api && plugins.getApi(file)
- // Get current valid tasks in project `package.json`
- const scriptKeys = Object.keys(pkg.scripts)
- let currentTasks = scriptKeys.map(
- name => {
- const id = `${file}:${name}`
- existing.set(id, true)
- const command = pkg.scripts[name]
- const moreData = pluginApi ? pluginApi.getDescribedTask(command) : null
- return {
- id,
- name,
- command,
- index: list.findIndex(t => t.id === id),
- prompts: [],
- views: [],
- path: file,
- ...moreData
- }
- }
- )
- if (api && pluginApi) {
- currentTasks = currentTasks.concat(plugins.getApi(file).addedTasks.map(
- task => {
- const id = `${file}:${task.name}`
- existing.set(id, true)
- return {
- id,
- index: list.findIndex(t => t.id === id),
- prompts: [],
- views: [],
- path: file,
- uiOnly: true,
- ...task
- }
- }
- ))
- }
- // Process existing tasks
- const existingTasks = currentTasks.filter(
- task => task.index !== -1
- )
- // Update tasks data
- existingTasks.forEach(task => {
- Object.assign(list[task.index], task)
- })
- // Process removed tasks
- const removedTasks = list.filter(
- t => currentTasks.findIndex(c => c.id === t.id) === -1
- )
- // Remove badges
- removedTasks.forEach(task => {
- updateViewBadges({ task }, context)
- })
- // Process new tasks
- const newTasks = currentTasks.filter(
- task => task.index === -1
- ).map(
- task => ({
- ...task,
- status: 'idle',
- child: null,
- logs: []
- })
- )
- // Keep existing running tasks
- list = list.filter(
- task => existing.get(task.id) ||
- task.status === 'running'
- )
- // Add the new tasks
- list = list.concat(newTasks)
- // Sort
- const getSortScore = task => {
- const index = scriptKeys.indexOf(task.name)
- if (index !== -1) return index
- return Infinity
- }
- list.sort((a, b) => getSortScore(a) - getSortScore(b))
- tasks.set(file, list)
- }
- return list
- }
- function findOne (id, context) {
- for (const [, list] of tasks) {
- const result = list.find(t => t.id === id)
- if (result) return result
- }
- }
- function getSavedData (id, context) {
- let data = context.db.get('tasks').find({
- id
- }).value()
- // Clone
- if (data != null) data = JSON.parse(JSON.stringify(data))
- return data
- }
- 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) {
- return restoreParameters({ id }, context)
- }
- function updateOne (data, context) {
- const task = findOne(data.id)
- if (task) {
- if (task.status !== data.status) {
- updateViewBadges({
- task,
- data
- }, context)
- }
- Object.assign(task, data)
- context.pubsub.publish(channels.TASK_CHANGED, {
- taskChanged: task
- })
- }
- return task
- }
- function updateViewBadges ({ task, data }, context) {
- const viewId = VIEW_ID
- // New badges
- if (data) {
- if (data.status === 'error') {
- views.addBadge({
- viewId,
- badge: {
- id: 'vue-task-error',
- type: 'error',
- label: 'org.vue.components.view-badge.labels.tasks.error',
- priority: 3
- }
- }, context)
- } else if (data.status === 'running') {
- views.addBadge({
- viewId,
- badge: {
- id: 'vue-task-running',
- type: 'info',
- label: 'org.vue.components.view-badge.labels.tasks.running',
- priority: 2
- }
- }, context)
- } else if (data.status === 'done') {
- views.addBadge({
- viewId,
- badge: {
- id: 'vue-task-done',
- type: 'success',
- label: 'org.vue.components.view-badge.labels.tasks.done',
- priority: 1,
- hidden: true
- }
- }, context)
- }
- }
- // Remove previous badges
- if (task.status === 'error') {
- views.removeBadge({ viewId, badgeId: 'vue-task-error' }, context)
- } else if (task.status === 'running') {
- views.removeBadge({ viewId, badgeId: 'vue-task-running' }, context)
- } else if (task.status === 'done') {
- views.removeBadge({ viewId, badgeId: 'vue-task-done' }, context)
- }
- }
- async function run (id, context) {
- const task = findOne(id, context)
- if (task && task.status !== 'running') {
- task._terminating = false
- // Answers
- const answers = prompts.getAnswers()
- let [command, ...args] = parseArgs(task.command)
- // Output colors
- // See: https://www.npmjs.com/package/supports-color
- process.env.FORCE_COLOR = 1
- // Plugin API
- if (task.onBeforeRun) {
- if (!answers.$_overrideArgs) {
- const origPush = args.push.bind(args)
- args.push = (...items) => {
- if (items.length && args.indexOf(items[0]) !== -1) return items.length
- return origPush(...items)
- }
- }
- await task.onBeforeRun({
- answers,
- args
- })
- }
- // Deduplicate arguments
- const dedupedArgs = []
- for (let i = args.length - 1; i >= 0; i--) {
- const arg = args[i]
- if (typeof arg === 'string' && arg.indexOf('--') === 0) {
- if (dedupedArgs.indexOf(arg) === -1) {
- dedupedArgs.push(arg)
- } else {
- const value = args[i + 1]
- if (value && value.indexOf('--') !== 0) {
- dedupedArgs.pop()
- }
- }
- } else {
- dedupedArgs.push(arg)
- }
- }
- args = dedupedArgs.reverse()
- if (command === 'npm') {
- args.splice(0, 0, '--')
- }
- log('Task run', command, args)
- updateOne({
- id: task.id,
- status: 'running'
- }, context)
- logs.add({
- message: `Task ${task.id} started`,
- type: 'info'
- }, context)
- addLog({
- taskId: task.id,
- type: 'stdout',
- text: chalk.grey(`$ ${command} ${args.join(' ')}`)
- }, context)
- task.time = Date.now()
- // Task env
- process.env.VUE_CLI_CONTEXT = cwd.get()
- process.env.VUE_CLI_PROJECT_ID = projects.getCurrent(context).id
- const nodeEnv = process.env.NODE_ENV
- delete process.env.NODE_ENV
- const child = execa(command, args, {
- cwd: cwd.get(),
- stdio: ['inherit', 'pipe', 'pipe'],
- shell: true
- })
- if (typeof nodeEnv !== 'undefined') {
- process.env.NODE_ENV = nodeEnv
- }
- task.child = child
- const outPipe = logPipe(queue => {
- addLog({
- taskId: task.id,
- type: 'stdout',
- text: queue
- }, context)
- })
- child.stdout.on('data', buffer => {
- outPipe.add(buffer.toString())
- })
- const errPipe = logPipe(queue => {
- addLog({
- taskId: task.id,
- type: 'stderr',
- text: queue
- }, context)
- })
- child.stderr.on('data', buffer => {
- errPipe.add(buffer.toString())
- })
- const onExit = async (code, signal) => {
- outPipe.flush()
- errPipe.flush()
- log('Task exit', command, args, 'code:', code, 'signal:', signal)
- const duration = Date.now() - task.time
- const seconds = Math.round(duration / 10) / 100
- addLog({
- taskId: task.id,
- type: 'stdout',
- text: chalk.grey(`Total task duration: ${seconds}s`)
- }, context)
- // Plugin API
- if (task.onExit) {
- await task.onExit({
- args,
- child,
- cwd: cwd.get(),
- code,
- signal
- })
- }
- if (code === null || task._terminating) {
- updateOne({
- id: task.id,
- status: 'terminated'
- }, context)
- logs.add({
- message: `Task ${task.id} was terminated`,
- type: 'info'
- }, context)
- } else if (code !== 0) {
- updateOne({
- id: task.id,
- status: 'error'
- }, context)
- logs.add({
- message: `Task ${task.id} ended with error code ${code}`,
- type: 'error'
- }, context)
- notify({
- title: 'Task error',
- message: `Task ${task.id} ended with error code ${code}`,
- icon: 'error'
- })
- } else {
- updateOne({
- id: task.id,
- status: 'done'
- }, context)
- logs.add({
- message: `Task ${task.id} completed`,
- type: 'done'
- }, context)
- notify({
- title: 'Task completed',
- message: `Task ${task.id} completed in ${seconds}s.`,
- icon: 'done'
- })
- }
- plugins.callHook({
- id: 'taskExit',
- args: [{
- task,
- args,
- child,
- cwd: cwd.get(),
- signal,
- code
- }],
- file: cwd.get()
- }, context)
- }
- child.on('exit', onExit)
- child.on('error', error => {
- const duration = Date.now() - task.time
- // hackish workaround for https://github.com/vuejs/vue-cli/issues/2096
- if (process.platform === 'win32' && error.code === 'ENOENT' && duration > WIN_ENOENT_THRESHOLD) {
- return onExit(null)
- }
- updateOne({
- id: task.id,
- status: 'error'
- }, context)
- logs.add({
- message: `Error while running task ${task.id} with message'${error.message}'`,
- type: 'error'
- }, context)
- notify({
- title: 'Task error',
- message: `Error while running task ${task.id} with message'${error.message}'`,
- icon: 'error'
- })
- addLog({
- taskId: task.id,
- type: 'stdout',
- text: chalk.red(`Error while running task ${task.id} with message '${error.message}'`)
- }, context)
- console.error(error)
- })
- // Plugin API
- if (task.onRun) {
- await task.onRun({
- args,
- child,
- cwd: cwd.get()
- })
- }
- plugins.callHook({
- id: 'taskRun',
- args: [{
- task,
- args,
- child,
- cwd: cwd.get()
- }],
- file: cwd.get()
- }, context)
- }
- return task
- }
- async function stop (id, context) {
- const task = findOne(id, context)
- if (task && task.status === 'running' && task.child) {
- task._terminating = true
- try {
- const { success, error } = await terminate(task.child, cwd.get())
- if (success) {
- updateOne({
- id: task.id,
- status: 'terminated'
- }, context)
- } else if (error) {
- throw error
- } else {
- throw new Error('Unknown error')
- }
- } catch (e) {
- console.log(chalk.red(`Can't terminate process ${task.child.pid}`))
- console.error(e)
- }
- }
- return task
- }
- function addLog (log, context) {
- const task = findOne(log.taskId, context)
- if (task) {
- if (task.logs.length === MAX_LOGS) {
- task.logs.shift()
- }
- task.logs.push(log)
- context.pubsub.publish(channels.TASK_LOG_ADDED, {
- taskLogAdded: log
- })
- }
- }
- function clearLogs (id, context) {
- const task = findOne(id, context)
- if (task) {
- task.logs = []
- }
- return task
- }
- function open (id, context) {
- const task = findOne(id, context)
- plugins.callHook({
- id: 'taskOpen',
- args: [{
- task,
- cwd: cwd.get()
- }],
- file: cwd.get()
- }, context)
- return true
- }
- function logPipe (action) {
- const maxTime = 300
- let queue = ''
- let size = 0
- let time = Date.now()
- let timeout
- const add = (string) => {
- queue += string
- size++
- if (size === 50 || Date.now() > time + maxTime) {
- flush()
- } else {
- clearTimeout(timeout)
- timeout = setTimeout(flush, maxTime)
- }
- }
- const flush = () => {
- clearTimeout(timeout)
- if (!size) return
- action(queue)
- queue = ''
- size = 0
- time = Date.now()
- }
- return {
- add,
- flush
- }
- }
- function saveParameters ({ id }, context) {
- // Answers
- const answers = prompts.getAnswers()
- // Save parameters
- updateSavedData({
- id,
- answers
- }, context)
- return prompts.list()
- }
- async function restoreParameters ({ id }, context) {
- const task = findOne(id, context)
- if (task) {
- await prompts.reset()
- if (task.prompts.length) {
- prompts.add({
- name: '$_overrideArgs',
- type: 'confirm',
- default: false,
- message: 'org.vue.views.project-task-details.override-args.message',
- description: 'org.vue.views.project-task-details.override-args.description'
- })
- }
- task.prompts.forEach(prompts.add)
- const data = getSavedData(id, context)
- if (data) {
- await prompts.setAnswers(data.answers)
- }
- await prompts.start()
- }
- return prompts.list()
- }
- module.exports = {
- list,
- findOne,
- getPrompts,
- run,
- stop,
- updateOne,
- clearLogs,
- open,
- saveParameters,
- restoreParameters
- }
|