UNPKG

create-vuepress-theme-plume

Version:

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

556 lines (540 loc) 16.7 kB
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 { };