mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-03 03:12:35 +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 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 {
|
export interface ChunkRegistry {
|
||||||
loader: string;
|
loader: string;
|
||||||
|
|
|
@ -18,22 +18,34 @@ import initPlugins from '../server/plugins/init';
|
||||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||||
|
|
||||||
export function getPluginNames(plugins: PluginConfig[]): string[] {
|
export function getPluginNames(plugins: PluginConfig[]): string[] {
|
||||||
return plugins.map((plugin) => {
|
return plugins
|
||||||
const pluginPath = Array.isArray(plugin) ? plugin[0] : plugin;
|
.filter(
|
||||||
let packagePath = path.dirname(pluginPath);
|
(plugin) =>
|
||||||
while (packagePath) {
|
typeof plugin === 'string' ||
|
||||||
if (fs.existsSync(path.join(packagePath, 'package.json'))) {
|
(Array.isArray(plugin) && typeof plugin[0] === 'string'),
|
||||||
break;
|
)
|
||||||
} else {
|
.map((plugin) => {
|
||||||
packagePath = path.dirname(packagePath);
|
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 '';
|
||||||
return pluginPath;
|
})
|
||||||
}
|
.filter((plugin) => plugin !== '');
|
||||||
return importFresh<{name: string}>(path.join(packagePath, 'package.json'))
|
|
||||||
.name;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function walk(dir: string): Array<string> {
|
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
|
// find the plugin from list of plugin and get options if specified
|
||||||
pluginConfigs.forEach((pluginConfig) => {
|
pluginConfigs.forEach((pluginConfig) => {
|
||||||
// plugin can be a [string], [string,object] or string.
|
// 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 (require.resolve(pluginConfig[0]) === resolvedThemeName) {
|
||||||
if (pluginConfig.length === 2) {
|
if (pluginConfig.length === 2) {
|
||||||
const [, options] = pluginConfig;
|
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`] = `
|
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`] = `
|
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`] = `
|
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`] = `
|
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([
|
test.each([
|
||||||
['should throw error if plugins is not array', {}],
|
['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",
|
"should throw error if plugins is not a string and it's not an array #1",
|
||||||
[123],
|
[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',
|
'should throw error if plugins is not an array of [string, object][] #1',
|
||||||
[['example/path', 'wrong parameter here']],
|
[['example/path', 'wrong parameter here']],
|
||||||
|
@ -153,6 +139,11 @@ describe('normalizeConfig', () => {
|
||||||
['this/should/work', {too: 'yes'}],
|
['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) => {
|
])(`%s for the input of: %p`, (_message, plugins) => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
normalizeConfig({
|
normalizeConfig({
|
||||||
|
|
|
@ -51,13 +51,36 @@ export const DEFAULT_CONFIG: Pick<
|
||||||
baseUrlIssueBanner: true,
|
baseUrlIssueBanner: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PluginSchema = Joi.alternatives().try(
|
const PluginSchema = Joi.alternatives()
|
||||||
Joi.string(),
|
.try(
|
||||||
Joi.array()
|
Joi.function(),
|
||||||
.ordered(Joi.string().required(), Joi.object().required())
|
Joi.array().ordered(Joi.function().required(), Joi.object().required()),
|
||||||
.length(2),
|
Joi.string(),
|
||||||
Joi.bool().equal(false), // In case of conditional adding of plugins.
|
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(
|
const ThemeSchema = Joi.alternatives().try(
|
||||||
Joi.string(),
|
Joi.string(),
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {
|
||||||
} from './translations/translations';
|
} from './translations/translations';
|
||||||
import {mapValues} from 'lodash';
|
import {mapValues} from 'lodash';
|
||||||
|
|
||||||
type LoadContextOptions = {
|
export type LoadContextOptions = {
|
||||||
customOutDir?: string;
|
customOutDir?: string;
|
||||||
customConfigFilePath?: string;
|
customConfigFilePath?: string;
|
||||||
locale?: 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,
|
DocusaurusPluginVersionInformation,
|
||||||
ImportedPluginModule,
|
ImportedPluginModule,
|
||||||
LoadContext,
|
LoadContext,
|
||||||
|
PluginModule,
|
||||||
Plugin,
|
Plugin,
|
||||||
PluginConfig,
|
PluginConfig,
|
||||||
PluginOptions,
|
PluginOptions,
|
||||||
|
@ -23,6 +24,106 @@ import {
|
||||||
normalizeThemeConfig,
|
normalizeThemeConfig,
|
||||||
} from '@docusaurus/utils-validation';
|
} 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> & {
|
export type InitPlugin = Plugin<unknown> & {
|
||||||
readonly options: PluginOptions;
|
readonly options: PluginOptions;
|
||||||
readonly version: DocusaurusPluginVersionInformation;
|
readonly version: DocusaurusPluginVersionInformation;
|
||||||
|
@ -42,75 +143,82 @@ export default function initPlugins({
|
||||||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||||
const pluginRequire = createRequire(context.siteConfigPath);
|
const pluginRequire = createRequire(context.siteConfigPath);
|
||||||
|
|
||||||
const plugins: InitPlugin[] = pluginConfigs
|
function doGetPluginVersion(
|
||||||
.map((pluginItem) => {
|
normalizedPluginConfig: NormalizedPluginConfig,
|
||||||
let pluginModuleImport: string | undefined;
|
): DocusaurusPluginVersionInformation {
|
||||||
let pluginOptions: PluginOptions = {};
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
const normalizedPluginConfig = normalizePluginConfig(
|
||||||
|
pluginConfig,
|
||||||
|
pluginRequire,
|
||||||
|
);
|
||||||
|
const pluginVersion: DocusaurusPluginVersionInformation = doGetPluginVersion(
|
||||||
|
normalizedPluginConfig,
|
||||||
|
);
|
||||||
|
const pluginOptions = doValidatePluginOptions(normalizedPluginConfig);
|
||||||
|
|
||||||
if (typeof pluginItem === 'string') {
|
// Side-effect: merge the normalized theme config in the original one
|
||||||
pluginModuleImport = pluginItem;
|
context.siteConfig.themeConfig = {
|
||||||
} else if (Array.isArray(pluginItem)) {
|
...context.siteConfig.themeConfig,
|
||||||
[pluginModuleImport, pluginOptions = {}] = pluginItem;
|
...doValidateThemeConfig(normalizedPluginConfig),
|
||||||
} else {
|
};
|
||||||
throw new TypeError(`You supplied a wrong type of plugin.
|
|
||||||
A plugin should be either string or [importPath: string, options?: object].
|
|
||||||
|
|
||||||
For more information, visit https://docusaurus.io/docs/using-plugins.`);
|
const pluginInstance = normalizedPluginConfig.plugin(
|
||||||
}
|
context,
|
||||||
|
pluginOptions,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...plugin(context, pluginOptions),
|
...pluginInstance,
|
||||||
options: pluginOptions,
|
options: pluginOptions,
|
||||||
version: pluginVersion,
|
version: pluginVersion,
|
||||||
};
|
};
|
||||||
|
|
|
@ -119,28 +119,62 @@ Docusaurus' implementation of the plugins system provides us with a convenient w
|
||||||
|
|
||||||
## Creating plugins {#creating-plugins}
|
## 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"
|
```js title="docusaurus.config.js"
|
||||||
module.exports = {
|
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
|
Then in the folder `my-plugin` you can create an index.js such as this
|
||||||
|
|
||||||
```js title="index.js"
|
```js title="my-plugin.js"
|
||||||
module.exports = function (context, options) {
|
module.exports = function myPlugin(context, options) {
|
||||||
// ...
|
// ...
|
||||||
return {
|
return {
|
||||||
name: 'my-docusaurus-plugin',
|
name: 'my-plugin',
|
||||||
async loadContent() {
|
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` {#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
|
```ts
|
||||||
interface LoadContext {
|
interface LoadContext {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue