projects.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. const path = require('path')
  2. const fs = require('fs')
  3. const shortId = require('shortid')
  4. const Creator = require('@vue/cli/lib/Creator')
  5. const { getPromptModules } = require('@vue/cli/lib/util/createTools')
  6. const { getFeatures } = require('@vue/cli/lib/util/features')
  7. const { defaults } = require('@vue/cli/lib/options')
  8. const { toShortPluginId, execa } = require('@vue/cli-shared-utils')
  9. const { progress: installProgress } = require('@vue/cli/lib/util/executeCommand')
  10. const parseGitConfig = require('parse-git-config')
  11. // Connectors
  12. const progress = require('./progress')
  13. const cwd = require('./cwd')
  14. const prompts = require('./prompts')
  15. const folders = require('./folders')
  16. const plugins = require('./plugins')
  17. const locales = require('./locales')
  18. const logs = require('./logs')
  19. // Context
  20. const getContext = require('../context')
  21. // Utils
  22. const { log } = require('../util/logger')
  23. const { notify } = require('../util/notification')
  24. const { getHttpsGitURL } = require('../util/strings')
  25. const PROGRESS_ID = 'project-create'
  26. let lastProject = null
  27. let currentProject = null
  28. let creator = null
  29. let presets = []
  30. let features = []
  31. let onCreationEvent = null
  32. let onInstallProgress = null
  33. let onInstallLog = null
  34. function list (context) {
  35. let projects = context.db.get('projects').value()
  36. projects = autoClean(projects, context)
  37. return projects
  38. }
  39. function findOne (id, context) {
  40. return context.db.get('projects').find({ id }).value()
  41. }
  42. function findByPath (file, context) {
  43. return context.db.get('projects').find({ path: file }).value()
  44. }
  45. function autoClean (projects, context) {
  46. const result = []
  47. for (const project of projects) {
  48. if (fs.existsSync(project.path)) {
  49. result.push(project)
  50. }
  51. }
  52. if (result.length !== projects.length) {
  53. console.log(`Auto cleaned ${projects.length - result.length} projects (folder not found).`)
  54. context.db.set('projects', result).write()
  55. }
  56. return result
  57. }
  58. function getCurrent (context) {
  59. if (currentProject && !fs.existsSync(currentProject.path)) {
  60. log('Project folder not found', currentProject.id, currentProject.path)
  61. return null
  62. }
  63. return currentProject
  64. }
  65. function getLast (context) {
  66. return lastProject
  67. }
  68. function generatePresetDescription (preset) {
  69. let description = `[Vue ${preset.raw.vueVersion || 2}] `
  70. description += preset.features.join(', ')
  71. if (preset.raw.useConfigFiles) {
  72. description += ' (Use config files)'
  73. }
  74. return description
  75. }
  76. function generateProjectCreation (creator) {
  77. return {
  78. presets,
  79. features,
  80. prompts: prompts.list()
  81. }
  82. }
  83. async function initCreator (context) {
  84. const creator = new Creator('', cwd.get(), getPromptModules())
  85. /* Event listeners */
  86. // Creator emits creation events (the project creation steps)
  87. onCreationEvent = ({ event }) => {
  88. progress.set({ id: PROGRESS_ID, status: event, info: null }, context)
  89. }
  90. creator.on('creation', onCreationEvent)
  91. // Progress bar
  92. onInstallProgress = value => {
  93. if (progress.get(PROGRESS_ID)) {
  94. progress.set({ id: PROGRESS_ID, progress: value }, context)
  95. }
  96. }
  97. installProgress.on('progress', onInstallProgress)
  98. // Package manager steps
  99. onInstallLog = message => {
  100. if (progress.get(PROGRESS_ID)) {
  101. progress.set({ id: PROGRESS_ID, info: message }, context)
  102. }
  103. }
  104. installProgress.on('log', onInstallLog)
  105. // Presets
  106. const manualPreset = {
  107. id: '__manual__',
  108. name: 'org.vue.views.project-create.tabs.presets.manual.name',
  109. description: 'org.vue.views.project-create.tabs.presets.manual.description',
  110. link: null,
  111. features: []
  112. }
  113. const presetsData = creator.getPresets()
  114. presets = [
  115. ...Object.keys(presetsData).map(
  116. key => {
  117. const preset = presetsData[key]
  118. const features = getFeatures(preset).map(
  119. f => toShortPluginId(f)
  120. )
  121. let name = key
  122. if (key === 'default') {
  123. name = 'org.vue.views.project-create.tabs.presets.default-preset'
  124. } else if (key === '__default_vue_3__') {
  125. name = 'org.vue.views.project-create.tabs.presets.default-preset-vue-3'
  126. }
  127. const info = {
  128. id: key,
  129. name,
  130. features,
  131. link: null,
  132. raw: preset
  133. }
  134. info.description = generatePresetDescription(info)
  135. return info
  136. }
  137. ),
  138. manualPreset
  139. ]
  140. // Features
  141. const featuresData = creator.featurePrompt.choices
  142. features = [
  143. ...featuresData.map(
  144. data => ({
  145. id: data.value,
  146. name: data.name,
  147. description: data.description || null,
  148. link: data.link || null,
  149. plugins: data.plugins || null,
  150. enabled: !!data.checked
  151. })
  152. ),
  153. {
  154. id: 'use-config-files',
  155. name: 'org.vue.views.project-create.tabs.features.userConfigFiles.name',
  156. description: 'org.vue.views.project-create.tabs.features.userConfigFiles.description',
  157. link: null,
  158. plugins: null,
  159. enabled: false
  160. }
  161. ]
  162. manualPreset.features = features.filter(
  163. f => f.enabled
  164. ).map(
  165. f => f.id
  166. )
  167. // Prompts
  168. await prompts.reset()
  169. creator.injectedPrompts.forEach(prompts.add)
  170. await updatePromptsFeatures()
  171. await prompts.start()
  172. return creator
  173. }
  174. function removeCreator (context) {
  175. if (creator) {
  176. creator.removeListener('creation', onCreationEvent)
  177. installProgress.removeListener('progress', onInstallProgress)
  178. installProgress.removeListener('log', onInstallLog)
  179. creator = null
  180. }
  181. return true
  182. }
  183. async function getCreation (context) {
  184. if (!creator) {
  185. creator = await initCreator(context)
  186. }
  187. return generateProjectCreation(creator)
  188. }
  189. async function updatePromptsFeatures () {
  190. await prompts.changeAnswers(answers => {
  191. answers.features = features.filter(
  192. f => f.enabled
  193. ).map(
  194. f => f.id
  195. )
  196. })
  197. }
  198. async function setFeatureEnabled ({ id, enabled, updatePrompts = true }, context) {
  199. const feature = features.find(f => f.id === id)
  200. if (feature) {
  201. feature.enabled = enabled
  202. } else {
  203. console.warn(`Feature '${id}' not found`)
  204. }
  205. if (updatePrompts) await updatePromptsFeatures()
  206. return feature
  207. }
  208. async function applyPreset (id, context) {
  209. const preset = presets.find(p => p.id === id)
  210. if (preset) {
  211. for (const feature of features) {
  212. feature.enabled = !!(
  213. preset.features.includes(feature.id) ||
  214. (feature.plugins && preset.features.some(f => feature.plugins.includes(f)))
  215. )
  216. }
  217. if (preset.raw) {
  218. if (preset.raw.router) {
  219. await setFeatureEnabled({ id: 'router', enabled: true, updatePrompts: false }, context)
  220. }
  221. if (preset.raw.vuex) {
  222. await setFeatureEnabled({ id: 'vuex', enabled: true, updatePrompts: false }, context)
  223. }
  224. if (preset.raw.cssPreprocessor) {
  225. await setFeatureEnabled({ id: 'css-preprocessor', enabled: true, updatePrompts: false }, context)
  226. }
  227. if (preset.raw.useConfigFiles) {
  228. await setFeatureEnabled({ id: 'use-config-files', enabled: true, updatePrompts: false }, context)
  229. }
  230. }
  231. await updatePromptsFeatures()
  232. } else {
  233. console.warn(`Preset '${id}' not found`)
  234. }
  235. return generateProjectCreation(creator)
  236. }
  237. async function create (input, context) {
  238. return progress.wrap(PROGRESS_ID, context, async setProgress => {
  239. setProgress({
  240. status: 'creating'
  241. })
  242. const targetDir = path.join(cwd.get(), input.folder)
  243. cwd.set(targetDir, context)
  244. creator.context = targetDir
  245. const inCurrent = input.folder === '.'
  246. const name = creator.name = (inCurrent ? path.relative('../', process.cwd()) : input.folder).toLowerCase()
  247. // Answers
  248. const answers = prompts.getAnswers()
  249. await prompts.reset()
  250. // Config files
  251. let index
  252. if ((index = answers.features.indexOf('use-config-files')) !== -1) {
  253. answers.features.splice(index, 1)
  254. answers.useConfigFiles = 'files'
  255. }
  256. // Preset
  257. answers.preset = input.preset
  258. if (input.save) {
  259. answers.save = true
  260. answers.saveName = input.save
  261. }
  262. setProgress({
  263. info: 'Resolving preset...'
  264. })
  265. let preset
  266. if (input.preset === '__remote__' && input.remote) {
  267. // vue create foo --preset bar
  268. preset = await creator.resolvePreset(input.remote, input.clone)
  269. } else if (input.preset === 'default') {
  270. // vue create foo --default
  271. preset = defaults.presets.default
  272. } else {
  273. preset = await creator.promptAndResolvePreset(answers)
  274. }
  275. setProgress({
  276. info: null
  277. })
  278. // Create
  279. const args = [
  280. '--skipGetStarted'
  281. ]
  282. if (input.packageManager) args.push('--packageManager', input.packageManager)
  283. if (input.bar) args.push('--bare')
  284. if (input.force) args.push('--force')
  285. // Git
  286. if (input.enableGit && input.gitCommitMessage) {
  287. args.push('--git', input.gitCommitMessage)
  288. } else if (!input.enableGit) {
  289. args.push('--no-git')
  290. }
  291. // Preset
  292. args.push('--inlinePreset', JSON.stringify(preset))
  293. log('create', name, args)
  294. const child = execa('vue', [
  295. 'create',
  296. name,
  297. ...args
  298. ], {
  299. cwd: cwd.get(),
  300. stdio: ['inherit', 'pipe', 'inherit']
  301. })
  302. const onData = buffer => {
  303. const text = buffer.toString().trim()
  304. if (text) {
  305. setProgress({
  306. info: text
  307. })
  308. logs.add({
  309. type: 'info',
  310. message: text
  311. }, context)
  312. }
  313. }
  314. child.stdout.on('data', onData)
  315. await child
  316. removeCreator()
  317. notify({
  318. title: 'Project created',
  319. message: `Project ${cwd.get()} created`,
  320. icon: 'done'
  321. })
  322. return importProject({
  323. path: targetDir
  324. }, context)
  325. })
  326. }
  327. async function importProject (input, context) {
  328. if (!input.force && !fs.existsSync(path.join(input.path, 'node_modules'))) {
  329. throw new Error('NO_MODULES')
  330. }
  331. const project = {
  332. id: shortId.generate(),
  333. path: input.path,
  334. favorite: 0,
  335. type: folders.isVueProject(input.path) ? 'vue' : 'unknown'
  336. }
  337. const packageData = folders.readPackage(project.path, context)
  338. project.name = packageData.name
  339. context.db.get('projects').push(project).write()
  340. return open(project.id, context)
  341. }
  342. async function open (id, context) {
  343. const project = findOne(id, context)
  344. if (!project) {
  345. log('Project not found', id)
  346. return null
  347. }
  348. if (!fs.existsSync(project.path)) {
  349. log('Project folder not found', id, project.path)
  350. return null
  351. }
  352. lastProject = currentProject
  353. currentProject = project
  354. cwd.set(project.path, context)
  355. // Reset locales
  356. locales.reset(context)
  357. // Load plugins
  358. await plugins.list(project.path, context)
  359. // Date
  360. context.db.get('projects').find({ id }).assign({
  361. openDate: Date.now()
  362. }).write()
  363. // Save for next time
  364. context.db.set('config.lastOpenProject', id).write()
  365. log('Project open', id, project.path)
  366. return project
  367. }
  368. async function remove (id, context) {
  369. if (currentProject && currentProject.id === id) {
  370. currentProject = null
  371. }
  372. context.db.get('projects').remove({ id }).write()
  373. if (context.db.get('config.lastOpenProject').value() === id) {
  374. context.db.set('config.lastOpenProject', undefined).write()
  375. }
  376. return true
  377. }
  378. function resetCwd (context) {
  379. if (currentProject) {
  380. cwd.set(currentProject.path, context)
  381. }
  382. }
  383. function setFavorite ({ id, favorite }, context) {
  384. context.db.get('projects').find({ id }).assign({ favorite }).write()
  385. return findOne(id, context)
  386. }
  387. function rename ({ id, name }, context) {
  388. context.db.get('projects').find({ id }).assign({ name }).write()
  389. return findOne(id, context)
  390. }
  391. function getType (project, context) {
  392. if (typeof project === 'string') {
  393. project = findByPath(project, context)
  394. }
  395. if (!project) return 'unknown'
  396. return !project.type ? 'vue' : project.type
  397. }
  398. function getHomepage (project, context) {
  399. const gitConfigPath = path.join(project.path, '.git', 'config')
  400. if (fs.existsSync(gitConfigPath)) {
  401. const gitConfig = parseGitConfig.sync({ path: gitConfigPath })
  402. const gitRemoteUrl = gitConfig['remote "origin"']
  403. if (gitRemoteUrl) {
  404. return getHttpsGitURL(gitRemoteUrl.url)
  405. }
  406. }
  407. const pkg = folders.readPackage(project.path, context)
  408. return pkg.homepage
  409. }
  410. // Open last project
  411. async function autoOpenLastProject () {
  412. const context = getContext()
  413. const id = context.db.get('config.lastOpenProject').value()
  414. if (id) {
  415. try {
  416. await open(id, context)
  417. } catch (e) {
  418. log('Project can\'t be auto-opened', id)
  419. }
  420. }
  421. }
  422. autoOpenLastProject()
  423. module.exports = {
  424. list,
  425. findOne,
  426. findByPath,
  427. getCurrent,
  428. getLast,
  429. getCreation,
  430. applyPreset,
  431. setFeatureEnabled,
  432. create,
  433. import: importProject,
  434. open,
  435. remove,
  436. resetCwd,
  437. setFavorite,
  438. rename,
  439. initCreator,
  440. removeCreator,
  441. getType,
  442. getHomepage
  443. }