diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 0ab274f42c..49ca673c88 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -294,7 +294,12 @@ export type ConfigurePostCssFn = Plugin['configurePostCss']; export type PluginOptions = {id?: string} & Record; -export type PluginConfig = [string, PluginOptions] | [string] | string; +export type PluginConfig = + | [string, PluginOptions] + | [string] + | string + | [PluginModule, PluginOptions] + | PluginModule; export interface ChunkRegistry { loader: string; diff --git a/packages/docusaurus/src/commands/swizzle.ts b/packages/docusaurus/src/commands/swizzle.ts index 52849f098a..2b4acda2d1 100644 --- a/packages/docusaurus/src/commands/swizzle.ts +++ b/packages/docusaurus/src/commands/swizzle.ts @@ -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 { @@ -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; diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap index 8a3b047686..69fa24bdb6 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/configValidation.test.ts.snap @@ -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 " `; diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index b8c8f4795f..bc4a633e65 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -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({ diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 60de610671..ce7d635735 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -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(), diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index da7e02907e..fd7ec3d4a1 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -38,7 +38,7 @@ import { } from './translations/translations'; import {mapValues} from 'lodash'; -type LoadContextOptions = { +export type LoadContextOptions = { customOutDir?: string; customConfigFilePath?: string; locale?: string; diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/badPlugins.docusaurus.config.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/badPlugins.docusaurus.config.js new file mode 100644 index 0000000000..1aa6972aba --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/badPlugins.docusaurus.config.js @@ -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], +}; diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docs/hello.md b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docs/hello.md new file mode 100644 index 0000000000..fec56017dc --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docs/hello.md @@ -0,0 +1 @@ +# Hello diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docusaurus.config.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docusaurus.config.js new file mode 100644 index 0000000000..d2f0d0862f --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/docusaurus.config.js @@ -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', {}], + ], +}; diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin3.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin3.js new file mode 100644 index 0000000000..b220cc30b7 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin3.js @@ -0,0 +1,5 @@ +module.exports = function (context, options) { + return { + name: 'third-plugin', + }; +}; diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin4.js b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin4.js new file mode 100644 index 0000000000..8417ead825 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/plugin4.js @@ -0,0 +1,5 @@ +module.exports = function (context, options) { + return { + name: 'fourth-plugin', + }; +}; diff --git a/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/sidebars.json b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/sidebars.json new file mode 100644 index 0000000000..3687442804 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/sidebars.json @@ -0,0 +1,5 @@ +{ + "docs": { + "Test": ["hello"] + } +} diff --git a/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/init.test.ts.snap b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/init.test.ts.snap new file mode 100644 index 0000000000..d34313a5bf --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/__snapshots__/init.test.ts.snap @@ -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] + ], +}; + +" +`; diff --git a/packages/docusaurus/src/server/plugins/__tests__/init.test.ts b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts new file mode 100644 index 0000000000..185781bfa4 --- /dev/null +++ b/packages/docusaurus/src/server/plugins/__tests__/init.test.ts @@ -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(); + }); +}); diff --git a/packages/docusaurus/src/server/plugins/init.ts b/packages/docusaurus/src/server/plugins/init.ts index 9b2366109d..b21875efa7 100644 --- a/packages/docusaurus/src/server/plugins/init.ts +++ b/packages/docusaurus/src/server/plugins/init.ts @@ -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(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(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 & { 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, }; diff --git a/website/docs/using-plugins.md b/website/docs/using-plugins.md index 82a05c4c65..f746338eae 100644 --- a/website/docs/using-plugins.md +++ b/website/docs/using-plugins.md @@ -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 {