@micro-cli/create
Version:
A Cli to quickly create modern Vite-based web app.
244 lines (213 loc) • 7.1 kB
text/typescript
import inquirer from 'inquirer';
// import path from 'node:path';
import {
hasPnpm3OrLater,
hasYarn,
resolvePkg,
hasPnpmVersionOrLater,
hasGit,
hasProjectGit,
execa,
loadModule,
chalk,
wrapLoading,
commandSpawn,
} from '@micro-cli/shared-utils';
import type { OptionsTypes } from '@micro-cli/core/types';
import PromptModuleAPI from './lib/promptModuleAPI';
import promptModules from './lib/promptModules';
import writeFileTree from './lib/writeFileTree';
import sortObject from './lib/sortObject';
import type {
PromptType,
presetTypes,
answersTypes,
onPromptCompleteCbsType,
presetPluginsTypes,
resolvePluginsType,
} from './types';
import Generator from './lib/Generator';
class Creator extends EventTarget {
private name: string;
public targetDir: string;
public featurePrompt: PromptType;
public injectedPrompts: Array<PromptType>;
private presetPrompt: PromptType;
public promptCompleteCbs: Array<onPromptCompleteCbsType>;
private answers: answersTypes;
constructor(name: string, targetDir: string) {
super();
this.name = name;
this.targetDir = targetDir;
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts();
this.presetPrompt = presetPrompt; // 选择框架
this.featurePrompt = featurePrompt; // 选择feature
this.injectedPrompts = [];
this.promptCompleteCbs = [];
this.answers = { preset: 'React' };
const promptAPI = new PromptModuleAPI(this);
promptModules.forEach((m: any) => m(promptAPI));
}
// eslint-disable-next-line class-methods-use-this
async create(cliOptions = {}) {
const preset: presetTypes = await this.promptAndResolvePreset();
preset.plugins['@micro-cli/cli-service'] = {
projectName: this.name,
...preset,
};
// eslint-disable-next-line no-nested-ternary
const packageManager: string = hasPnpm3OrLater()
? 'pnpm'
: hasYarn()
? 'yarn'
: 'npm';
console.log(`✨ Creating project in ${chalk.yellow(this.targetDir)}.`);
// eslint-disable-next-line no-unused-vars
const pkg = {
name: this.name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(this.targetDir),
};
const deps: string[] = Object.keys(preset.plugins);
deps.forEach((dep) => {
(pkg.devDependencies as any)[dep] = `^1.0.0`;
});
writeFileTree(this.targetDir, {
'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')
? // pnpm v7 makes breaking change to set strict-peer-dependencies=true by default, which may cause some problems when installing
'shamefully-hoist=true\nstrict-peer-dependencies=false\n'
: 'shamefully-flatten=true\n';
writeFileTree(this.targetDir, {
'.npmrc': pnpmConfig,
});
}
// initialize git repository before installing deps
// so that vue-cli-service can setup git hooks.
const shouldInitGit = this.shouldInitGit(cliOptions);
if (shouldInitGit) {
console.log(`🗃 Initializing git repository...`);
await execa('git init', { cwd: this.targetDir });
}
// install plugins
console.log();
await wrapLoading(
() => commandSpawn(packageManager, ['install'], { cwd: this.targetDir }),
`⚙\u{fe0f} `
);
console.log(`🚀 Invoking generators...`);
// 5.遍历插件,generator方法设置为插件的apply方法
const plugins: Array<resolvePluginsType> = await this.resolvePlugins(
preset.plugins
);
// 创建Generator实例
const generator = new Generator(this.targetDir, {
pkg,
plugins,
answers: this.answers,
});
// 调用generator的generate方法
await generator.generate();
// commandSpawn
await wrapLoading(
() => commandSpawn(packageManager, ['install'], { cwd: this.targetDir }),
`📦 `
);
console.log(`🎉 Successfully created project ${chalk.yellow(this.name)}.`);
console.log();
console.log(
`👉 Get started with the following commands:\n\n${
this.targetDir === process.cwd()
? ``
: chalk.cyan(` ${chalk.gray('$')} cd ${this.name}\n`)
}${chalk.cyan(
` ${chalk.gray('$')} ${
// eslint-disable-next-line no-nested-ternary
packageManager === 'yarn'
? 'yarn run dev'
: packageManager === 'pnpm'
? 'pnpm run dev'
: 'npm run dev'
}`
)}`
);
// await execa(`${packageManager} install`, { cwd: this.targetDir });
}
// { id: options } => [{ id, apply, options }]
async resolvePlugins(
rawPlugins: Partial<presetPluginsTypes>
): Promise<Array<resolvePluginsType>> {
// ensure cli-service is invoked first
// eslint-disable-next-line no-param-reassign
rawPlugins = sortObject(rawPlugins, ['@micro-cli/cli-service'], true);
const plugins: any = [];
// eslint-disable-next-line no-empty, no-restricted-syntax
for (const id of Object.keys(rawPlugins)) {
// eslint-disable-next-line no-await-in-loop
const apply = (await loadModule(`${id}`, this.targetDir)) || (() => {});
const options = { ...(rawPlugins as any)[id], projectName: this.name };
plugins.push({ id, apply, options, answers: this.answers });
}
return plugins;
}
async promptAndResolvePreset() {
const answers: answersTypes = await inquirer.prompt(this.getFinalPrompts());
this.answers = answers;
const preset = {
useConfigFiles: true,
plugins: {},
};
answers.features = answers.features || [];
this.promptCompleteCbs.forEach((cb) => cb(answers, preset));
return preset;
}
getFinalPrompts() {
return [this.presetPrompt, this.featurePrompt, ...this.injectedPrompts];
}
// eslint-disable-next-line class-methods-use-this
resolveIntroPrompts() {
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a frameWork:`,
choices: [
{
name: 'React',
value: 'React',
},
{
name: 'Vue',
value: 'Vue',
},
],
};
const featurePrompt = {
name: 'features',
// when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 10,
};
return {
featurePrompt,
presetPrompt,
};
}
// 判断有没有安装git
shouldInitGit(cliOptions: OptionsTypes) {
if (!hasGit()) {
return false;
}
if (cliOptions.git) {
return true;
}
return !hasProjectGit(this.targetDir);
}
}
export default Creator;