mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 18:32:52 +02:00
feat(v2): allow config plugins as function or [function,options] (#4618)
* feat : update PluginSchema validation * feat : update plugin init functionality * test : add and update tests * fix : tests * refactor : init.ts * test : update test * docs : add functional plugin docs * fix little issues * refactor : refactor code * minor refactors * simplify initPlugins code * simplify initPlugin + add custom validation error message * fix snapshots * improve function plugin doc Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
e092910627
commit
69be003e12
16 changed files with 460 additions and 124 deletions
7
packages/docusaurus-types/src/index.d.ts
vendored
7
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -294,7 +294,12 @@ export type ConfigurePostCssFn = Plugin<unknown>['configurePostCss'];
|
|||
|
||||
export type PluginOptions = {id?: string} & Record<string, unknown>;
|
||||
|
||||
export type PluginConfig = [string, PluginOptions] | [string] | string;
|
||||
export type PluginConfig =
|
||||
| [string, PluginOptions]
|
||||
| [string]
|
||||
| string
|
||||
| [PluginModule, PluginOptions]
|
||||
| PluginModule;
|
||||
|
||||
export interface ChunkRegistry {
|
||||
loader: string;
|
||||
|
|
|
@ -18,22 +18,34 @@ import initPlugins from '../server/plugins/init';
|
|||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
|
||||
export function getPluginNames(plugins: PluginConfig[]): string[] {
|
||||
return plugins.map((plugin) => {
|
||||
const pluginPath = Array.isArray(plugin) ? plugin[0] : plugin;
|
||||
let packagePath = path.dirname(pluginPath);
|
||||
while (packagePath) {
|
||||
if (fs.existsSync(path.join(packagePath, 'package.json'))) {
|
||||
break;
|
||||
} else {
|
||||
packagePath = path.dirname(packagePath);
|
||||
return plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
typeof plugin === 'string' ||
|
||||
(Array.isArray(plugin) && typeof plugin[0] === 'string'),
|
||||
)
|
||||
.map((plugin) => {
|
||||
const pluginPath = Array.isArray(plugin) ? plugin[0] : plugin;
|
||||
if (typeof pluginPath === 'string') {
|
||||
let packagePath = path.dirname(pluginPath);
|
||||
while (packagePath) {
|
||||
if (fs.existsSync(path.join(packagePath, 'package.json'))) {
|
||||
break;
|
||||
} else {
|
||||
packagePath = path.dirname(packagePath);
|
||||
}
|
||||
}
|
||||
if (packagePath === '.') {
|
||||
return pluginPath;
|
||||
}
|
||||
return importFresh<{name: string}>(
|
||||
path.join(packagePath, 'package.json'),
|
||||
).name;
|
||||
}
|
||||
}
|
||||
if (packagePath === '.') {
|
||||
return pluginPath;
|
||||
}
|
||||
return importFresh<{name: string}>(path.join(packagePath, 'package.json'))
|
||||
.name;
|
||||
});
|
||||
|
||||
return '';
|
||||
})
|
||||
.filter((plugin) => plugin !== '');
|
||||
}
|
||||
|
||||
function walk(dir: string): Array<string> {
|
||||
|
@ -178,7 +190,7 @@ export default async function swizzle(
|
|||
// find the plugin from list of plugin and get options if specified
|
||||
pluginConfigs.forEach((pluginConfig) => {
|
||||
// plugin can be a [string], [string,object] or string.
|
||||
if (Array.isArray(pluginConfig)) {
|
||||
if (Array.isArray(pluginConfig) && typeof pluginConfig[0] === 'string') {
|
||||
if (require.resolve(pluginConfig[0]) === resolvedThemeName) {
|
||||
if (pluginConfig.length === 2) {
|
||||
const [, options] = pluginConfig;
|
||||
|
|
|
@ -31,32 +31,66 @@ exports[`normalizeConfig should throw error if css doesn't have href 1`] = `
|
|||
`;
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not a string and it's not an array #1 for the input of: [123] 1`] = `
|
||||
"\\"plugins[0]\\" does not match any of the allowed types
|
||||
"
|
||||
`;
|
||||
" => Bad Docusaurus plugin value as path [plugins,0].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not a string and it's not an array #2 for the input of: [[Function anonymous]] 1`] = `
|
||||
"\\"plugins[0]\\" does not match any of the allowed types
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #1 for the input of: [[Array]] 1`] = `
|
||||
"\\"plugins[0]\\" does not match any of the allowed types
|
||||
" => Bad Docusaurus plugin value as path [plugins,0].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #2 for the input of: [[Array]] 1`] = `
|
||||
"\\"plugins[0]\\" does not match any of the allowed types
|
||||
" => Bad Docusaurus plugin value as path [plugins,0].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not an array of [string, object][] #3 for the input of: [[Array]] 1`] = `
|
||||
"\\"plugins[0]\\" does not match any of the allowed types
|
||||
"
|
||||
`;
|
||||
" => Bad Docusaurus plugin value as path [plugins,0].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
exports[`normalizeConfig should throw error if plugins is not array for the input of: [Function anonymous] 1`] = `
|
||||
"\\"plugins\\" must be an array
|
||||
"
|
||||
`;
|
||||
|
||||
|
|
|
@ -90,24 +90,10 @@ describe('normalizeConfig', () => {
|
|||
|
||||
test.each([
|
||||
['should throw error if plugins is not array', {}],
|
||||
[
|
||||
'should throw error if plugins is not array',
|
||||
function () {
|
||||
console.log('noop');
|
||||
},
|
||||
],
|
||||
[
|
||||
"should throw error if plugins is not a string and it's not an array #1",
|
||||
[123],
|
||||
],
|
||||
[
|
||||
"should throw error if plugins is not a string and it's not an array #2",
|
||||
[
|
||||
function () {
|
||||
console.log('noop');
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'should throw error if plugins is not an array of [string, object][] #1',
|
||||
[['example/path', 'wrong parameter here']],
|
||||
|
@ -153,6 +139,11 @@ describe('normalizeConfig', () => {
|
|||
['this/should/work', {too: 'yes'}],
|
||||
],
|
||||
],
|
||||
['should accept function for plugin', [function (_context, _options) {}]],
|
||||
[
|
||||
'should accept [function, object] for plugin',
|
||||
[[function (_context, _options) {}, {it: 'should work'}]],
|
||||
],
|
||||
])(`%s for the input of: %p`, (_message, plugins) => {
|
||||
expect(() => {
|
||||
normalizeConfig({
|
||||
|
|
|
@ -51,13 +51,36 @@ export const DEFAULT_CONFIG: Pick<
|
|||
baseUrlIssueBanner: true,
|
||||
};
|
||||
|
||||
const PluginSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.ordered(Joi.string().required(), Joi.object().required())
|
||||
.length(2),
|
||||
Joi.bool().equal(false), // In case of conditional adding of plugins.
|
||||
);
|
||||
const PluginSchema = Joi.alternatives()
|
||||
.try(
|
||||
Joi.function(),
|
||||
Joi.array().ordered(Joi.function().required(), Joi.object().required()),
|
||||
Joi.string(),
|
||||
Joi.array()
|
||||
.ordered(Joi.string().required(), Joi.object().required())
|
||||
.length(2),
|
||||
Joi.bool().equal(false), // In case of conditional adding of plugins.
|
||||
)
|
||||
// TODO isn't there a simpler way to customize the default Joi error message???
|
||||
// Not sure why Joi makes it complicated to add a custom error message...
|
||||
// See https://stackoverflow.com/a/54657686/82609
|
||||
.error((errors) => {
|
||||
errors.forEach((error) => {
|
||||
error.message = ` => Bad Docusaurus plugin value as path [${error.path}].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
["@docusaurus/plugin-content-docs",options],
|
||||
"./myPlugin",
|
||||
["./myPlugin",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
`;
|
||||
});
|
||||
return errors as any;
|
||||
});
|
||||
|
||||
const ThemeSchema = Joi.alternatives().try(
|
||||
Joi.string(),
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
} from './translations/translations';
|
||||
import {mapValues} from 'lodash';
|
||||
|
||||
type LoadContextOptions = {
|
||||
export type LoadContextOptions = {
|
||||
customOutDir?: string;
|
||||
customConfigFilePath?: string;
|
||||
locale?: string;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
title: 'My Site',
|
||||
tagline: 'The tagline of my site',
|
||||
url: 'https://your-docusaurus-test-site.com',
|
||||
baseUrl: '/',
|
||||
favicon: 'img/favicon.ico',
|
||||
plugins: [42, true],
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
# Hello
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
title: 'My Site',
|
||||
tagline: 'The tagline of my site',
|
||||
url: 'https://your-docusaurus-test-site.com',
|
||||
baseUrl: '/',
|
||||
favicon: 'img/favicon.ico',
|
||||
plugins: [
|
||||
function (context, options) {
|
||||
return {name: 'first-plugin'};
|
||||
},
|
||||
[
|
||||
function (context, options) {
|
||||
return {name: 'second-plugin'};
|
||||
},
|
||||
{it: 'should work'},
|
||||
],
|
||||
'./plugin3.js',
|
||||
['./plugin4.js', {}],
|
||||
],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = function (context, options) {
|
||||
return {
|
||||
name: 'third-plugin',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = function (context, options) {
|
||||
return {
|
||||
name: 'fourth-plugin',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"docs": {
|
||||
"Test": ["hello"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`initPlugins plugins with bad values throw user-friendly error message 1`] = `
|
||||
" => Bad Docusaurus plugin value as path [plugins,0].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
=> Bad Docusaurus plugin value as path [plugins,1].
|
||||
Example valid plugin config:
|
||||
{
|
||||
plugins: [
|
||||
[\\"@docusaurus/plugin-content-docs\\",options],
|
||||
\\"./myPlugin\\",
|
||||
[\\"./myPlugin\\",{someOption: 42}],
|
||||
function myPlugin() { },
|
||||
[function myPlugin() { },options]
|
||||
],
|
||||
};
|
||||
|
||||
"
|
||||
`;
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {loadContext, LoadContextOptions, loadPluginConfigs} from '../../index';
|
||||
import initPlugins from '../init';
|
||||
|
||||
describe('initPlugins', () => {
|
||||
async function loadSite(options: LoadContextOptions = {}) {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'site-with-plugin');
|
||||
const context = await loadContext(siteDir, options);
|
||||
const pluginConfigs = loadPluginConfigs(context);
|
||||
const plugins = initPlugins({
|
||||
pluginConfigs,
|
||||
context,
|
||||
});
|
||||
|
||||
return {siteDir, context, plugins};
|
||||
}
|
||||
|
||||
test('plugins gets parsed correctly and loads in correct order', async () => {
|
||||
const {context, plugins} = await loadSite();
|
||||
expect(context.siteConfig.plugins?.length).toBe(4);
|
||||
expect(plugins.length).toBe(4);
|
||||
|
||||
expect(plugins[0].name).toBe('first-plugin');
|
||||
expect(plugins[1].name).toBe('second-plugin');
|
||||
expect(plugins[2].name).toBe('third-plugin');
|
||||
expect(plugins[3].name).toBe('fourth-plugin');
|
||||
});
|
||||
|
||||
test('plugins with bad values throw user-friendly error message', async () => {
|
||||
await expect(() =>
|
||||
loadSite({
|
||||
customConfigFilePath: 'badPlugins.docusaurus.config.js',
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
|
@ -11,6 +11,7 @@ import {
|
|||
DocusaurusPluginVersionInformation,
|
||||
ImportedPluginModule,
|
||||
LoadContext,
|
||||
PluginModule,
|
||||
Plugin,
|
||||
PluginConfig,
|
||||
PluginOptions,
|
||||
|
@ -23,6 +24,106 @@ import {
|
|||
normalizeThemeConfig,
|
||||
} from '@docusaurus/utils-validation';
|
||||
|
||||
type NormalizedPluginConfig = {
|
||||
plugin: PluginModule;
|
||||
options: PluginOptions;
|
||||
// Only available when a string is provided in config
|
||||
pluginModule?: {
|
||||
path: string;
|
||||
module: ImportedPluginModule;
|
||||
};
|
||||
};
|
||||
|
||||
function normalizePluginConfig(
|
||||
pluginConfig: PluginConfig,
|
||||
pluginRequire: NodeRequire,
|
||||
): NormalizedPluginConfig {
|
||||
// plugins: ['./plugin']
|
||||
if (typeof pluginConfig === 'string') {
|
||||
const pluginModuleImport = pluginConfig;
|
||||
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
||||
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
||||
return {
|
||||
plugin: pluginModule?.default ?? pluginModule,
|
||||
options: {},
|
||||
pluginModule: {
|
||||
path: pluginModuleImport,
|
||||
module: pluginModule,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// plugins: [function plugin() { }]
|
||||
if (typeof pluginConfig === 'function') {
|
||||
return {
|
||||
plugin: pluginConfig,
|
||||
options: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (Array.isArray(pluginConfig)) {
|
||||
// plugins: [
|
||||
// ['./plugin',options],
|
||||
// ]
|
||||
if (typeof pluginConfig[0] === 'string') {
|
||||
const pluginModuleImport = pluginConfig[0];
|
||||
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
||||
const pluginModule = importFresh<ImportedPluginModule>(pluginPath);
|
||||
return {
|
||||
plugin: pluginModule?.default ?? pluginModule,
|
||||
options: pluginConfig[1] ?? {},
|
||||
pluginModule: {
|
||||
path: pluginModuleImport,
|
||||
module: pluginModule,
|
||||
},
|
||||
};
|
||||
}
|
||||
// plugins: [
|
||||
// [function plugin() { },options],
|
||||
// ]
|
||||
if (typeof pluginConfig[0] === 'function') {
|
||||
return {
|
||||
plugin: pluginConfig[0],
|
||||
options: pluginConfig[1] ?? {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected: cant load plugin for plugin config = ${JSON.stringify(
|
||||
pluginConfig,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function getOptionValidationFunction(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
): PluginModule['validateOptions'] {
|
||||
if (normalizedPluginConfig.pluginModule) {
|
||||
// support both commonjs and ES modules
|
||||
return (
|
||||
normalizedPluginConfig.pluginModule.module?.default?.validateOptions ??
|
||||
normalizedPluginConfig.pluginModule.module?.validateOptions
|
||||
);
|
||||
} else {
|
||||
return normalizedPluginConfig.plugin.validateOptions;
|
||||
}
|
||||
}
|
||||
|
||||
function getThemeValidationFunction(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
): PluginModule['validateThemeConfig'] {
|
||||
if (normalizedPluginConfig.pluginModule) {
|
||||
// support both commonjs and ES modules
|
||||
return (
|
||||
normalizedPluginConfig.pluginModule.module.default?.validateThemeConfig ??
|
||||
normalizedPluginConfig.pluginModule.module.validateThemeConfig
|
||||
);
|
||||
} else {
|
||||
return normalizedPluginConfig.plugin.validateThemeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export type InitPlugin = Plugin<unknown> & {
|
||||
readonly options: PluginOptions;
|
||||
readonly version: DocusaurusPluginVersionInformation;
|
||||
|
@ -42,75 +143,82 @@ export default function initPlugins({
|
|||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||
const pluginRequire = createRequire(context.siteConfigPath);
|
||||
|
||||
const plugins: InitPlugin[] = pluginConfigs
|
||||
.map((pluginItem) => {
|
||||
let pluginModuleImport: string | undefined;
|
||||
let pluginOptions: PluginOptions = {};
|
||||
function doGetPluginVersion(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
): DocusaurusPluginVersionInformation {
|
||||
// get plugin version
|
||||
if (normalizedPluginConfig.pluginModule?.path) {
|
||||
const pluginPath = pluginRequire.resolve(
|
||||
normalizedPluginConfig.pluginModule?.path,
|
||||
);
|
||||
return getPluginVersion(pluginPath, context.siteDir);
|
||||
} else {
|
||||
return {type: 'local'};
|
||||
}
|
||||
}
|
||||
|
||||
if (!pluginItem) {
|
||||
function doValidateThemeConfig(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
) {
|
||||
const validateThemeConfig = getThemeValidationFunction(
|
||||
normalizedPluginConfig,
|
||||
);
|
||||
if (validateThemeConfig) {
|
||||
return validateThemeConfig({
|
||||
validate: normalizeThemeConfig,
|
||||
themeConfig: context.siteConfig.themeConfig,
|
||||
});
|
||||
} else {
|
||||
return context.siteConfig.themeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
function doValidatePluginOptions(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
) {
|
||||
const validateOptions = getOptionValidationFunction(normalizedPluginConfig);
|
||||
if (validateOptions) {
|
||||
return validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: normalizedPluginConfig.options,
|
||||
});
|
||||
} else {
|
||||
// Important to ensure all plugins have an id
|
||||
// as we don't go through the Joi schema that adds it
|
||||
return {
|
||||
...normalizedPluginConfig.options,
|
||||
id: normalizedPluginConfig.options.id ?? DEFAULT_PLUGIN_ID,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const plugins: InitPlugin[] = pluginConfigs
|
||||
.map((pluginConfig) => {
|
||||
if (!pluginConfig) {
|
||||
return null;
|
||||
}
|
||||
const normalizedPluginConfig = normalizePluginConfig(
|
||||
pluginConfig,
|
||||
pluginRequire,
|
||||
);
|
||||
const pluginVersion: DocusaurusPluginVersionInformation = doGetPluginVersion(
|
||||
normalizedPluginConfig,
|
||||
);
|
||||
const pluginOptions = doValidatePluginOptions(normalizedPluginConfig);
|
||||
|
||||
if (typeof pluginItem === 'string') {
|
||||
pluginModuleImport = pluginItem;
|
||||
} else if (Array.isArray(pluginItem)) {
|
||||
[pluginModuleImport, pluginOptions = {}] = pluginItem;
|
||||
} else {
|
||||
throw new TypeError(`You supplied a wrong type of plugin.
|
||||
A plugin should be either string or [importPath: string, options?: object].
|
||||
// Side-effect: merge the normalized theme config in the original one
|
||||
context.siteConfig.themeConfig = {
|
||||
...context.siteConfig.themeConfig,
|
||||
...doValidateThemeConfig(normalizedPluginConfig),
|
||||
};
|
||||
|
||||
For more information, visit https://docusaurus.io/docs/using-plugins.`);
|
||||
}
|
||||
|
||||
if (!pluginModuleImport) {
|
||||
throw new Error('The path to the plugin is either undefined or null.');
|
||||
}
|
||||
|
||||
// The pluginModuleImport value is any valid
|
||||
// module identifier - npm package or locally-resolved path.
|
||||
const pluginPath = pluginRequire.resolve(pluginModuleImport);
|
||||
const pluginModule: ImportedPluginModule = importFresh(pluginPath);
|
||||
const pluginVersion = getPluginVersion(pluginPath, context.siteDir);
|
||||
|
||||
const plugin = pluginModule.default || pluginModule;
|
||||
|
||||
// support both commonjs and ES modules
|
||||
const validateOptions =
|
||||
pluginModule.default?.validateOptions ?? pluginModule.validateOptions;
|
||||
|
||||
if (validateOptions) {
|
||||
pluginOptions = validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: pluginOptions,
|
||||
});
|
||||
} else {
|
||||
// Important to ensure all plugins have an id
|
||||
// as we don't go through the Joi schema that adds it
|
||||
pluginOptions = {
|
||||
...pluginOptions,
|
||||
id: pluginOptions.id ?? DEFAULT_PLUGIN_ID,
|
||||
};
|
||||
}
|
||||
|
||||
// support both commonjs and ES modules
|
||||
const validateThemeConfig =
|
||||
pluginModule.default?.validateThemeConfig ??
|
||||
pluginModule.validateThemeConfig;
|
||||
|
||||
if (validateThemeConfig) {
|
||||
const normalizedThemeConfig = validateThemeConfig({
|
||||
validate: normalizeThemeConfig,
|
||||
themeConfig: context.siteConfig.themeConfig,
|
||||
});
|
||||
|
||||
context.siteConfig.themeConfig = {
|
||||
...context.siteConfig.themeConfig,
|
||||
...normalizedThemeConfig,
|
||||
};
|
||||
}
|
||||
const pluginInstance = normalizedPluginConfig.plugin(
|
||||
context,
|
||||
pluginOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
...plugin(context, pluginOptions),
|
||||
...pluginInstance,
|
||||
options: pluginOptions,
|
||||
version: pluginVersion,
|
||||
};
|
||||
|
|
|
@ -119,28 +119,62 @@ Docusaurus' implementation of the plugins system provides us with a convenient w
|
|||
|
||||
## Creating plugins {#creating-plugins}
|
||||
|
||||
A plugin is a module which exports a function that takes two parameters and returns an object when executed.
|
||||
A plugin is a function that takes two parameters: `context` and `options`.
|
||||
|
||||
### Module definition {#module-definition}
|
||||
It returns a plugin instance object, containing plugin [lifecycle APIs](./lifecycle-apis.md).
|
||||
|
||||
The exported modules for plugins are called with two parameters: `context` and `options` and returns a JavaScript object with defining the [lifecycle APIs](./lifecycle-apis.md).
|
||||
It can be defined as a function or a module.
|
||||
|
||||
For example if you have a reference to a local folder such as this in your `docusaurus.config.js`:
|
||||
### Functional definition {#functional-definition}
|
||||
|
||||
You can use a plugin as a function, directly in the Docusaurus config file:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
// ...
|
||||
plugins: [path.resolve(__dirname, 'my-plugin')],
|
||||
plugins: [
|
||||
// highligh-start
|
||||
function myPlugin(contex, options) {
|
||||
// ...
|
||||
return {
|
||||
name: 'my-plugin',
|
||||
async loadContent() {
|
||||
// ...
|
||||
},
|
||||
async contentLoaded({content, actions}) {
|
||||
// ...
|
||||
},
|
||||
/* other lifecycle API */
|
||||
};
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Module definition {#module-definition}
|
||||
|
||||
You can use a plugin as a module, loading it from a separate file or NPM package:
|
||||
|
||||
```js title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
// ...
|
||||
plugins: [
|
||||
// without options:
|
||||
'./my-plugin',
|
||||
// or with options:
|
||||
['./my-plugin', options],
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Then in the folder `my-plugin` you can create an index.js such as this
|
||||
|
||||
```js title="index.js"
|
||||
module.exports = function (context, options) {
|
||||
```js title="my-plugin.js"
|
||||
module.exports = function myPlugin(context, options) {
|
||||
// ...
|
||||
return {
|
||||
name: 'my-docusaurus-plugin',
|
||||
name: 'my-plugin',
|
||||
async loadContent() {
|
||||
/* ... */
|
||||
},
|
||||
|
@ -152,11 +186,9 @@ module.exports = function (context, options) {
|
|||
};
|
||||
```
|
||||
|
||||
The `my-plugin` folder could also be a fully fledged package with it's own package.json and a `src/index.js` file for example
|
||||
|
||||
#### `context` {#context}
|
||||
|
||||
`context` is plugin-agnostic and the same object will be passed into all plugins used for a Docusaurus website. The `context` object contains the following fields:
|
||||
`context` is plugin-agnostic, and the same object will be passed into all plugins used for a Docusaurus website. The `context` object contains the following fields:
|
||||
|
||||
```ts
|
||||
interface LoadContext {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue