1 | const path = require('path')
|
2 | const debug = require('debug')
|
3 | const inquirer = require('inquirer')
|
4 | const EventEmitter = require('events')
|
5 | const Generator = require('./Generator')
|
6 | const cloneDeep = require('lodash.clonedeep')
|
7 | const sortObject = require('./util/sortObject')
|
8 | const getVersions = require('./util/getVersions')
|
9 | const PackageManager = require('./util/ProjectPackageManager')
|
10 | const { clearConsole } = require('./util/clearConsole')
|
11 | const PromptModuleAPI = require('./PromptModuleAPI')
|
12 | const writeFileTree = require('./util/writeFileTree')
|
13 | const { formatFeatures } = require('./util/features')
|
14 | const loadLocalPreset = require('./util/loadLocalPreset')
|
15 | const loadRemotePreset = require('./util/loadRemotePreset')
|
16 | const generateReadme = require('./util/generateReadme')
|
17 | const { resolvePkg, isOfficialPlugin } = require('@vue/cli-shared-utils')
|
18 |
|
19 | const {
|
20 | defaults,
|
21 | saveOptions,
|
22 | loadOptions,
|
23 | savePreset,
|
24 | validatePreset,
|
25 | rcPath
|
26 | } = require('./options')
|
27 |
|
28 | const {
|
29 | chalk,
|
30 | execa,
|
31 |
|
32 | log,
|
33 | warn,
|
34 | error,
|
35 |
|
36 | hasGit,
|
37 | hasProjectGit,
|
38 | hasYarn,
|
39 | hasPnpm3OrLater,
|
40 | hasPnpmVersionOrLater,
|
41 |
|
42 | exit,
|
43 | loadModule
|
44 | } = require('@vue/cli-shared-utils')
|
45 |
|
46 | const isManualMode = answers => answers.preset === '__manual__'
|
47 |
|
48 | module.exports = class Creator extends EventEmitter {
|
49 | constructor (name, context, promptModules) {
|
50 | super()
|
51 |
|
52 | this.name = name
|
53 | this.context = process.env.VUE_CLI_CONTEXT = context
|
54 | const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
|
55 |
|
56 | this.presetPrompt = presetPrompt
|
57 | this.featurePrompt = featurePrompt
|
58 | this.outroPrompts = this.resolveOutroPrompts()
|
59 | this.injectedPrompts = []
|
60 | this.promptCompleteCbs = []
|
61 | this.afterInvokeCbs = []
|
62 | this.afterAnyInvokeCbs = []
|
63 |
|
64 | this.run = this.run.bind(this)
|
65 |
|
66 | const promptAPI = new PromptModuleAPI(this)
|
67 | promptModules.forEach(m => m(promptAPI))
|
68 | }
|
69 |
|
70 | async create (cliOptions = {}, preset = null) {
|
71 | const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
|
72 | const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
|
73 |
|
74 | if (!preset) {
|
75 | if (cliOptions.preset) {
|
76 |
|
77 | preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
|
78 | } else if (cliOptions.default) {
|
79 |
|
80 | preset = defaults.presets.default
|
81 | } else if (cliOptions.inlinePreset) {
|
82 |
|
83 | try {
|
84 | preset = JSON.parse(cliOptions.inlinePreset)
|
85 | } catch (e) {
|
86 | error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
|
87 | exit(1)
|
88 | }
|
89 | } else {
|
90 | preset = await this.promptAndResolvePreset()
|
91 | }
|
92 | }
|
93 |
|
94 |
|
95 | preset = cloneDeep(preset)
|
96 |
|
97 | preset.plugins['@vue/cli-service'] = Object.assign({
|
98 | projectName: name
|
99 | }, preset)
|
100 |
|
101 | if (cliOptions.bare) {
|
102 | preset.plugins['@vue/cli-service'].bare = true
|
103 | }
|
104 |
|
105 |
|
106 | if (preset.router) {
|
107 | preset.plugins['@vue/cli-plugin-router'] = {}
|
108 |
|
109 | if (preset.routerHistoryMode) {
|
110 | preset.plugins['@vue/cli-plugin-router'].historyMode = true
|
111 | }
|
112 | }
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | if (preset.plugins['@vue/cli-plugin-router'] && preset.plugins['@vue/cli-plugin-typescript']) {
|
119 | const tmp = preset.plugins['@vue/cli-plugin-typescript']
|
120 | delete preset.plugins['@vue/cli-plugin-typescript']
|
121 | preset.plugins['@vue/cli-plugin-typescript'] = tmp
|
122 | }
|
123 |
|
124 |
|
125 | if (preset.vuex) {
|
126 | preset.plugins['@vue/cli-plugin-vuex'] = {}
|
127 | }
|
128 |
|
129 | const packageManager = (
|
130 | cliOptions.packageManager ||
|
131 | loadOptions().packageManager ||
|
132 | (hasYarn() ? 'yarn' : null) ||
|
133 | (hasPnpm3OrLater() ? 'pnpm' : 'npm')
|
134 | )
|
135 | const pm = new PackageManager({ context, forcePackageManager: packageManager })
|
136 |
|
137 | await clearConsole()
|
138 | log(`✨ Creating project in ${chalk.yellow(context)}.`)
|
139 | this.emit('creation', { event: 'creating' })
|
140 |
|
141 |
|
142 | const { latestMinor } = await getVersions()
|
143 |
|
144 |
|
145 | const pkg = {
|
146 | name,
|
147 | version: '0.1.0',
|
148 | private: true,
|
149 | devDependencies: {},
|
150 | ...resolvePkg(context)
|
151 | }
|
152 | const deps = Object.keys(preset.plugins)
|
153 | deps.forEach(dep => {
|
154 | if (preset.plugins[dep]._isPreset) {
|
155 | return
|
156 | }
|
157 |
|
158 | let { version } = preset.plugins[dep]
|
159 |
|
160 | if (!version) {
|
161 | if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
|
162 | version = isTestOrDebug ? `file:${path.resolve(__dirname, '../../../', dep)}` : `~${latestMinor}`
|
163 | } else {
|
164 | version = 'latest'
|
165 | }
|
166 | }
|
167 |
|
168 | pkg.devDependencies[dep] = version
|
169 | })
|
170 |
|
171 |
|
172 | await writeFileTree(context, {
|
173 | 'package.json': JSON.stringify(pkg, null, 2)
|
174 | })
|
175 |
|
176 |
|
177 |
|
178 | const shouldInitGit = this.shouldInitGit(cliOptions)
|
179 | if (shouldInitGit) {
|
180 | log(`🗃 Initializing git repository...`)
|
181 | this.emit('creation', { event: 'git-init' })
|
182 | await run('git init')
|
183 | }
|
184 |
|
185 |
|
186 | log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`)
|
187 | log()
|
188 | this.emit('creation', { event: 'plugins-install' })
|
189 |
|
190 | if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
|
191 |
|
192 | await require('./util/setupDevProject')(context)
|
193 | } else {
|
194 | await pm.install()
|
195 | }
|
196 |
|
197 |
|
198 | log(`🚀 Invoking generators...`)
|
199 | this.emit('creation', { event: 'invoking-generators' })
|
200 | const plugins = await this.resolvePlugins(preset.plugins, pkg)
|
201 | const generator = new Generator(context, {
|
202 | pkg,
|
203 | plugins,
|
204 | afterInvokeCbs,
|
205 | afterAnyInvokeCbs
|
206 | })
|
207 | await generator.generate({
|
208 | extractConfigFiles: preset.useConfigFiles
|
209 | })
|
210 |
|
211 |
|
212 | log(`📦 Installing additional dependencies...`)
|
213 | this.emit('creation', { event: 'deps-install' })
|
214 | log()
|
215 | if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
|
216 | await pm.install()
|
217 | }
|
218 |
|
219 |
|
220 | log(`⚓ Running completion hooks...`)
|
221 | this.emit('creation', { event: 'completion-hooks' })
|
222 | for (const cb of afterInvokeCbs) {
|
223 | await cb()
|
224 | }
|
225 | for (const cb of afterAnyInvokeCbs) {
|
226 | await cb()
|
227 | }
|
228 |
|
229 | if (!generator.files['README.md']) {
|
230 |
|
231 | log()
|
232 | log('📄 Generating README.md...')
|
233 | await writeFileTree(context, {
|
234 | 'README.md': generateReadme(generator.pkg, packageManager)
|
235 | })
|
236 | }
|
237 |
|
238 |
|
239 | if (packageManager === 'pnpm') {
|
240 | const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
|
241 | ? 'shamefully-hoist=true\n'
|
242 | : 'shamefully-flatten=true\n'
|
243 |
|
244 | await writeFileTree(context, {
|
245 | '.npmrc': pnpmConfig
|
246 | })
|
247 | }
|
248 |
|
249 |
|
250 | let gitCommitFailed = false
|
251 | if (shouldInitGit) {
|
252 | await run('git add -A')
|
253 | if (isTestOrDebug) {
|
254 | await run('git', ['config', 'user.name', 'test'])
|
255 | await run('git', ['config', 'user.email', 'test@test.com'])
|
256 | }
|
257 | const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
|
258 | try {
|
259 | await run('git', ['commit', '-m', msg, '--no-verify'])
|
260 | } catch (e) {
|
261 | gitCommitFailed = true
|
262 | }
|
263 | }
|
264 |
|
265 |
|
266 | log()
|
267 | log(`🎉 Successfully created project ${chalk.yellow(name)}.`)
|
268 | if (!cliOptions.skipGetStarted) {
|
269 | log(
|
270 | `👉 Get started with the following commands:\n\n` +
|
271 | (this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
|
272 | chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
|
273 | )
|
274 | }
|
275 | log()
|
276 | this.emit('creation', { event: 'done' })
|
277 |
|
278 | if (gitCommitFailed) {
|
279 | warn(
|
280 | `Skipped git commit due to missing username and email in git config.\n` +
|
281 | `You will need to perform the initial commit yourself.\n`
|
282 | )
|
283 | }
|
284 |
|
285 | generator.printExitLogs()
|
286 | }
|
287 |
|
288 | run (command, args) {
|
289 | if (!args) { [command, ...args] = command.split(/\s+/) }
|
290 | return execa(command, args, { cwd: this.context })
|
291 | }
|
292 |
|
293 | async promptAndResolvePreset (answers = null) {
|
294 |
|
295 | if (!answers) {
|
296 | await clearConsole(true)
|
297 | answers = await inquirer.prompt(this.resolveFinalPrompts())
|
298 | }
|
299 | debug('vue-cli:answers')(answers)
|
300 |
|
301 | if (answers.packageManager) {
|
302 | saveOptions({
|
303 | packageManager: answers.packageManager
|
304 | })
|
305 | }
|
306 |
|
307 | let preset
|
308 | if (answers.preset && answers.preset !== '__manual__') {
|
309 | preset = await this.resolvePreset(answers.preset)
|
310 | } else {
|
311 |
|
312 | preset = {
|
313 | useConfigFiles: answers.useConfigFiles === 'files',
|
314 | plugins: {}
|
315 | }
|
316 | answers.features = answers.features || []
|
317 |
|
318 | this.promptCompleteCbs.forEach(cb => cb(answers, preset))
|
319 | }
|
320 |
|
321 |
|
322 | validatePreset(preset)
|
323 |
|
324 |
|
325 | if (answers.save && answers.saveName && savePreset(answers.saveName, preset)) {
|
326 | log()
|
327 | log(`🎉 Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(rcPath)}`)
|
328 | }
|
329 |
|
330 | debug('vue-cli:preset')(preset)
|
331 | return preset
|
332 | }
|
333 |
|
334 | async resolvePreset (name, clone) {
|
335 | let preset
|
336 | const savedPresets = this.getPresets()
|
337 |
|
338 | if (name in savedPresets) {
|
339 | preset = savedPresets[name]
|
340 | } else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {
|
341 | preset = await loadLocalPreset(path.resolve(name))
|
342 | } else if (name.includes('/')) {
|
343 | log(`Fetching remote preset ${chalk.cyan(name)}...`)
|
344 | this.emit('creation', { event: 'fetch-remote-preset' })
|
345 | try {
|
346 | preset = await loadRemotePreset(name, clone)
|
347 | } catch (e) {
|
348 | error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
|
349 | throw e
|
350 | }
|
351 | }
|
352 |
|
353 | if (!preset) {
|
354 | error(`preset "${name}" not found.`)
|
355 | const presets = Object.keys(savedPresets)
|
356 | if (presets.length) {
|
357 | log()
|
358 | log(`available presets:\n${presets.join(`\n`)}`)
|
359 | } else {
|
360 | log(`you don't seem to have any saved preset.`)
|
361 | log(`run vue-cli in manual mode to create a preset.`)
|
362 | }
|
363 | exit(1)
|
364 | }
|
365 | return preset
|
366 | }
|
367 |
|
368 |
|
369 | async resolvePlugins (rawPlugins, pkg) {
|
370 |
|
371 | rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
|
372 | const plugins = []
|
373 | for (const id of Object.keys(rawPlugins)) {
|
374 | const apply = loadModule(`${id}/generator`, this.context) || (() => {})
|
375 | let options = rawPlugins[id] || {}
|
376 |
|
377 | if (options.prompts) {
|
378 | let pluginPrompts = loadModule(`${id}/prompts`, this.context)
|
379 |
|
380 | if (pluginPrompts) {
|
381 | const prompt = inquirer.createPromptModule()
|
382 |
|
383 | if (typeof pluginPrompts === 'function') {
|
384 | pluginPrompts = pluginPrompts(pkg, prompt)
|
385 | }
|
386 | if (typeof pluginPrompts.getPrompts === 'function') {
|
387 | pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
|
388 | }
|
389 |
|
390 | log()
|
391 | log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
|
392 | options = await prompt(pluginPrompts)
|
393 | }
|
394 | }
|
395 |
|
396 | plugins.push({ id, apply, options })
|
397 | }
|
398 | return plugins
|
399 | }
|
400 |
|
401 | getPresets () {
|
402 | const savedOptions = loadOptions()
|
403 | return Object.assign({}, savedOptions.presets, defaults.presets)
|
404 | }
|
405 |
|
406 | resolveIntroPrompts () {
|
407 | const presets = this.getPresets()
|
408 | const presetChoices = Object.entries(presets).map(([name, preset]) => {
|
409 | let displayName = name
|
410 | if (name === 'default') {
|
411 | displayName = 'Default'
|
412 | } else if (name === '__default_vue_3__') {
|
413 | displayName = 'Default (Vue 3 Preview)'
|
414 | }
|
415 |
|
416 | return {
|
417 | name: `${displayName} (${formatFeatures(preset)})`,
|
418 | value: name
|
419 | }
|
420 | })
|
421 | const presetPrompt = {
|
422 | name: 'preset',
|
423 | type: 'list',
|
424 | message: `Please pick a preset:`,
|
425 | choices: [
|
426 | ...presetChoices,
|
427 | {
|
428 | name: 'Manually select features',
|
429 | value: '__manual__'
|
430 | }
|
431 | ]
|
432 | }
|
433 | const featurePrompt = {
|
434 | name: 'features',
|
435 | when: isManualMode,
|
436 | type: 'checkbox',
|
437 | message: 'Check the features needed for your project:',
|
438 | choices: [],
|
439 | pageSize: 10
|
440 | }
|
441 | return {
|
442 | presetPrompt,
|
443 | featurePrompt
|
444 | }
|
445 | }
|
446 |
|
447 | resolveOutroPrompts () {
|
448 | const outroPrompts = [
|
449 | {
|
450 | name: 'useConfigFiles',
|
451 | when: isManualMode,
|
452 | type: 'list',
|
453 | message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
|
454 | choices: [
|
455 | {
|
456 | name: 'In dedicated config files',
|
457 | value: 'files'
|
458 | },
|
459 | {
|
460 | name: 'In package.json',
|
461 | value: 'pkg'
|
462 | }
|
463 | ]
|
464 | },
|
465 | {
|
466 | name: 'save',
|
467 | when: isManualMode,
|
468 | type: 'confirm',
|
469 | message: 'Save this as a preset for future projects?',
|
470 | default: false
|
471 | },
|
472 | {
|
473 | name: 'saveName',
|
474 | when: answers => answers.save,
|
475 | type: 'input',
|
476 | message: 'Save preset as:'
|
477 | }
|
478 | ]
|
479 |
|
480 |
|
481 | const savedOptions = loadOptions()
|
482 | if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
|
483 | const packageManagerChoices = []
|
484 |
|
485 | if (hasYarn()) {
|
486 | packageManagerChoices.push({
|
487 | name: 'Use Yarn',
|
488 | value: 'yarn',
|
489 | short: 'Yarn'
|
490 | })
|
491 | }
|
492 |
|
493 | if (hasPnpm3OrLater()) {
|
494 | packageManagerChoices.push({
|
495 | name: 'Use PNPM',
|
496 | value: 'pnpm',
|
497 | short: 'PNPM'
|
498 | })
|
499 | }
|
500 |
|
501 | packageManagerChoices.push({
|
502 | name: 'Use NPM',
|
503 | value: 'npm',
|
504 | short: 'NPM'
|
505 | })
|
506 |
|
507 | outroPrompts.push({
|
508 | name: 'packageManager',
|
509 | type: 'list',
|
510 | message: 'Pick the package manager to use when installing dependencies:',
|
511 | choices: packageManagerChoices
|
512 | })
|
513 | }
|
514 |
|
515 | return outroPrompts
|
516 | }
|
517 |
|
518 | resolveFinalPrompts () {
|
519 |
|
520 | this.injectedPrompts.forEach(prompt => {
|
521 | const originalWhen = prompt.when || (() => true)
|
522 | prompt.when = answers => {
|
523 | return isManualMode(answers) && originalWhen(answers)
|
524 | }
|
525 | })
|
526 |
|
527 | const prompts = [
|
528 | this.presetPrompt,
|
529 | this.featurePrompt,
|
530 | ...this.injectedPrompts,
|
531 | ...this.outroPrompts
|
532 | ]
|
533 | debug('vue-cli:prompts')(prompts)
|
534 | return prompts
|
535 | }
|
536 |
|
537 | shouldInitGit (cliOptions) {
|
538 | if (!hasGit()) {
|
539 | return false
|
540 | }
|
541 |
|
542 | if (cliOptions.forceGit) {
|
543 | return true
|
544 | }
|
545 |
|
546 | if (cliOptions.git === false || cliOptions.git === 'false') {
|
547 | return false
|
548 | }
|
549 |
|
550 | return !hasProjectGit(this.context)
|
551 | }
|
552 | }
|