mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-01-13 19:01:25 -06:00
555 lines
15 KiB
JavaScript
555 lines
15 KiB
JavaScript
const path = require('path')
|
|
const debug = require('debug')
|
|
const inquirer = require('inquirer')
|
|
const EventEmitter = require('events')
|
|
const Generator = require('./Generator')
|
|
const cloneDeep = require('lodash.clonedeep')
|
|
const sortObject = require('./util/sortObject')
|
|
const getVersions = require('./util/getVersions')
|
|
const PackageManager = require('./util/ProjectPackageManager')
|
|
const { clearConsole } = require('./util/clearConsole')
|
|
const PromptModuleAPI = require('./PromptModuleAPI')
|
|
const writeFileTree = require('./util/writeFileTree')
|
|
const { formatFeatures } = require('./util/features')
|
|
const loadLocalPreset = require('./util/loadLocalPreset')
|
|
const loadRemotePreset = require('./util/loadRemotePreset')
|
|
const generateReadme = require('./util/generateReadme')
|
|
const { resolvePkg, isOfficialPlugin } = require('@vue/cli-shared-utils')
|
|
|
|
const {
|
|
defaults,
|
|
saveOptions,
|
|
loadOptions,
|
|
savePreset,
|
|
validatePreset,
|
|
rcPath
|
|
} = require('./options')
|
|
|
|
const {
|
|
chalk,
|
|
execa,
|
|
|
|
log,
|
|
warn,
|
|
error,
|
|
|
|
hasGit,
|
|
hasProjectGit,
|
|
hasYarn,
|
|
hasPnpm3OrLater,
|
|
hasPnpmVersionOrLater,
|
|
|
|
exit,
|
|
loadModule
|
|
} = require('@vue/cli-shared-utils')
|
|
|
|
const isManualMode = answers => answers.preset === '__manual__'
|
|
|
|
module.exports = class Creator extends EventEmitter {
|
|
constructor (name, context, promptModules) {
|
|
super()
|
|
|
|
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.afterInvokeCbs = []
|
|
this.afterAnyInvokeCbs = []
|
|
|
|
this.run = this.run.bind(this)
|
|
|
|
const promptAPI = new PromptModuleAPI(this)
|
|
promptModules.forEach(m => m(promptAPI))
|
|
}
|
|
|
|
async create (cliOptions = {}, preset = null) {
|
|
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
|
|
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
|
|
|
|
if (!preset) {
|
|
if (cliOptions.preset) {
|
|
// vue create foo --preset bar
|
|
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
|
|
} 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}`)
|
|
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)
|
|
|
|
if (cliOptions.bare) {
|
|
preset.plugins['@vue/cli-service'].bare = true
|
|
}
|
|
|
|
// legacy support for router
|
|
if (preset.router) {
|
|
preset.plugins['@vue/cli-plugin-router'] = {}
|
|
|
|
if (preset.routerHistoryMode) {
|
|
preset.plugins['@vue/cli-plugin-router'].historyMode = true
|
|
}
|
|
}
|
|
|
|
// Introducing this hack because typescript plugin must be invoked after router.
|
|
// Currently we rely on the `plugins` object enumeration order,
|
|
// which depends on the order of the field initialization.
|
|
// FIXME: Remove this ugly hack after the plugin ordering API settled down
|
|
if (preset.plugins['@vue/cli-plugin-router'] && preset.plugins['@vue/cli-plugin-typescript']) {
|
|
const tmp = preset.plugins['@vue/cli-plugin-typescript']
|
|
delete preset.plugins['@vue/cli-plugin-typescript']
|
|
preset.plugins['@vue/cli-plugin-typescript'] = tmp
|
|
}
|
|
|
|
// legacy support for vuex
|
|
if (preset.vuex) {
|
|
preset.plugins['@vue/cli-plugin-vuex'] = {}
|
|
}
|
|
|
|
const packageManager = (
|
|
cliOptions.packageManager ||
|
|
loadOptions().packageManager ||
|
|
(hasYarn() ? 'yarn' : null) ||
|
|
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
|
|
)
|
|
|
|
await clearConsole()
|
|
const pm = new PackageManager({ context, forcePackageManager: packageManager })
|
|
|
|
log(`✨ Creating project in ${chalk.yellow(context)}.`)
|
|
this.emit('creation', { event: 'creating' })
|
|
|
|
// get latest CLI plugin version
|
|
const { latestMinor } = await getVersions()
|
|
|
|
// generate package.json with plugin dependencies
|
|
const pkg = {
|
|
name,
|
|
version: '0.1.0',
|
|
private: true,
|
|
devDependencies: {},
|
|
...resolvePkg(context)
|
|
}
|
|
const deps = Object.keys(preset.plugins)
|
|
deps.forEach(dep => {
|
|
if (preset.plugins[dep]._isPreset) {
|
|
return
|
|
}
|
|
|
|
let { version } = preset.plugins[dep]
|
|
|
|
if (!version) {
|
|
if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
|
|
version = isTestOrDebug ? `latest` : `~${latestMinor}`
|
|
} else {
|
|
version = 'latest'
|
|
}
|
|
}
|
|
|
|
pkg.devDependencies[dep] = version
|
|
})
|
|
|
|
// write package.json
|
|
await writeFileTree(context, {
|
|
'package.json': JSON.stringify(pkg, null, 2)
|
|
})
|
|
|
|
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
|
|
if (packageManager === 'pnpm') {
|
|
const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
|
|
? 'shamefully-hoist=true\n'
|
|
: 'shamefully-flatten=true\n'
|
|
|
|
await writeFileTree(context, {
|
|
'.npmrc': pnpmConfig
|
|
})
|
|
}
|
|
|
|
// intilaize git repository before installing deps
|
|
// so that vue-cli-service can setup git hooks.
|
|
const shouldInitGit = this.shouldInitGit(cliOptions)
|
|
if (shouldInitGit) {
|
|
log(`🗃 Initializing git repository...`)
|
|
this.emit('creation', { event: 'git-init' })
|
|
await run('git init')
|
|
}
|
|
|
|
// install plugins
|
|
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`)
|
|
log()
|
|
this.emit('creation', { event: 'plugins-install' })
|
|
|
|
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
|
|
// in development, avoid installation process
|
|
await require('./util/setupDevProject')(context)
|
|
} else {
|
|
await pm.install()
|
|
}
|
|
|
|
// run generator
|
|
log(`🚀 Invoking generators...`)
|
|
this.emit('creation', { event: 'invoking-generators' })
|
|
const plugins = await this.resolvePlugins(preset.plugins, pkg)
|
|
const generator = new Generator(context, {
|
|
pkg,
|
|
plugins,
|
|
afterInvokeCbs,
|
|
afterAnyInvokeCbs
|
|
})
|
|
await generator.generate({
|
|
extractConfigFiles: preset.useConfigFiles
|
|
})
|
|
|
|
// install additional deps (injected by generators)
|
|
log(`📦 Installing additional dependencies...`)
|
|
this.emit('creation', { event: 'deps-install' })
|
|
log()
|
|
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
|
|
await pm.install()
|
|
}
|
|
|
|
// run complete cbs if any (injected by generators)
|
|
log(`⚓ Running completion hooks...`)
|
|
this.emit('creation', { event: 'completion-hooks' })
|
|
for (const cb of afterInvokeCbs) {
|
|
await cb()
|
|
}
|
|
for (const cb of afterAnyInvokeCbs) {
|
|
await cb()
|
|
}
|
|
|
|
if (!generator.files['README.md']) {
|
|
// generate README.md
|
|
log()
|
|
log('📄 Generating README.md...')
|
|
await writeFileTree(context, {
|
|
'README.md': generateReadme(generator.pkg, packageManager)
|
|
})
|
|
}
|
|
|
|
// commit initial state
|
|
let gitCommitFailed = false
|
|
if (shouldInitGit) {
|
|
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', ['config', 'commit.gpgSign', 'false'])
|
|
}
|
|
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
|
|
try {
|
|
await run('git', ['commit', '-m', msg, '--no-verify'])
|
|
} catch (e) {
|
|
gitCommitFailed = true
|
|
}
|
|
}
|
|
|
|
// log instructions
|
|
log()
|
|
log(`🎉 Successfully created project ${chalk.yellow(name)}.`)
|
|
if (!cliOptions.skipGetStarted) {
|
|
log(
|
|
`👉 Get started with the following commands:\n\n` +
|
|
(this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
|
|
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
|
|
)
|
|
}
|
|
log()
|
|
this.emit('creation', { event: 'done' })
|
|
|
|
if (gitCommitFailed) {
|
|
warn(
|
|
`Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
|
|
`You will need to perform the initial commit yourself.\n`
|
|
)
|
|
}
|
|
|
|
generator.printExitLogs()
|
|
}
|
|
|
|
run (command, args) {
|
|
if (!args) { [command, ...args] = command.split(/\s+/) }
|
|
return execa(command, args, { cwd: this.context })
|
|
}
|
|
|
|
async promptAndResolvePreset (answers = null) {
|
|
// prompt
|
|
if (!answers) {
|
|
await clearConsole(true)
|
|
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 = await 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)) {
|
|
log()
|
|
log(`🎉 Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(rcPath)}`)
|
|
}
|
|
|
|
debug('vue-cli:preset')(preset)
|
|
return preset
|
|
}
|
|
|
|
async resolvePreset (name, clone) {
|
|
let preset
|
|
const savedPresets = this.getPresets()
|
|
|
|
if (name in savedPresets) {
|
|
preset = savedPresets[name]
|
|
} else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {
|
|
preset = await loadLocalPreset(path.resolve(name))
|
|
} else if (name.includes('/')) {
|
|
log(`Fetching remote preset ${chalk.cyan(name)}...`)
|
|
this.emit('creation', { event: 'fetch-remote-preset' })
|
|
try {
|
|
preset = await loadRemotePreset(name, clone)
|
|
} catch (e) {
|
|
error(`Failed fetching remote preset ${chalk.cyan(name)}:`)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
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.`)
|
|
}
|
|
exit(1)
|
|
}
|
|
return preset
|
|
}
|
|
|
|
// { id: options } => [{ id, apply, options }]
|
|
async resolvePlugins (rawPlugins, pkg) {
|
|
// ensure cli-service is invoked first
|
|
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
|
|
const plugins = []
|
|
for (const id of Object.keys(rawPlugins)) {
|
|
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
|
|
let options = rawPlugins[id] || {}
|
|
|
|
if (options.prompts) {
|
|
let pluginPrompts = loadModule(`${id}/prompts`, this.context)
|
|
|
|
if (pluginPrompts) {
|
|
const prompt = inquirer.createPromptModule()
|
|
|
|
if (typeof pluginPrompts === 'function') {
|
|
pluginPrompts = pluginPrompts(pkg, prompt)
|
|
}
|
|
if (typeof pluginPrompts.getPrompts === 'function') {
|
|
pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
|
|
}
|
|
|
|
log()
|
|
log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
|
|
options = await prompt(pluginPrompts)
|
|
}
|
|
}
|
|
|
|
plugins.push({ id, apply, options })
|
|
}
|
|
return plugins
|
|
}
|
|
|
|
getPresets () {
|
|
const savedOptions = loadOptions()
|
|
return Object.assign({}, savedOptions.presets, defaults.presets)
|
|
}
|
|
|
|
resolveIntroPrompts () {
|
|
const presets = this.getPresets()
|
|
const presetChoices = Object.entries(presets).map(([name, preset]) => {
|
|
let displayName = name
|
|
if (name === 'default') {
|
|
displayName = 'Default'
|
|
} else if (name === '__default_vue_3__') {
|
|
displayName = 'Default (Vue 3 Preview)'
|
|
}
|
|
|
|
return {
|
|
name: `${displayName} (${formatFeatures(preset)})`,
|
|
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: 10
|
|
}
|
|
return {
|
|
presetPrompt,
|
|
featurePrompt
|
|
}
|
|
}
|
|
|
|
resolveOutroPrompts () {
|
|
const outroPrompts = [
|
|
{
|
|
name: 'useConfigFiles',
|
|
when: isManualMode,
|
|
type: 'list',
|
|
message: 'Where do you prefer placing config for Babel, 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?',
|
|
default: false
|
|
},
|
|
{
|
|
name: 'saveName',
|
|
when: answers => answers.save,
|
|
type: 'input',
|
|
message: 'Save preset as:'
|
|
}
|
|
]
|
|
|
|
// ask for packageManager once
|
|
const savedOptions = loadOptions()
|
|
if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
|
|
const packageManagerChoices = []
|
|
|
|
if (hasYarn()) {
|
|
packageManagerChoices.push({
|
|
name: 'Use Yarn',
|
|
value: 'yarn',
|
|
short: 'Yarn'
|
|
})
|
|
}
|
|
|
|
if (hasPnpm3OrLater()) {
|
|
packageManagerChoices.push({
|
|
name: 'Use PNPM',
|
|
value: 'pnpm',
|
|
short: 'PNPM'
|
|
})
|
|
}
|
|
|
|
packageManagerChoices.push({
|
|
name: 'Use NPM',
|
|
value: 'npm',
|
|
short: 'NPM'
|
|
})
|
|
|
|
outroPrompts.push({
|
|
name: 'packageManager',
|
|
type: 'list',
|
|
message: 'Pick the package manager to use when installing dependencies:',
|
|
choices: packageManagerChoices
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
shouldInitGit (cliOptions) {
|
|
if (!hasGit()) {
|
|
return false
|
|
}
|
|
// --git
|
|
if (cliOptions.forceGit) {
|
|
return true
|
|
}
|
|
// --no-git
|
|
if (cliOptions.git === false || cliOptions.git === 'false') {
|
|
return false
|
|
}
|
|
// default: true unless already in a git repo
|
|
return !hasProjectGit(this.context)
|
|
}
|
|
}
|