zoro-cli
Version:
https://github.com/vuejs/vue-cli
381 lines (308 loc) • 11 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
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(_ref = {}) {
let {
cliPkg,
pluginDirs: morePluginDirs = [],
docSite,
done = () => {}
} = _ref,
args = _objectWithoutProperties(_ref, ["cliPkg", "pluginDirs", "docSite", "done"]);
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(_objectSpread({}, 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;