create-vuepress-theme-hope
Version:
Create vuepress-theme-hope demo project helper
625 lines (598 loc) • 21.6 kB
JavaScript
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