feat(v2): allow plugin to extendCli (#1843)

* feat(v2): allow plugin to extendCli

* rename to externalCommand
This commit is contained in:
Endi 2019-10-16 13:16:26 +07:00 committed by GitHub
parent 46e8e03be0
commit b1f6951fbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 137 additions and 52 deletions

View file

@ -3,6 +3,7 @@
## 2.0.0-alpha.28 ## 2.0.0-alpha.28
- Further reduce memory usage to avoid heap memory allocation failure. - Further reduce memory usage to avoid heap memory allocation failure.
- Fix `keywords` frontmatter for SEO not working properly. - Fix `keywords` frontmatter for SEO not working properly.
- Add `extendCli` api for plugins. This will allow plugin to further extend Docusaurus CLI.
## 2.0.0-alpha.27 ## 2.0.0-alpha.27

View file

@ -9,6 +9,7 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"commander": "^2.20.0",
"@types/webpack": "^4.32.0", "@types/webpack": "^4.32.0",
"querystring": "0.2.0" "querystring": "0.2.0"
} }

View file

@ -1,4 +1,5 @@
import {Loader, Configuration} from 'webpack'; import {Loader, Configuration} from 'webpack';
import {CommanderStatic} from 'commander';
import {ParsedUrlQueryInput} from 'querystring'; import {ParsedUrlQueryInput} from 'querystring';
export interface DocusaurusConfig { export interface DocusaurusConfig {
@ -92,6 +93,7 @@ export interface Plugin<T> {
getThemePath?(): string; getThemePath?(): string;
getPathsToWatch?(): string[]; getPathsToWatch?(): string[];
getClientModules?(): string[]; getClientModules?(): string[];
extendCli?(cli: CommanderStatic): any;
} }
export type PluginConfig = [string, Object] | [string] | string; export type PluginConfig = [string, Object] | [string] | string;

View file

@ -10,8 +10,8 @@
const chalk = require('chalk'); const chalk = require('chalk');
const semver = require('semver'); const semver = require('semver');
const path = require('path'); const path = require('path');
const program = require('commander'); const cli = require('commander');
const {build, swizzle, deploy, start} = require('../lib'); const {build, swizzle, deploy, start, externalCommand} = require('../lib');
const requiredVersion = require('../package.json').engines.node; const requiredVersion = require('../package.json').engines.node;
if (!semver.satisfies(process.version, requiredVersion)) { if (!semver.satisfies(process.version, requiredVersion)) {
@ -34,11 +34,9 @@ function wrapCommand(fn) {
}); });
} }
program cli.version(require('../package.json').version).usage('<command> [options]');
.version(require('../package.json').version)
.usage('<command> [options]');
program cli
.command('build [siteDir]') .command('build [siteDir]')
.description('Build website') .description('Build website')
.option( .option(
@ -51,21 +49,21 @@ program
}); });
}); });
program cli
.command('swizzle <themeName> [componentName] [siteDir]') .command('swizzle <themeName> [componentName] [siteDir]')
.description('Copy the theme files into website folder for customization.') .description('Copy the theme files into website folder for customization.')
.action((themeName, componentName, siteDir = '.') => { .action((themeName, componentName, siteDir = '.') => {
wrapCommand(swizzle)(path.resolve(siteDir), themeName, componentName); wrapCommand(swizzle)(path.resolve(siteDir), themeName, componentName);
}); });
program cli
.command('deploy [siteDir]') .command('deploy [siteDir]')
.description('Deploy website to GitHub pages') .description('Deploy website to GitHub pages')
.action((siteDir = '.') => { .action((siteDir = '.') => {
wrapCommand(deploy)(path.resolve(siteDir)); wrapCommand(deploy)(path.resolve(siteDir));
}); });
program cli
.command('start [siteDir]') .command('start [siteDir]')
.description('Start development server') .description('Start development server')
.option('-p, --port <port>', 'use specified port (default: 3000)') .option('-p, --port <port>', 'use specified port (default: 3000)')
@ -82,14 +80,22 @@ program
}); });
}); });
program.arguments('<command>').action(cmd => { cli.arguments('<command>').action(cmd => {
program.outputHelp(); cli.outputHelp();
console.log(` ${chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)}`); console.log(` ${chalk.red(`\n Unknown command ${chalk.yellow(cmd)}.`)}`);
console.log(); console.log();
}); });
program.parse(process.argv); function isInternalCommand(command) {
return ['start', 'build', 'swizzle', 'deploy'].includes(command);
}
if (!isInternalCommand(process.argv.slice(2)[0])) {
externalCommand(cli, path.resolve('.'));
}
cli.parse(process.argv);
if (!process.argv.slice(2).length) { if (!process.argv.slice(2).length) {
program.outputHelp(); cli.outputHelp();
} }

