mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 10:20:09 +02:00
feat(v2): allow plugin to extendCli (#1843)
* feat(v2): allow plugin to extendCli * rename to externalCommand
This commit is contained in:
parent
46e8e03be0
commit
b1f6951fbb
10 changed files with 137 additions and 52 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
2
packages/docusaurus-types/src/index.d.ts
vendored
2
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
25
packages/docusaurus/src/commands/external.ts
Normal file
25
packages/docusaurus/src/commands/external.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
45
packages/docusaurus/src/server/plugins/init.ts
Normal file
45
packages/docusaurus/src/server/plugins/init.ts
Normal 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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue