create-vuepress-theme-plume
Version:
The cli for create vuepress-theme-plume's project
501 lines (451 loc) • 14.5 kB
JavaScript
import cac from "cac";
import path from "node:path";
import process from "node:process";
import { cancel, confirm, group, intro, outro, select, spinner, text } from "@clack/prompts";
import { attemptAsync, kebabCase, sleep } from "@pengzhanbo/utils";
import spawn from "nano-spawn";
import colors from "picocolors";
import fs from "node:fs";
import _sortPackageJson from "sort-package-json";
import { fileURLToPath } from "node:url";
import fs$1 from "node:fs/promises";
import { Eta } from "eta";
import osLocale from "os-locale";
var version = "1.0.0-rc.205";
const languageOptions = [{
label: "English",
value: "en-US"
}, {
label: "简体中文",
value: "zh-CN"
}];
const bundlerOptions = [{
label: "Vite",
value: "vite"
}, {
label: "Webpack",
value: "webpack"
}];
const deployOptions = [
{
label: "Custom",
value: "custom"
},
{
label: "GitHub Pages",
value: "github"
},
{
label: "Vercel",
value: "vercel"
},
{
label: "Netlify",
value: "netlify"
}
];
async function readFiles(dir) {
const filepaths = await fs$1.readdir(dir, { recursive: true });
const files = [];
for (const file of filepaths) {
const filepath = path.join(dir, file);
if ((await fs$1.stat(filepath)).isFile()) files.push({
filepath: file,
content: await fs$1.readFile(filepath, "utf-8")
});
}
return files;
}
async function writeFiles(files, target) {
for (const { filepath, content } of files) {
const file = path.join(target, filepath).replace(/\.tpl$/, "");
await fs$1.mkdir(path.dirname(file), { recursive: true });
await fs$1.writeFile(file, content);
}
}
async function readJsonFile(filepath) {
try {
const content = await fs$1.readFile(filepath, "utf-8");
return JSON.parse(content);
} catch {
return null;
}
}
function getPackageManager() {
return (process.env?.npm_config_user_agent || "npm").split("/")[0];
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args) => path.resolve(__dirname, "../", ...args);
const getTemplate = (dir) => resolve("templates", dir);
function sortPackageJson(json) {
return _sortPackageJson(json, { sortOrder: [
"name",
"type",
"version",
"private",
"description",
"packageManager",
"author",
"license",
"scripts",
"devDependencies",
"dependencies",
"pnpm"
] });
}
async function createPackageJson(mode, pkg, { packageManager, docsDir, siteName, siteDescription, bundler, injectNpmScripts }) {
if (mode === 1) {
pkg.name = kebabCase(siteName);
pkg.type = "module";
pkg.version = "1.0.0";
pkg.description = siteDescription;
if (packageManager !== "npm") {
let [, version] = await attemptAsync(getPackageManagerVersion, packageManager);
if (version) {
if (packageManager === "yarn" && version.startsWith("1")) version = "4.10.3";
pkg.packageManager = `${packageManager}@${version}`;
if (packageManager === "pnpm" && version.startsWith("10")) pkg.pnpm = { onlyBuiltDependencies: ["@parcel/watcher"] };
}
}
const [, userInfo] = await attemptAsync(getUserInfo);
if (userInfo) pkg.author = userInfo.username + (userInfo.email ? ` <${userInfo.email}>` : "");
pkg.license = "MIT";
pkg.engines = { node: "^20.19.0 || >=22.0.0" };
}
if (injectNpmScripts) {
pkg.scripts ??= {};
pkg.scripts = {
...pkg.scripts,
"docs:dev": `vuepress dev ${docsDir}`,
"docs:dev-clean": `vuepress dev ${docsDir} --clean-cache --clean-temp`,
"docs:build": `vuepress build ${docsDir} --clean-cache --clean-temp`,
"docs:preview": `http-server ${docsDir}/.vuepress/dist`
};
if (mode === 1) pkg.scripts["vp-update"] = `${packageManager === "npm" ? "npx" : `${packageManager} dlx`} vp-update`;
}
pkg.devDependencies ??= {};
const hasDep = (dep) => pkg.devDependencies?.[dep] || pkg.dependencies?.[dep];
const context = await readJsonFile(resolve("package.json"));
const meta = context["plume-deps"];
pkg.devDependencies[`@vuepress/bundler-${bundler}`] = `${meta.vuepress}`;
pkg.devDependencies.vuepress = `${meta.vuepress}`;
pkg.devDependencies["vuepress-theme-plume"] = `${context.version}`;
const deps = ["http-server"];
if (!hasDep("vue")) deps.push("vue");
deps.push("typescript");
for (const dep of deps) pkg.devDependencies[dep] = meta[dep];
return {
filepath: "package.json",
content: JSON.stringify(sortPackageJson(pkg), null, 2)
};
}
async function getUserInfo() {
const { output: username } = await spawn("git", [
"config",
"--global",
"user.name"
]);
const { output: email } = await spawn("git", [
"config",
"--global",
"user.email"
]);
return {
username,
email
};
}
async function getPackageManagerVersion(pkg) {
const { output } = await spawn(pkg, ["--version"]);
return output;
}
function createRender(result) {
const eta = new Eta({ functionHeader: "const t = it.t" });
const isEN = result.defaultLanguage === "en-US";
const data = {
...result,
name: kebabCase(result.siteName),
isEN,
locales: isEN ? [{
path: "/",
lang: "en-US",
isEn: true,
prefix: "en"
}, {
path: "/zh/",
lang: "zh-CN",
isEn: false,
prefix: "zh"
}] : [{
path: "/",
lang: "zh-CN",
isEn: false,
prefix: "zh"
}, {
path: "/en/",
lang: "en-US",
isEn: true,
prefix: "en"
}],
t: (en, zh) => isEN ? en : zh
};
return function render(source) {
return eta.renderString(source, data);
};
}
async function generate(mode, data, cwd = process.cwd()) {
let userPkg = {};
if (mode === 0) {
const pkgPath = path.join(cwd, "package.json");
if (fs.existsSync(pkgPath)) userPkg = await readJsonFile(pkgPath) || {};
}
const fileList = [
await createPackageJson(mode, userPkg, data),
...await createDocsFiles(data),
...updateFileListTarget(await readFiles(getTemplate(".vuepress")), `${data.docsDir}/.vuepress`)
];
if (mode === 1) {
fileList.push(...await readFiles(getTemplate("common")));
if (data.packageManager === "pnpm") fileList.push({
filepath: "pnpm-workspace.yaml",
content: "shamefullyHoist: true\nshellEmulator: true\n"
});
if (data.packageManager === "yarn") {
const { output } = await spawn("yarn", ["--version"]);
if (output.startsWith("2")) fileList.push({
filepath: ".yarnrc.yml",
content: "nodeLinker: 'node-modules'\n"
});
}
}
if (data.git) {
const gitFiles = await readFiles(getTemplate("git"));
if (mode === 0) {
const gitignorePath = path.join(cwd, ".gitignore");
if (fs.existsSync(gitignorePath)) {
const content = await fs.promises.readFile(gitignorePath, "utf-8");
fileList.push({
filepath: ".gitignore",
content: `${content}\n# VuePress\n.vuepress/.cache\n.vuepress/.temp\n.vuepress/dist\n`
});
fileList.push(...gitFiles.filter(({ filepath }) => filepath !== ".gitignore"));
} else fileList.push(...gitFiles);
} else fileList.push(...gitFiles);
}
if (data.packageManager === "yarn") fileList.push({
filepath: ".yarnrc.yml",
content: "nodeLinker: 'node-modules'\n"
});
if (data.deploy !== "custom") fileList.push(...await readFiles(getTemplate(`deploy/${data.deploy}`)));
const render = createRender(data);
await writeFiles(fileList.map((file) => {
if (file.filepath.endsWith(".tpl")) file.content = render(file.content);
return file;
}), mode === 1 ? path.join(cwd, data.root) : cwd);
}
async function createDocsFiles(data) {
const fileList = [];
if (data.multiLanguage) {
const enDocs = await readFiles(getTemplate("docs/en"));
const zhDocs = await readFiles(getTemplate("docs/zh"));
if (data.defaultLanguage === "en-US") {
fileList.push(...enDocs);
fileList.push(...updateFileListTarget(zhDocs, "zh"));
} else {
fileList.push(...zhDocs);
fileList.push(...updateFileListTarget(enDocs, "en"));
}
} else if (data.defaultLanguage === "en-US") fileList.push(...await readFiles(getTemplate("docs/en")));
else fileList.push(...await readFiles(getTemplate("docs/zh")));
return updateFileListTarget(fileList, data.docsDir);
}
function updateFileListTarget(fileList, target) {
return fileList.map(({ filepath, content }) => ({
filepath: path.join(target, filepath),
content
}));
}
const locales = {
"zh-CN": {
"question.root": "您想在哪里初始化 VuePress?",
"question.site.name": "站点名称:",
"question.site.description": "站点描述信息:",
"question.bundler": "请选择打包工具",
"question.multiLanguage": "是否使用多语言?",
"question.defaultLanguage": "请选择站点默认语言",
"question.injectNpmScripts": "是否注入 npm 脚本?",
"question.deploy": "部署方式:",
"question.git": "是否初始化 git 仓库?",
"question.installDeps": "是否安装依赖?",
"spinner.start": "🚀 正在创建...",
"spinner.stop": "🎉 创建成功!",
"spinner.git": "📄 初始化 git 仓库...",
"spinner.install": "📦 安装依赖...",
"spinner.command": "🔨 执行以下命令即可启动:",
"hint.cancel": "操作已取消。",
"hint.root": "文件路径不能是绝对路径,不能包含父路径。",
"hint.root.illegal": "文件夹不能包含特殊字符。"
},
"en-US": {
"question.root": "Where would you want to initialize VuePress?",
"question.site.name": "Site Name:",
"question.site.description": "Site Description:",
"question.bundler": "Select a bundler",
"question.multiLanguage": "Do you want to use multiple languages?",
"question.defaultLanguage": "Select the default language of the site",
"question.injectNpmScripts": "Inject npm scripts?",
"question.deploy": "Deploy type:",
"question.git": "Initialize a git repository?",
"question.installDeps": "Install dependencies?",
"spinner.start": "🚀 Creating...",
"spinner.stop": "🎉 Create success!",
"spinner.git": "📄 Initializing git repository...",
"spinner.install": "📦 Installing dependencies...",
"spinner.command": "🔨 Execute the following command to start:",
"hint.cancel": "Operation cancelled.",
"hint.root": "The path cannot be an absolute path, and cannot contain the parent path.",
"hint.root.illegal": "Project names cannot contain special characters."
}
};
function createTranslate(lang) {
let current = lang || "en-US";
return {
setLang: (lang) => {
current = lang;
return lang;
},
t: (key) => locales[current][key]
};
}
const translate = createTranslate();
const t = translate.t;
const setLang = translate.setLang;
const REG_DIR_CHAR = /[<>:"\\|?*[\]]/;
async function prompt(mode, root) {
return await group({
displayLang: async () => {
const locale = osLocale();
if (locale === "zh-CN" || locale === "zh-Hans") return setLang("zh-CN");
if (locale === "en-US") return setLang("en-US");
return setLang(await select({
message: "Select a language to display / 选择显示语言",
options: languageOptions
}));
},
root: async () => {
if (root) return root;
const DEFAULT_ROOT = mode === 0 ? "./docs" : "./my-project";
return await text({
message: t("question.root"),
placeholder: DEFAULT_ROOT,
validate(value) {
if (value?.startsWith("/") || value?.startsWith("..")) return t("hint.root");
if (value && REG_DIR_CHAR.test(value)) return t("hint.root.illegal");
},
defaultValue: DEFAULT_ROOT
});
},
siteName: () => text({
message: t("question.site.name"),
placeholder: "My Vuepress Site",
defaultValue: "My Vuepress Site"
}),
siteDescription: () => text({
message: t("question.site.description"),
defaultValue: ""
}),
multiLanguage: () => confirm({
message: t("question.multiLanguage"),
initialValue: false
}),
defaultLanguage: () => select({
message: t("question.defaultLanguage"),
options: languageOptions
}),
injectNpmScripts: async () => {
if (mode === 1) return true;
return await confirm({
message: t("question.injectNpmScripts"),
initialValue: true
});
},
bundler: () => select({
message: t("question.bundler"),
options: bundlerOptions
}),
deploy: async () => {
if (mode === 0) return "custom";
return await select({
message: t("question.deploy"),
options: deployOptions,
initialValue: "custom"
});
},
git: async () => {
if (mode === 0) return false;
return confirm({
message: t("question.git"),
initialValue: true
});
},
install: () => confirm({
message: t("question.installDeps"),
initialValue: true
})
}, { onCancel: () => {
cancel(t("hint.cancel"));
process.exit(0);
} });
}
async function run(mode, root) {
intro(colors.cyan("Welcome to VuePress and vuepress-theme-plume !\n欢迎使用 VuePress 和 vuepress-theme-plume !"));
const result = await prompt(mode, root);
const data = {
...result,
packageManager: getPackageManager(),
docsDir: mode === 1 ? "docs" : result.root.replace(/^\.\//, "").replace(/\/$/, "")
};
const progress = spinner();
progress.start(t("spinner.start"));
const [err] = await attemptAsync(generate, mode, data);
if (err) {
progress.error(colors.red("generate files error: "));
console.error(err);
process.exit(1);
}
await sleep(200);
const cwd = path.join(process.cwd(), data.root);
if (data.git) {
progress.message(t("spinner.git"));
const [err] = await attemptAsync(() => spawn("git", ["init"], { cwd }));
if (err) {
progress.error(colors.red("git init error: "));
console.error(err);
process.exit(1);
}
}
const pm = data.packageManager;
if (data.install) {
progress.message(t("spinner.install"));
const [err] = await attemptAsync(() => spawn(pm, ["install"], { cwd }));
if (err) {
progress.error(colors.red("install dependencies error: "));
console.error(err);
process.exit(1);
}
}
const cdCommand = mode === 1 ? colors.green(`cd ${data.root}`) : "";
const runCommand = colors.green(`${pm} run docs:dev`);
const installCommand = colors.green(`${pm} install`);
progress.stop(t("spinner.stop"));
if (mode === 1) outro(`${t("spinner.command")}
${cdCommand}
${data.install ? "" : `${installCommand} && `}${runCommand}`);
}
const cli = cac("create-vuepress-theme-plume");
cli.command("[root]", "create a new vuepress-theme-plume project / 创建新的 vuepress-theme-plume 项目").action((root) => run(1, root));
cli.command("init [root]", "Initial vuepress-theme-plume in the existing project / 在现有项目中初始化 vuepress-theme-plume").action((root) => run(0, root));
cli.help();
cli.version(version);
cli.parse();
export {};