zoro-cli
Version:
https://github.com/vuejs/vue-cli
352 lines (320 loc) • 9.19 kB
JavaScript
const debug = require('zoro-cli-util/debug')('cli-creator')
const { importFrom } = require('zoro-cli-util/import')
const { gitNotClean, ensureGitClean } = require('zoro-cli-util/git')
const run = require('zoro-cli-util/run')
const {
log,
info,
warn,
success,
stopSpinner,
} = require('zoro-cli-util/logger')
const { removeFiles } = require('zoro-cli-util/fs')
const { installDeps } = require('zoro-cli-util/pkg')
const { isFunction } = require('zoro-cli-util/is')
const inquirer = require('inquirer')
const fs = require('fs-extra')
const path = require('path')
const cloneDeep = require('lodash/cloneDeep')
const chalk = require('chalk')
const Generator = require('./Generator')
const Types = require('./types')
const PromptAPI = require('./PromptAPI')
async function shouldReConfig() {
const { reConfig } = await inquirer.prompt([
{
type: 'list',
name: 'reConfig',
message: '是否重新配置',
default: true,
choices: [
{
name:
'Yes, 重新配置 (会自动读取以前的配置作为默认值, 如果你的项目距离上次配置过了很久, 推荐重新配置)',
value: true,
short: 'Yes',
},
{
name: 'No, 不用配置了',
value: false,
short: 'No',
},
],
},
])
return reConfig
}
class Creator {
constructor({
cliPkg,
pluginDirs: morePluginDirs = [],
docSite,
done = () => {},
...args
} = {}) {
this.context = process.cwd()
this.pkg = null
this.cliPkg = cliPkg
this.pluginDirs = [path.join(__dirname, './plugins'), ...morePluginDirs]
debug.key('pluginDirs')
debug(this.pluginDirs)
this.plugins = []
this.configFilepath = path.join(this.context, '.clirc.json')
this.askIfReConfig = false
this.options = {}
this.types = []
this.promptCompleteCbs = []
this.files = {}
this.docSite = docSite
this.done = done
this.args = args
}
async create() {
// ensure git
await this.ensureGit()
await this.ensureSrcClean()
// read pkg
await this.initPkg()
await this.loadOptions()
// resolve plugins
await this.resolvePlugins()
// resolve plugin prompts first to let plugin have change to inject something that intro prompts need, but prompt them later
this.pluginPrompts = this.resolvePluginPrompts()
this.introPrompts = this.resolveIntroPrompts()
await this.inquiryOptions()
// run generator
log('🚀 Invoking generators...')
const generator = new Generator({
context: this.context,
pkg: this.pkg,
plugins: this.plugins,
rootOptions: this.options,
files: this.files,
args: this.args,
})
await generator.generate()
// install
stopSpinner()
log('⚙ Installing npm packages. This might take a while...')
await installDeps({ ...this.args })
log()
// git hint
const hasGit = await this.isGitRepo()
if (!hasGit) {
warn('you can use git to manage your source code')
log()
} else if (gitNotClean()) {
log(' The following files have been updated / added / deleted:')
const { stdout } = await run(
'git ls-files --deleted --modified --others --directory --exclude-standard',
)
log(
chalk.red(
stdout
.split(/\r?\n/g)
.map(line => ` ${line}`)
.join('\n'),
),
)
log(
` You should review these changes with ${chalk.cyan(
'git diff',
)} and commit them.`,
)
log()
}
await this.done({
options: this.options,
Types,
})
// log instructions
stopSpinner()
success('🎉 Successfully init project')
info('👉 Get started with the following commands: ')
info(' npm run dev')
if (this.docSite) {
info(`文档: ${this.docSite}`)
}
log()
}
isGitRepo() {
return fs.existsSync(path.join(this.context, '.git'))
}
async ensureGit() {
const hasGit = await this.isGitRepo()
if (!hasGit) {
const { initGit } = await inquirer.prompt([
{
name: 'initGit',
type: 'list',
message: '当前文件夹未检测到 git, 是否执行 git init 来初始化 git',
choices: [
{
name: '不要, 我就随便看看',
value: false,
short: '不要',
},
{
name: '要, 我需要版本控制',
value: true,
short: '要',
},
],
},
])
if (initGit) {
await run.log('git init')
}
} else {
// ensure git clean
await ensureGitClean()
}
}
async ensureSrcClean() {
const isFirstTime = !fs.existsSync(this.configFilepath)
if (fs.existsSync(path.join(this.context, './src'))) {
const { removeSrc } = await inquirer.prompt([
{
name: 'removeSrc',
type: 'list',
message: '如果你是第一次初始化, 建议删除 src 后再继续操作',
default: !!isFirstTime,
choices: [
{
name: '不删',
value: false,
},
{
name: '删',
value: true,
},
],
},
])
if (removeSrc) {
await removeFiles(this.context, ['src'])
}
}
}
async initPkg() {
if (!this.pkg || !Object.keys(this.pkg)) {
const pkgPath = path.join(this.context, 'package.json')
if (fs.pathExistsSync(pkgPath)) {
// do not use readPkg, use the generated package.json as it is
/* eslint-disable import/no-dynamic-require */
this.pkg = require(pkgPath)
/* eslint-enable import/no-dynamic-require */
} else {
this.pkg = {
name: path.basename(this.context),
version: '0.0.1',
private: true,
devDependencies: {},
}
}
}
}
async resolvePlugins() {
const idToPlugin = (dir, id) => ({
id: id.replace(/^\.\//, 'built-in:'),
dir,
prepare: importFrom(path.join(dir, id), './prepare'),
prompts: importFrom(path.join(dir, id), './prompts'),
apply: importFrom(path.join(dir, id), './generator'),
})
/* eslint-disable no-restricted-syntax, no-await-in-loop */
for (const dir of this.pluginDirs) {
const names = await fs.readdir(dir)
const plugins = names
.filter(name => name.charAt(0) !== '.')
.map(name => idToPlugin(dir, name))
this.plugins.push(...plugins)
}
/* eslint-enable no-restricted-syntax, no-await-in-loop */
debug.key('plugins')
debug(this.plugins)
}
async loadOptions() {
const { configFilepath } = this
if (fs.existsSync(configFilepath)) {
try {
/* eslint-disable import/no-dynamic-require */
this.saveOptions(require(configFilepath))
/* eslint-enable import/no-dynamic-require */
debug(`load config from ${configFilepath}`)
debug(JSON.stringify(this.options, null, 2))
log()
this.askIfReConfig = true
} catch (err) {
// ignore err
}
}
}
async inquiryOptions() {
const { configFilepath } = this
if (this.askIfReConfig) {
const reConfig = await shouldReConfig()
if (!reConfig) {
return
}
}
const options = await inquirer.prompt(this.resolveFinalPrompts())
// save options
this.saveOptions(options)
debug.key('options')
debug(this.options)
this.files[configFilepath] =
JSON.stringify(this.packOptionsToSave(), null, 2) + '\n'
// run cb registered by prompt modules
/* eslint-disable no-restricted-syntax, no-await-in-loop */
for (const cb of this.promptCompleteCbs) {
await cb(options)
}
/* eslint-enable no-restricted-syntax, no-await-in-loop */
}
resolveFinalPrompts() {
const prompts = [...this.introPrompts, ...this.pluginPrompts]
debug.key('prompts')
debug(prompts)
return prompts
}
resolveIntroPrompts() {
// subtype has been removed, just for back-compatibility
const defaultType = this.options.subtype || this.options.type
return [
{
type: 'list',
name: 'type',
message: '请选择项目类型',
default: defaultType,
choices: this.types,
},
]
}
resolvePluginPrompts() {
return this.plugins.reduce((prompts, plugin) => {
let ps = plugin.prompts
if (isFunction(ps)) {
const api = new PromptAPI(this)
ps = ps({ api, Types, rootOptions: this.options, pkg: this.pkg })
}
if (Array.isArray(ps)) {
prompts.push(...ps)
}
return prompts
}, [])
}
saveOptions(options) {
Object.assign(this.options, options)
}
packOptionsToSave() {
const obj = cloneDeep(this.options)
// reset version
delete obj.version
obj.version = this.cliPkg.version
// remove deprecated options
delete obj.vue
return obj
}
}
module.exports = Creator