UNPKG

zoro-cli

Version:

https://github.com/vuejs/vue-cli

352 lines (320 loc) 9.19 kB
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