tasks.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. const path = require('path')
  2. const fs = require('fs-extra')
  3. const { processStats } = require('./utils/stats')
  4. /** @typedef {import('../apollo-server/api/PluginApi')} PluginApi */
  5. /**
  6. * @param {PluginApi} api
  7. */
  8. module.exports = api => {
  9. const { getSharedData, setSharedData, removeSharedData } = api.namespace('org.vue.webpack.')
  10. let firstRun = true
  11. let hadFailed = false
  12. // Specific to each modes (serve, build, ...)
  13. const fields = {
  14. status: null,
  15. progress: {},
  16. operations: null,
  17. sizes: null,
  18. problems: null,
  19. url: null
  20. }
  21. // Common fields for all mode
  22. const commonFields = {
  23. 'modern-mode': false
  24. }
  25. // Init data
  26. api.onProjectOpen(setup)
  27. api.onPluginReload(setup)
  28. function setup () {
  29. for (const key of ['serve', 'build', 'build-modern']) {
  30. setupSharedData(key)
  31. }
  32. setupCommonData()
  33. }
  34. // Called when opening a project
  35. function setupSharedData (mode) {
  36. resetSharedData(mode)
  37. }
  38. // Called when opening a project
  39. function setupCommonData () {
  40. for (const field in commonFields) {
  41. setSharedData(field, getSharedDataInitialValue(field, commonFields[field]))
  42. }
  43. }
  44. function resetSharedData (mode, clear = false) {
  45. for (const field in fields) {
  46. const id = `${mode}-${field}`
  47. setSharedData(id, getSharedDataInitialValue(id, fields[field], clear))
  48. }
  49. }
  50. function getSharedDataInitialValue (id, defaultValue, clear) {
  51. if (!clear) {
  52. const data = getSharedData(id)
  53. if (data != null) return data.value
  54. }
  55. return defaultValue
  56. }
  57. async function onWebpackMessage ({ data: message }) {
  58. if (message.webpackDashboardData) {
  59. const modernMode = getSharedData('modern-mode').value
  60. const type = message.webpackDashboardData.type
  61. for (const data of message.webpackDashboardData.value) {
  62. const id = `${type}-${data.type}`
  63. if (data.type === 'stats') {
  64. // Stats are read from a file
  65. const statsFile = path.resolve(api.getCwd(), `./node_modules/.stats-${type}.json`)
  66. const value = await fs.readJson(statsFile)
  67. const { stats, analyzer } = processStats(value)
  68. setSharedData(id, stats, { disk: true })
  69. setSharedData(`${id}-analyzer`, analyzer, { disk: true })
  70. await fs.remove(statsFile)
  71. } else if (data.type === 'progress') {
  72. if (type === 'serve' || !modernMode) {
  73. setSharedData(id, {
  74. [type]: data.value
  75. })
  76. } else {
  77. // Display two progress bars
  78. const progress = getSharedData(id).value
  79. progress[type] = data.value
  80. for (const t of ['build', 'build-modern']) {
  81. setSharedData(`${t}-${data.type}`, {
  82. build: progress.build || 0,
  83. 'build-modern': progress['build-modern'] || 0
  84. })
  85. }
  86. }
  87. } else {
  88. // Don't display success until both build and build-modern are done
  89. if (type !== 'serve' && modernMode && data.type === 'status' && data.value === 'Success') {
  90. if (type === 'build-modern') {
  91. for (const t of ['build', 'build-modern']) {
  92. setSharedData(`${t}-status`, data.value)
  93. }
  94. }
  95. } else {
  96. setSharedData(id, data.value)
  97. }
  98. // Notifications
  99. if (type === 'serve' && data.type === 'status') {
  100. if (data.value === 'Failed') {
  101. api.notify({
  102. title: 'Build failed',
  103. message: 'The build has errors.',
  104. icon: 'error'
  105. })
  106. hadFailed = true
  107. } else if (data.value === 'Success') {
  108. if (hadFailed) {
  109. api.notify({
  110. title: 'Build fixed',
  111. message: 'The build succeeded.',
  112. icon: 'done'
  113. })
  114. hadFailed = false
  115. } else if (firstRun) {
  116. api.notify({
  117. title: 'App ready',
  118. message: 'The build succeeded.',
  119. icon: 'done'
  120. })
  121. firstRun = false
  122. }
  123. }
  124. }
  125. }
  126. }
  127. }
  128. }
  129. // Tasks
  130. const views = {
  131. views: [
  132. {
  133. id: 'org.vue.webpack.views.dashboard',
  134. label: 'org.vue.vue-webpack.dashboard.title',
  135. icon: 'dashboard',
  136. component: 'org.vue.webpack.components.dashboard'
  137. },
  138. {
  139. id: 'org.vue.webpack.views.analyzer',
  140. label: 'org.vue.vue-webpack.analyzer.title',
  141. icon: 'donut_large',
  142. component: 'org.vue.webpack.components.analyzer'
  143. }
  144. ],
  145. defaultView: 'org.vue.webpack.views.dashboard'
  146. }
  147. api.describeTask({
  148. match: /vue-cli-service serve(\s+--\S+(\s+\S+)?)*$/,
  149. description: 'org.vue.vue-webpack.tasks.serve.description',
  150. link: 'https://cli.vuejs.org/guide/cli-service.html#vue-cli-service-serve',
  151. icon: '/public/webpack-logo.png',
  152. prompts: [
  153. {
  154. name: 'open',
  155. type: 'confirm',
  156. default: false,
  157. description: 'org.vue.vue-webpack.tasks.serve.open'
  158. },
  159. {
  160. name: 'mode',
  161. type: 'list',
  162. default: 'development',
  163. choices: [
  164. {
  165. name: 'development',
  166. value: 'development'
  167. },
  168. {
  169. name: 'production',
  170. value: 'production'
  171. },
  172. {
  173. name: 'test',
  174. value: 'test'
  175. },
  176. {
  177. name: '(unset)',
  178. value: ''
  179. }
  180. ],
  181. description: 'org.vue.vue-webpack.tasks.serve.mode'
  182. },
  183. {
  184. name: 'host',
  185. type: 'input',
  186. default: '',
  187. description: 'org.vue.vue-webpack.tasks.serve.host'
  188. },
  189. {
  190. name: 'port',
  191. type: 'input',
  192. default: undefined,
  193. description: 'org.vue.vue-webpack.tasks.serve.port'
  194. },
  195. {
  196. name: 'https',
  197. type: 'confirm',
  198. default: false,
  199. description: 'org.vue.vue-webpack.tasks.serve.https'
  200. }
  201. ],
  202. onBeforeRun: ({ answers, args }) => {
  203. // Args
  204. if (answers.open) args.push('--open')
  205. if (answers.mode) args.push('--mode', answers.mode)
  206. if (answers.host) args.push('--host', answers.host)
  207. if (answers.port) args.push('--port', answers.port)
  208. if (answers.https) args.push('--https')
  209. args.push('--dashboard')
  210. // Data
  211. resetSharedData('serve', true)
  212. firstRun = true
  213. hadFailed = false
  214. },
  215. onRun: () => {
  216. api.ipcOn(onWebpackMessage)
  217. },
  218. onExit: () => {
  219. api.ipcOff(onWebpackMessage)
  220. removeSharedData('serve-url')
  221. },
  222. ...views
  223. })
  224. api.describeTask({
  225. match: /vue-cli-service build(\s+--\S+(\s+\S+)?)*$/,
  226. description: 'org.vue.vue-webpack.tasks.build.description',
  227. link: 'https://cli.vuejs.org/guide/cli-service.html#vue-cli-service-build',
  228. icon: '/public/webpack-logo.png',
  229. prompts: [
  230. {
  231. name: 'modern',
  232. type: 'confirm',
  233. default: false,
  234. message: 'org.vue.vue-webpack.tasks.build.modern.label',
  235. description: 'org.vue.vue-webpack.tasks.build.modern.description',
  236. link: 'https://cli.vuejs.org/guide/browser-compatibility.html#modern-mode'
  237. },
  238. {
  239. name: 'mode',
  240. type: 'list',
  241. default: 'production',
  242. choices: [
  243. {
  244. name: 'development',
  245. value: 'development'
  246. },
  247. {
  248. name: 'production',
  249. value: 'production'
  250. },
  251. {
  252. name: 'test',
  253. value: 'test'
  254. },
  255. {
  256. name: '(unset)',
  257. value: ''
  258. }
  259. ],
  260. description: 'org.vue.vue-webpack.tasks.build.mode'
  261. },
  262. {
  263. name: 'dest',
  264. type: 'input',
  265. default: '',
  266. description: 'org.vue.vue-webpack.tasks.build.dest'
  267. },
  268. {
  269. name: 'target',
  270. type: 'list',
  271. default: 'app',
  272. choices: [
  273. {
  274. name: 'org.vue.vue-webpack.tasks.build.target.app',
  275. value: 'app'
  276. },
  277. {
  278. name: 'org.vue.vue-webpack.tasks.build.target.lib',
  279. value: 'lib'
  280. },
  281. {
  282. name: 'org.vue.vue-webpack.tasks.build.target.wc',
  283. value: 'wc'
  284. },
  285. {
  286. name: 'org.vue.vue-webpack.tasks.build.target.wc-async',
  287. value: 'wc-async'
  288. }
  289. ],
  290. description: 'org.vue.vue-webpack.tasks.build.target.description'
  291. },
  292. {
  293. name: 'name',
  294. type: 'input',
  295. default: '',
  296. description: 'org.vue.vue-webpack.tasks.build.name'
  297. },
  298. {
  299. name: 'watch',
  300. type: 'confirm',
  301. default: false,
  302. description: 'org.vue.vue-webpack.tasks.build.watch'
  303. }
  304. ],
  305. onBeforeRun: ({ answers, args }) => {
  306. // Args
  307. if (answers.mode) args.push('--mode', answers.mode)
  308. if (answers.dest) args.push('--dest', answers.dest)
  309. if (answers.target) args.push('--target', answers.target)
  310. if (answers.name) args.push('--name', answers.name)
  311. if (answers.watch) args.push('--watch')
  312. if (answers.modern) args.push('--modern')
  313. setSharedData('modern-mode', !!answers.modern)
  314. args.push('--dashboard')
  315. // Data
  316. resetSharedData('build', true)
  317. resetSharedData('build-modern', true)
  318. },
  319. onRun: () => {
  320. api.ipcOn(onWebpackMessage)
  321. },
  322. onExit: () => {
  323. api.ipcOff(onWebpackMessage)
  324. },
  325. ...views
  326. })
  327. // vue inspect
  328. api.addTask({
  329. name: 'inspect',
  330. command: 'vue-cli-service inspect',
  331. description: 'org.vue.vue-webpack.tasks.inspect.description',
  332. link: 'https://cli.vuejs.org/guide/webpack.html#inspecting-the-project-s-webpack-config',
  333. icon: '/public/webpack-inspect-logo.png',
  334. prompts: [
  335. {
  336. name: 'mode',
  337. type: 'list',
  338. default: 'production',
  339. choices: [
  340. {
  341. name: 'development',
  342. value: 'development'
  343. },
  344. {
  345. name: 'production',
  346. value: 'production'
  347. },
  348. {
  349. name: 'test',
  350. value: 'test'
  351. },
  352. {
  353. name: '(unset)',
  354. value: ''
  355. }
  356. ],
  357. description: 'org.vue.vue-webpack.tasks.inspect.mode'
  358. },
  359. {
  360. name: 'verbose',
  361. type: 'confirm',
  362. default: false,
  363. description: 'org.vue.vue-webpack.tasks.inspect.verbose'
  364. }
  365. ],
  366. onBeforeRun: ({ answers, args }) => {
  367. if (answers.mode) args.push('--mode', answers.mode)
  368. if (answers.verbose) args.push('--verbose')
  369. }
  370. })
  371. if (process.env.VUE_APP_CLI_UI_DEV) {
  372. // Add dynamic components in dev mode (webpack dashboard & analyzer)
  373. api.addClientAddon({
  374. id: 'org.vue.webpack.client-addon.dev',
  375. url: 'http://localhost:8096/index.js'
  376. })
  377. } else {
  378. // Webpack dashboard
  379. api.addClientAddon({
  380. id: 'org.vue.webpack.client-addon',
  381. path: '@vue/cli-ui-addon-webpack/dist'
  382. })
  383. }
  384. // Open app button
  385. api.ipcOn(({ data }) => {
  386. if (data.vueServe) {
  387. setSharedData('serve-url', data.vueServe.url)
  388. }
  389. })
  390. }