diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs index 66130dc183..2ec0c0f2c9 100644 --- a/.commitlintrc.cjs +++ b/.commitlintrc.cjs @@ -8,11 +8,16 @@ const getSubDirectories = (dir) => const pluginPackages = getSubDirectories(path.resolve(__dirname, 'plugins')) const themePackages = getSubDirectories(path.resolve(__dirname, 'themes')) +const toolsPackages = getSubDirectories(path.resolve(__dirname, 'tools')) module.exports = { extends: ['@commitlint/config-conventional'], rules: { - 'scope-enum': [2, 'always', ['e2e', ...pluginPackages, ...themePackages]], + 'scope-enum': [ + 2, + 'always', + ['e2e', ...pluginPackages, ...themePackages, ...toolsPackages], + ], 'footer-max-line-length': [0], }, } diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e36154fb2a..cb61b75740 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -21,9 +21,16 @@ module.exports = { }, }, { - files: ['**/e2e/**/*.cy.ts', '**/e2e/cypress/**/*.ts'], + files: ['e2e/**/*.cy.ts', 'e2e/cypress/**/*.ts'], extends: 'plugin:cypress/recommended', }, + { + files: ['tools/create-helper/**/*.ts', 'tools/create-helper/**/*.vue'], + rules: { + 'import/no-extraneous-dependencies': 'off', + 'import/named': 'off', + }, + }, { files: ['**/tests/**/*.ts'], rules: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0e2240c24..c78cc53284 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -389,6 +389,22 @@ importers: specifier: 2.0.0-rc.2 version: 2.0.0-rc.2(@vuepress/bundler-vite@2.0.0-rc.2)(@vuepress/bundler-webpack@2.0.0-rc.2)(typescript@5.3.3)(vue@3.4.15) + tools/create-vuepress: + dependencies: + cac: + specifier: ^6.7.14 + version: 6.7.14 + execa: + specifier: ^8.0.1 + version: 8.0.1 + inquirer: + specifier: ^9.2.12 + version: 9.2.12 + devDependencies: + '@types/inquirer': + specifier: 9.0.7 + version: 9.0.7 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -2446,7 +2462,6 @@ packages: engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.5 - dev: true /@mdit-vue/plugin-component@2.0.0: resolution: {integrity: sha512-cTRxlocav/+mfgDcp0P2z/gWuWBez+iNuN4D+b74LpX4AR6UAx2ZvWtCrUZ8VXrO4eCt1/G0YC/Af7mpIb3aoQ==} @@ -3118,6 +3133,13 @@ packages: dependencies: '@types/node': 20.11.6 + /@types/inquirer@9.0.7: + resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} + dependencies: + '@types/through': 0.0.33 + rxjs: 7.8.1 + dev: true + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3228,6 +3250,12 @@ packages: dependencies: '@types/node': 20.11.6 + /@types/through@0.0.33: + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + dependencies: + '@types/node': 20.11.6 + dev: true + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false @@ -3933,7 +3961,6 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 - dev: true /ansi-escapes@6.2.0: resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} @@ -4241,7 +4268,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true /base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} @@ -4292,7 +4318,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true /blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -4387,7 +4412,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} @@ -4526,7 +4550,6 @@ packages: /chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -4598,7 +4621,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} @@ -4638,7 +4660,6 @@ packages: /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - dev: true /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -4660,7 +4681,6 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - dev: true /cmd-shim@6.0.2: resolution: {integrity: sha512-+FFYbB0YLaAkhkcrjkyNLYDiOsFSfRjwjY19LXk/psmMx1z00xlCv7hhQoTGXXIKi+YXHL/iiFo8NqMVQX9nOw==} @@ -5242,7 +5262,6 @@ packages: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: clone: 1.0.4 - dev: true /define-data-property@1.1.1: resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} @@ -5432,7 +5451,6 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -5629,7 +5647,6 @@ packages: /escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - dev: true /eslint-compat-utils@0.1.2(eslint@8.56.0): resolution: {integrity: sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==} @@ -6123,7 +6140,6 @@ packages: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 - dev: true /extglob@2.0.4: resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} @@ -6218,7 +6234,6 @@ packages: dependencies: escape-string-regexp: 5.0.0 is-unicode-supported: 1.3.0 - dev: true /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -7074,7 +7089,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore-walk@6.0.4: resolution: {integrity: sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==} @@ -7156,7 +7170,6 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - dev: true /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} @@ -7292,7 +7305,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-fullwidth-code-point@4.0.0: resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} @@ -7323,7 +7335,6 @@ packages: /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - dev: true /is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} @@ -7456,7 +7467,6 @@ packages: /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - dev: true /is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} @@ -7974,7 +7984,6 @@ packages: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true /log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} @@ -8403,7 +8412,6 @@ packages: /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -8762,7 +8770,6 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true /ora@8.0.1: resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} @@ -8781,7 +8788,6 @@ packages: /os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - dev: true /ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} @@ -9607,7 +9613,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} @@ -9698,7 +9703,6 @@ packages: /run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} engines: {node: '>=0.12.0'} - dev: true /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -9709,7 +9713,6 @@ packages: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: tslib: 2.6.2 - dev: true /safe-array-concat@1.1.0: resolution: {integrity: sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==} @@ -10326,7 +10329,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} @@ -10621,7 +10623,6 @@ packages: engines: {node: '>=0.6.0'} dependencies: os-tmpdir: 1.0.2 - dev: true /tmp@0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} @@ -10775,7 +10776,6 @@ packages: /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - dev: true /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} @@ -11304,7 +11304,6 @@ packages: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.4 - dev: true /web-streams-polyfill@3.3.2: resolution: {integrity: sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==} @@ -11672,7 +11671,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d3c07ecab9..bb6afc94ff 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - e2e - plugins/* - themes/* + - tools/* diff --git a/tools/create-vuepress/package.json b/tools/create-vuepress/package.json new file mode 100644 index 0000000000..eee147b151 --- /dev/null +++ b/tools/create-vuepress/package.json @@ -0,0 +1,59 @@ +{ + "name": "create-vuepress", + "version": "2.0.0-rc.0", + "description": "VuePress template helper", + "keywords": [ + "vuepress", + "create", + "create-vuepress", + "template" + ], + "homepage": "https://github.com/vuepress", + "bugs": { + "url": "https://github.com/vuepress/ecosystem/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/ecosystem.git" + }, + "license": "MIT", + "author": { + "name": "Mr.Hope", + "email": "mister-hope@outlook.com", + "url": "https://mister-hope.com" + }, + "type": "module", + "exports": { + ".": "./lib/index.js", + "./package.json": "./package.json" + }, + "main": "./lib/index.js", + "bin": { + "create-vuepress": "./lib/index.js" + }, + "files": [ + "lib", + "template" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf lib *.tsbuildinfo" + }, + "dependencies": { + "cac": "^6.7.14", + "execa": "^8.0.1", + "inquirer": "^9.2.12" + }, + "devDependencies": { + "@types/inquirer": "9.0.7" + }, + "engines": { + "node": ">=18.16.0", + "npm": ">=8", + "pnpm": ">=7", + "yarn": ">=2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/tools/create-vuepress/src/action.ts b/tools/create-vuepress/src/action.ts new file mode 100644 index 0000000000..892b707500 --- /dev/null +++ b/tools/create-vuepress/src/action.ts @@ -0,0 +1,154 @@ +#!/usr/bin/env node +import { existsSync, readdirSync } from 'node:fs' +import { resolve } from 'node:path' +import { execaCommand, execaCommandSync } from 'execa' +import inquirer from 'inquirer' +import { KNOWN_THEME_COMMANDS } from './config/index.js' +import { createPackageJson, generateTemplate } from './flow/index.js' +import { getLanguage } from './i18n/index.js' +import { + ensureDirExistSync, + getPackageManager, + getRegistry, + normalizeThemeName, +} from './utils/index.js' + +interface CreateOptions { + bundler?: 'vite' | 'webpack' | null + preset?: 'docs' | 'blog' | null + theme?: string +} + +export const mainAction = async ( + targetDir: string, + { + bundler = null, + preset = null, + theme = '@vuepress/theme-default', + }: CreateOptions, +): Promise => { + // get language + const { lang, locale } = await getLanguage() + + // get packageManager + const packageManager = await getPackageManager(locale.question.packageManager) + + // handle theme + const themePackageName = normalizeThemeName(theme) + + if (KNOWN_THEME_COMMANDS[themePackageName]) { + await execaCommandSync( + `${packageManager} ${KNOWN_THEME_COMMANDS[themePackageName]} ${targetDir}`, + { stdio: 'inherit' }, + ) + + return + } + + if (theme !== '@vuepress/theme-default') console.warn(locale.error.theme) + + // check bundler + if (bundler && !['vite', 'webpack'].includes(bundler)) + return console.log(locale.error.bundler) + + // check presets + if (preset && !['docs', 'blog'].includes(preset)) + return console.log(locale.error.preset) + + // check if the user is a noob and warn him + if (!targetDir || (targetDir.startsWith('[') && targetDir.endsWith(']'))) + return console.log(locale.error.dirMissing(packageManager)) + + const targetDirPath = resolve(process.cwd(), targetDir) + + // check if the user is trying to cover his files + if (existsSync(targetDirPath) && readdirSync(targetDirPath).length) + return console.error(locale.error.dirNotEmpty(targetDir)) + + ensureDirExistSync(targetDirPath) + + // complete bundler + if (!bundler) + bundler = ( + await inquirer.prompt<{ bundler: 'vite' | 'webpack' }>([ + { + name: 'bundler', + type: 'list', + message: locale.question.bundler, + choices: ['vite', 'webpack'], + }, + ]) + ).bundler + + // complete preset + if (!preset) + preset = ( + await inquirer.prompt<{ preset: 'blog' | 'docs' }>([ + { + name: 'preset', + type: 'list', + message: locale.question.preset, + choices: ['blog', 'docs'], + }, + ]) + ).preset + + /* + * Generate template + */ + + await createPackageJson({ + targetDir, + packageManager, + locale, + preset, + bundler, + }) + await generateTemplate({ + targetDirPath, + packageManager, + lang, + locale, + preset, + bundler, + }) + + /* + * Install deps + */ + const registry = + packageManager === 'pnpm' ? '' : await getRegistry(packageManager, lang) + + console.log(locale.flow.install) + console.warn(locale.hint.install) + + execaCommandSync( + `${packageManager} install ${registry ? `--registry ${registry}` : ''}`, + { cwd: targetDirPath, stdout: 'inherit' }, + ) + + console.log(locale.hint.finish) + + /* + * Open dev server + */ + const { devServer } = await inquirer.prompt<{ devServer: boolean }>([ + { + name: 'devServer', + type: 'confirm', + message: locale.question.devServer, + default: true, + }, + ]) + + if (devServer) { + console.log(locale.flow.devServer) + + await execaCommand(`${packageManager} run docs:dev`, { + cwd: targetDir, + stdout: 'inherit', + }) + } else { + console.log(locale.hint.devServer(packageManager)) + } +} diff --git a/tools/create-vuepress/src/config/index.ts b/tools/create-vuepress/src/config/index.ts new file mode 100644 index 0000000000..50be8f5dfc --- /dev/null +++ b/tools/create-vuepress/src/config/index.ts @@ -0,0 +1,3 @@ +export const KNOWN_THEME_COMMANDS = { + hope: 'create-vuepress-theme-hope', +} diff --git a/tools/create-vuepress/src/flow/createPackageJson.ts b/tools/create-vuepress/src/flow/createPackageJson.ts new file mode 100644 index 0000000000..ae80b44bf4 --- /dev/null +++ b/tools/create-vuepress/src/flow/createPackageJson.ts @@ -0,0 +1,108 @@ +import { writeFileSync } from 'node:fs' +import { join } from 'node:path' +import inquirer from 'inquirer' +import type { CreateLocaleOptions } from '../i18n/index.js' +import type { PackageManager } from '../utils/index.js' + +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 + +interface CreatePackageJsonOptions { + targetDir: string + packageManager: PackageManager + locale: CreateLocaleOptions + preset: 'blog' | 'docs' + bundler: 'vite' | 'webpack' +} + +interface PackageJsonAnswer { + name: string + version: string + description: string + license: string +} + +/** + * generate package.json + */ +export const createPackageJson = async ({ + targetDir, + packageManager, + locale, + preset, + bundler, +}: CreatePackageJsonOptions): Promise => { + const packageJsonPath = join(targetDir, 'package.json') + // TODO: Update it + const devDependencies = { + '@vuepress/client': '^2.0.0-rc.0', + [`@vuepress/bundler-${bundler}`]: '^2.0.0-rc.0', + '@vuepress/theme-default': '^2.0.0-rc.0', + 'vue': '^3.4.0', + 'vuepress': '^2.0.0-rc.0', + } + + if (preset === 'blog') { + devDependencies['@vuepress/core'] = '^2.0.0-rc.0' + devDependencies['vue-router'] = '^4.2.5' + } + + console.log(locale.flow.createPackage) + + const result = await inquirer.prompt([ + { + name: 'name', + type: 'input', + message: locale.question.name, + default: 'my-vuepress-site', + validate: (input: string): true | string => + PACKAGE_NAME_REG.exec(input) ? true : locale.error.name, + }, + { + name: 'version', + type: 'input', + message: locale.question.version, + default: '0.0.1', + validate: (input: string): true | string => + VERSION_REG.exec(input) ? true : locale.error.version, + }, + { + name: 'description', + type: 'input', + message: locale.question.description, + default: 'A VuePress project', + }, + { + name: 'license', + type: 'input', + message: locale.question.license, + default: 'MIT', + }, + ]) + + const packageContent = { + ...result, + type: 'module', + scripts: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'docs:build': `vuepress build src`, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'docs:clean-dev': `vuepress dev src --clean-cache`, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'docs:dev': `vuepress dev src`, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'docs:update-package': `${ + packageManager === 'npm' ? 'npx' : `${packageManager} dlx` + } vp-update`, + }, + devDependencies, + } + + writeFileSync( + packageJsonPath, + `${JSON.stringify(packageContent, null, 2)}\n`, + { encoding: 'utf-8' }, + ) +} diff --git a/tools/create-vuepress/src/flow/generateTemplate.ts b/tools/create-vuepress/src/flow/generateTemplate.ts new file mode 100644 index 0000000000..e2e5e19194 --- /dev/null +++ b/tools/create-vuepress/src/flow/generateTemplate.ts @@ -0,0 +1,159 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import inquirer from 'inquirer' +import type { CreateLocaleOptions, Lang } from '../i18n/index.js' +import type { PackageManager } from '../utils/index.js' +import { copy, ensureDirExistSync } from '../utils/index.js' + +const templateFolder = join( + dirname( + createRequire(import.meta.url).resolve('create-vuepress/package.json'), + ), + './template', +) + +const getWorkflowContent = ( + packageManager: PackageManager, + lang: Lang, +): string => + ` +name: ${lang === '简体中文' ? '部署文档' : 'Deploy Docs'} + +on: + push: + branches: + # ${ + lang === '简体中文' + ? '确保这是你正在使用的分支名称' + : 'make sure this is the branch you are using' + } + - main + +permissions: + contents: write + +jobs: + deploy-gh-pages: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + # ${ + lang === '简体中文' + ? '如果你文档需要 Git 子模块,取消注释下一行' + : 'if your docs needs submodules, uncomment the following line' + } + # submodules: true + +${ + packageManager === 'pnpm' + ? `\ + - name: ${lang === '简体中文' ? '安装 pnpm' : 'Install pnpm'} + uses: pnpm/action-setup@v2 + with: + run_install: true + version: 8 +` + : '' +} + + - name: ${lang === '简体中文' ? '设置 Node.js' : 'Setup Node.js'} + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: ${packageManager} + +${ + packageManager !== 'pnpm' + ? `\ + - name: ${lang === '简体中文' ? '安装依赖' : 'Install Deps'} + run: ${ + packageManager === 'npm' + ? 'npm ci' + : `${packageManager} install --frozen-lockfile` + } +` + : '' +} + - name: ${lang === '简体中文' ? '构建文档' : 'Build Docs'} + env: + NODE_OPTIONS: --max_old_space_size=8192 + run: |- + ${packageManager} run docs:build + > src/.vuepress/dist/.nojekyll + + - name: ${lang === '简体中文' ? '部署文档' : 'Deploy Docs'} + uses: JamesIves/github-pages-deploy-action@v4 + with: + # ${ + lang === '简体中文' + ? '这是文档部署到的分支名称' + : 'This is the branch where the docs are deployed to' + } + branch: gh-pages + folder: src/.vuepress/dist +` + +interface GenerateTemplateOptions { + targetDirPath: string + lang: Lang + packageManager: PackageManager + locale: CreateLocaleOptions + preset: 'blog' | 'docs' + bundler: 'vite' | 'webpack' +} + +export const generateTemplate = async ({ + targetDirPath, + packageManager, + lang, + locale, + preset, + bundler, +}: GenerateTemplateOptions): Promise => { + const { workflow } = await inquirer.prompt<{ + workflow: boolean + }>([ + { + name: 'workflow', + type: 'confirm', + message: locale.question.workflow, + default: true, + }, + ]) + + console.log(locale.flow.generateTemplate) + + // copy template + copy(join(templateFolder, preset), join(targetDirPath, 'src')) + + const configFilePath = join(targetDirPath, 'src/.vuepress/config.js') + + const content = readFileSync(configFilePath, { encoding: 'utf-8' }) + + writeFileSync( + configFilePath, + content + .replace( + /\n\nexport default defineUserConfig\(\{/, + `\nimport { ${bundler}Bundler } from '@vuepress/bundler-${bundler}'\n\nexport default defineUserConfig({`, + ) + .replace(/\}\)\n$/, `\n bundler: ${bundler}Bundler(),\n})\n`), + { encoding: 'utf-8' }, + ) + + if (workflow) { + const workflowDir = join(targetDirPath, '.github/workflows') + + ensureDirExistSync(workflowDir) + + writeFileSync( + join(workflowDir, 'deploy-docs.yml'), + getWorkflowContent(packageManager, lang), + { encoding: 'utf-8' }, + ) + } +} diff --git a/tools/create-vuepress/src/flow/index.ts b/tools/create-vuepress/src/flow/index.ts new file mode 100644 index 0000000000..b37e9a38c7 --- /dev/null +++ b/tools/create-vuepress/src/flow/index.ts @@ -0,0 +1,2 @@ +export * from './createPackageJson.js' +export * from './generateTemplate.js' diff --git a/tools/create-vuepress/src/i18n/en.ts b/tools/create-vuepress/src/i18n/en.ts new file mode 100644 index 0000000000..6e8202ec96 --- /dev/null +++ b/tools/create-vuepress/src/i18n/en.ts @@ -0,0 +1,47 @@ +import type { PackageManager } from '../utils/index.js' +import type { CreateLocaleOptions } from './typings.js' + +export const en: CreateLocaleOptions = { + flow: { + getVersion: 'Getting latest version of deps...', + createPackage: 'Generating package.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?', + 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: PackageManager): string => + `Hint: You should execute "${packageManager} run docs:dev" to start dev server.`, + }, + + 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 "doc" or "blog"', + theme: + 'Current theme is not supported yet, using @vuepress/theme-default instead.', + dirMissing: (packageManager: PackageManager): string => + `You should specific a folder name for your project, e.g.: "my-blog", "my-docs"\nFor example: "${packageManager} init vuepress my-docs"`, + dirNotEmpty: (dir: string) => + `Target folder "${dir}" is not empty, please choose an empty folder or delete files in it.`, + }, +} diff --git a/tools/create-vuepress/src/i18n/index.ts b/tools/create-vuepress/src/i18n/index.ts new file mode 100644 index 0000000000..9c15b74849 --- /dev/null +++ b/tools/create-vuepress/src/i18n/index.ts @@ -0,0 +1,32 @@ +import inquirer from 'inquirer' +import { en } from './en.js' +import type { CreateLocaleOptions, Lang } from './typings.js' +import { zh } from './zh.js' + +export * from './typings.js' + +const i18n: Record = { + 'english (US)': en, + '简体中文': zh, +} + +interface LanguageResult { + lang: Lang + locale: CreateLocaleOptions +} + +export const getLanguage = async (): Promise => { + const { language } = await inquirer.prompt<{ language: Lang }>([ + { + name: 'language', + type: 'list', + message: 'Select a language to display / 选择显示语言', + choices: ['english (US)', '简体中文'], + }, + ]) + + return { + lang: language, + locale: i18n[language], + } +} diff --git a/tools/create-vuepress/src/i18n/typings.ts b/tools/create-vuepress/src/i18n/typings.ts new file mode 100644 index 0000000000..45579f1263 --- /dev/null +++ b/tools/create-vuepress/src/i18n/typings.ts @@ -0,0 +1,42 @@ +import type { PackageManager } from '../utils/index.js' + +export type Lang = 'english (US)' | '简体中文' + +export interface CreateLocaleOptions { + flow: { + getVersion: string + createPackage: string + generateTemplate: string + install: string + devServer: string + } + + question: { + i18n: string + packageManager: string + name: string + version: string + description: string + license: string + workflow: string + bundler: string + preset: string + devServer: string + } + + hint: { + install: string + finish: string + devServer: (packageManager: PackageManager) => string + } + + error: { + name: string + version: string + bundler: string + preset: string + theme: string + dirMissing: (packageManager: PackageManager) => string + dirNotEmpty: (targetDir: string) => string + } +} diff --git a/tools/create-vuepress/src/i18n/zh.ts b/tools/create-vuepress/src/i18n/zh.ts new file mode 100644 index 0000000000..12c6addc5e --- /dev/null +++ b/tools/create-vuepress/src/i18n/zh.ts @@ -0,0 +1,46 @@ +import type { PackageManager } from '../utils/index.js' +import type { CreateLocaleOptions } from './typings.js' + +export const zh: CreateLocaleOptions = { + flow: { + getVersion: '获取依赖的最新版本...', + createPackage: '生成 package.json...', + generateTemplate: '生成模板...', + install: '安装依赖...', + devServer: + "启动开发服务器...\n启动成功后,请在浏览器输入给出的开发服务器地址(默认为 'localhost:8080')", + }, + + question: { + packageManager: '选择包管理器', + i18n: '项目需要用到多语言么?', + workflow: '是否需要一个自动部署文档到 GitHub Pages 的工作流?', + bundler: '你想要使用哪个打包器?', + preset: '你想要创建什么类型的项目?', + devServer: '是否想要现在启动 Demo 查看?', + name: '设置应用名称', + version: '设置应用版本号', + description: '设置应用描述', + license: '设置协议', + }, + + hint: { + install: + '这可能需要数分钟,请耐心等待.\n我们无法正确输出子进程的进度条,所以进程可能会看似未响应', + finish: '模板已成功生成!', + devServer: (packageManager: PackageManager): string => + `提示: 请使用 "${packageManager} run docs:dev" 命令启动开发服务器`, + }, + + error: { + name: '应用名称应只包含小写字母、数字和连接线 (-)', + version: "此版本无效,版本号应为 'x.x.x'", + bundler: '打包器 (--bundler) 仅支持 vite 或 webpack', + preset: '预设 (--preset) 仅支持 doc 或 blog', + theme: '当前主题暂不支持,将使用 @vuepress/theme-default 代替', + dirMissing: (packageManager: PackageManager): string => + `你应该指定一个项目文件夹名称,例如 "my-blog", "my-docs"。\n例如: "${packageManager} init vuepress my-docs"`, + dirNotEmpty: (dir: string) => + `目标文件夹 "${dir}" 不为空,请选择一个空文件夹或者手动删除文件夹中的文件`, + }, +} diff --git a/tools/create-vuepress/src/index.ts b/tools/create-vuepress/src/index.ts new file mode 100644 index 0000000000..a554fe1716 --- /dev/null +++ b/tools/create-vuepress/src/index.ts @@ -0,0 +1,25 @@ +#!/usr/bin/env node +import { cac } from 'cac' +import { mainAction } from './action.js' +import { version } from './utils/index.js' + +const cli = cac('create-vuepress') + +cli + .command('[dir]', 'Generate a new vuepress project in [dir]') + .option('-t, --theme ', 'Theme to use') + .option('-p, --preset ', 'Preset to use, can be docs or blog') + .usage( + `\ +[dir] + +Generate vuepress template in dir.`, + ) + .example('vuepress-project') + .action(mainAction) + +cli.help() + +cli.version(version) + +cli.parse() diff --git a/tools/create-vuepress/src/utils/file.ts b/tools/create-vuepress/src/utils/file.ts new file mode 100644 index 0000000000..f42fe0f969 --- /dev/null +++ b/tools/create-vuepress/src/utils/file.ts @@ -0,0 +1,45 @@ +import { + mkdirSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from 'node:fs' +import { dirname } from 'node:path' + +export const ensureDirExistSync = (dirPath: string): void => { + try { + readdirSync(dirPath) + } catch (err) { + try { + mkdirSync(dirPath, { recursive: true }) + } catch (err) { + // this is the case where the directory already exists but can not read, e.g.: D:\ + } + } +} + +export const copyFile = (srcFile: string, targetFile: string): void => { + const targetDir = dirname(targetFile) + + ensureDirExistSync(targetDir) + writeFileSync(targetFile, readFileSync(srcFile)) +} + +export const copyDir = (srcDir: string, targetDir: string): void => { + ensureDirExistSync(targetDir) + + const files = readdirSync(srcDir, { withFileTypes: true }) + + files.forEach((file) => { + if (file.isFile()) + copyFile(`${srcDir}/${file.name}`, `${targetDir}/${file.name}`) + else if (file.isDirectory()) + copyDir(`${srcDir}/${file.name}`, `${targetDir}/${file.name}`) + }) +} + +export const copy = (src: string, target: string): void => { + if (statSync(src).isDirectory()) copyDir(src, target) + else if (statSync(src).isFile()) copyFile(src, target) +} diff --git a/tools/create-vuepress/src/utils/getPackageManager.ts b/tools/create-vuepress/src/utils/getPackageManager.ts new file mode 100644 index 0000000000..7610f92d88 --- /dev/null +++ b/tools/create-vuepress/src/utils/getPackageManager.ts @@ -0,0 +1,47 @@ +import { execaCommandSync } from 'execa' +import inquirer from 'inquirer' + +const checkPnpmInstalled = (): boolean => { + try { + return ( + execaCommandSync('pnpm --version', { stdio: 'ignore' }).exitCode === 0 + ) + } catch (e) { + return false + } +} + +const checkYarnInstalled = (): boolean => { + try { + return ( + execaCommandSync('yarn --version', { stdio: 'ignore' }).exitCode === 0 + ) + } catch (e) { + return false + } +} + +const availablePackageManagers = ['npm'] + +if (checkYarnInstalled()) availablePackageManagers.unshift('yarn') +if (checkPnpmInstalled()) availablePackageManagers.unshift('pnpm') + +export type PackageManager = 'npm' | 'yarn' | 'pnpm' + +export interface PackageManagerAnswer { + packageManager: PackageManager +} + +export const getPackageManager = async ( + message: string, +): Promise => + ( + await inquirer.prompt([ + { + name: 'packageManager', + type: 'list', + message, + choices: availablePackageManagers, + }, + ]) + ).packageManager diff --git a/tools/create-vuepress/src/utils/getRegistry.ts b/tools/create-vuepress/src/utils/getRegistry.ts new file mode 100644 index 0000000000..a1f9c3e0f3 --- /dev/null +++ b/tools/create-vuepress/src/utils/getRegistry.ts @@ -0,0 +1,58 @@ +import { execaCommandSync } from 'execa' +import inquirer from 'inquirer' +import type { Lang } from '../i18n/index.js' +import type { PackageManager } from './getPackageManager.js' + +interface RegistryAnswer { + registry: '国内镜像源' | '当前源' +} + +const NPM_MIRROR_REGISTRY = 'https://registry.npmmirror.com/' + +const getUserRegistry = ( + packageManager: PackageManager, + isYarnModern: boolean, +): string => + execaCommandSync( + `${packageManager} config get ${ + isYarnModern ? 'npmRegistryServer' : 'registry' + }`, + ).stdout + +export const getRegistry = async ( + packageManager: PackageManager, + lang: Lang, +): Promise => { + const isYarnModern = + packageManager === 'yarn' && + !execaCommandSync('yarn --version').stdout.startsWith('1') + + const userRegistry = getUserRegistry(packageManager, isYarnModern) + + if (/https:\/\/registry\.npm\.taobao\.org\/?/.test(userRegistry)) { + console.error( + 'npm.taobao.org is no longer available, resetting it to npmmirror.com', + ) + + execaCommandSync( + `${packageManager} config set ${ + isYarnModern ? 'npmRegistryServer' : 'registry' + }} ${NPM_MIRROR_REGISTRY}`, + ) + } + + if (lang === '简体中文') { + const { registry } = await inquirer.prompt([ + { + name: 'registry', + type: 'list', + message: '选择你想使用的源', + choices: ['国内镜像源', '当前源'], + }, + ]) + + return registry === '国内镜像源' ? NPM_MIRROR_REGISTRY : userRegistry + } + + return userRegistry +} diff --git a/tools/create-vuepress/src/utils/index.ts b/tools/create-vuepress/src/utils/index.ts new file mode 100644 index 0000000000..68870aa33a --- /dev/null +++ b/tools/create-vuepress/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './file.js' +export * from './getPackageManager.js' +export * from './normalizeThemeName.js' +export * from './getRegistry.js' +export * from './version.js' diff --git a/tools/create-vuepress/src/utils/normalizeThemeName.ts b/tools/create-vuepress/src/utils/normalizeThemeName.ts new file mode 100644 index 0000000000..95d7d7aac5 --- /dev/null +++ b/tools/create-vuepress/src/utils/normalizeThemeName.ts @@ -0,0 +1,25 @@ +const THEME_PREFIX = 'vuepress-theme-' + +/** + * Normalize theme name + */ +export const normalizeThemeName = (name: string): string => { + // scoped package pattern + const scopedMatch = name.match(/^@(.*)\/(.*)$/) + + // handle non-scoped package + if (scopedMatch === null) + return name.startsWith(THEME_PREFIX) ? name : `${THEME_PREFIX}${name}` + + // handle scoped package + const [, reqOrg, reqName] = scopedMatch + + // handle @vuepress/ themes + if (reqOrg === 'vuepress') + return reqName.startsWith('theme-') ? name : `@vuepress/theme-${reqName}` + + // handle other org + return reqName.startsWith(THEME_PREFIX) + ? name + : `@${reqOrg}/${THEME_PREFIX}${reqName}` +} diff --git a/tools/create-vuepress/src/utils/version.ts b/tools/create-vuepress/src/utils/version.ts new file mode 100644 index 0000000000..752b8c3014 --- /dev/null +++ b/tools/create-vuepress/src/utils/version.ts @@ -0,0 +1,7 @@ +import { createRequire } from 'node:module' + +export const version = ( + createRequire(import.meta.url)('create-vuepress/package.json') as { + version: string + } +).version diff --git a/tools/create-vuepress/template/blog/.vuepress/blog-plugin.js b/tools/create-vuepress/template/blog/.vuepress/blog-plugin.js new file mode 100644 index 0000000000..dccdb0f112 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/blog-plugin.js @@ -0,0 +1,111 @@ +import { createPage } from '@vuepress/core' + +const slugify = (name) => + name + .replace(/[ _]/g, '-') + .replace(/[:?*|\\/<>]/g, '') + .toLowerCase() + +const generateCategory = async (app, name) => { + const map = {} + + await app.pages.map(async (page) => { + const value = page.frontmatter[name] + + await (Array.isArray(value) ? value : [value]).map(async (item) => { + if (item) { + if (!map[item]) { + const itemPath = `/${name}/${slugify(item)}/` + + map[item] = { + path: itemPath, + keys: [], + } + + app.pages.push( + await createPage(app, { + path: itemPath, + frontmatter: { + title: `${name} > ${item}`, + key: item, + layout: name[0].toUpperCase() + name.slice(1), + sidebar: false, + }, + }), + ) + } + + map[item].keys.push(page.key) + } + }) + }) + + app.pages.push( + await createPage(app, { + path: `/${name}/`, + frontmatter: { + title: name, + layout: name[0].toUpperCase() + name.slice(1), + sidebar: false, + }, + }), + ) + + await app.writeTemp( + `blog/${name}.js`, + `\ +export const map = ${JSON.stringify(map)}; +`, + ) +} + +const generateType = async ( + app, + name, + { filter = () => true, sorter = () => 0 }, +) => { + app.pages.push( + await createPage(app, { + path: `/${name}/`, + frontmatter: { + title: name, + layout: name[0].toUpperCase() + name.slice(1), + sidebar: false, + }, + }), + ) + + const keys = app.pages.filter(filter).map(({ key }) => key) + + await app.writeTemp( + `blog/${name}.js`, + `\ +export const keys = ${JSON.stringify(keys)}; +`, + ) +} + +export const simpleBlogPlugin = ({ + filter = () => true, + getInfo, + category = [], + type = [], +}) => ({ + name: 'simple-blog-plugin', + + onInitialized: async (app) => { + // inject meta information + app.pages.filter(filter).forEach((page) => { + page.routeMeta = { + ...page.routeMeta, + ...getInfo(page), + } + }) + + // generate pages and temp files + await Promise.all([ + ...category.map((name) => generateCategory(app, name)), + ...type.map(({ key, ...options }) => generateType(app, key, options)), + ]) + }, +}) diff --git a/tools/create-vuepress/template/blog/.vuepress/client.js b/tools/create-vuepress/template/blog/.vuepress/client.js new file mode 100644 index 0000000000..7b2c000713 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/client.js @@ -0,0 +1,15 @@ +import { defineClientConfig } from '@vuepress/client' +import Article from './layouts/Article.vue' +import Category from './layouts/Category.vue' +import Tag from './layouts/Tag.vue' +import Timeline from './layouts/Timeline.vue' + +export default defineClientConfig({ + // we provide some blog layouts + layouts: { + Article, + Category, + Tag, + Timeline, + }, +}) diff --git a/tools/create-vuepress/template/blog/.vuepress/components/ArticleList.vue b/tools/create-vuepress/template/blog/.vuepress/components/ArticleList.vue new file mode 100644 index 0000000000..6f19d76f9b --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/components/ArticleList.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/tools/create-vuepress/template/blog/.vuepress/config.js b/tools/create-vuepress/template/blog/.vuepress/config.js new file mode 100644 index 0000000000..4d5a307198 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/config.js @@ -0,0 +1,88 @@ +import { defaultTheme } from '@vuepress/theme-default' +import { defineUserConfig } from 'vuepress/cli' +import { simpleBlogPlugin } from './blog-plugin.js' + +export default defineUserConfig({ + lang: 'en-US', + + title: 'VuePress', + description: 'My first VuePress Site', + + theme: defaultTheme({ + logo: 'https://vuejs.press/images/hero.png', + + navbar: [ + '/', + { + text: 'Article', + link: '/article/', + }, + { + text: 'Category', + link: '/category/', + }, + { + text: 'Tag', + link: '/tag/', + }, + { + text: 'Timeline', + link: '/timeline/', + }, + ], + }), + + plugins: [ + simpleBlogPlugin({ + // only files under posts are articles + filter: ({ filePathRelative }) => + filePathRelative ? filePathRelative.startsWith('posts/') : false, + + // getting article info + getInfo: ({ frontmatter, title }) => ({ + title, + author: frontmatter.author || '', + date: frontmatter.date || null, + category: frontmatter.category || [], + tag: frontmatter.tag || [], + excerpt: frontmatter.excerpt || '', + }), + + category: ['category', 'tag'], + + type: [ + { + key: 'article', + // remove archive articles + filter: (page) => !page.frontmatter.archive, + + sorter: (pageA, pageB) => { + if (pageA.frontmatter.sticky && pageB.frontmatter.sticky) + return pageB.frontmatter.sticky - pageA.frontmatter.sticky + + if (pageA.frontmatter.sticky && !pageB.frontmatter.sticky) return -1 + + if (!pageA.frontmatter.sticky && pageB.frontmatter.sticky) return 1 + + if (!pageB.frontmatter.date) return 1 + if (!pageA.frontmatter.date) return -1 + + return ( + new Date(pageB.frontmatter.date).getTime() - + new Date(pageA.frontmatter.date).getTime() + ) + }, + }, + { + key: 'timeline', + // only article with date should be added to timeline + filter: (page) => page.frontmatter.date instanceof Date, + // sort pages with time + sorter: (pageA, pageB) => + new Date(pageB.frontmatter.date).getTime() - + new Date(pageA.frontmatter.date).getTime(), + }, + ], + }), + ], +}) diff --git a/tools/create-vuepress/template/blog/.vuepress/layouts/Article.vue b/tools/create-vuepress/template/blog/.vuepress/layouts/Article.vue new file mode 100644 index 0000000000..6ca19a4a88 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/layouts/Article.vue @@ -0,0 +1,22 @@ + + + diff --git a/tools/create-vuepress/template/blog/.vuepress/layouts/Category.vue b/tools/create-vuepress/template/blog/.vuepress/layouts/Category.vue new file mode 100644 index 0000000000..0bcc135395 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/layouts/Category.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/tools/create-vuepress/template/blog/.vuepress/layouts/Tag.vue b/tools/create-vuepress/template/blog/.vuepress/layouts/Tag.vue new file mode 100644 index 0000000000..65df0debd1 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/layouts/Tag.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/tools/create-vuepress/template/blog/.vuepress/layouts/Timeline.vue b/tools/create-vuepress/template/blog/.vuepress/layouts/Timeline.vue new file mode 100644 index 0000000000..b93bd27ea3 --- /dev/null +++ b/tools/create-vuepress/template/blog/.vuepress/layouts/Timeline.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/tools/create-vuepress/template/blog/README.md b/tools/create-vuepress/template/blog/README.md new file mode 100644 index 0000000000..49f0744e02 --- /dev/null +++ b/tools/create-vuepress/template/blog/README.md @@ -0,0 +1,33 @@ +--- +home: true +title: Home +heroImage: https://vuejs.press/images/hero.png +actions: + - text: Get Started + link: /getting-started.html + type: primary + + - text: Introduction + link: https://vuejs.press/guide/introduction.html + type: secondary + +features: + - title: Simplicity First + details: Minimal setup with markdown-centered project structure helps you focus on writing. + - title: Vue-Powered + details: Enjoy the dev experience of Vue, use Vue components in markdown, and develop custom themes with Vue. + - title: Performant + details: VuePress generates pre-rendered static HTML for each page, and runs as an SPA once a page is loaded. + - title: Themes + details: Providing a default theme out of the box. You can also choose a community theme or create your own one. + - title: Plugins + details: Flexible plugin API, allowing plugins to provide lots of plug-and-play features for your site. + - title: Bundlers + details: Default bundler is Vite, while Webpack is also supported. Choose the one you like! + +footer: MIT Licensed | Copyright © 2018-present VuePress Community +--- + +This is the content of home page. Check [Home Page Docs][default-theme-home] for more details. + +[default-theme-home]: https://vuejs.press/reference/default-theme/frontmatter.html#home-page diff --git a/tools/create-vuepress/template/blog/get-started.md b/tools/create-vuepress/template/blog/get-started.md new file mode 100644 index 0000000000..0b7b0319a9 --- /dev/null +++ b/tools/create-vuepress/template/blog/get-started.md @@ -0,0 +1,46 @@ +# Get Started + +This is a normal page, which contains VuePress basics. + +## Pages + +You can add markdown files in your vuepress directory, every markdown file will be converted to a page in your site. + +See [routing][] for more details. + +## Content + +Every markdown file [will be rendered to HTML, then converted to a Vue SFC][content]. + +VuePress support basic markdown syntax and [some extensions][synatex-extensions], you can also [use Vue features][vue-feature] in it. + +## Configuration + +VuePress use a `.vuepress/config.js`(or .ts) file as [site configuration][config], you can use it to config your site. + +For [client side configuration][client-config], you can create `.vuepress/client.js`(or .ts). + +Meanwhile, you can also add configuration per page with [frontmatter][]. + +## Layouts and customization + +Here are common configuration controlling layout of `@vuepress/theme-default`: + +- [navbar][] +- [sidebar][] + +Check [default theme docs][default-theme] for full reference. + +You can [add extra style][style] with `.vuepress/styles/index.scss` file. + +[routing]: https://vuejs.press/guide/page.html#routing +[content]: https://vuejs.press/guide/page.html#content +[synatex-extensions]: https://vuejs.press/guide/markdown.html#syntax-extensions +[vue-feature]: https://vuejs.press/guide/markdown.html#using-vue-in-markdown +[config]: https://vuejs.press/guide/configuration.html#client-config-file +[client-config]: https://vuejs.press/guide/configuration.html#client-config-file +[frontmatter]: https://vuejs.press/guide/page.html#frontmatter +[navbar]: https://vuejs.press/reference/default-theme/config.html#navbar +[sidebar]: https://vuejs.press/reference/default-theme/config.html#sidebar +[default-theme]: https://vuejs.press/reference/default-theme/ +[style]: https://vuejs.press/reference/default-theme/styles.html#style-file diff --git a/tools/create-vuepress/template/blog/posts/archive1.md b/tools/create-vuepress/template/blog/posts/archive1.md new file mode 100644 index 0000000000..fb583eaddc --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/archive1.md @@ -0,0 +1,18 @@ +--- +date: 1998-01-01 +category: + - History +tag: + - WWI +archive: true +--- + +# Archive Article1 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/archive2.md b/tools/create-vuepress/template/blog/posts/archive2.md new file mode 100644 index 0000000000..087ad8a8cc --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/archive2.md @@ -0,0 +1,18 @@ +--- +date: 1998-01-02 +category: + - History +tag: + - WWII +archive: true +--- + +# Archive Article2 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article1.md b/tools/create-vuepress/template/blog/posts/article1.md new file mode 100644 index 0000000000..35bdbeee4b --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article1.md @@ -0,0 +1,18 @@ +--- +date: 2022-01-01 +category: + - CategoryA +tag: + - tag A + - tag B +--- + +# Article 1 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article10.md b/tools/create-vuepress/template/blog/posts/article10.md new file mode 100644 index 0000000000..992b3b267f --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article10.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-10 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 10 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article11.md b/tools/create-vuepress/template/blog/posts/article11.md new file mode 100644 index 0000000000..4d4038bf6f --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article11.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-11 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 11 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article12.md b/tools/create-vuepress/template/blog/posts/article12.md new file mode 100644 index 0000000000..9d32cf8070 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article12.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-12 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 12 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article2.md b/tools/create-vuepress/template/blog/posts/article2.md new file mode 100644 index 0000000000..e995e373a1 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article2.md @@ -0,0 +1,18 @@ +--- +date: 2022-01-02 +category: + - CategoryA +tag: + - tag A + - tag B +--- + +# Article 2 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article3.md b/tools/create-vuepress/template/blog/posts/article3.md new file mode 100644 index 0000000000..28a8727572 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article3.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-03 +category: + - CategoryA + - CategoryB +tag: + - tag A + - tag B +--- + +# Article 3 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article4.md b/tools/create-vuepress/template/blog/posts/article4.md new file mode 100644 index 0000000000..ac868755e5 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article4.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-04 +category: + - CategoryA + - CategoryB +tag: + - tag A + - tag B +--- + +# Article 4 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article5.md b/tools/create-vuepress/template/blog/posts/article5.md new file mode 100644 index 0000000000..3193aac23d --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article5.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-05 +category: + - CategoryA + - CategoryB +tag: + - tag A + - tag B +--- + +# Article 5 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article6.md b/tools/create-vuepress/template/blog/posts/article6.md new file mode 100644 index 0000000000..fd7fb69c5b --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article6.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-06 +category: + - CategoryA + - CategoryB +tag: + - tag A + - tag B +--- + +# Article 6 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article7.md b/tools/create-vuepress/template/blog/posts/article7.md new file mode 100644 index 0000000000..a1095b7c6c --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article7.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-07 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 7 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article8.md b/tools/create-vuepress/template/blog/posts/article8.md new file mode 100644 index 0000000000..f142fd615f --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article8.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-08 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 8 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/article9.md b/tools/create-vuepress/template/blog/posts/article9.md new file mode 100644 index 0000000000..0b3a2d5df3 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/article9.md @@ -0,0 +1,19 @@ +--- +date: 2022-01-09 +category: + - CategoryA + - CategoryB +tag: + - tag C + - tag D +--- + +# Article 9 + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/sticky.md b/tools/create-vuepress/template/blog/posts/sticky.md new file mode 100644 index 0000000000..42869248d2 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/sticky.md @@ -0,0 +1,19 @@ +--- +date: 2021-01-01 +category: + - CategoryC +tag: + - tag E +sticky: true +excerpt:

A sticky article demo.

+--- + +# Sticky Article + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/blog/posts/sticky2.md b/tools/create-vuepress/template/blog/posts/sticky2.md new file mode 100644 index 0000000000..861ba9eb36 --- /dev/null +++ b/tools/create-vuepress/template/blog/posts/sticky2.md @@ -0,0 +1,22 @@ +--- +date: 2020-01-01 +category: + - CategoryC +tag: + - tag E +sticky: 10 +--- + +# Sticky Article with Higher Priority + +Excerpt information which is added manually. + + + +## Heading 2 + +Here is the content. + +### Heading 3 + +Here is the content. diff --git a/tools/create-vuepress/template/docs/.vuepress/config.js b/tools/create-vuepress/template/docs/.vuepress/config.js new file mode 100644 index 0000000000..3bf74af455 --- /dev/null +++ b/tools/create-vuepress/template/docs/.vuepress/config.js @@ -0,0 +1,15 @@ +import { defaultTheme } from '@vuepress/theme-default' +import { defineUserConfig } from 'vuepress/cli' + +export default defineUserConfig({ + lang: 'en-US', + + title: 'VuePress', + description: 'My first VuePress Site', + + theme: defaultTheme({ + logo: 'https://vuejs.press/images/hero.png', + + navbar: ['/', '/get-started'], + }), +}) diff --git a/tools/create-vuepress/template/docs/README.md b/tools/create-vuepress/template/docs/README.md new file mode 100644 index 0000000000..49f0744e02 --- /dev/null +++ b/tools/create-vuepress/template/docs/README.md @@ -0,0 +1,33 @@ +--- +home: true +title: Home +heroImage: https://vuejs.press/images/hero.png +actions: + - text: Get Started + link: /getting-started.html + type: primary + + - text: Introduction + link: https://vuejs.press/guide/introduction.html + type: secondary + +features: + - title: Simplicity First + details: Minimal setup with markdown-centered project structure helps you focus on writing. + - title: Vue-Powered + details: Enjoy the dev experience of Vue, use Vue components in markdown, and develop custom themes with Vue. + - title: Performant + details: VuePress generates pre-rendered static HTML for each page, and runs as an SPA once a page is loaded. + - title: Themes + details: Providing a default theme out of the box. You can also choose a community theme or create your own one. + - title: Plugins + details: Flexible plugin API, allowing plugins to provide lots of plug-and-play features for your site. + - title: Bundlers + details: Default bundler is Vite, while Webpack is also supported. Choose the one you like! + +footer: MIT Licensed | Copyright © 2018-present VuePress Community +--- + +This is the content of home page. Check [Home Page Docs][default-theme-home] for more details. + +[default-theme-home]: https://vuejs.press/reference/default-theme/frontmatter.html#home-page diff --git a/tools/create-vuepress/template/docs/get-started.md b/tools/create-vuepress/template/docs/get-started.md new file mode 100644 index 0000000000..0b7b0319a9 --- /dev/null +++ b/tools/create-vuepress/template/docs/get-started.md @@ -0,0 +1,46 @@ +# Get Started + +This is a normal page, which contains VuePress basics. + +## Pages + +You can add markdown files in your vuepress directory, every markdown file will be converted to a page in your site. + +See [routing][] for more details. + +## Content + +Every markdown file [will be rendered to HTML, then converted to a Vue SFC][content]. + +VuePress support basic markdown syntax and [some extensions][synatex-extensions], you can also [use Vue features][vue-feature] in it. + +## Configuration + +VuePress use a `.vuepress/config.js`(or .ts) file as [site configuration][config], you can use it to config your site. + +For [client side configuration][client-config], you can create `.vuepress/client.js`(or .ts). + +Meanwhile, you can also add configuration per page with [frontmatter][]. + +## Layouts and customization + +Here are common configuration controlling layout of `@vuepress/theme-default`: + +- [navbar][] +- [sidebar][] + +Check [default theme docs][default-theme] for full reference. + +You can [add extra style][style] with `.vuepress/styles/index.scss` file. + +[routing]: https://vuejs.press/guide/page.html#routing +[content]: https://vuejs.press/guide/page.html#content +[synatex-extensions]: https://vuejs.press/guide/markdown.html#syntax-extensions +[vue-feature]: https://vuejs.press/guide/markdown.html#using-vue-in-markdown +[config]: https://vuejs.press/guide/configuration.html#client-config-file +[client-config]: https://vuejs.press/guide/configuration.html#client-config-file +[frontmatter]: https://vuejs.press/guide/page.html#frontmatter +[navbar]: https://vuejs.press/reference/default-theme/config.html#navbar +[sidebar]: https://vuejs.press/reference/default-theme/config.html#sidebar +[default-theme]: https://vuejs.press/reference/default-theme/ +[style]: https://vuejs.press/reference/default-theme/styles.html#style-file diff --git a/tools/create-vuepress/tsconfig.build.json b/tools/create-vuepress/tsconfig.build.json new file mode 100644 index 0000000000..4f60f73883 --- /dev/null +++ b/tools/create-vuepress/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "baseUrl": "." + }, + "include": ["./src"] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 9ee2ff7c73..91be61435a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -34,7 +34,8 @@ { "path": "./plugins/plugin-shiki/tsconfig.build.json" }, { "path": "./plugins/plugin-theme-data/tsconfig.build.json" }, { "path": "./plugins/plugin-toc/tsconfig.build.json" }, - { "path": "./themes/theme-default/tsconfig.build.json" } + { "path": "./themes/theme-default/tsconfig.build.json" }, + { "path": "./tools/create-vuepress/tsconfig.build.json" } ], "files": [] } diff --git a/tsconfig.json b/tsconfig.json index 2ce4a99344..999bd64088 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "e2e/**/*", "plugins/**/*", "themes/**/*", + "tools/**/*", "vitest.config.ts", ], "exclude": ["node_modules", ".temp", "lib", "dist"],