diff --git a/package.json b/package.json index d204b21bbc..3c8473c60d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "boot": "node scripts/bootstrap.js", "dev": "yarn workspace docs dev", "build": "yarn workspace docs build", + "show-help": "yarn workspace docs show-help", "dev:blog": "yarn workspace blog dev", "build:blog": "yarn workspace blog build", "register-vuepress": "lerna exec --scope vuepress -- yarn link", diff --git a/packages/@vuepress/cli/.npmignore b/packages/@vuepress/cli/.npmignore deleted file mode 100644 index 13c38ea313..0000000000 --- a/packages/@vuepress/cli/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -__tests__ -__mocks__ -.temp diff --git a/packages/@vuepress/cli/README.md b/packages/@vuepress/cli/README.md deleted file mode 100644 index e2524b698b..0000000000 --- a/packages/@vuepress/cli/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# @vuepress/cli - -> cli for vuepress - -## APIs - -### program - -Current instance of [commander.js](https://github.com/tj/commander.js) - -### bootstrap(options) - -Launch the cli. - -#### options.plugins - -#### options.theme - \ No newline at end of file diff --git a/packages/@vuepress/cli/index.js b/packages/@vuepress/cli/index.js deleted file mode 100644 index 371ac2a032..0000000000 --- a/packages/@vuepress/cli/index.js +++ /dev/null @@ -1,134 +0,0 @@ -const { chalk, performance } = require('@vuepress/shared-utils') -const semver = require('semver') - -try { - require.resolve('@vuepress/core') -} catch (err) { - console.log(chalk.red( - `\n[vuepress] @vuepress/cli ` + - `requires @vuepress/core to be installed.\n` - )) - process.exit(1) -} - -const pkg = require('@vuepress/core/package.json') -const requiredVersion = pkg.engines.node - -if (!semver.satisfies(process.version, requiredVersion)) { - console.log(chalk.red( - `\n[vuepress] minimum Node version not met:` + - `\nYou are using Node ${process.version}, but VuePress ` + - `requires Node ${requiredVersion}.\nPlease upgrade your Node version.\n` - )) - process.exit(1) -} - -const cli = require('cac')() - -exports.cli = cli -exports.bootstrap = function ({ - plugins, - theme -} = {}) { - const { path, logger, env } = require('@vuepress/shared-utils') - const { dev, build, eject } = require('@vuepress/core') - - performance.start() - - cli - .version(pkg.version) - .help() - - cli - .command('dev [targetDir]', 'start development server') - .allowUnknownOptions() - .option('-p, --port ', 'use specified port (default: 8080)') - .option('-t, --temp ', 'set the directory of the temporary file') - .option('-c, --cache [cache]', 'set the directory of cache') - .option('--host ', 'use specified host (default: 0.0.0.0)') - .option('--no-cache', 'clean the cache before build') - .option('--debug', 'start development server in debug mode') - .option('--silent', 'start development server in silent mode') - .action((sourceDir = '.', options) => { - const { - host, - port, - debug, - temp, - cache, - silent - } = options - logger.setOptions({ logLevel: silent ? 1 : debug ? 4 : 3 }) - logger.debug('cli_options', options) - env.setOptions({ isDebug: debug, isTest: process.env.NODE_ENV === 'test' }) - - wrapCommand(dev)(path.resolve(sourceDir), { - host, - port, - temp, - cache, - plugins, - theme - }) - }) - - cli - .command('build [targetDir]', 'build dir as static site') - .allowUnknownOptions() - .option('-d, --dest ', 'specify build output dir (default: .vuepress/dist)') - .option('-t, --temp ', 'set the directory of the temporary file') - .option('-c, --cache [cache]', 'set the directory of cache') - .option('--no-cache', 'clean the cache before build') - .option('--debug', 'build in development mode for debugging') - .option('--silent', 'build static site in silent mode') - .action((sourceDir = '.', options) => { - const { - debug, - dest, - temp, - cache, - silent - } = options - logger.setOptions({ logLevel: silent ? 1 : debug ? 4 : 3 }) - logger.debug('cli_options', options) - env.setOptions({ isDebug: debug, isTest: process.env.NODE_ENV === 'test' }) - - wrapCommand(build)(path.resolve(sourceDir), { - debug, - dest, - plugins, - theme, - temp, - cache, - silent - }) - }) - - cli - .command('eject [targetDir]', 'copy the default theme into .vuepress/theme for customization.') - .option('--debug', 'eject in debug mode') - .action((dir = '.') => { - wrapCommand(eject)(path.resolve(dir)) - }) - - // output help information on unknown commands - cli.on('command:*', () => { - console.error('Unknown command: %s', cli.args.join(' ')) - console.log() - }) - - function wrapCommand (fn) { - return (...args) => { - return fn(...args).catch(err => { - console.error(chalk.red(err.stack)) - process.exitCode = 1 - }) - } - } - - cli.parse(process.argv) - if (!process.argv.slice(2).length) { - cli.outputHelp() - } -} - diff --git a/packages/@vuepress/cli/package.json b/packages/@vuepress/cli/package.json deleted file mode 100644 index c43a9a1fd5..0000000000 --- a/packages/@vuepress/cli/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@vuepress/cli", - "version": "1.0.0-alpha.27", - "description": "cli for vuepress", - "main": "index.js", - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/vuejs/vue-cli.git" - }, - "keywords": [ - "documentation", - "vue", - "vuepress", - "generator" - ], - "author": "Evan You", - "maintainers": [ - { - "name": "ULIVZ", - "email": "chl814@foxmail.com" - } - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/vuejs/vuepress/issues" - }, - "dependencies": { - "cac": "^6.3.6", - "chalk": "^2.3.2", - "semver": "^5.5.0" - }, - "peerDependencies": { - "@vuepress/core": "^1.0.0-alpha.1" - }, - "homepage": "https://github.com/vuejs/vuepress/packages/@vuepress/cli#readme" -} diff --git a/packages/@vuepress/core/lib/plugin-api/constants.js b/packages/@vuepress/core/lib/plugin-api/constants.js index b9339f57de..883447930d 100644 --- a/packages/@vuepress/core/lib/plugin-api/constants.js +++ b/packages/@vuepress/core/lib/plugin-api/constants.js @@ -19,7 +19,8 @@ const PLUGIN_OPTION_META_MAP = { ADDITIONAL_PAGES: { name: 'additionalPages', types: [Function, Array] }, GLOBAL_UI_COMPONENTS: { name: 'globalUIComponents', types: [String, Array] }, DEFINE: { name: 'define', types: [Function, Object] }, - ALIAS: { name: 'alias', types: [Function, Object] } + ALIAS: { name: 'alias', types: [Function, Object] }, + EXTEND_CLI: { name: 'extendCli', types: [Function] } } const PLUGIN_OPTION_MAP = {} diff --git a/packages/@vuepress/core/lib/plugin-api/index.js b/packages/@vuepress/core/lib/plugin-api/index.js index 324fe28da7..c4aa89e4f9 100644 --- a/packages/@vuepress/core/lib/plugin-api/index.js +++ b/packages/@vuepress/core/lib/plugin-api/index.js @@ -206,7 +206,8 @@ module.exports = class PluginAPI { additionalPages, globalUIComponents, define, - alias + alias, + extendCli }) { const isInternalPlugin = pluginName.startsWith('@vuepress/internal-') logger[isInternalPlugin ? 'debug' : 'tip'](pluginLog(pluginName, shortcut)) @@ -229,6 +230,7 @@ module.exports = class PluginAPI { .registerOption(PLUGIN_OPTION_MAP.GLOBAL_UI_COMPONENTS.key, globalUIComponents, pluginName) .registerOption(PLUGIN_OPTION_MAP.DEFINE.key, define, pluginName) .registerOption(PLUGIN_OPTION_MAP.ALIAS.key, alias, pluginName) + .registerOption(PLUGIN_OPTION_MAP.EXTEND_CLI.key, extendCli, pluginName) } } diff --git a/packages/@vuepress/core/lib/prepare/AppContext.js b/packages/@vuepress/core/lib/prepare/AppContext.js index f5bf258f0c..035dc09182 100644 --- a/packages/@vuepress/core/lib/prepare/AppContext.js +++ b/packages/@vuepress/core/lib/prepare/AppContext.js @@ -43,6 +43,7 @@ module.exports = class AppContext { */ constructor (sourceDir, cliOptions = {}, isProd) { + logger.debug('sourceDir', sourceDir) this.sourceDir = sourceDir this.cliOptions = cliOptions this.isProd = isProd diff --git a/packages/@vuepress/core/package.json b/packages/@vuepress/core/package.json index c525fdc6e3..c849c2caa2 100644 --- a/packages/@vuepress/core/package.json +++ b/packages/@vuepress/core/package.json @@ -68,10 +68,12 @@ "webpack-chain": "^4.6.0", "webpack-merge": "^4.1.2", "webpack-serve": "^1.0.2", - "webpackbar": "^2.6.1" + "webpackbar": "^2.6.1", + "semver": "^5.5.0", + "cac": "^6.3.9" }, "engines": { - "node": ">=8" + "node": ">=8.6" }, "browserslist": [ ">1%" diff --git a/packages/docs/docs/miscellaneous/design-concepts.md b/packages/docs/docs/miscellaneous/design-concepts.md index 665c4c2891..1dc40d857a 100644 --- a/packages/docs/docs/miscellaneous/design-concepts.md +++ b/packages/docs/docs/miscellaneous/design-concepts.md @@ -187,9 +187,8 @@ Then the final route of i18n UI is `/i18n/`. ## Others -With the goal of decoupling, we were able to separate VuePress into the following libraries by introducing monorepo: +With the goal of decoupling, we were able to separate VuePress into the following two libraries by introducing monorepo: -- [@vuepress/cli](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/cli): Management of command line; - [@vuepress/core](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/core):Including the core implementation of `dev`, `build` and `Plugin API`; - [@vuepress/theme-default](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-default):The default theme you see now. diff --git a/packages/docs/docs/plugin/option-api.md b/packages/docs/docs/plugin/option-api.md index 23c2578a1d..eeb1b334e5 100644 --- a/packages/docs/docs/plugin/option-api.md +++ b/packages/docs/docs/plugin/option-api.md @@ -434,3 +434,29 @@ Then, VuePress will automatically inject these components behind the layout comp ``` + +## registerCommand + +- Type: `function` +- Default: `undefined` + +Register a extra command to enhance the CLI of vuepress. The function will be called with a [CAC](https://github.com/cacjs/cac)'s instance as the first argument. + +```js +module.exports = { + registerCommand (cli) { + cli + .command('info [targetDir]', '') + .option('--debug', 'display info in debug mode') + .action((dir = '.') => { + console.log('Display info of your website') + }) + } +} +``` + +Now you can use `vuepress info [targetDir]` a in your project! + +::: tip +Note that a custom command registered by a plugin requires VuePress to locate your site configuration like `vuepress dev` and `vuepress build`, so when developing a command, be sure to lead the user to pass `targetDir` as an CLI argument. +::: diff --git a/packages/docs/docs/zh/miscellaneous/design-concepts.md b/packages/docs/docs/zh/miscellaneous/design-concepts.md index ea64376650..6adb30cb10 100644 --- a/packages/docs/docs/zh/miscellaneous/design-concepts.md +++ b/packages/docs/docs/zh/miscellaneous/design-concepts.md @@ -187,9 +187,8 @@ i18n UI 最终的路由将是 `/i18n/`. ## 其他 -本着解耦的目标,引入 monorepo 后,我们也得以将 VuePress 分离成以下几个库: +本着解耦的目标,引入 monorepo 后,我们也得以将 VuePress 分离成以下两个库: -- [@vuepress/cli](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/cli): 命令行指令的管理; - [@vuepress/core](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/core):包含 dev、build 的核心实现和 Plugin API; - [@vuepress/theme-default](https://github.com/vuejs/vuepress/tree/master/packages/@vuepress/theme-default):你现在所看到的默认主题。 diff --git a/packages/docs/docs/zh/plugin/option-api.md b/packages/docs/docs/zh/plugin/option-api.md index 188ddaf363..1ed3a75dd6 100644 --- a/packages/docs/docs/zh/plugin/option-api.md +++ b/packages/docs/docs/zh/plugin/option-api.md @@ -436,3 +436,30 @@ VuePress 将会自动将这些组件注入到布局组件的隔壁: ``` + +## registerCommand + +- 类型: `function` +- 默认值: `undefined` + +注册一个额外的 command 来增强 vuepress 的 CLI。这个函数将会以一个 [CAC](https://github.com/cacjs/cac) 的实例作为第一个参数被调用。 + +```js +module.exports = { + registerCommand (cli) { + cli + .command('info [targetDir]', '') + .option('--debug', 'display info in debug mode') + .action((dir = '.') => { + console.log('Display info of your website') + }) + } +} +``` + +现在你可以在你项目中使用 `vuepress info [targetDir]` 了! + +::: tip +值得注意的是,一个自定义的 command 需要 VuePress 像 `vuepress dev` 或 `vuepress build` 去定位到你的站点配置,所以在开发一个 command 时,请确保引导用户去传入 `targetDir` 作为 CLI 参数的一部分。 +::: + diff --git a/packages/docs/package.json b/packages/docs/package.json index 78592c2103..eadfedde84 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -5,7 +5,8 @@ "description": "docs of VuePress", "scripts": { "dev": "vuepress dev docs --temp .temp", - "build": "vuepress build docs --temp .temp" + "build": "vuepress build docs --temp .temp", + "show-help": "vuepress --help" }, "repository": { "type": "git", diff --git a/packages/vuepress/lib/checkEnv.js b/packages/vuepress/lib/checkEnv.js new file mode 100644 index 0000000000..8b0de2959c --- /dev/null +++ b/packages/vuepress/lib/checkEnv.js @@ -0,0 +1,25 @@ +'use strict' + +/** + * Module dependencies. + */ + +const { chalk } = require('@vuepress/shared-utils') +const semver = require('semver') + +/** + * Expose handleUnknownCommand function. + */ + +module.exports = function checkEnv (pkg) { + const requiredVersion = pkg.engines.node + + if (!semver.satisfies(process.version, requiredVersion)) { + console.log(chalk.red( + `\n[vuepress] minimum Node version not met:` + + `\nYou are using Node ${process.version}, but VuePress ` + + `requires Node ${requiredVersion}.\nPlease upgrade your Node version.\n` + )) + process.exit(1) + } +} diff --git a/packages/vuepress/lib/handleUnknownCommand.js b/packages/vuepress/lib/handleUnknownCommand.js new file mode 100644 index 0000000000..cc20a2aed7 --- /dev/null +++ b/packages/vuepress/lib/handleUnknownCommand.js @@ -0,0 +1,133 @@ +'use strict' + +/** + * Module dependencies. + */ + +const prepare = require('@vuepress/core/lib/prepare') +const { path, logger, globby, chalk } = require('@vuepress/shared-utils') +const { isKnownCommand, CLI } = require('./util') +const pwd = process.cwd() + +/** + * Expose handleUnknownCommand function. + */ + +module.exports = async function (cli, options) { + registerUnknownCommands(cli, options) + + const argv = process.argv.slice(2) + const inferredUserDocsDirectory = await inferUserDocsDirectory(pwd) + logger.developer('inferredUserDocsDirectory', inferredUserDocsDirectory) + + const needPrepareBeforeLaunchCLI = inferredUserDocsDirectory && + (isHelpFlag(argv[0]) || isUnknownCommandHelp(argv)) + + logger.developer('needPrepareBeforeLaunchCLI', needPrepareBeforeLaunchCLI) + + if (needPrepareBeforeLaunchCLI) { + let context + let [, sourceDir] = argv + + if (!sourceDir || sourceDir.startsWith('-')) { + sourceDir = inferredUserDocsDirectory + } else { + sourceDir = pwd + } + + logger.setOptions({ logLevel: 1 }) + + if (sourceDir) { + context = await prepare(sourceDir, options) + context.pluginAPI.options.extendCli.apply(cli) + } + + logger.setOptions({ logLevel: 3 }) + } +} + +// When user type `vuepress [customCommand] --help`, +// VuePress will try to detect where user to place docs. + +async function inferUserDocsDirectory (cwd) { + const paths = await globby([ + '**/.vuepress/config.js', + '!node_modules' + ], { + cwd, + dot: true + }) + const siteConfigPath = paths && paths[0] + if (siteConfigPath) { + return path.resolve( + cwd, + siteConfigPath.replace('.vuepress/config.js', '') + ) + } + return null +} + +/** + * Register a command to match all unmatched commands + * @param {CAC} cli + */ + +function registerUnknownCommands (cli, options) { + cli.on('command:*', async () => { + const { args, options: commandoptions } = cli + + logger.debug('global_options', options) + logger.debug('cli_options', commandoptions) + logger.debug('cli_args', args) + + const [commandName] = args + const sourceDir = args[1] ? path.resolve(args[1]) : pwd + const inferredUserDocsDirectory = await inferUserDocsDirectory(pwd) + logger.developer('inferredUserDocsDirectory', inferredUserDocsDirectory) + logger.developer('sourceDir', sourceDir) + + if (inferredUserDocsDirectory && sourceDir !== inferredUserDocsDirectory) { + logUnknownCommand(cli) + console.log() + logger.tip(`Did you miss to specify the target docs dir? e.g. ${chalk.cyan(`vuepress ${commandName} [targetDir]`)}.`) + logger.tip(`A custom command registered by a plugin requires VuePress to locate your site configuration like ${chalk.cyan('vuepress dev')} or ${chalk.cyan('vuepress build')}.`) + console.log() + process.exit(1) + } + + if (!inferredUserDocsDirectory) { + logUnknownCommand(cli) + process.exit(1) + } + + logger.debug('Custom command', chalk.cyan(commandName)) + CLI({ + async beforeParse (subCli) { + const context = await prepare(sourceDir, { + ...options, + ...commandoptions + }, false /* isProd */) + await context.pluginAPI.options.extendCli.apply(subCli) + }, + async afterParse (subCli) { + if (!subCli.matchedCommand) { + logUnknownCommand(subCli) + console.log() + } + } + }) + }) +} + +function isHelpFlag (v) { + return v === '--help' || v === '-h' +} + +function isUnknownCommandHelp (argv) { + return !isKnownCommand(argv) && isHelpFlag(argv[1]) +} + +function logUnknownCommand (cli) { + console.error('Unknown command: %s', cli.args.join(' ')) +} + diff --git a/packages/vuepress/lib/registerCoreCommands.js b/packages/vuepress/lib/registerCoreCommands.js new file mode 100644 index 0000000000..3301fae914 --- /dev/null +++ b/packages/vuepress/lib/registerCoreCommands.js @@ -0,0 +1,67 @@ +'use strict' + +/** + * Module dependencies. + */ + +const { dev, build, eject } = require('@vuepress/core') +const { path, logger, env } = require('@vuepress/shared-utils') +const { wrapCommand } = require('./util') + +/** + * Expose registerCoreCommands function. + */ + +module.exports = function (cli, options) { + cli + .command(`dev [targetDir]`, 'start development server') + .option('-p, --port ', 'use specified port (default: 8080)') + .option('-t, --temp ', 'set the directory of the temporary file') + .option('-c, --cache [cache]', 'set the directory of cache') + .option('--host ', 'use specified host (default: 0.0.0.0)') + .option('--no-cache', 'clean the cache before build') + .option('--debug', 'start development server in debug mode') + .option('--silent', 'start development server in silent mode') + .action((sourceDir = '.', commandOptions) => { + const { debug, silent } = commandOptions + + logger.setOptions({ logLevel: silent ? 1 : debug ? 4 : 3 }) + logger.debug('global_options', options) + logger.debug('dev_options', commandOptions) + env.setOptions({ isDebug: debug, isTest: process.env.NODE_ENV === 'test' }) + + wrapCommand(dev)(path.resolve(sourceDir), { + ...options, + ...commandOptions + }) + }) + + cli + .command('build [targetDir]', 'build dir as static site') + .option('-d, --dest ', 'specify build output dir (default: .vuepress/dist)') + .option('-t, --temp ', 'set the directory of the temporary file') + .option('-c, --cache [cache]', 'set the directory of cache') + .option('--no-cache', 'clean the cache before build') + .option('--debug', 'build in development mode for debugging') + .option('--silent', 'build static site in silent mode') + .action((sourceDir = '.', commandOptions) => { + const { debug, silent } = commandOptions + + logger.setOptions({ logLevel: silent ? 1 : debug ? 4 : 3 }) + logger.debug('global_options', options) + logger.debug('build_options', commandOptions) + env.setOptions({ isDebug: debug, isTest: process.env.NODE_ENV === 'test' }) + + wrapCommand(build)(path.resolve(sourceDir), { + ...options, + ...commandOptions + }) + }) + + cli + .command('eject [targetDir]', 'copy the default theme into .vuepress/theme for customization.') + .option('--debug', 'eject in debug mode') + .action((dir = '.') => { + wrapCommand(eject)(path.resolve(dir)) + }) +} diff --git a/packages/vuepress/lib/util.js b/packages/vuepress/lib/util.js new file mode 100644 index 0000000000..6c14e277ea --- /dev/null +++ b/packages/vuepress/lib/util.js @@ -0,0 +1,56 @@ +'use strict' + +/** + * Module dependencies. + */ + +const { chalk } = require('@vuepress/shared-utils') +const CAC = require('cac') + +/** + * Bootstrap a CAC cli + * @param {function} beforeParse + * @param {function} adterParse + * @returns {Promise} + */ + +async function CLI ({ + beforeParse, + afterParse +}) { + const cli = CAC() + beforeParse && await beforeParse(cli) + cli.parse(process.argv) + afterParse && await afterParse(cli) +} + +/** + * Wrap a function to catch error. + * @param {function} fn + * @returns {function(...[*]): (*|Promise|Promise)} + */ + +function wrapCommand (fn) { + return (...args) => { + return fn(...args).catch(err => { + console.error(chalk.red(err.stack)) + process.exitCode = 1 + }) + } +} + +/** + * Check if a command is built-in + * @param {array} argv + * @returns {boolean} + */ + +function isKnownCommand (argv) { + return ['dev', 'build', 'eject'].includes(argv[0]) +} + +module.exports = { + CLI, + isKnownCommand, + wrapCommand +} diff --git a/packages/vuepress/package.json b/packages/vuepress/package.json index 5341c6fbdb..579a262cb0 100644 --- a/packages/vuepress/package.json +++ b/packages/vuepress/package.json @@ -28,9 +28,8 @@ }, "homepage": "https://github.com/vuejs/vuepress#readme", "dependencies": { - "@vuepress/cli": "^1.0.0-alpha.27", - "@vuepress/core": "^1.0.0-alpha.27", - "@vuepress/theme-default": "^1.0.0-alpha.27" + "@vuepress/core": "^1.0.0-alpha.24", + "@vuepress/theme-default": "^1.0.0-alpha.24" }, "engines": { "node": ">=8" diff --git a/packages/vuepress/vuepress.js b/packages/vuepress/vuepress.js index c885579271..35050b4f62 100755 --- a/packages/vuepress/vuepress.js +++ b/packages/vuepress/vuepress.js @@ -1,3 +1,27 @@ #!/usr/bin/env node -require('@vuepress/cli').bootstrap({ theme: '@vuepress/default' }) +const checkEnv = require('./lib/checkEnv') +const { CLI } = require('./lib/util') +const registerCoreCommands = require('./lib/registerCoreCommands') +const handleUnknownCommand = require('./lib/handleUnknownCommand') + +const OPTIONS = { + theme: '@vuepress/default' +} + +CLI({ + async beforeParse (cli) { + const pkg = require('@vuepress/core/package.json') + checkEnv(pkg) + registerCoreCommands(cli, OPTIONS) + await handleUnknownCommand(cli, OPTIONS) + cli.version(pkg.version).help() + }, + + async afterParse (cli) { + if (!process.argv.slice(2).length) { + cli.outputHelp() + } + } +}) +