const chalk = require('chalk') const debug = require('debug') const execa = require('execa') const resolve = require('resolve') const inquirer = require('inquirer') const Generator = require('./Generator') const cloneDeep = require('lodash.clonedeep') const sortObject = require('./util/sortObject') const getVersions = require('./util/getVersions') const installDeps = require('./util/installDeps') const clearConsole = require('./util/clearConsole') const PromptModuleAPI = require('./PromptModuleAPI') const writeFileTree = require('./util/writeFileTree') const formatFeatures = require('./util/formatFeatures') const setupDevProject = require('./util/setupDevProject') const { defaults, saveOptions, loadOptions, savePreset, validatePreset } = require('./options') const { log, error, hasGit, hasYarn, logWithSpinner, stopSpinner } = require('@vue/cli-shared-utils') const isManualMode = answers => answers.preset === '__manual__' module.exports = class Creator { constructor (name, context, promptModules) { this.name = name this.context = process.env.VUE_CLI_CONTEXT = context const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() this.presetPrompt = presetPrompt this.featurePrompt = featurePrompt this.outroPrompts = this.resolveOutroPrompts() this.injectedPrompts = [] this.promptCompleteCbs = [] this.createCompleteCbs = [] const promptAPI = new PromptModuleAPI(this) promptModules.forEach(m => m(promptAPI)) } async create (cliOptions = {}) { const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG const { name, context, createCompleteCbs } = this const run = (command, args) => { if (!args) { [command, ...args] = command.split(/\s+/) } return execa(command, args, { cwd: context }) } let preset if (cliOptions.preset) { // vue create foo --preset bar preset = this.resolvePreset(cliOptions.preset) } else if (cliOptions.default) { // vue create foo --default preset = defaults.presets.default } else if (cliOptions.inlinePreset) { // vue create foo --inlinePreset {...} try { preset = JSON.parse(cliOptions.inlinePreset) } catch (e) { error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`) process.exit(1) } } else { preset = await this.promptAndResolvePreset() } // clone before mutating preset = cloneDeep(preset) // inject core service preset.plugins['@vue/cli-service'] = Object.assign({ projectName: name }, preset) const packageManager = ( cliOptions.packageManager || loadOptions().packageManager || (hasYarn() ? 'yarn' : 'npm') ) await clearConsole() logWithSpinner(`✨`, `Creating project in ${chalk.yellow(context)}.`) // get latest CLI version const { latest } = await getVersions() // generate package.json with plugin dependencies const pkg = { name, version: '0.1.0', private: true, devDependencies: {} } const deps = Object.keys(preset.plugins) deps.forEach(dep => { pkg.devDependencies[dep] = `^${latest}` }) // write package.json await writeFileTree(context, { 'package.json': JSON.stringify(pkg, null, 2) }) // intilaize git repository before installing deps // so that vue-cli-service can setup git hooks. if (hasGit()) { logWithSpinner(`🗃`, `Initializing git repository...`) await run('git init') } // install plugins stopSpinner() log(`⚙ Installing CLI plugins. This might take a while...`) log() if (isTestOrDebug) { // in development, avoid installation process await setupDevProject(context) } else { await installDeps(context, packageManager, cliOptions.registry) } // run generator log() log(`🚀 Invoking generators...`) const plugins = this.resolvePlugins(preset.plugins) const generator = new Generator( context, pkg, plugins, createCompleteCbs ) await generator.generate({ extractConfigFiles: preset.useConfigFiles }) // install additional deps (injected by generators) log(`📦 Installing additional dependencies...`) log() if (!isTestOrDebug) { await installDeps(context, packageManager, cliOptions.registry) } // run complete cbs if any (injected by generators) log() logWithSpinner('⚓', `Running completion hooks...`) for (const cb of createCompleteCbs) { await cb() } // commit initial state if (hasGit()) { await run('git add -A') if (isTestOrDebug) { await run('git', ['config', 'user.name', 'test']) await run('git', ['config', 'user.email', 'test@test.com']) } await run(`git commit -m init`) } // log instructions stopSpinner() log() log(`🎉 Successfully created project ${chalk.yellow(name)}.`) log( `👉 Get started with the following commands:\n\n` + chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`) + chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : 'npm run serve'}`) ) log() } async promptAndResolvePreset () { // prompt await clearConsole(true) const answers = await inquirer.prompt(this.resolveFinalPrompts()) debug('vue-cli:answers')(answers) if (answers.packageManager) { saveOptions({ packageManager: answers.packageManager }) } let preset if (answers.preset && answers.preset !== '__manual__') { preset = this.resolvePreset(answers.preset) } else { // manual preset = { useConfigFiles: answers.useConfigFiles === 'files', plugins: {} } answers.features = answers.features || [] // run cb registered by prompt modules to finalize the preset this.promptCompleteCbs.forEach(cb => cb(answers, preset)) } // validate validatePreset(preset) // save preset if (answers.save && answers.saveName) { savePreset(answers.saveName, preset) } debug('vue-cli:preset')(preset) return preset } resolvePreset (name) { const savedPresets = loadOptions().presets || {} let preset = savedPresets[name] // use default preset if user has not overwritten it if (name === 'default' && !preset) { preset = defaults.presets.default } if (!preset) { error(`preset "${name}" not found.`) const presets = Object.keys(savedPresets) if (presets.length) { log() log(`available presets:\n${presets.join(`\n`)}`) } else { log(`you don't seem to have any saved preset.`) log(`run vue-cli in manual mode to create a preset.`) } process.exit(1) } return preset } // { id: options } => [{ id, apply, options }] resolvePlugins (rawPlugins) { // ensure cli-service is invoked first rawPlugins = sortObject(rawPlugins, ['@vue/cli-service']) return Object.keys(rawPlugins).map(id => { const module = resolve.sync(`${id}/generator`, { basedir: this.context }) return { id, apply: require(module), options: rawPlugins[id] } }) } resolveIntroPrompts () { const savedOptions = loadOptions() const presets = Object.assign({}, savedOptions.presets, defaults.presets) const presetChoices = Object.keys(presets).map(name => { return { name: `${name} (${formatFeatures(presets[name])})`, value: name } }) const presetPrompt = { name: 'preset', type: 'list', message: `Please pick a preset:`, choices: [ ...presetChoices, { name: 'Manually select features', value: '__manual__' } ] } const featurePrompt = { name: 'features', when: isManualMode, type: 'checkbox', message: 'Check the features needed for your project:', choices: [], pageSize: 8 } return { presetPrompt, featurePrompt } } resolveOutroPrompts () { const outroPrompts = [ { name: 'useConfigFiles', when: isManualMode, type: 'list', message: 'Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?', choices: [ { name: 'In dedicated config files', value: 'files' }, { name: 'In package.json', value: 'pkg' } ] }, { name: 'save', when: isManualMode, type: 'confirm', message: 'Save this as a preset for future projects?' }, { name: 'saveName', when: answers => answers.save, type: 'input', message: 'Save preset as:' } ] // ask for packageManager once const savedOptions = loadOptions() if (!savedOptions.packageManager && hasYarn()) { outroPrompts.push({ name: 'packageManager', type: 'list', message: 'Pick the package manager to use when installing dependencies:', choices: [ { name: 'Use Yarn', value: 'yarn', short: 'Yarn' }, { name: 'Use NPM', value: 'npm', short: 'NPM' } ] }) } return outroPrompts } resolveFinalPrompts () { // patch generator-injected prompts to only show in manual mode this.injectedPrompts.forEach(prompt => { const originalWhen = prompt.when || (() => true) prompt.when = answers => { return isManualMode(answers) && originalWhen(answers) } }) const prompts = [ this.presetPrompt, this.featurePrompt, ...this.injectedPrompts, ...this.outroPrompts ] debug('vue-cli:prompts')(prompts) return prompts } }