UNPKG

create-lemon

Version:
340 lines (330 loc) 10.5 kB
#!/usr/bin/env node import process from "node:process"; import { cac } from "cac"; import debug from "debug"; import * as fs from "node:fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import * as path$1 from "node:path"; import path, { join } from "node:path"; import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts"; import { bgBlue, bgRed, bgYellow, blue, bold, green, red, yellow } from "ansis"; import { downloadTemplate } from "giget"; //#region package.json var version = "0.6.0"; //#endregion //#region src/utils/file.ts /** * 校验项目名称 * @param v 项目名称 * @returns 是否合法 */ function isValidProjectName(v) { return /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i.test(v); } /** * 判断目录是否为空 * @param dir 目录 * @returns 是否为空 */ function canSkipEmptying(dir) { if (!fs.existsSync(dir)) return true; const files = fs.readdirSync(dir); if (files.length === 0) return true; if (files.length === 1 && files[0] === ".git") return true; return false; } /** * 后序遍历目录 * @param dir 目录 * @param dirCallback 目录回调 * @param fileCallback 文件回调 */ const postOrderDirectoryTraverse = (dir, dirCallback, fileCallback) => { for (const filename of fs.readdirSync(dir)) { if (filename === ".git") continue; const fullPath = path$1.resolve(dir, filename); if (fs.lstatSync(fullPath).isDirectory()) { postOrderDirectoryTraverse(fullPath, dirCallback, fileCallback); dirCallback(fullPath); continue; } fileCallback(fullPath); } }; /** * 清空目标文件夹下的所有文件和目录 * @param dir 目录 */ function emptyDir(dir) { if (!fs.existsSync(dir)) return; postOrderDirectoryTraverse(dir, (dirPath) => fs.rmdirSync(dirPath), (filePath) => fs.unlinkSync(filePath)); } //#endregion //#region src/utils/logger.ts var Logger = class { silent = false; setSilent(value) { this.silent = value; } filter(...args) { return args.filter((arg) => arg !== void 0 && arg !== false); } info(...args) { if (!this.silent) console.info(bgBlue(` INFO `), ...this.filter(...args).map((arg) => blue(arg))); } warn(...args) { if (!this.silent) console.warn("\n", bgYellow(` WARN `), ...this.filter(...args).map((arg) => yellow(arg)), "\n"); } error(...args) { if (!this.silent) console.error("\n", bgRed(` ERROR `), ...this.filter(...args).map((arg) => red(arg)), "\n"); } success(...args) { if (!this.silent) console.log(green(`✔`), ...this.filter(...args).map((arg) => green(arg))); } fail(...args) { if (!this.silent) console.log(red(`✖`), ...this.filter(...args).map((arg) => red(arg))); } }; const logger = new Logger(); //#endregion //#region src/utils/index.ts /** * 将值转换为数组 * @param val 值 * @param defaultValue 默认值 * @returns 数组 */ function toArray(val, defaultValue) { if (Array.isArray(val)) return val; else if (val === null || val === void 0) { if (defaultValue) return [defaultValue]; return []; } else return [val]; } /** * 解析逗号分隔的字符串 * @param arr 数组 * @returns 数组 */ function resolveComma(arr) { return arr.flatMap((item) => item.split(",")); } //#endregion //#region src/question/package.ts function replaceContent(filePath, projectName) { const fileContent = JSON.parse(readFileSync(filePath, "utf8")); fileContent.name = projectName; fileContent.version = "0.0.0"; writeFileSync(filePath, JSON.stringify(fileContent, null, 2)); } /** * 修改package.json文件 * @param downloadPath - 下载模版的文件夹路径 * @param projectName - 项目名称 */ function modifyPackageJson(downloadPath, projectName) { const loading = spinner(); loading.start(`${bold("正在创建项目...")}`); const packagePath = join(downloadPath, "package.json"); if (existsSync(packagePath)) { replaceContent(packagePath, projectName); loading.stop(); logger.success(`项目创建成功!`); } else { loading.stop(); logger.fail("项目创建失败!"); emptyDir(downloadPath); throw new Error("没有找到 package.json"); } } //#endregion //#region src/question/templateData.ts const templateList = [ { label: "starter-ts", hint: `${green("ts项目基础模版")}`, value: "ts", path: "starter-template-ts", url: { github: "https://github.com/sankeyangshu/starter-template-ts.git" } }, { label: "starter-vscode", hint: `${green("vscode插件基础模版")}`, value: "vscode", path: "starter-template-vscode", url: { github: "https://github.com/sankeyangshu/starter-template-vscode.git" } }, { label: "starter-react", hint: `${green("react项目基础模版")}`, value: "react", path: "starter-template-react", url: { github: "https://github.com/sankeyangshu/starter-template-react.git" } }, { label: "starter-vue", hint: `${green("vue项目基础模版")}`, value: "vue", path: "starter-template-vue", url: { github: "https://github.com/sankeyangshu/starter-template-vue.git" } }, { label: "starter-unplugin", hint: `${green("unplugin插件基础模版")}`, value: "unplugin", path: "starter-template-unplugin", url: { github: "https://github.com/sankeyangshu/starter-template-unplugin.git" } }, { label: "lemon-react", hint: `${green("基于 React 生态系统的移动 web 应用模板")}`, value: "lemon-react", path: "lemon-template-react", url: { github: "https://github.com/sankeyangshu/lemon-template-react.git" } }, { label: "lemon-vue", hint: `${green("基于 Vue3 生态系统的移动 web 应用模板")}`, value: "lemon-vue", path: "lemon-template-vue", url: { github: "https://github.com/sankeyangshu/lemon-template-vue.git" } }, { label: "lemon-uniapp", hint: `${green("基于 Uniapp 生态系统的小程序应用模板")}`, value: "lemon-uniapp", path: "lemon-template-uniapp", url: { github: "https://github.com/sankeyangshu/lemon-template-uniapp.git" } } ]; const templateOptions = templateList.map((item) => ({ label: item.label, hint: item.hint, value: item.value })); //#endregion //#region src/question/download.ts function getRepoUrlList(value) { const path$2 = templateList.find((item) => item.value === value)?.path; if (path$2) return path$2; return "starter-template-ts"; } /** * 创建项目 * @param projectName 项目名称 * @param templatePath 模板路径 * @param downloadPath 下载路径 */ async function createTemplate(projectName, templatePath, downloadPath) { const loading = spinner(); loading.start(`${bold("正在创建模板...")}`); const template = getRepoUrlList(templatePath); try { await downloadTemplate(`gh:sankeyangshu/${template}`, { dir: projectName }); loading.stop(); logger.success(`${bold("模板创建成功!")}`); } catch { loading.stop(); logger.fail(`${bold("模板创建失败!")}`); process.exit(1); } modifyPackageJson(downloadPath, projectName); } //#endregion //#region src/question/index.ts const debug$1 = debug("lemon-create:options"); async function unwrapPrompt(maybeCancelPromise) { const result = await maybeCancelPromise; if (isCancel(result)) { cancel(`${red("✖")} ${bold("操作已取消")}`); process.exit(0); } return result; } async function question(options) { debug$1("options %O", options); intro(`${yellow("lemon-create")} - 快速创建 前后端 项目`); let targetDir = options?.name; const defaultProjectName = targetDir || "my-project"; const forceOverwrite = options?.force; const result = { projectName: defaultProjectName, shouldOverwrite: forceOverwrite, template: options?.template, silent: !!options.silent, debug: !!options.debug }; if (!targetDir) targetDir = result.projectName = (await unwrapPrompt(text({ message: "请输入项目名称:", placeholder: "my-project", validate: (value) => isValidProjectName(value) ? void 0 : "请输入合法的项目名称" }))).trim(); if (!canSkipEmptying(targetDir) && !forceOverwrite) { result.shouldOverwrite = await unwrapPrompt(confirm({ message: `${targetDir === "." ? "当前文件" : `目标文件 "${targetDir}"`} 非空,是否覆盖?`, initialValue: false })); if (!result.shouldOverwrite) { cancel(`${red("✖")} ${bold("操作已取消")}`); process.exit(0); } } if (options.template) { if (!templateOptions.find((item) => item.value === options.template)) throw new Error(`无效的模版 "${options.template}". 可用的模版: ${templateOptions.map((item) => item.value).join(", ")}`); } else result.template = await unwrapPrompt(select({ message: "请选择项目模版:", options: templateOptions, initialValue: "ts" })); const cwd = process.cwd(); const root = path.join(cwd, result.projectName); if (existsSync(root) && result.shouldOverwrite) emptyDir(root); else if (!existsSync(root)) mkdirSync(root); await createTemplate(result.projectName, result.template, root); outro(`${bold("项目创建已完成! 现在运行:")}\n ${green(`cd ${result.projectName}`)}\n ${green(`pnpm install`)}\n ${green(`pnpm run dev`)}\n\n`); } //#endregion //#region src/index.ts const cli = cac("create-lemon"); /** * Register the command. * @descCN 注册命令 */ async function registerCommand() { cli.command("[name]", "创建新项目").option("-t, --template <template>", "项目模版: ts, vscode, vue, unplugin, lemon-react, lemon-vue, lemon-uniapp").option("-f, --force", "是否强制初始化项目").option("-d, --debug", "是否显示调试日志").option("-s, --silent", "是否显示非错误日志").action((name, options) => question({ name, ...options })); cli.help().version(version); cli.on("command:*", () => { const availableCommands = cli.commands.map((cmd) => cmd.name); logger.error(`未知的命令:${cli.args[0]}`); if (availableCommands.length > 0) logger.info(`可用命令:${availableCommands.join(",")}`); }); cli.parse(process.argv, { run: false }); if (cli.options.debug) { let namespace; if (cli.options.debug === true) namespace = "lemon:*"; else namespace = resolveComma(toArray(cli.options.debug)).map((v) => `lemon:${v}`).join(","); const enabled = debug.disable(); if (enabled) namespace += `,${enabled}`; debug.enable(namespace); debug("lemon:debug")("Debugging enabled", namespace); } await cli.runMatchedCommand(); } /** * Initialize the project. * @descCN 初始化项目 */ async function init() { try { await registerCommand(); } catch (err) { logger.error(err); process.exit(1); } } init(); //#endregion export { };