UNPKG

create-vuepress-theme-hope

Version:

Create vuepress-theme-hope demo project helper

625 lines (598 loc) 21.6 kB
#!/usr/bin/env node import { execSync, spawn, spawnSync } from "node:child_process"; import { createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import path, { dirname, join, resolve } from "node:path"; import { confirm, input, select } from "@inquirer/prompts"; import { createCommand } from "commander"; import { fileURLToPath } from "node:url"; //#region src/utils/constant.ts const PACKAGE_NAME_REG = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u; const VERSION_REG = /^[0-9]+\.[0-9]+\.(?:[0-9]+|[0-9]+-[a-z]+\.[0-9])$/u; //#endregion //#region src/utils/deepAssign.ts const { isArray } = Array; const isPlainObject = (val) => Object.prototype.toString.call(val) === "[object Object]"; /** * Deep merge objects to the first one * * @param originObject Object to be merged into * @param overrideObjects Objects to merge into the first one * @returns Merged object */ const deepAssign = (originObject, ...overrideObjects) => { if (overrideObjects.length === 0) return originObject; /** Object being merged */ const assignObject = overrideObjects.shift(); if (assignObject) Object.entries(assignObject).forEach(([property, value]) => { if (property === "__proto__" || property === "constructor") return; if (isPlainObject(originObject[property]) && isPlainObject(value)) deepAssign(originObject[property], value); else if (isArray(value)) originObject[property] = [...value]; else if (isPlainObject(value)) originObject[property] = { ...value }; else originObject[property] = assignObject[property]; }); return deepAssign(originObject, ...overrideObjects); }; //#endregion //#region src/utils/file.ts const ensureDirExistSync = (dirPath) => { try { readdirSync(dirPath); } catch { try { mkdirSync(dirPath, { recursive: true }); } catch {} } }; const copyFile = (srcFile, targetFile) => { ensureDirExistSync(dirname(targetFile)); const readStream = createReadStream(srcFile); const writeStream = createWriteStream(targetFile); readStream.pipe(writeStream); }; const copyDir = (srcDir, targetDir) => { ensureDirExistSync(targetDir); readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { if (file.isFile()) copyFile(`${srcDir}/${file.name}`, `${targetDir}/${file.name}`); else if (file.isDirectory()) copyDir(`${srcDir}/${file.name}`, `${targetDir}/${file.name}`); }); }; const copy = (src, target) => { if (statSync(src).isDirectory()) copyDir(src, target); else if (statSync(src).isFile()) copyFile(src, target); }; //#endregion //#region src/utils/git.ts const checkGitRepo = (cwd = process.cwd()) => { try { execSync("git status", { cwd, stdio: "ignore" }); return true; } catch { return false; } }; const checkGitInstalled = () => { try { execSync("git --version", { stdio: "ignore" }); return true; } catch { return false; } }; //#endregion //#region src/utils/registry.ts const NPM_MIRROR_REGISTRY = "https://registry.npmmirror.com/"; const getUserRegistry = (packageManager, isYarnModern) => execSync(`${packageManager} config get ${isYarnModern ? "npmRegistryServer" : "registry"}`, { encoding: "utf-8" }).trim(); const getRegistry = async (packageManager, lang) => { const userRegistry = getUserRegistry(packageManager, packageManager === "yarn" && !spawnSync(`${packageManager} --version`, { shell: true }).stdout.toString().startsWith("1")); if (lang === "zh") return await select({ message: "选择你想使用的源", choices: ["国内镜像源", "当前源"].map((item) => ({ name: item, value: item })) }) === "国内镜像源" ? NPM_MIRROR_REGISTRY : userRegistry; return userRegistry; }; //#endregion //#region src/config/config.ts const supportedBundlers = ["vite", "webpack"]; const supportedPresets = ["blog", "docs"]; const checkPnpmInstalled = () => { try { return spawnSync("pnpm --version", [], { stdio: "ignore", shell: true }).status === 0; } catch { return false; } }; const checkYarnInstalled = () => { try { return spawnSync("yarn --version", [], { stdio: "ignore", shell: true }).status === 0; } catch { return false; } }; const availablePackageManagers = ["npm"]; if (checkPnpmInstalled()) availablePackageManagers.unshift("pnpm"); if (checkYarnInstalled()) availablePackageManagers.unshift("yarn"); //#endregion //#region src/config/gitignore.ts const getGitIgnorePath = (dir) => ` node_modules/ ${dir}/.vuepress/.cache/ ${dir}/.vuepress/.temp/ ${dir}/.vuepress/dist/ `; const updateGitIgnore = (dir, cwd = process.cwd()) => { const gitignorePath = resolve(cwd, ".gitignore"); writeFileSync(gitignorePath, `${existsSync(gitignorePath) ? readFileSync(gitignorePath, { encoding: "utf-8" }) : ""}${getGitIgnorePath(dir)}`, { encoding: "utf-8" }); }; //#endregion //#region src/config/workflow.ts const getWorkflowContent = (packageManager, cwd, dir, { workflow }) => { const currentBranch = execSync("git branch --show-current", { cwd, encoding: "utf-8" }).trim(); return ` name: ${workflow.name} on: push: branches: - ${currentBranch} permissions: contents: write jobs: deploy-gh-pages: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # ${workflow.submodule} # submodules: true ${packageManager === "pnpm" ? `\ - name: ${workflow.setupPnpm} uses: pnpm/action-setup@v6 ` : ""} - name: ${workflow.setupNode} uses: actions/setup-node@v6 with: node-version: 24 cache: ${packageManager} - name: ${workflow.install} run: | corepack enable ${packageManager === "yarn" ? "yarn install --frozen-lockfile" : `${packageManager} ci`} - name: ${workflow.build} env: NODE_OPTIONS: --max_old_space_size=4096 run: |- ${packageManager} run docs:build > ${join(dir, ".vuepress/dist/.nojekyll").replaceAll(String.raw`\\`, "/")} - name: ${workflow.deploy} uses: JamesIves/github-pages-deploy-action@v4 with: # ${workflow.deploy} branch: gh-pages folder: ${join(dir, ".vuepress/dist").replaceAll(String.raw`\\`, "/")} `; }; //#endregion //#region src/config/template.ts const __dirname = import.meta.dirname; const generateTemplate = async ({ cwd = process.cwd(), targetDir, lang, locale, preset, packageManager }) => { preset ??= await select({ message: locale.question.preset, choices: supportedPresets.map((item) => ({ name: item, value: item })) }); const enableI18n = await confirm({ message: locale.question.i18n, default: false }); console.log(locale.flow.generateTemplate); const templateFolder = preset; copy(resolve(__dirname, "../template/public"), resolve(cwd, targetDir, "./.vuepress/public")); copy(resolve(__dirname, "../template", templateFolder, "config/base"), resolve(cwd, targetDir, ".vuepress")); if (enableI18n) { copy(resolve(__dirname, "../template", templateFolder, "en"), resolve(cwd, targetDir)); copy(resolve(__dirname, "../template", templateFolder, "zh"), resolve(cwd, targetDir, "zh")); copy(resolve(__dirname, "../template", templateFolder, "config/multi"), resolve(cwd, targetDir, ".vuepress")); } else if (lang === "zh") { copy(resolve(__dirname, "../template", templateFolder, "zh"), resolve(cwd, targetDir)); copy(resolve(__dirname, "../template", templateFolder, "config/zh"), resolve(cwd, targetDir, ".vuepress")); } else { copy(resolve(__dirname, "../template", templateFolder, "en"), resolve(cwd, targetDir)); copy(resolve(__dirname, "../template", templateFolder, "config/en"), resolve(cwd, targetDir, ".vuepress")); } let isGitRepo = checkGitRepo(cwd); if (isGitRepo) updateGitIgnore(targetDir, cwd); else if (checkGitInstalled() && await confirm({ message: locale.question.git, default: true })) { execSync("git init -b main", { cwd, stdio: "ignore" }); updateGitIgnore(targetDir, cwd); isGitRepo = true; } if (isGitRepo && await confirm({ message: locale.question.workflow, default: true })) { const workflowDir = resolve(cwd, ".github/workflows"); ensureDirExistSync(workflowDir); writeFileSync(resolve(workflowDir, "deploy-docs.yml"), getWorkflowContent(packageManager, cwd, targetDir, locale), { encoding: "utf-8" }); } }; //#endregion //#region src/config/version.ts const packageJSON = JSON.parse(readFileSync(fileURLToPath(import.meta.resolve("create-vuepress-theme-hope/package.json")), "utf-8")); //#endregion //#region src/i18n/en.ts const en = { flow: { getVersion: "Getting latest version of deps...", createPackage: "Generating package.json...", updatePackage: "Updating package.json...", createTsConfig: "Generating tsconfig.json...", updateTsConfig: "Updating tsconfig.json...", generateTemplate: "Generating Template...", install: "Installing Deps...", devServer: "Staring dev server...\nAfter the dev server starts running, please visit the given server link ('localhost:8080' by default)" }, question: { i18n: "Does the project need multiple languages?", git: "Initialize a git repository?", workflow: "Do you need a GitHub workflow to deploy docs on GitHub pages?", bundler: "Which bundler do you want to use?", preset: "What type of project do you want to create?", packageManager: "Choose package manager", devServer: "Would you like to preview template now?", name: "Your project name", version: "Your project version", description: "Your project description", license: "Your project license" }, hint: { install: "This may take a few minutes, please be patient.\nWe can not correctly output progress bar from child process, so the process may look stuck.", finish: "Successful Generated!", devServer: (packageManager) => `Hint: You should execute "${packageManager} run docs:dev" to start dev server.` }, workflow: { name: "Deploy Docs", checkout: "Checkout code", submodule: "if your docs needs submodules, uncomment the following line", setupPnpm: "Setup pnpm", setupNode: "Setup Node.js", install: "Install Deps", build: "Build Docs", deploy: "Deploy Docs", deployBranch: "This is the branch where the docs are deployed to" }, error: { name: "package name should only contain lowercase characters, numbers and dash", version: "This version is not a valid one. Version should be like 'x.x.x'", bundler: "bundler (--bundler) only support \"vite\" or \"webpack\"", preset: "preset (--preset) only support \"docs\" or \"blog\"", outputDirHint: (packageManager) => `The angle brackets in "<dir>" means it is a required argument, you should replace it with folder name you want to use! E.g.: "my-blog", "project-docs"\nFor example: "${packageManager} init vuepress-theme-hope project-docs"`, addDirHint: (packageManager) => `The brackets in "<dir>" means it is a required argument, you should replace it with folder name you want to use! E.g.: "src", "docs"\nFor example: "${packageManager} init vuepress-theme-hope add docs"`, dirNotEmpty: (dir) => `Target folder "${dir}" is not empty, please choose an empty folder or delete files in it.` } }; //#endregion //#region src/i18n/zh.ts const zh = { flow: { getVersion: "获取依赖的最新版本...", createPackage: "生成 package.json...", updatePackage: "更新 package.json...", createTsConfig: "生成 tsconfig.json...", updateTsConfig: "更新 tsconfig.json...", generateTemplate: "生成模板...", install: "安装依赖...", devServer: "启动开发服务器...\n启动成功后,请在浏览器输入给出的开发服务器地址(默认为 'localhost:8080')" }, question: { packageManager: "选择包管理器", i18n: "项目需要用到多语言么?", git: "是否初始化 Git 仓库?", workflow: "是否需要一个自动部署文档到 GitHub Pages 的工作流?", bundler: "你想要使用哪个打包器?", preset: "你想要创建什么类型的项目?", devServer: "是否想要现在启动 Demo 查看?", name: "设置应用名称", version: "设置应用版本号", description: "设置应用描述", license: "设置协议" }, hint: { install: "这可能需要数分钟,请耐心等待.\n我们无法正确输出子进程的进度条,所以进程可能会看似未响应", finish: "模板已成功生成!", devServer: (packageManager) => `提示: 请使用 "${packageManager} run docs:dev" 命令启动开发服务器` }, workflow: { name: "部署文档", checkout: "检出代码", submodule: "如果你文档需要 Git 子模块,取消注释下一行", setupPnpm: "设置 pnpm", setupNode: "设置 Node.js", install: "安装依赖", build: "构建文档", deploy: "部署文档", deployBranch: "这是文档部署到的分支名称" }, error: { name: "应用名称应只包含小写字母、数字和连接线 (-)", version: "此版本无效,版本号应为 'x.x.x'", bundler: "打包器 (--bundler) 仅支持 vite 或 webpack", preset: "预设 (--preset) 仅支持 docs 或 blog", outputDirHint: (packageManager) => `"<dir>" 的尖括号表示此处为一个必填参数,你应该替换为自己想使用的文件夹名称,如 "my-blog", "project-docs" 等!\n例如: "${packageManager} init vuepress-theme-hope project-docs"`, addDirHint: (packageManager) => `"<dir>" 的尖括号表示此处为一个必填参数,你应该替换为自己想使用的文件夹名称,如 "src", "docs" 等!\n例如: "${packageManager} init vuepress-theme-hope add docs"`, dirNotEmpty: (dir) => `目标文件夹 "${dir}" 不为空,请选择一个空文件夹或者手动删除文件夹中的文件` } }; //#endregion //#region src/i18n/index.ts const i18n = { en, zh }; const getLanguage = async () => { const language = await select({ message: "Select a language to display / 选择显示语言", choices: [{ name: "English", value: "en" }, { name: "简体中文", value: "zh" }] }); return { lang: language, locale: i18n[language] }; }; //#endregion //#region src/packageJson.ts const getScript = (packageManager, bundler, dir) => ({ "docs:build": `vuepress-${bundler} build ${dir}`, "docs:clean-dev": `vuepress-${bundler} dev ${dir} --clean-cache`, "docs:dev": `vuepress-${bundler} dev ${dir}`, "docs:update-package": `${packageManager === "npm" ? "npx" : `${packageManager} dlx`} vp-update` }); const createPackageJson = async ({ packageManager, locale, source, bundler, cwd = process.cwd() }) => { bundler ??= await select({ message: locale.question.bundler, choices: supportedBundlers.map((item) => ({ name: item, value: item })) }); /** Generate package.json */ const packageJsonPath = resolve(cwd, "package.json"); const newContent = { scripts: getScript(packageManager, bundler, source), devDependencies: { [`@vuepress/bundler-${bundler}`]: packageJSON.devDependencies.vuepress, "sass-embedded": packageJSON.devDependencies["sass-embedded"], ...bundler === "webpack" ? { "sass-loader": packageJSON.devDependencies["sass-loader"] } : {}, vue: packageJSON.devDependencies.vue, vuepress: packageJSON.devDependencies.vuepress, "vuepress-theme-hope": packageJSON.devDependencies["vuepress-theme-hope"] }, ...packageManager === "pnpm" ? { pnpm: { onlyBuiltDependencies: ["esbuild"] } } : {} }; let packageContent; if (existsSync(packageJsonPath)) { console.log(locale.flow.updatePackage); packageContent = JSON.parse(readFileSync(packageJsonPath, { encoding: "utf-8" })); deepAssign(packageContent, newContent); } else { console.log(locale.flow.createPackage); packageContent = { name: await input({ message: locale.question.name, default: "vuepress-theme-hope-template", validate: (userInput) => PACKAGE_NAME_REG.test(userInput) || locale.error.name }), description: await input({ message: locale.question.description, default: "A project of vuepress-theme-hope" }), version: await input({ message: locale.question.version, default: "2.0.0", validate: (userInput) => VERSION_REG.test(userInput) || locale.error.version }), license: await input({ message: locale.question.license, default: "MIT" }), type: "module", ...newContent }; } writeFileSync(packageJsonPath, `${JSON.stringify(packageContent, null, 2)}\n`, { encoding: "utf-8" }); }; //#endregion //#region src/tsconfig.ts const createTsConfig = ({ source, locale, cwd = process.cwd() }) => { const tsconfigPath = resolve(cwd, "tsconfig.json"); let tsconfigContent; if (existsSync(tsconfigPath)) { console.log(locale.flow.updateTsConfig); tsconfigContent = JSON.parse(readFileSync(tsconfigPath, { encoding: "utf-8" })); deepAssign(tsconfigContent, { compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext" }, include: [ ...tsconfigContent.include ?? [], `${source}/.vuepress/**/*.ts`, `${source}/.vuepress/**/*.vue` ] }); } else { console.log(locale.flow.createTsConfig); tsconfigContent = { compilerOptions: { module: "NodeNext", moduleResolution: "NodeNext", target: "ES2022" }, include: [`${source}/.vuepress/**/*.ts`, `${source}/.vuepress/**/*.vue`], exclude: ["node_modules"] }; } writeFileSync(tsconfigPath, `${JSON.stringify(tsconfigContent, null, 2)}\n`, { encoding: "utf-8" }); }; //#endregion //#region src/index.ts const program = createCommand("create-vuepress-theme-hope"); const preAction = async (targetDir, { bundler, preset, add }) => { const { lang, locale } = await getLanguage(); if (bundler && !supportedBundlers.includes(bundler)) program.error(locale.error.bundler); if (preset && !supportedPresets.includes(preset)) program.error(locale.error.preset); const targetDirPath = path.resolve(process.cwd(), targetDir); if (existsSync(targetDirPath) && readdirSync(targetDirPath).length > 0) program.error(locale.error.dirNotEmpty(targetDir)); const packageManager = await select({ message: locale.question.packageManager, choices: availablePackageManagers.map((manager) => ({ name: manager, value: manager })) }); if (targetDir.startsWith("<") && targetDir.endsWith(">")) program.error(locale.error[add ? "addDirHint" : "outputDirHint"](packageManager)); ensureDirExistSync(targetDirPath); return { lang, locale, packageManager }; }; const postAction = async ({ cwd = process.cwd(), lang, locale, packageManager }) => { const registry = packageManager === "pnpm" ? "" : await getRegistry(packageManager, lang); console.log(locale.flow.install); console.warn(locale.hint.install); execSync(`${packageManager} install ${registry ? `--registry ${registry}` : ""}`, { cwd, stdio: "inherit" }); console.log(locale.hint.finish); if (await confirm({ message: locale.question.devServer, default: true })) { console.log(locale.flow.devServer); await new Promise((resolve, reject) => { const child = spawn(`${packageManager} run docs:dev`, [], { cwd, stdio: "inherit", shell: true }); child.on("close", (code) => { if (code === 0) resolve(); else reject(/* @__PURE__ */ new Error(`code: ${code}`)); }); child.on("error", (err) => { reject(/* @__PURE__ */ new Error(`commandError: ${err}`)); }); }); } else console.info(locale.hint.devServer(packageManager)); }; program.description(`\ Generate a new vuepress-theme-hope template · pnpm create vuepress-theme-hope <dir> · npm init vuepress-theme-hope@latest <dir> · yarn create vuepress-theme-hope <dir> `).option("-b, --bundler [bundler]", "Bundler to use, vite or webpack only").option("-p, --preset [preset]", "Preset to use, docs or blog only").argument("<dir>", "Dir to create the template in").action(async (targetDir, { bundler, preset }) => { const workingCWD = path.resolve(process.cwd(), targetDir); const { lang, locale, packageManager } = await preAction(targetDir, { bundler, preset, add: false }); await createPackageJson({ bundler, locale, packageManager, cwd: targetDir, source: "src" }); createTsConfig({ cwd: targetDir, source: "src", locale }); await generateTemplate({ preset, lang, locale, packageManager, cwd: workingCWD, targetDir: "src" }); await postAction({ lang, locale, packageManager, cwd: workingCWD }); }); program.command("add").alias("inject").summary("Add template to <dir> inside project").description(`\ Add vuepress-theme-hope template in <dir> under current project · pnpm create vuepress-theme-hope add <dir> · npm init vuepress-theme-hope@latest add <dir> · yarn create vuepress-theme-hope add <dir> `).usage("").option("-b, --bundler [bundler]", "Choose bundler to use").option("-p, --preset [preset]", "Choose preset to use").argument("<dir>", "Dir to create the template in").action(async (targetDir, { bundler, preset }) => { const { lang, locale, packageManager } = await preAction(targetDir, { bundler, preset, add: true }); await createPackageJson({ bundler, packageManager, locale, source: targetDir }); createTsConfig({ source: targetDir, locale }); await generateTemplate({ packageManager, lang, locale, preset, targetDir }); await postAction({ lang, locale, packageManager }); }); program.version(packageJSON.version); program.showHelpAfterError("add --help for additional information"); await program.parseAsync(); //#endregion export { }; //# sourceMappingURL=index.js.map