View file

@ -0,0 +1,25 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CommanderStatic} from 'commander';
import {loadContext, loadPluginConfigs} from '../server';
import {initPlugins} from '../server/plugins/init';
export function externalCommand(cli: CommanderStatic, siteDir: string): void {
const context = loadContext(siteDir);
const pluginConfigs = loadPluginConfigs(context);
const plugins = initPlugins({pluginConfigs, context});
// Plugin lifecycle - extendCli
plugins.forEach(plugin => {
const {extendCli} = plugin;
if (!extendCli) {
return;
}
extendCli(cli);
});
}

View file

@ -9,3 +9,4 @@ export {build} from './commands/build';
export {start} from './commands/start'; export {start} from './commands/start';
export {swizzle} from './commands/swizzle'; export {swizzle} from './commands/swizzle';
export {deploy} from './commands/deploy'; export {deploy} from './commands/deploy';
export {externalCommand} from './commands/external';

View file

@ -27,41 +27,48 @@ import {
Props, Props,
} from '@docusaurus/types'; } from '@docusaurus/types';
export async function load(siteDir: string): Promise<Props> { export function loadContext(siteDir: string): LoadContext {
const generatedFilesDir: string = path.resolve( const generatedFilesDir: string = path.resolve(
siteDir, siteDir,
GENERATED_FILES_DIR_NAME, GENERATED_FILES_DIR_NAME,
); );
const siteConfig: DocusaurusConfig = loadConfig(siteDir); const siteConfig: DocusaurusConfig = loadConfig(siteDir);
const genSiteConfig = generate(
generatedFilesDir,
CONFIG_FILE_NAME,
`export default ${JSON.stringify(siteConfig, null, 2)};`,
);
const outDir = path.resolve(siteDir, BUILD_DIR_NAME); const outDir = path.resolve(siteDir, BUILD_DIR_NAME);
const {baseUrl} = siteConfig; const {baseUrl} = siteConfig;
const context: LoadContext = { return {
siteDir, siteDir,
generatedFilesDir, generatedFilesDir,
siteConfig, siteConfig,
outDir, outDir,
baseUrl, baseUrl,
}; };
}
// Presets. export function loadPluginConfigs(context: LoadContext): PluginConfig[] {
const {plugins: presetPlugins, themes: presetThemes} = loadPresets(context); const {plugins: presetPlugins, themes: presetThemes} = loadPresets(context);
const {siteConfig} = context;
// Plugins. return [
const pluginConfigs: PluginConfig[] = [
...presetPlugins, ...presetPlugins,
...presetThemes, ...presetThemes,
// Site config should the highest priority. // Site config should the highest priority.
...(siteConfig.plugins || []), ...(siteConfig.plugins || []),
...(siteConfig.themes || []), ...(siteConfig.themes || []),
]; ];
}
export async function load(siteDir: string): Promise<Props> {
// Context
const context: LoadContext = loadContext(siteDir);
const {generatedFilesDir, siteConfig, outDir, baseUrl} = context;
const genSiteConfig = generate(
generatedFilesDir,
CONFIG_FILE_NAME,
`export default ${JSON.stringify(siteConfig, null, 2)};`,
);
// Plugins
const pluginConfigs: PluginConfig[] = loadPluginConfigs(context);
const {plugins, pluginsRouteConfigs} = await loadPlugins({ const {plugins, pluginsRouteConfigs} = await loadPlugins({
pluginConfigs, pluginConfigs,
context, context,

View file

@ -5,10 +5,8 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import _ from 'lodash';
import {generate} from '@docusaurus/utils'; import {generate} from '@docusaurus/utils';
import fs from 'fs-extra'; import fs from 'fs-extra';
import importFresh from 'import-fresh';
import path from 'path'; import path from 'path';
import { import {
LoadContext, LoadContext,
@ -17,6 +15,7 @@ import {
PluginContentLoadedActions, PluginContentLoadedActions,
RouteConfig, RouteConfig,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {initPlugins} from './init';
export async function loadPlugins({ export async function loadPlugins({
pluginConfigs, pluginConfigs,
@ -29,30 +28,7 @@ export async function loadPlugins({
pluginsRouteConfigs: RouteConfig[]; pluginsRouteConfigs: RouteConfig[];
}> { }> {
// 1. Plugin Lifecycle - Initialization/Constructor // 1. Plugin Lifecycle - Initialization/Constructor
const plugins: Plugin<any>[] = _.compact( const plugins: Plugin<any>[] = initPlugins({pluginConfigs, context});
pluginConfigs.map(pluginItem => {
let pluginModuleImport;
let pluginOptions = {};
if (!pluginItem) {
return null;
}
if (typeof pluginItem === 'string') {
pluginModuleImport = pluginItem;
} else if (Array.isArray(pluginItem)) {
pluginModuleImport = pluginItem[0];
pluginOptions = pluginItem[1] || {};
}
if (!pluginModuleImport) {
return null;
}
// module is any valid module identifier - npm package or locally-resolved path.
const pluginModule: any = importFresh(pluginModuleImport);
return (pluginModule.default || pluginModule)(context, pluginOptions);
}),
);
// 2. Plugin lifecycle - loadContent // 2. Plugin lifecycle - loadContent
// Currently plugins run lifecycle in parallel and are not order-dependent. We could change // Currently plugins run lifecycle in parallel and are not order-dependent. We could change

View file

@ -0,0 +1,45 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import importFresh from 'import-fresh';
import {LoadContext, Plugin, PluginConfig} from '@docusaurus/types';
export function initPlugins({
pluginConfigs,
context,
}: {
pluginConfigs: PluginConfig[];
context: LoadContext;
}): Plugin<any>[] {
const plugins: Plugin<any>[] = _.compact(
pluginConfigs.map(pluginItem => {
let pluginModuleImport;
let pluginOptions = {};
if (!pluginItem) {
return null;
}
if (typeof pluginItem === 'string') {
pluginModuleImport = pluginItem;
} else if (Array.isArray(pluginItem)) {
pluginModuleImport = pluginItem[0];
pluginOptions = pluginItem[1] || {};
}
if (!pluginModuleImport) {
return null;
}
// module is any valid module identifier - npm package or locally-resolved path.
const pluginModule: any = importFresh(pluginModuleImport);
return (pluginModule.default || pluginModule)(context, pluginOptions);
}),
);
return plugins;
}

View file

@ -130,6 +130,23 @@ configureWebpack(config, isServer, {getBabelLoader, getCacheLoader}) {
}, },
``` ```
## extendCli(cli)
Register an extra command to enhance the CLI of docusaurus. `cli` is [commander](https://www.npmjs.com/package/commander) object.
Example:
```js
extendCli(cli) {
cli
.command('roll')
.description('Roll a random number between 1 and 1000')
.action(() => {
console.log(Math.floor(Math.random() * 1000 + 1));
});
},
```
<!-- <!--
For example, the in docusaurus-plugin-content-docs: For example, the in docusaurus-plugin-content-docs:
@ -210,6 +227,10 @@ module.exports = function(context, opts) {
// in the client bundle. These modules are imported globally before // in the client bundle. These modules are imported globally before
// React even renders the initial UI. // React even renders the initial UI.
}, },
extendCli(cli) {
// Register an extra command to enhance the CLI of docusaurus
},
}; };
}; };
``` ```