import execa, { ExecaChildProcess } from "execa"; import inquirer, { QuestionCollection, Question, ListQuestion, CheckboxQuestion, DistinctQuestion } from "inquirer"; import chalk from "chalk"; import { copy } from "fs-extra"; import { resolve } from "path"; import { PackageManager } from "../utils/packageManager"; import { PromptModuleAPI } from "./promptModuleAPI"; import { sortObject } from "../utils/sortObject"; import { generateReadme } from "../utils/generateReadme"; import { logWithSpinner, stopSpinner, log, warn, clearConsole, hasGit, hasProjectGit, writeFileTree, loadModule, } from "luban-cli-shared-utils"; import { Generator } from "./generator"; import { CliOptions, Preset, RawPlugin, ResolvedPlugin, PromptCompleteCallback, BasePkgFields, PLUGIN_ID, SUPPORTED_PACKAGE_MANAGER, } from "../types"; import cloneDeep from "lodash.clonedeep"; type FeaturePrompt = CheckboxQuestion>; const isManualMode: boolean = true; /** * @type {import("./../types").Creator} */ class Creator { private name: string; private readonly context: string; private options: CliOptions; public readonly featurePrompt: FeaturePrompt; public promptCompletedCallbacks: Array; private readonly outroPrompts: Question[]; private isTestOrDebug: boolean | undefined; public readonly injectedPrompts: DistinctQuestion[]; private _pkgManager: SUPPORTED_PACKAGE_MANAGER | undefined; /** * * @param {string} name project name * @param {string} context project path * @param {import("./../types/").CliOptions} options 初始化项目时的可选选项 * @param promptModules */ constructor( name: string, context: string, options: CliOptions, promptModules: Array<(api: PromptModuleAPI) => void>, ) { this.name = name; this.options = options; this.context = context; this.run = this.run.bind(this); this.shouldInitGit = this.shouldInitGit.bind(this); const { featurePrompt } = this.resolveIntroPrompts(); // this.presetPrompt = presetPrompt; this.featurePrompt = featurePrompt; this.injectedPrompts = []; this.promptCompletedCallbacks = []; this.outroPrompts = this.resolveOutroPrompts(); this.isTestOrDebug = Boolean(process.env.LUBAN_CLI_TEST) || Boolean(process.env.LUBAN_CLI_DEBUG); const promptAPI = new PromptModuleAPI(this); promptModules.forEach((m) => m(promptAPI)); } public async create(): Promise { const { options, context, name, shouldInitGit, run, isTestOrDebug } = this; const preset = await this.promptAndResolvePreset(); const adaptedPreset = cloneDeep(preset); const rootOptions = { projectName: name, preset }; adaptedPreset.plugins["cli-plugin-service"] = rootOptions; /** * @ignore if (router) { adaptedPreset.plugins["cli-plugin-router"] = { historyMode: false }; if (routerHistoryMode) { adaptedPreset.plugins["cli-plugin-router"].historyMode = true; } } */ /** * @ignore if (store) { adaptedPreset.plugins["cli-plugin-store"] = {}; } */ const resolvedPlugins = await this.resolvePlugins(adaptedPreset.plugins); const pkg: BasePkgFields = { name, version: "0.1.0", private: true, devDependencies: { "luban-cli-service": "latest", }, ["__luban_config__"]: adaptedPreset, }; await writeFileTree(context, { "package.json": JSON.stringify(pkg, null, 2), }); log(); log(`⚙ Copy plugins...`); await this.copyBuiltInPlugins(Object.keys(adaptedPreset.plugins), `${this.context}/.plugins/`); log(); log(`🚀 Invoking plugin's generators...`); const generator = new Generator(context, { plugins: resolvedPlugins, pkg: pkg }); await generator.generate(); // TODO supported the npm client `yarn`. `this._pkgManager` is always undefined const packageManager = this._pkgManager || "npm"; const pkgManager = new PackageManager({ context, forcePackageManager: packageManager }); log(); const shouldInitGitFlag = shouldInitGit(options); if (shouldInitGitFlag) { logWithSpinner(`🗃`, `Initializing git repository...`); await run("git init"); } stopSpinner(); log(); let gitCommitFailed = false; if (shouldInitGitFlag) { await run("git add -A"); if (isTestOrDebug) { await run("git", ["config", "user.name", "test"]); await run("git", ["config", "user.email", "test@test.com"]); } const msg = typeof options.git === "string" ? options.git : ":rocket: init project"; try { await run("git", ["commit", "-m", msg]); } catch (e) { gitCommitFailed = true; } } log(`📦 Installing dependencies...`); await pkgManager.install(); log(); stopSpinner(); log("📄 Generating README.md..."); await writeFileTree(context, { "README.md": generateReadme(generator.pkg, packageManager), }); log(); log(chalk.green("🎉 create project successfully!")); log(` ${chalk.bgWhiteBright.black("🚀 Run Application ")} ${chalk.yellow(`cd ${name}`)} ${chalk.yellow(packageManager === "yarn" ? "yarn start" : "npm start")} `); log(chalk.redBright("💻 Happy coding")); log(); if (gitCommitFailed) { warn( `Skipped git commit due to missing 'user.name' and 'user.email' in git config.\n` + `You will need to perform the initial commit yourself.\n`, ); } generator.printExitLogs(); process.exit(1); } public run(command: string, args?: any): ExecaChildProcess { if (!args) { [command, ...args] = command.split(/\s+/); } return execa(command, args, { cwd: this.context }); } private async copyBuiltInPlugins(pluginIDs: string[], dest: string): Promise { const directories = pluginIDs.map((id: string) => resolve(__dirname, `../plugins/${id}`)); return Promise.all(directories.map((directory, index) => copy(directory, `${dest}/${pluginIDs[index]}`))); } public async promptAndResolvePreset(): Promise { await clearConsole(); /** * @type FinallyAnswers */ const answers = await inquirer.prompt(this.resolveFinalPrompts()); // NOTE unsupported yarn just supported npm client // if (answers.packageManager) { // saveLocalConfig({ // packageManager: answers.packageManager, // }); // this._pkgManager = answers.packageManager; // } const preset: Preset = { useConfigFiles: answers.useConfigFiles === "files", plugins: { "cli-plugin-service": {} }, }; answers.features = answers.features || []; this.promptCompletedCallbacks.forEach((cb) => cb(answers, preset)); return preset; } public shouldInitGit(cliOptions: CliOptions): boolean { if (!hasGit()) { return false; } if (cliOptions.skipGit) { return false; } // --git if (cliOptions.forceGit) { return true; } // --no-git return cliOptions.git === "false"; return !hasProjectGit(this.context); } public resolveIntroPrompts(): { featurePrompt: FeaturePrompt } { const featurePrompt: FeaturePrompt = { name: "features", when: isManualMode, type: "checkbox", message: "Check the features needed for your project", choices: [], pageSize: 10, }; return { // presetPrompt, featurePrompt, }; } public resolveFinalPrompts(): QuestionCollection { this.injectedPrompts.forEach((prompt: DistinctQuestion) => { const originalWhen = prompt.when || (() => true); // CHECK TYPE answer prompt.when = function(answers: any): boolean | Promise { if (typeof originalWhen === "function") { return originalWhen(answers); } return originalWhen; }; }); return [this.featurePrompt, ...this.injectedPrompts, ...this.outroPrompts]; } public resolveOutroPrompts(): ListQuestion[] { const outroPrompts: ListQuestion[] = []; // const { packageManager } = this.options; // NOTE unsupported yarn, just supported npm // const savedOptions = loadLocalConfig(); // if (!savedOptions.packageManager && hasYarn()) { // const packageManagerChoices = []; // if (hasYarn()) { // packageManagerChoices.push({ // name: "Use Yarn", // value: "yarn", // short: "Yarn", // }); // } // packageManagerChoices.push({ // name: "Use NPM", // value: "npm", // short: "NPM", // }); // outroPrompts.push({ // name: "packageManager", // type: "list", // when: !packageManager, // message: "Pick the package manager to use when installing dependencies:", // choices: packageManagerChoices, // }); // } return outroPrompts; } public async resolvePlugins(rawPlugins: RawPlugin): Promise { const pluginsPath = resolve(__dirname, "./../plugins"); // ensure cli-plugin-service is invoked first const sortedRawPlugins = sortObject(rawPlugins, ["cli-plugin-service"], true); const plugins: ResolvedPlugin[] = []; const pluginIDs = Object.keys(sortedRawPlugins); for (const id of pluginIDs) { const apply = loadModule(`${id}/generator`, pluginsPath) || (() => {}); plugins.push({ id: id as PLUGIN_ID, apply, options: sortedRawPlugins[id] || {} }); } return plugins; } } export { Creator };