create-vuepress-theme-plume
Version:
The cli for create vuepress-theme-plume's project
556 lines (540 loc) • 16.7 kB
JavaScript
import { createRequire } from "node:module";
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 { 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 handlebars from "handlebars";
import { osLocale } from "os-locale";
//#region package.json
var version = "1.0.0-rc.177";
//#endregion
//#region src/constants.ts
const languageOptions = [{
label: "English",
value: "en-US"
}, {
label: "简体中文",
value: "zh-CN"
}];
const bundlerOptions = [{
label: "Vite",
value: "vite"
}, {
label: "Webpack",
value: "webpack"
}];
let Mode = /* @__PURE__ */ function(Mode$1) {
Mode$1[Mode$1["init"] = 0] = "init";
Mode$1[Mode$1["create"] = 1] = "create";
return Mode$1;
}({});
let DeployType = /* @__PURE__ */ function(DeployType$1) {
DeployType$1["github"] = "github";
DeployType$1["vercel"] = "vercel";
DeployType$1["netlify"] = "netlify";
DeployType$1["custom"] = "custom";
return DeployType$1;
}({});
const deployOptions = [
{
label: "Custom",
value: DeployType.custom
},
{
label: "GitHub Pages",
value: DeployType.github
},
{
label: "Vercel",
value: DeployType.vercel
},
{
label: "Netlify",
value: DeployType.netlify
}
];
//#endregion
//#region src/utils/fs.ts
async function readFiles(root) {
const filepaths = await fs$1.readdir(root, { recursive: true });
const files = [];
for (const file of filepaths) {
const filepath = path.join(root, 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, rewrite) {
for (const { filepath, content } of files) {
let root = path.join(target, filepath).replace(/\.handlebars$/, "");
if (rewrite) root = rewrite(root);
await fs$1.mkdir(path.dirname(root), { recursive: true });
await fs$1.writeFile(root, content);
}
}
async function readJsonFile(filepath) {
try {
const content = await fs$1.readFile(filepath, "utf-8");
return JSON.parse(content);
} catch {
return null;
}
}
//#endregion
//#region src/utils/getPackageManager.ts
function getPackageManager() {
return (process.env?.npm_config_user_agent || "npm").split("/")[0];
}
//#endregion
//#region src/utils/index.ts
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args) => path.resolve(__dirname, "../", ...args);
const getTemplate = (dir) => resolve("templates", dir);
//#endregion
//#region src/packageJson.ts
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 === Mode.create) {
pkg.name = kebabCase(siteName);
pkg.type = "module";
pkg.version = "1.0.0";
pkg.description = siteDescription;
if (packageManager !== "npm") {
let version$1 = await getPackageManagerVersion(packageManager);
if (version$1) {
if (packageManager === "yarn" && version$1.startsWith("1")) version$1 = "4.10.3";
pkg.packageManager = `${packageManager}@${version$1}`;
if (packageManager === "pnpm" && version$1.startsWith("10")) pkg.pnpm = { onlyBuiltDependencies: ["@parcel/watcher", "esbuild"] };
}
}
const userInfo = await getUserInfo();
if (userInfo) pkg.author = userInfo.username + (userInfo.email ? ` <${userInfo.email}>` : "");
pkg.license = "MIT";
pkg.engines = { node: "^20.6.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 === Mode.create) 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() {
try {
const { output: username } = await spawn("git", [
"config",
"--global",
"user.name"
]);
const { output: email } = await spawn("git", [
"config",
"--global",
"user.email"
]);
console.log("userInfo", username, email);
return {
username,
email
};
} catch {
return null;
}
}
async function getPackageManagerVersion(pkg) {
try {
const { output } = await spawn(pkg, ["--version"]);
return output;
} catch {
return null;
}
}
//#endregion
//#region src/render.ts
handlebars.registerHelper("removeLeadingSlash", (path$1) => path$1.replace(/^\//, ""));
handlebars.registerHelper("equal", (a, b) => a === b);
function createRender(result) {
const data = {
...result,
name: kebabCase(result.siteName),
isEN: result.defaultLanguage === "en-US",
locales: result.defaultLanguage === "en-US" ? [{
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"
}]
};
return function render(source) {
try {
return handlebars.compile(source)(data);
} catch (e) {
console.error(e);
return source;
}
};
}
//#endregion
//#region src/generate.ts
async function generate(mode, data, cwd = process.cwd()) {
let userPkg = {};
if (mode === Mode.init) {
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 === Mode.create) {
fileList.push(...await readFiles(getTemplate("common")));
if (data.packageManager === "pnpm") fileList.push({
filepath: ".npmrc",
content: "shamefully-hoist=true\nshell-emulator=true"
});
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 === Mode.init) {
const gitignorePath = path.join(cwd, ".gitignore");
const docs = data.docsDir;
if (fs.existsSync(gitignorePath)) {
const content = await fs.promises.readFile(gitignorePath, "utf-8");
fileList.push({
filepath: ".gitignore",
content: `${content}\n${docs}/.vuepress/.cache\n${docs}/.vuepress/.temp\n${docs}/.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 !== DeployType.custom) fileList.push(...await readFiles(getTemplate(`deploy/${data.deploy}`)));
const render = createRender(data);
const renderedFiles = fileList.map((file) => {
if (file.filepath.endsWith(".handlebars")) file.content = render(file.content);
return file;
});
const ext = data.useTs ? "" : userPkg.type !== "module" ? ".mjs" : ".js";
const REG_EXT = /\.ts$/;
await writeFiles(renderedFiles, mode === Mode.create ? path.join(cwd, data.root) : cwd, (filepath) => {
if (filepath.endsWith(".d.ts")) return filepath;
if (ext) return filepath.replace(REG_EXT, ext);
return filepath;
});
}
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
}));
}
//#endregion
//#region src/locales/en.ts
const en = {
"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.useTs": "Use TypeScript?",
"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."
};
//#endregion
//#region src/locales/zh.ts
const zh = {
"question.root": "您想在哪里初始化 VuePress?",
"question.site.name": "站点名称:",
"question.site.description": "站点描述信息:",
"question.bundler": "请选择打包工具",
"question.multiLanguage": "是否使用多语言?",
"question.defaultLanguage": "请选择站点默认语言",
"question.useTs": "是否使用 TypeScript?",
"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": "文件夹不能包含特殊字符。"
};
//#endregion
//#region src/locales/index.ts
const locales = {
"zh-CN": zh,
"en-US": en
};
//#endregion
//#region src/translate.ts
function createTranslate(lang) {
let current = lang || "en-US";
return {
setLang: (lang$1) => {
current = lang$1;
},
t: (key) => locales[current][key]
};
}
const translate = createTranslate();
const t = translate.t;
const setLang = translate.setLang;
//#endregion
//#region src/prompt.ts
const require = createRequire(process.cwd());
const REG_DIR_CHAR = /[<>:"\\|?*[\]]/;
async function prompt(mode, root) {
let hasTs = false;
if (mode === Mode.init) try {
hasTs = !!require.resolve("typescript");
} catch {}
return await group({
displayLang: async () => {
const locale = await osLocale();
if (locale === "zh-CN" || locale === "zh-Hans") {
setLang("zh-CN");
return "zh-CN";
}
if (locale === "en-US") {
setLang("en-US");
return "en-US";
}
const lang = await select({
message: "Select a language to display / 选择显示语言",
options: languageOptions
});
if (typeof lang === "string") setLang(lang);
return lang;
},
root: async () => {
if (root) return root;
const DEFAULT_ROOT = mode === Mode.init ? "./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") }),
multiLanguage: () => confirm({
message: t("question.multiLanguage"),
initialValue: false
}),
defaultLanguage: () => select({
message: t("question.defaultLanguage"),
options: languageOptions
}),
useTs: async () => {
if (mode === Mode.init) return hasTs;
if (hasTs) return true;
return await confirm({
message: t("question.useTs"),
initialValue: true
});
},
injectNpmScripts: async () => {
if (mode === Mode.create) return true;
return await confirm({
message: t("question.injectNpmScripts"),
initialValue: true
});
},
bundler: () => select({
message: t("question.bundler"),
options: bundlerOptions
}),
deploy: async () => {
if (mode === Mode.init) return DeployType.custom;
return await select({
message: t("question.deploy"),
options: deployOptions,
initialValue: DeployType.custom
});
},
git: async () => {
if (mode === Mode.init) 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);
} });
}
//#endregion
//#region src/run.ts
async function run(mode, root) {
intro(colors.cyan("Welcome to VuePress and vuepress-theme-plume !"));
const data = resolveData(await prompt(mode, root), mode);
const progress = spinner();
progress.start(t("spinner.start"));
try {
await generate(mode, data);
} catch (e) {
console.error(`${colors.red("generate files error: ")}\n`, e);
process.exit(1);
}
await sleep(200);
const cwd = path.join(process.cwd(), data.root);
if (data.git) {
progress.message(t("spinner.git"));
try {
await spawn("git", ["init"], { cwd });
} catch (e) {
console.error(`${colors.red("git init error: ")}\n`, e);
process.exit(1);
}
}
const pm = data.packageManager;
if (data.install) {
progress.message(t("spinner.install"));
try {
await spawn(pm, ["install"], { cwd });
} catch (e) {
console.error(`${colors.red("install dependencies error: ")}\n`, e);
process.exit(1);
}
}
const cdCommand = mode === Mode.create ? 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 === Mode.create) outro(`${t("spinner.command")}
${cdCommand}
${data.install ? "" : `${installCommand} && `}${runCommand}`);
}
function resolveData(result, mode) {
return {
...result,
packageManager: getPackageManager(),
docsDir: mode === Mode.create ? "docs" : result.root.replace(/^\.\//, "").replace(/\/$/, ""),
siteDescription: result.siteDescription || ""
};
}
//#endregion
//#region src/index.ts
const cli = cac("create-vuepress-theme-plume");
cli.command("[root]", "create a new vuepress-theme-plume project / 创建新的 vuepress-theme-plume 项目").action((root) => run(Mode.create, root));
cli.command("init [root]", "Initial vuepress-theme-plume in the existing project / 在现有项目中初始化 vuepress-theme-plume").action((root) => run(Mode.init, root));
cli.help();
cli.version(version);
cli.parse();
//#endregion
export { };