UNPKG

create-vuepress-theme-plume

Version:

The cli for create vuepress-theme-plume's project

501 lines (451 loc) 14.5 kB
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 {};