create-lemon
Version:
The Elegant Bundler for Libraries
340 lines (330 loc) • 10.5 kB
JavaScript
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 { };