executeCommand.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. const { chalk, execa } = require('@vue/cli-shared-utils')
  2. const EventEmitter = require('events')
  3. const readline = require('readline')
  4. const debug = require('debug')('vue-cli:install')
  5. class InstallProgress extends EventEmitter {
  6. constructor () {
  7. super()
  8. this._progress = -1
  9. }
  10. get progress () {
  11. return this._progress
  12. }
  13. set progress (value) {
  14. this._progress = value
  15. this.emit('progress', value)
  16. }
  17. get enabled () {
  18. return this._progress !== -1
  19. }
  20. set enabled (value) {
  21. this.progress = value ? 0 : -1
  22. }
  23. log (value) {
  24. this.emit('log', value)
  25. }
  26. }
  27. function toStartOfLine (stream) {
  28. if (!chalk.supportsColor) {
  29. stream.write('\r')
  30. return
  31. }
  32. readline.cursorTo(stream, 0)
  33. }
  34. function renderProgressBar (curr, total) {
  35. const ratio = Math.min(Math.max(curr / total, 0), 1)
  36. const bar = ` ${curr}/${total}`
  37. const availableSpace = Math.max(0, process.stderr.columns - bar.length - 3)
  38. const width = Math.min(total, availableSpace)
  39. const completeLength = Math.round(width * ratio)
  40. const complete = `#`.repeat(completeLength)
  41. const incomplete = `-`.repeat(width - completeLength)
  42. toStartOfLine(process.stderr)
  43. process.stderr.write(`[${complete}${incomplete}]${bar}`)
  44. }
  45. const progress = exports.progress = new InstallProgress()
  46. exports.executeCommand = function executeCommand (command, args, cwd) {
  47. debug(`command: `, command)
  48. debug(`args: `, args)
  49. return new Promise((resolve, reject) => {
  50. const apiMode = process.env.VUE_CLI_API_MODE
  51. progress.enabled = false
  52. if (apiMode) {
  53. if (command === 'npm') {
  54. // TODO when this is supported
  55. } else if (command === 'yarn') {
  56. args.push('--json')
  57. }
  58. }
  59. const child = execa(command, args, {
  60. cwd,
  61. stdio: ['inherit', apiMode ? 'pipe' : 'inherit', !apiMode && command === 'yarn' ? 'pipe' : 'inherit']
  62. })
  63. if (apiMode) {
  64. let progressTotal = 0
  65. let progressTime = Date.now()
  66. child.stdout.on('data', buffer => {
  67. let str = buffer.toString().trim()
  68. if (str && command === 'yarn' && str.indexOf('"type":') !== -1) {
  69. const newLineIndex = str.lastIndexOf('\n')
  70. if (newLineIndex !== -1) {
  71. str = str.substr(newLineIndex)
  72. }
  73. try {
  74. const data = JSON.parse(str)
  75. if (data.type === 'step') {
  76. progress.enabled = false
  77. progress.log(data.data.message)
  78. } else if (data.type === 'progressStart') {
  79. progressTotal = data.data.total
  80. } else if (data.type === 'progressTick') {
  81. const time = Date.now()
  82. if (time - progressTime > 20) {
  83. progressTime = time
  84. progress.progress = data.data.current / progressTotal
  85. }
  86. } else {
  87. progress.enabled = false
  88. }
  89. } catch (e) {
  90. console.error(e)
  91. console.log(str)
  92. }
  93. } else {
  94. process.stdout.write(buffer)
  95. }
  96. })
  97. } else {
  98. // filter out unwanted yarn output
  99. if (command === 'yarn') {
  100. child.stderr.on('data', buf => {
  101. const str = buf.toString()
  102. if (/warning/.test(str)) {
  103. return
  104. }
  105. // progress bar
  106. const progressBarMatch = str.match(/\[.*\] (\d+)\/(\d+)/)
  107. if (progressBarMatch) {
  108. // since yarn is in a child process, it's unable to get the width of
  109. // the terminal. reimplement the progress bar ourselves!
  110. renderProgressBar(progressBarMatch[1], progressBarMatch[2])
  111. return
  112. }
  113. process.stderr.write(buf)
  114. })
  115. }
  116. }
  117. child.on('close', code => {
  118. if (code !== 0) {
  119. reject(`command failed: ${command} ${args.join(' ')}`)
  120. return
  121. }
  122. resolve()
  123. })
  124. })
  125. }