diff --git a/cli/LICENSE b/cli/LICENSE new file mode 100644 index 00000000..9f677c90 --- /dev/null +++ b/cli/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (C) 2021 - PRESENT by pengzhanbo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..e68f1581 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,14 @@ +# create-vuepress-theme-plume + +The cli for create vuepress-theme-plume's project + +## Usage + +```sh +# npm +npm init vuepress-theme-plume@latest +# pnpm +pnpm create vuepress-theme-plume@latest +# yarn +yarn create vuepress-theme-plume@latest +``` diff --git a/cli/bin/index.js b/cli/bin/index.js new file mode 100755 index 00000000..0c1edb80 --- /dev/null +++ b/cli/bin/index.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import '../lib/index.js' diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..12ad9611 --- /dev/null +++ b/cli/package.json @@ -0,0 +1,43 @@ +{ + "name": "create-vuepress-theme-plume", + "type": "module", + "version": "1.0.0-rc.90", + "description": "The cli for create vuepress-theme-plume's project", + "author": "pengzhanbo (https://github.com/pengzhanbo/)", + "license": "MIT", + "homepage": "https://theme-plume.vuejs.press/", + "repository": { + "type": "git", + "url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git", + "directory": "cli" + }, + "bugs": { + "url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues" + }, + "keywords": [ + "VuePress", + "theme", + "plume", + "cli" + ], + "bin": "./bin/index.js", + "files": [ + "bin", + "lib", + "templates" + ], + "scripts": { + "build": "tsup" + }, + "dependencies": { + "@clack/prompts": "^0.7.0", + "@pengzhanbo/utils": "^1.1.2", + "cac": "^6.7.14", + "execa": "^9.3.1", + "handlebars": "^4.7.8", + "picocolors": "^1.0.1" + }, + "theme-plume": { + "vuepress": "2.0.0-rc.14" + } +} diff --git a/cli/src/constants.ts b/cli/src/constants.ts new file mode 100644 index 00000000..1a8e524a --- /dev/null +++ b/cli/src/constants.ts @@ -0,0 +1,30 @@ +import type { Bundler, Langs, Options } from './types.js' + +export const languageOptions: Options = [ + { label: 'English', value: 'en-US' }, + { label: '简体中文', value: 'zh-CN' }, +] + +export const bundlerOptions: Options = [ + { label: 'Vite', value: 'vite' }, + { label: 'Webpack', value: 'webpack' }, +] + +export enum Mode { + init, + create, +} + +export enum DeployType { + github = 'github', + vercel = 'vercel', + netlify = 'netlify', + custom = 'custom', +} + +export const deployOptions: Options = [ + { label: 'Custom', value: DeployType.custom }, + { label: 'GitHub Pages', value: DeployType.github }, + { label: 'Vercel', value: DeployType.vercel }, + { label: 'Netlify', value: DeployType.netlify }, +] diff --git a/cli/src/generate.ts b/cli/src/generate.ts new file mode 100644 index 00000000..48908561 --- /dev/null +++ b/cli/src/generate.ts @@ -0,0 +1,123 @@ +import path from 'node:path' +import process from 'node:process' +import fs from 'node:fs' +import { execaCommand } from 'execa' +import { createPackageJson } from './packageJson.js' +import { createRender } from './render.js' +import { getTemplate, readFiles, readJsonFile, writeFiles } from './utils/index.js' +import type { File, ResolvedData } from './types.js' +import { DeployType, Mode } from './constants.js' + +export async function generate(mode: Mode, data: ResolvedData): Promise { + const cwd = process.cwd() + + let userPkg: Record = {} + if (mode === Mode.init) { + const pkgPath = path.join(cwd, 'package.json') + if (fs.existsSync(pkgPath)) { + userPkg = (await readJsonFile(pkgPath)) || {} + } + } + + const fileList: File[] = [ + // add package.json + await createPackageJson(mode, userPkg, data), + // add docs files + ...await createDocsFiles(data), + // add vuepress and theme-plume configs + ...updateFileListTarget(await readFiles(getTemplate('.vuepress')), `${data.docsDir}/.vuepress`), + ] + + // add repo root files + 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 { stdout: yarnVersion } = await execaCommand('yarn --version') + if (yarnVersion.startsWith('2')) { + fileList.push({ + filepath: '.yarnrc.yml', + content: 'nodeLinker: \'node-modules\'\n', + }) + } + } + } + + // rewrite git files begin ================================== + if (data.git) + fileList.push(...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`, + }) + } + } + // rewrite git files end ==================================== + + 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$/ + const output = mode === Mode.create ? path.join(cwd, data.root) : cwd + await writeFiles(renderedFiles, output, (filepath) => { + if (filepath.endsWith('.d.ts')) + return filepath + if (ext) + return filepath.replace(REG_EXT, ext) + return filepath + }) +} + +async function createDocsFiles(data: ResolvedData): Promise { + const fileList: File[] = [] + 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: File[], target: string): File[] { + return fileList.map(({ filepath, content }) => ({ + filepath: path.join(target, filepath), + content, + })) +} diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 00000000..d7d14ffe --- /dev/null +++ b/cli/src/index.ts @@ -0,0 +1,21 @@ +import cac from 'cac' +import { run } from './run.js' +import { Mode } from './constants.js' + +declare const __CLI_VERSION__: string + +const cli = cac('create-vuepress-theme-plume') + +cli + .command('[root]', 'create a new vuepress-theme-plume project / 创建新的 vuepress-theme-plume 项目') + .action((root: string) => run(Mode.create, root)) + +cli + .command('init [root]', 'Initial vuepress-theme-plume in the existing project / 在现有项目中初始化 vuepress-theme-plume') + .action((root: string) => run(Mode.init, root)) + +cli.help() + +cli.version(__CLI_VERSION__) + +cli.parse() diff --git a/cli/src/locales/en.ts b/cli/src/locales/en.ts new file mode 100644 index 00000000..37181d16 --- /dev/null +++ b/cli/src/locales/en.ts @@ -0,0 +1,25 @@ +import type { Locale } from '../types.js' + +export const en: Locale = { + '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.', +} diff --git a/cli/src/locales/index.ts b/cli/src/locales/index.ts new file mode 100644 index 00000000..133b7b48 --- /dev/null +++ b/cli/src/locales/index.ts @@ -0,0 +1,8 @@ +import type { Langs, Locale } from '../types.js' +import { en } from './en.js' +import { zh } from './zh.js' + +export const locales: Record = { + 'zh-CN': zh, + 'en-US': en, +} diff --git a/cli/src/locales/zh.ts b/cli/src/locales/zh.ts new file mode 100644 index 00000000..e5aa7fc4 --- /dev/null +++ b/cli/src/locales/zh.ts @@ -0,0 +1,25 @@ +import type { Locale } from '../types.js' + +export const zh: Locale = { + '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': '文件夹不能包含特殊字符。', +} diff --git a/cli/src/packageJson.ts b/cli/src/packageJson.ts new file mode 100644 index 00000000..ab6093c5 --- /dev/null +++ b/cli/src/packageJson.ts @@ -0,0 +1,79 @@ +import { execaCommand } from 'execa' +import { kebabCase } from '@pengzhanbo/utils' +import { getDependenciesVersion, readJsonFile, resolve } from './utils/index.js' +import type { File, ResolvedData } from './types.js' +import { Mode } from './constants.js' + +export async function createPackageJson( + mode: Mode, + pkg: Record, + { + docsDir, + siteName, + siteDescription, + bundler, + injectNpmScripts, + }: ResolvedData, +): Promise { + if (mode === Mode.create) { + pkg.name = kebabCase(siteName) + pkg.type = 'module' + pkg.version = '1.0.0' + pkg.description = siteDescription + const userInfo = await getUserInfo() + if (userInfo) { + pkg.author = userInfo.username + (userInfo.email ? ` <${userInfo.email}>` : '') + } + pkg.license = 'MIT' + } + + 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'] = 'vp-update' + } + } + + pkg.devDependencies ??= {} + + const context = (await readJsonFile(resolve('package.json')))! + const meta = context['theme-plume'] + pkg.devDependencies.vuepress = `${meta.vuepress}` + pkg.devDependencies['vuepress-theme-plume'] = `${context.version}` + pkg.devDependencies[`@vuepress/bundler-${bundler}`] = `${meta.vuepress}` + pkg.devDependencies['http-server'] = '^14.1.1' + + const deps: string[] = [] + if (!pkg.dependencies?.vue && !pkg.devDependencies.vue) + deps.push('vue') + if (bundler === 'webpack' && !pkg.dependencies?.['sass-loader'] && !pkg.devDependencies['sass-loader']) + deps.push('sass-loader') + + const dv = await getDependenciesVersion(deps) + + for (const [d, v] of Object.entries(dv)) + pkg.devDependencies[d] = `^${v}` + + return { + filepath: 'package.json', + content: JSON.stringify(pkg, null, 2), + } +} + +async function getUserInfo() { + try { + const { stdout: username } = await execaCommand('git config --global user.name') + const { stdout: email } = await execaCommand('git config --global user.email') + return { username, email } + } + catch { + return null + } +} diff --git a/cli/src/prompt.ts b/cli/src/prompt.ts new file mode 100644 index 00000000..020c1c21 --- /dev/null +++ b/cli/src/prompt.ts @@ -0,0 +1,165 @@ +import process from 'node:process' +import path from 'node:path' +import { createRequire } from 'node:module' +import { cancel, confirm, group, select, text } from '@clack/prompts' +import { setLang, t } from './translate.js' +import type { Bundler, Langs, Options, PromptResult } from './types.js' +import { DeployType, Mode, bundlerOptions, deployOptions, languageOptions } from './constants.js' + +const require = createRequire(process.cwd()) + +const REG_DIR_CHAR = /[<>:"\\|?*[\]]/ + +export async function prompt(mode: Mode, root?: string): Promise { + let hasTs = false + if (mode === Mode.init) { + try { + hasTs = !!require.resolve('typescript') + } + catch {} + } + + const result: PromptResult = await group({ + displayLang: async () => { + const lang = await select, Langs>({ + 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) { + // not absolute path or parent path + if (value?.startsWith('/') || value?.startsWith('..')) + return t('hint.root') + + // not contains illegal characters + if (value && REG_DIR_CHAR.test(value)) + return t('hint.root.illegal') + + return undefined + }, + 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, Langs>({ + 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, Bundler>({ + message: t('question.bundler'), + options: bundlerOptions, + }), + + deploy: async () => { + if (mode === Mode.init) { + return DeployType.custom + } + return await select, DeployType>({ + 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) + }, + }) + + return result +} + +export async function getTargetDir(cwd: string, dir?: string) { + if (dir === '.') + return cwd + + if (typeof dir === 'string' && dir) { + return path.resolve(cwd, dir) + } + + const DEFAULT_DIR = 'my-project' + + const dirPath = await text({ + message: t('question.root'), + placeholder: DEFAULT_DIR, + validate(value) { + if (value && REG_DIR_CHAR.test(value)) + return t('hint.root.illegal') + + return undefined + }, + defaultValue: DEFAULT_DIR, + }) + + if (typeof dirPath === 'string') { + if (dirPath === '.') + return cwd + + return path.join(cwd, dirPath || DEFAULT_DIR) + } + return dirPath +} diff --git a/cli/src/render.ts b/cli/src/render.ts new file mode 100644 index 00000000..99de1e4f --- /dev/null +++ b/cli/src/render.ts @@ -0,0 +1,40 @@ +import handlebars from 'handlebars' +import { kebabCase } from '@pengzhanbo/utils' +import type { ResolvedData } from './types.js' + +export interface RenderData extends ResolvedData { + name: string + siteName: string + locales: { path: string, lang: string, isEn: boolean, prefix: string }[] + isEN: boolean +} + +handlebars.registerHelper('removeLeadingSlash', (path: string) => path.replace(/^\//, '')) +handlebars.registerHelper('equal', (a: string, b: string) => a === b) + +export function createRender(result: ResolvedData) { + const data: RenderData = { + ...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: string): string { + try { + const template = handlebars.compile(source) + return template(data) + } + catch (e) { + console.error(e) + return source + } + } +} diff --git a/cli/src/run.ts b/cli/src/run.ts new file mode 100644 index 00000000..3c153672 --- /dev/null +++ b/cli/src/run.ts @@ -0,0 +1,54 @@ +import { intro, outro, spinner } from '@clack/prompts' +import { execaCommand } from 'execa' +import colors from 'picocolors' +import { prompt } from './prompt.js' +import { generate } from './generate.js' +import { t } from './translate.js' +import { Mode } from './constants.js' +import type { PromptResult, ResolvedData } from './types.js' +import { getPackageManager } from './utils/index.js' + +export async function run(mode: Mode, root?: string) { + intro(colors.cyan('Welcome to VuePress and vuepress-theme-plume !')) + + const result = await prompt(mode, root) + const data = resolveData(result, mode) + + const progress = spinner() + progress.start(t('spinner.start')) + + await generate(mode, data) + + if (data.git) { + progress.message(t('spinner.git')) + await execaCommand('git init') + } + + const pm = data.packageManager + + if (data.install) { + progress.message(t('spinner.install')) + await execaCommand(pm === 'yarn' ? 'yarn' : `${pm} install`) + } + + const cdCommand = mode === Mode.create ? colors.green(`cd ${data.root}`) : '' + const runCommand = colors.green(pm === 'yarn' ? 'yarn dev' : `${pm} run dev`) + const installCommand = colors.green(pm === 'yarn' ? 'yarn' : `${pm} install`) + + progress.stop(t('spinner.stop')) + + if (mode === Mode.create) { + outro(`${t('spinner.command')} + ${cdCommand} + ${data.install ? '' : `${installCommand} && `}${runCommand}`) + } +} + +function resolveData(result: PromptResult, mode: Mode): ResolvedData { + return { + ...result, + packageManager: getPackageManager(), + docsDir: mode === Mode.create ? 'docs' : result.root.replace(/^\.\//, '').replace(/\/$/, ''), + siteDescription: result.siteDescription || '', + } +} diff --git a/cli/src/translate.ts b/cli/src/translate.ts new file mode 100644 index 00000000..9fdf0aa4 --- /dev/null +++ b/cli/src/translate.ts @@ -0,0 +1,15 @@ +import type { Langs, Locale } from './types.js' +import { locales } from './locales/index.js' + +function createTranslate(lang?: Langs) { + let current: Langs = lang || 'en-US' + + return { + setLang: (lang: Langs) => { + current = lang + }, + t: (key: keyof Locale) => locales[current][key], + } +} + +export const { t, setLang } = createTranslate() diff --git a/cli/src/types.ts b/cli/src/types.ts new file mode 100644 index 00000000..3332af9b --- /dev/null +++ b/cli/src/types.ts @@ -0,0 +1,57 @@ +import type { DeployType } from './constants.js' + +export type Langs = 'zh-CN' | 'en-US' + +export interface Locale { + 'question.root': string + 'question.site.name': string + 'question.site.description': string + 'question.multiLanguage': string + 'question.defaultLanguage': string + 'question.bundler': string + 'question.useTs': string + 'question.injectNpmScripts': string + 'question.git': string + 'question.deploy': string + 'question.installDeps': string + + 'spinner.start': string + 'spinner.stop': string + 'spinner.git': string + 'spinner.install': string + 'spinner.command': string + + 'hint.cancel': string + 'hint.root': string + 'hint.root.illegal': string +} + +export type PackageManager = 'npm' | 'yarn' | 'pnpm' +export type Bundler = 'vite' | 'webpack' + +export type Options = { label: Label, value: Value }[] + +export interface File { + filepath: string + content: string +} + +export interface PromptResult { + displayLang: string // cli display language + root: string + siteName: string + siteDescription: string + bundler: Bundler + multiLanguage: boolean + defaultLanguage: Langs + useTs: boolean + injectNpmScripts: boolean + deploy: DeployType + git: boolean + install: boolean +} + +export interface ResolvedData extends PromptResult { + packageManager: PackageManager + docsDir: string +} diff --git a/cli/src/utils/depsVersion.ts b/cli/src/utils/depsVersion.ts new file mode 100644 index 00000000..69948c6f --- /dev/null +++ b/cli/src/utils/depsVersion.ts @@ -0,0 +1,16 @@ +export type DependencyVersion = 'latest' | 'next' | 'pre' | string + +const api = 'https://api.pengzhanbo.cn/npm/dependencies/version' + +export async function getDependenciesVersion( + dependencies: string[], + version: DependencyVersion = 'latest', +): Promise> { + const result = await fetch(api, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ dependencies, version }), + }).then(res => res.json()) + + return result +} diff --git a/cli/src/utils/fs.ts b/cli/src/utils/fs.ts new file mode 100644 index 00000000..ef4f2ce6 --- /dev/null +++ b/cli/src/utils/fs.ts @@ -0,0 +1,43 @@ +import path from 'node:path' +import fs from 'node:fs/promises' +import type { File } from '../types.js' + +export async function readFiles(root: string): Promise { + const filepaths = await fs.readdir(root, { recursive: true }) + const files: File[] = [] + for (const file of filepaths) { + const filepath = path.join(root, file) + if ((await fs.stat(filepath)).isFile()) { + files.push({ + filepath: file, + content: await fs.readFile(filepath, 'utf-8'), + }) + } + } + + return files +} + +export async function writeFiles( + files: File[], + target: string, + rewrite?: (path: string) => string, +) { + for (const { filepath, content } of files) { + let root = path.join(target, filepath).replace(/\.handlebars$/, '') + if (rewrite) + root = rewrite(root) + await fs.mkdir(path.dirname(root), { recursive: true }) + await fs.writeFile(root, content) + } +} + +export async function readJsonFile = Record>(filepath: string): Promise { + try { + const content = await fs.readFile(filepath, 'utf-8') + return JSON.parse(content) + } + catch { + return null + } +} diff --git a/cli/src/utils/getPackageManager.ts b/cli/src/utils/getPackageManager.ts new file mode 100644 index 00000000..d9324771 --- /dev/null +++ b/cli/src/utils/getPackageManager.ts @@ -0,0 +1,7 @@ +import process from 'node:process' +import type { PackageManager } from '../types.js' + +export function getPackageManager(): PackageManager { + const name = process.env?.npm_config_user_agent || 'npm' + return name.split('/')[0] as PackageManager +} diff --git a/cli/src/utils/index.ts b/cli/src/utils/index.ts new file mode 100644 index 00000000..311366a3 --- /dev/null +++ b/cli/src/utils/index.ts @@ -0,0 +1,12 @@ +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +export const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +export const resolve = (...args: string[]) => path.resolve(__dirname, '../', ...args) + +export const getTemplate = (dir: string) => resolve('templates', dir) + +export * from './fs.js' +export * from './depsVersion.js' +export * from './getPackageManager.js' diff --git a/cli/templates/.vuepress/client.ts.handlebars b/cli/templates/.vuepress/client.ts.handlebars new file mode 100644 index 00000000..77839062 --- /dev/null +++ b/cli/templates/.vuepress/client.ts.handlebars @@ -0,0 +1,12 @@ +import { defineClientConfig } from 'vuepress/client' +import RepoCard from 'vuepress-theme-plume/features/RepoCard.vue' +import CustomComponent from './theme/components/Custom.vue' + +import './theme/styles/custom.css' + +export default defineClientConfig({ + enhance({ app }) { + app.component('RepoCard', RepoCard) + app.component('CustomComponent', CustomComponent) + }, +}) diff --git a/cli/templates/.vuepress/config.ts.handlebars b/cli/templates/.vuepress/config.ts.handlebars new file mode 100644 index 00000000..4d29c155 --- /dev/null +++ b/cli/templates/.vuepress/config.ts.handlebars @@ -0,0 +1,90 @@ +import { defineUserConfig } from 'vuepress' +import { {{ bundler }}Bundler } from '@vuepress/bundler-{{ bundler }}' +import { plumeTheme } from 'vuepress-theme-plume' + +export default defineUserConfig({ + base: '/', + lang: '{{ defaultLanguage }}', + {{#if multiLanguage}} + locales: { + {{#each locales}} + '{{ path }}': { + title: '{{ ../siteName }}', + lang: '{{ lang }}', + description: '{{ ../siteDescription }}', + }, + {{/each}} + }, + {{else}} + title: '{{ siteName }}', + description: '{{ siteDescription }}', + {{/if}} + + bundler: {{ bundler }}Bundler(), + + theme: plumeTheme({ + // 添加您的部署域名 + // hostname: 'https://your_site_url', + + plugins: { + /** + * Shiki 代码高亮 + * @see https://theme-plume.vuejs.press/config/plugins/code-highlight/ + */ + // shiki: { + // languages: ['shell', 'bash', 'typescript', 'javascript'], + // twoslash: true, + // }, + + /** + * markdown enhance + * @see https://theme-plume.vuejs.press/config/plugins/markdown-enhance/ + */ + markdownEnhance: { + demo: true, + // include: true, + // chart: true, + // echarts: true, + // mermaid: true, + // flowchart: true, + }, + + /** + * markdown power + * @see https://theme-plume.vuejs.press/config/plugin/markdown-power/ + */ + // markdownPower: { + // pdf: true, + // caniuse: true, + // plot: true, + // bilibili: true, + // youtube: true, + // icons: true, + // codepen: true, + // replit: true, + // codeSandbox: true, + // jsfiddle: true, + // repl: { + // go: true, + // rust: true, + // kotlin: true, + // }, + // }, + + /** + * comments + * @see https://theme-plume.vuejs.press/guide/features/comments/ + */ + // comment: { + // provider: '', // "Artalk" | "Giscus" | "Twikoo" | "Waline" + // comment: true, + // repo: '', + // repoId: '', + // categoryId: '', + // mapping: 'pathname', + // reactionsEnabled: true, + // inputPosition: 'top', + // }, + }, + }), +}) diff --git a/cli/templates/.vuepress/navbar.ts.handlebars b/cli/templates/.vuepress/navbar.ts.handlebars new file mode 100644 index 00000000..6f3cace0 --- /dev/null +++ b/cli/templates/.vuepress/navbar.ts.handlebars @@ -0,0 +1,28 @@ +import { defineNavbarConfig } from 'vuepress-theme-plume' + +{{#if multiLanguage}} +{{#each locales}} +export const {{prefix}}Navbar = defineNavbarConfig([ + { text: '{{#if isEn}}Home{{else}}首页{{/if}}', link: '{{ path }}' }, + { text: '{{#if isEn}}Blog{{else}}博客{{/if}}', link: '{{ path }}blog/' }, + { text: '{{#if isEn}}Tags{{else}}标签{{/if}}', link: '{{ path }}blog/tags/' }, + { text: '{{#if isEn}}Archives{{else}}归档{{/if}}', link: '{{ path }}blog/archives/' }, + { + text: '{{#if isEn}}Notes{{else}}笔记{{/if}}', + items: [{ text: '{{#if isEn}}Demo{{else}}示例{{/if}}', link: '{{ path }}notes/demo/README.md' }] + }, +]) + +{{/each}} +{{else}} +export const navbar = defineNavbarConfig([ + { text: '{{#if isEn}}Home{{else}}首页{{/if}}', link: '/' }, + { text: '{{#if isEn}}Blog{{else}}博客{{/if}}', link: '/blog/' }, + { text: '{{#if isEn}}Tags{{else}}标签{{/if}}', link: '/blog/tags/' }, + { text: '{{#if isEn}}Archives{{else}}归档{{/if}}', link: '/blog/archives/' }, + { + text: '{{#if isEn}}Notes{{else}}笔记{{/if}}', + items: [{ text: '{{#if isEn}}Demo{{else}}示例{{/if}}', link: '/notes/demo/README.md' }] + }, +]) +{{/if}} diff --git a/cli/templates/.vuepress/notes.ts.handlebars b/cli/templates/.vuepress/notes.ts.handlebars new file mode 100644 index 00000000..b5e4118a --- /dev/null +++ b/cli/templates/.vuepress/notes.ts.handlebars @@ -0,0 +1,32 @@ +import { defineNoteConfig, defineNotesConfig } from 'vuepress-theme-plume' + +{{#if multiLanguage}} +{{#each locales}} +/* =================== locale: {{ lang }} ======================= */ + +const {{ prefix }}DemoNote = defineNoteConfig({ + dir: 'demo', + link: '/demo', + sidebar: ['', 'foo', 'bar'], +}) + +export const {{ prefix }}Notes = defineNotesConfig({ + dir: '{{ removeLeadingSlash path }}notes', + link: '{{ path }}', + notes: [{{ prefix }}DemoNote], +}) + +{{/each}} +{{else}} +const demoNote = defineNoteConfig({ + dir: 'demo', + link: '/demo', + sidebar: ['', 'foo', 'bar'], +}) + +export const notes = defineNotesConfig({ + dir: 'notes', + link: '/', + notes: [demoNote], +}) +{{/if}} diff --git a/cli/templates/.vuepress/plume.config.ts.handlebars b/cli/templates/.vuepress/plume.config.ts.handlebars new file mode 100644 index 00000000..367a2420 --- /dev/null +++ b/cli/templates/.vuepress/plume.config.ts.handlebars @@ -0,0 +1,57 @@ +import { defineThemeConfig } from 'vuepress-theme-plume' +{{#if multiLanguage}} +import { enNavbar, zhNavbar } from './navbar' +import { enNotes, zhNotes } from './notes' +{{else}} +import { navbar } from './navbar' +import { notes } from './notes' +{{/if}} + +/** + * @see https://theme-plume.vuejs.press/config/basic/ + */ +export default defineThemeConfig({ + logo: 'https://theme-plume.vuejs.press/plume.png', + // your git repo url + docsRepo: '', + docsDir: '{{ docsDir }}', + + appearance: true, + + {{#unless multiLanguage}} + profile: { + avatar: 'https://theme-plume.vuejs.press/plume.png', + name: '{{ siteName }}', + description: '{{ siteDescription }}', + // circle: true, + // location: '', + // organization: '', + }, + + navbar, + notes, + {{/unless}} + social: [ + { icon: 'github', link: '/' }, + ], + + {{#if multiLanguage}} + locales: { + {{#each locales}} + '{{ path }}': { + profile: { + avatar: 'https://theme-plume.vuejs.press/plume.png', + name: '{{ ../siteName }}', + description: '{{ ../siteDescription }}', + // circle: true, + // location: '', + // organization: '', + }, + + navbar: {{ prefix }}Navbar, + notes: {{ prefix }}Notes, + }, + {{/each}} + }, + {{/if}} +}) diff --git a/cli/templates/.vuepress/public/plume.svg b/cli/templates/.vuepress/public/plume.svg new file mode 100644 index 00000000..62ee70c6 --- /dev/null +++ b/cli/templates/.vuepress/public/plume.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cli/templates/.vuepress/theme/components/Custom.vue b/cli/templates/.vuepress/theme/components/Custom.vue new file mode 100644 index 00000000..04bac0a3 --- /dev/null +++ b/cli/templates/.vuepress/theme/components/Custom.vue @@ -0,0 +1,11 @@ + + + diff --git a/cli/templates/.vuepress/theme/shim.d.ts b/cli/templates/.vuepress/theme/shim.d.ts new file mode 100644 index 00000000..f07bbd33 --- /dev/null +++ b/cli/templates/.vuepress/theme/shim.d.ts @@ -0,0 +1,6 @@ +declare module '*.vue' { + import type { ComponentOptions } from 'vue' + + const comp: ComponentOptions + export default comp +} diff --git a/cli/templates/.vuepress/theme/styles/custom.css b/cli/templates/.vuepress/theme/styles/custom.css new file mode 100644 index 00000000..d3a3696d --- /dev/null +++ b/cli/templates/.vuepress/theme/styles/custom.css @@ -0,0 +1,50 @@ +:root { + /** 主题颜色 */ + + /* + --vp-c-brand-1: #5086a1; + --vp-c-brand-2: #6aa1b7; + --vp-c-brand-3: #8cccd5; + --vp-c-brand-soft: rgba(131, 208, 218, 0.314); + */ + + /** 背景颜色 */ + + /* + --vp-c-bg: #fff; + --vp-c-bg-alt: #f6f6f7; + --vp-c-bg-elv: #fff; + --vp-c-bg-soft: #f6f6f7; + */ + + /** 文本颜色 */ + + /* + --vp-c-text-1: rgba(60, 60, 67); + --vp-c-text-2: rgba(60, 60, 67, 0.78); + --vp-c-text-3: rgba(60, 60, 67, 0.56); + */ +} + +/** 深色模式 */ +.dark { + /* + --vp-c-brand-1: #8cccd5; + --vp-c-brand-2: #6aa1b7; + --vp-c-brand-3: #5086a1; + --vp-c-brand-soft: rgba(131, 208, 218, 0.314); + */ + + /* + --vp-c-bg: #1b1b1f; + --vp-c-bg-alt: #161618; + --vp-c-bg-elv: #202127; + --vp-c-bg-soft: #202127; + */ + + /* + --vp-c-text-1: rgba(255, 255, 245, 0.86); + --vp-c-text-2: rgba(235, 235, 245, 0.6); + --vp-c-text-3: rgba(235, 235, 245, 0.38); + */ +} diff --git a/cli/templates/common/README.md.handlebars b/cli/templates/common/README.md.handlebars new file mode 100644 index 00000000..3dec863c --- /dev/null +++ b/cli/templates/common/README.md.handlebars @@ -0,0 +1,57 @@ +# {{ name }} + +The Site is generated using [vuepress](https://vuepress.vuejs.org/) and [vuepress-theme-plume](https://github.com/pengzhanbo/vuepress-theme-plume) + +## Install + +```sh +{{#if (equal packageManager "pnpm")}} +pnpm i +{{else if (equal packageManager "yarn")}} +yarn +{{else}} +npm i +{{/if}} +``` + +## Usage + +{{#if (equal packageManager "pnpm")}} +```sh +# start dev server +pnpm docs:dev +# build for production +pnpm docs:build +# preview production build in local +pnpm docs:preview +# update vuepress and theme +pnpm vp-update +``` +{{else if (equal packageManager "yarn")}} +```sh +# start dev server +yarn docs:dev +# build for production +yarn docs:build +# preview production build in local +yarn docs:preview +# update vuepress and theme +yarn vp-update +``` +{{else}} +```sh +# start dev server +npm run docs:dev +# build for production +npm run docs:build +# preview production build in local +npm run docs:preview +# update vuepress and theme +npm run vp-update +``` +{{/if}} + +## Documents + +- [vuepress](https://vuepress.vuejs.org/) +- [vuepress-theme-plume](https://theme-plume.vuejs.press/) diff --git a/cli/templates/common/README.zh-CN.md.handlebars b/cli/templates/common/README.zh-CN.md.handlebars new file mode 100644 index 00000000..3d28bcaa --- /dev/null +++ b/cli/templates/common/README.zh-CN.md.handlebars @@ -0,0 +1,57 @@ +# {{ name }} + +网站使用 [vuepress](https://vuepress.vuejs.org/) 和 [vuepress-theme-plume](https://github.com/pengzhanbo/vuepress-theme-plume) 构建生成。 + +## Install + +```sh +{{#if (equal packageManager "pnpm")}} +pnpm i +{{else if (equal packageManager "yarn")}} +yarn +{{else}} +npm i +{{/if}} +``` + +## Usage + +{{#if (equal packageManager "pnpm")}} +```sh +# 启动开发服务 +pnpm docs:dev +# 构建生产包 +pnpm docs:build +# 本地预览生产服务 +pnpm docs:preview +# 更新 vuepress 和主题 +pnpm vp-update +``` +{{else if (equal packageManager "yarn")}} +```sh +# 启动开发服务 +yarn docs:dev +# 构建生产包 +yarn docs:build +# 本地预览生产服务 +yarn docs:preview +# update vuepress and theme +yarn vp-update +``` +{{else}} +```sh +# 启动开发服务 +npm run docs:dev +# 构建生产包 +npm run docs:build +# 本地预览生产服务 +npm run docs:preview +# 更新 vuepress 和主题 +npm run vp-update +``` +{{/if}} + +## 文档 + +- [vuepress](https://vuepress.vuejs.org/) +- [vuepress-theme-plume](https://theme-plume.vuejs.press/) diff --git a/cli/templates/deploy/github/.github/workflows/deploy.yml.handlebars b/cli/templates/deploy/github/.github/workflows/deploy.yml.handlebars new file mode 100644 index 00000000..289ede19 --- /dev/null +++ b/cli/templates/deploy/github/.github/workflows/deploy.yml.handlebars @@ -0,0 +1,70 @@ +name: deploy + +on: + # 每当 push 到 main 分支时触发部署 + # Deployment is triggered whenever a push is made to the main branch. + push: + branches: [main] + # 手动触发部署 + # Manually trigger deployment + workflow_dispatch: + +jobs: + docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + # “最近更新时间” 等 git 日志相关信息,需要拉取全部提交记录 + # "Last updated time" and other git log-related information require fetching all commit records. + fetch-depth: 0 + +{{#if (equal packageManager "pnpm")}} + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + # 选择要使用的 pnpm 版本 + version: 9 + # 使用 pnpm 安装依赖 + run_install: true +{{/if}} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + # 选择要使用的 node 版本 + node-version: 20 + +{{#if (equal packageManager "yarn")}} + - name: Run install + uses: borales/actions-yarn@v4 + with: + cmd: install +{{/if}} + + # 运行构建脚本 + # Run the build script +{{#unless (equal packageManager "yarn")}} + - name: Build VuePress site + run: {{packageManager}} run docs:build +{{/unless}} +{{#if (equal packageManager "yarn")}} + - name: Build VuePress site + uses: borales/actions-yarn@v4 + with: + cmd: docs:build +{{/if}} + + + # 查看 workflow 的文档来获取更多信息 + # @see https://github.com/crazy-max/ghaction-github-pages + - name: Deploy to GitHub Pages + uses: crazy-max/ghaction-github-pages@v4 + with: + # 部署到 gh-pages 分支 + target_branch: gh-pages + # 部署目录为 VuePress 的默认输出目录 + build_dir: {{docsDir}}/.vuepress/dist + env: + # @see https://docs.github.com/cn/actions/reference/authentication-in-a-workflow#about-the-github_token-secret + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/cli/templates/deploy/netlify/netlify.toml.handlebars b/cli/templates/deploy/netlify/netlify.toml.handlebars new file mode 100644 index 00000000..70ac84f2 --- /dev/null +++ b/cli/templates/deploy/netlify/netlify.toml.handlebars @@ -0,0 +1,9 @@ +# prevent Netlify npm install + +[build] +publish = "{{ docsDir }}/.vuepress/dist" +command = "{{#if (equal packageManager 'yarn')}}yarn && yarn{{else}}{{packageManager}} run{{/if}} docs:build" + +[build.environment] +NODE_VERSION = "20" +NPM_FLAGS = "--version" diff --git a/cli/templates/deploy/vercel/vercel.json.handlebars b/cli/templates/deploy/vercel/vercel.json.handlebars new file mode 100644 index 00000000..ab5f50f4 --- /dev/null +++ b/cli/templates/deploy/vercel/vercel.json.handlebars @@ -0,0 +1,6 @@ +{ + "framework": null, + "buildCommand": "{{#if (equal packageManager 'yarn')}}yarn{{else}}{{packageManager}} run{{/if}} docs:build", + "installCommand": "{{#if (equal packageManager 'yarn')}}yarn{{else}}{{packageManager}} install{{/if}}", + "outputDirectory": "{{ docsDir }}/.vuepress/dist" +} diff --git a/cli/templates/docs/en/README.md.handlebars b/cli/templates/docs/en/README.md.handlebars new file mode 100644 index 00000000..da437fdf --- /dev/null +++ b/cli/templates/docs/en/README.md.handlebars @@ -0,0 +1,22 @@ +--- +pageLayout: home +externalLinkIcon: false +config: + - + type: hero + full: true + background: tint-plate + hero: + name: Theme Plume + tagline: VuePress Next Theme + text: A simple, feature-rich, document & blog + actions: + - + theme: brand + text: Blog + link: {{#if (equal defaultLanguage 'en-US')}}/{{else}}/en/{{/if}}blog/ + - + theme: alt + text: Github → + link: https://github.com/pengzhanbo/vuepress-theme-plume +--- diff --git a/cli/templates/docs/en/notes/demo/README.md b/cli/templates/docs/en/notes/demo/README.md new file mode 100644 index 00000000..e7adb9c2 --- /dev/null +++ b/cli/templates/docs/en/notes/demo/README.md @@ -0,0 +1,6 @@ +--- +title: Demo +--- + +- [bar](./bar.md) +- [foo](./foo.md) diff --git a/cli/templates/docs/en/notes/demo/bar.md b/cli/templates/docs/en/notes/demo/bar.md new file mode 100644 index 00000000..12db1219 --- /dev/null +++ b/cli/templates/docs/en/notes/demo/bar.md @@ -0,0 +1,5 @@ +--- +title: bar +--- + +[foo](./foo.md) diff --git a/cli/templates/docs/en/notes/demo/foo.md b/cli/templates/docs/en/notes/demo/foo.md new file mode 100644 index 00000000..5ca76e68 --- /dev/null +++ b/cli/templates/docs/en/notes/demo/foo.md @@ -0,0 +1,5 @@ +--- +title: foo +--- + +[bar](./bar.md) diff --git a/cli/templates/docs/en/preview/custom-component.example.md b/cli/templates/docs/en/preview/custom-component.example.md new file mode 100644 index 00000000..3fdfa215 --- /dev/null +++ b/cli/templates/docs/en/preview/custom-component.example.md @@ -0,0 +1,8 @@ +--- +title: Custom Component +tags: + - preview + - component +--- + + diff --git a/cli/templates/docs/en/preview/markdown.md b/cli/templates/docs/en/preview/markdown.md new file mode 100644 index 00000000..256ce4d5 --- /dev/null +++ b/cli/templates/docs/en/preview/markdown.md @@ -0,0 +1,312 @@ +--- +title: Markdown +tags: + - markdown +--- + +## Heading 2 + +### Heading 3 + +#### Heading 4 + +##### Heading 5 + +###### Heading 6 + +Bold: **Bold text** + +Italic: _Italic text_ + +~~Deleted text~~ + +Content ==Highlight== + +Mathematical expression: $-(2^{n-1})$ ~ $2^{n-1} -1$ + +$\frac {\partial^r} {\partial \omega^r} \left(\frac {y^{\omega}} {\omega}\right) += \left(\frac {y^{\omega}} {\omega}\right) \left\{(\log y)^r + \sum_{i=1}^r \frac {(-1)^ Ir \cdots (r-i+1) (\log y)^{ri}} {\omega^i} \right\}$ + +19^th^ + +H~2~O + +::: center +content center +::: + +::: right +content right +::: + +- Unordered List 1 +- Unordered List 2 +- Unordered List 3 + +1. Ordered List 1 +2. Ordered List 2 +3. Ordered List 3 + +- [ ] Task List 1 +- [ ] Task List 2 +- [x] Task List 3 +- [x] Task List 4 + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +> quote content +> +> quote content + +[links](/) + +[outside links](https://github.com/pengzhanbo) + +**Badge:** + +- +- +- +- + +**icons:** + +- home - +- vscode - +- twitter - + +**demo wrapper:** + +::: demo-wrapper title="Demo" no-padding height="200px" + + +
+
main
+
aside
+
+ +::: + +**code block:** + +```js whitespace +const a = 1 +const b = 2 +const c = a + b + +// [!code word:obj] +const obj = { + toLong: { + deep: { + deep: { + deep: { + value: 'this is to long text. this is to long text. this is to long text. this is to long text.', // [!code highlight] + } + } + } + } +} +``` + +**code groups:** + +::: code-tabs +@tab tab1 + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +@tab tab2 + +```ts +const a: number = 1 +const b: number = 2 +const c: number = a + b +``` + +::: + +**code highlight:** + +```ts +function foo() { + const a = 1 // [!code highlight] + + console.log(a) + + const b = 2 // [!code ++] + const c = 3 // [!code --] + + console.log(a + b + c) // [!code error] + console.log(a + b) // [!code warning] +} +``` + +**code focus:** + +```ts +function foo() { + const a = 1 // [!code focus] +} +``` + +::: note +note content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: info +content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: tip +content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: warning +content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: caution +content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: important +content [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +**GFM alert:** + +> [!note] +> note + +> [!info] +> info + +> [!tip] +> tip + +> [!warning] +> warning + +> [!caution] +> caution + +> [!important] +> important + +**code demo:** + +::: normal-demo Demo 演示 + +```html +

Hello Word!

+

VeryPowerful!

+``` + +```js +document.querySelector('#very').addEventListener('click', () => { + alert('Very Powerful') +}) +``` + +```css +span { + color: red; +} +``` + +::: + +**tab card:** + +::: tabs +@tab title 1 +content block + +@tab title 2 +content block +::: + +:::: warning +::: tabs +@tab title 1 +content block + +@tab title 2 +content block +::: +:::: + +**footnote:** + +footnote 1 link[^first]。 + +footnote 2 link[^second]。 + +inline footnote ^[^first] definition。 + +Repeated footnote definition[^second]。 + +[^first]: footnote **you can contain special mark** + + also can contain paragraph + +[^second]: footnote content. diff --git a/cli/templates/docs/zh/README.md.handlebars b/cli/templates/docs/zh/README.md.handlebars new file mode 100644 index 00000000..ba8e79f1 --- /dev/null +++ b/cli/templates/docs/zh/README.md.handlebars @@ -0,0 +1,22 @@ +--- +pageLayout: home +externalLinkIcon: false +config: + - + type: hero + full: true + background: tint-plate + hero: + name: Theme Plume + tagline: VuePress Next Theme + text: 一个简约的,功能丰富的 vuepress 文档&博客 主题 + actions: + - + theme: brand + text: 博客 + link: {{#if (equal defaultLanguage 'zh-CN')}}/{{else}}/zh/{{/if}}blog/ + - + theme: alt + text: Github → + link: https://github.com/pengzhanbo/vuepress-theme-plume +--- diff --git a/cli/templates/docs/zh/notes/demo/README.md b/cli/templates/docs/zh/notes/demo/README.md new file mode 100644 index 00000000..e7adb9c2 --- /dev/null +++ b/cli/templates/docs/zh/notes/demo/README.md @@ -0,0 +1,6 @@ +--- +title: Demo +--- + +- [bar](./bar.md) +- [foo](./foo.md) diff --git a/cli/templates/docs/zh/notes/demo/bar.md b/cli/templates/docs/zh/notes/demo/bar.md new file mode 100644 index 00000000..12db1219 --- /dev/null +++ b/cli/templates/docs/zh/notes/demo/bar.md @@ -0,0 +1,5 @@ +--- +title: bar +--- + +[foo](./foo.md) diff --git a/cli/templates/docs/zh/notes/demo/foo.md b/cli/templates/docs/zh/notes/demo/foo.md new file mode 100644 index 00000000..5ca76e68 --- /dev/null +++ b/cli/templates/docs/zh/notes/demo/foo.md @@ -0,0 +1,5 @@ +--- +title: foo +--- + +[bar](./bar.md) diff --git a/cli/templates/docs/zh/preview/custom-component.example.md b/cli/templates/docs/zh/preview/custom-component.example.md new file mode 100644 index 00000000..2e353d5d --- /dev/null +++ b/cli/templates/docs/zh/preview/custom-component.example.md @@ -0,0 +1,8 @@ +--- +title: 自定义组件 +tags: + - 预览 + - 组件 +--- + + diff --git a/cli/templates/docs/zh/preview/markdown.md b/cli/templates/docs/zh/preview/markdown.md new file mode 100644 index 00000000..002feb80 --- /dev/null +++ b/cli/templates/docs/zh/preview/markdown.md @@ -0,0 +1,312 @@ +--- +title: Markdown +tags: + - markdown +--- + +## 标题 2 + +### 标题 3 + +#### 标题 4 + +##### 标题 5 + +###### 标题 6 + +加粗:**加粗文字** + +斜体: _斜体文字_ + +~~删除文字~~ + +内容 ==标记== + +数学表达式: $-(2^{n-1})$ ~ $2^{n-1} -1$ + +$\frac {\partial^r} {\partial \omega^r} \left(\frac {y^{\omega}} {\omega}\right) += \left(\frac {y^{\omega}} {\omega}\right) \left\{(\log y)^r + \sum_{i=1}^r \frac {(-1)^ Ir \cdots (r-i+1) (\log y)^{ri}} {\omega^i} \right\}$ + +19^th^ + +H~2~O + +::: center +内容居中 +::: + +::: right +内容右对齐 +::: + +- 无序列表1 +- 无序列表2 +- 无序列表3 + +1. 有序列表1 +2. 有序列表2 +3. 有序列表3 + +- [ ] 任务列表1 +- [ ] 任务列表2 +- [x] 任务列表3 +- [x] 任务列表4 + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +> 引用内容 +> +> 引用内容 + +[链接](/) + +[外部链接](https://github.com/pengzhanbo) + +**Badge:** + +- +- +- +- + +**图标:** + +- home - +- vscode - +- twitter - + +**demo wrapper:** + +::: demo-wrapper title="示例" no-padding height="200px" + + +
+
main
+
aside
+
+ +::: + +**代码:** + +```js whitespace +const a = 1 +const b = 2 +const c = a + b + +// [!code word:obj] +const obj = { + toLong: { + deep: { + deep: { + deep: { + value: 'this is to long text. this is to long text. this is to long text. this is to long text.', // [!code highlight] + } + } + } + } +} +``` + +**代码分组:** + +::: code-tabs +@tab tab1 + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +@tab tab2 + +```ts +const a: number = 1 +const b: number = 2 +const c: number = a + b +``` + +::: + +**代码块高亮:** + +```ts +function foo() { + const a = 1 // [!code highlight] + + console.log(a) + + const b = 2 // [!code ++] + const c = 3 // [!code --] + + console.log(a + b + c) // [!code error] + console.log(a + b) // [!code warning] +} +``` + +**代码块聚焦:** + +```ts +function foo() { + const a = 1 // [!code focus] +} +``` + +::: note 注释 +注释内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: info 信息 +信息内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: tip 提示 +提示内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: warning 警告 +警告内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: caution 错误 +错误内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +::: important 重要 +重要内容 [link](https://github.com/pengzhanbo) `inline code` + +```js +const a = 1 +const b = 2 +const c = a + b +``` + +::: + +**GFM alert:** + +> [!note] +> note + +> [!info] +> info + +> [!tip] +> tip + +> [!warning] +> warning + +> [!caution] +> caution + +> [!important] +> important + +**代码演示:** + +::: normal-demo Demo 演示 + +```html +

Hello Word!

+

非常强大!

+``` + +```js +document.querySelector('#very').addEventListener('click', () => { + alert('非常强大') +}) +``` + +```css +span { + color: red; +} +``` + +::: + +**选项卡:** + +::: tabs +@tab 标题1 +内容区块 + +@tab 标题2 +内容区块 +::: + +:::: warning +::: tabs +@tab 标题1 +内容区块 + +@tab 标题2 +内容区块 +::: +:::: + +**脚注:** + +脚注 1 链接[^first]。 + +脚注 2 链接[^second]。 + +行内的脚注^[行内脚注文本] 定义。 + +重复的页脚定义[^second]。 + +[^first]: 脚注 **可以包含特殊标记** + + 也可以由多个段落组成 + +[^second]: 脚注文字。 diff --git a/cli/templates/git/.gitattributes.handlebars b/cli/templates/git/.gitattributes.handlebars new file mode 100644 index 00000000..d022441a --- /dev/null +++ b/cli/templates/git/.gitattributes.handlebars @@ -0,0 +1,10 @@ +* text eol=lf +*.txt text eol=crlf + +*.png binary +*.jpg binary +*.jpeg binary +*.ico binary +*.tff binary +*.woff binary +*.woff2 binary diff --git a/cli/templates/git/.gitignore.handlebars b/cli/templates/git/.gitignore.handlebars new file mode 100644 index 00000000..5b4a3211 --- /dev/null +++ b/cli/templates/git/.gitignore.handlebars @@ -0,0 +1,8 @@ +node_modules + +{{ docsDir }}/.vuepress/.cache +{{ docsDir }}/.vuepress/.temp +{{ docsDir }}/.vuepress/dist + +.DS_Store +*.log diff --git a/cli/tsup.config.ts b/cli/tsup.config.ts new file mode 100644 index 00000000..4388f109 --- /dev/null +++ b/cli/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup' +import { version } from './package.json' + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'lib', + dts: true, + format: 'esm', + sourcemap: false, + splitting: false, + clean: true, + define: { + __CLI_VERSION__: JSON.stringify(version), + }, +}) diff --git a/commitlint.config.js b/commitlint.config.js index acb17f0b..1a4af805 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -9,7 +9,7 @@ const packages = fs.readdirSync(path.resolve(__dirname, 'plugins')) export default { extends: ['@commitlint/config-conventional'], rules: { - 'scope-enum': [2, 'always', ['docs', 'theme', ...packages]], + 'scope-enum': [2, 'always', ['docs', 'theme', 'cli', ...packages]], 'footer-max-line-length': [0], }, } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e119e4ac..0dea1480 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,27 @@ importers: specifier: ^8.0.0 version: 8.0.0 + cli: + dependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 + '@pengzhanbo/utils': + specifier: ^1.1.2 + version: 1.1.2 + cac: + specifier: ^6.7.14 + version: 6.7.14 + execa: + specifier: ^9.3.1 + version: 9.3.1 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + picocolors: + specifier: ^1.0.1 + version: 1.0.1 + docs: dependencies: '@iconify/json': @@ -463,6 +484,14 @@ packages: '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@clack/core@0.3.4': + resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} + + '@clack/prompts@0.7.0': + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + bundledDependencies: + - is-unicode-supported + '@commitlint/cli@19.4.0': resolution: {integrity: sha512-sJX4J9UioVwZHq7JWM9tjT5bgWYaIN3rC4FP7YwfEwBYiIO+wMyRttRvQLNkow0vCdM0D67r9NEWU0Ui03I4Eg==} engines: {node: '>=v18'} @@ -2965,10 +2994,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -3204,6 +3229,10 @@ packages: resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} engines: {node: ^18.19.0 || >=20.5.0} + execa@9.3.1: + resolution: {integrity: sha512-gdhefCCNy/8tpH/2+ajP9IQc14vXchNdd0weyzSJEFURhRMGncQ+zKFxwjAufIewPEJm9BPOaJnvg2UtlH2gPQ==} + engines: {node: ^18.19.0 || >=20.5.0} + expand-tilde@2.0.2: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} @@ -3581,6 +3610,10 @@ packages: resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} engines: {node: '>=18.18.0'} + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} + engines: {node: '>=18.18.0'} + husky@9.1.5: resolution: {integrity: sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==} engines: {node: '>=18'} @@ -4629,9 +4662,6 @@ packages: resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} engines: {node: '>= 0.12.0'} - picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -5868,6 +5898,17 @@ snapshots: '@braintree/sanitize-url@6.0.4': {} + '@clack/core@0.3.4': + dependencies: + picocolors: 1.0.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.7.0': + dependencies: + '@clack/core': 0.3.4 + picocolors: 1.0.1 + sisteransi: 1.0.5 + '@commitlint/cli@19.4.0(@types/node@20.12.10)(typescript@5.5.4)': dependencies: '@commitlint/format': 19.3.0 @@ -5999,7 +6040,7 @@ snapshots: '@conventional-changelog/git-client@1.0.0(conventional-commits-parser@6.0.0)': dependencies: '@types/semver': 7.5.8 - semver: 7.6.0 + semver: 7.6.3 optionalDependencies: conventional-commits-parser: 6.0.0 @@ -8496,8 +8537,6 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 - escalade@3.1.1: {} - escalade@3.1.2: {} escape-string-regexp@1.0.5: {} @@ -8838,6 +8877,21 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.0.0 + execa@9.3.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.3 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.0 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.0.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.0.0 + expand-tilde@2.0.2: dependencies: homedir-polyfill: 1.0.3 @@ -9266,6 +9320,8 @@ snapshots: human-signals@7.0.0: {} + human-signals@8.0.0: {} + husky@9.1.5: {} iconv-lite@0.4.24: @@ -10481,8 +10537,6 @@ snapshots: photoswipe@5.4.4: {} - picocolors@1.0.0: {} - picocolors@1.0.1: {} picomatch@2.3.1: {} @@ -10570,7 +10624,7 @@ snapshots: postcss@8.4.38: dependencies: nanoid: 3.3.7 - picocolors: 1.0.0 + picocolors: 1.0.1 source-map-js: 1.2.0 postcss@8.4.40: @@ -11330,8 +11384,8 @@ snapshots: update-browserslist-db@1.0.13(browserslist@4.23.0): dependencies: browserslist: 4.23.0 - escalade: 3.1.1 - picocolors: 1.0.0 + escalade: 3.1.2 + picocolors: 1.0.1 uri-js@4.4.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 012a5c0f..fa905b97 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: - docs - theme + - cli - plugins/* diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts index e7834a72..5d5d1927 100644 --- a/theme/src/node/theme.ts +++ b/theme/src/node/theme.ts @@ -1,4 +1,5 @@ import type { Page, Theme } from 'vuepress/core' +import { sleep } from '@pengzhanbo/utils' import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js' import { getPlugins } from './plugins/index.js' import { extendsPageData, setupPage } from './setupPages.js' @@ -47,9 +48,10 @@ export function plumeTheme(options: PlumeThemeOptions = {}): Theme { }, }) - waitForConfigLoaded().then(({ autoFrontmatter }) => { + waitForConfigLoaded().then(async ({ autoFrontmatter }) => { autoFrontmatter ??= pluginOptions.frontmatter if (autoFrontmatter !== false) { + await sleep(100) generateAutoFrontmatter(app) } }) diff --git a/tsconfig.json b/tsconfig.json index ec56fd1f..6505ea0b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,7 @@ "plugins/**/*", "theme/**/*", "docs/.vuepress/**/*", - "scripts/**/*" + "cli/**/*" ], "exclude": [ "**/node_modules/**",