mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-22 04:28:10 +02:00
feat(core): allow plugins to self-disable by returning null (#10286)
This commit is contained in:
parent
8c2943421b
commit
80203b385d
13 changed files with 181 additions and 86 deletions
|
@ -21,7 +21,7 @@ const PluginName = 'docusaurus-plugin-client-redirects';
|
|||
export default function pluginClientRedirectsPages(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
): Plugin<void> | null {
|
||||
const {trailingSlash} = context.siteConfig;
|
||||
const router = context.siteConfig.future.experimental_router;
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default function pluginClientRedirectsPages(
|
|||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -18,21 +18,21 @@ import type {PluginOptions, Options} from './options';
|
|||
export default function pluginGoogleAnalytics(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin {
|
||||
): Plugin | null {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {trackingID, anonymizeIP} = options;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-google-analytics',
|
||||
|
||||
getClientModules() {
|
||||
return isProd ? ['./analytics'] : [];
|
||||
return ['./analytics'];
|
||||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
if (!isProd) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
headTags: [
|
||||
{
|
||||
|
|
|
@ -32,8 +32,10 @@ function createConfigSnippets({
|
|||
export default function pluginGoogleGtag(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
): Plugin | null {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstTrackingId = options.trackingID[0];
|
||||
|
||||
|
@ -45,13 +47,10 @@ export default function pluginGoogleGtag(
|
|||
},
|
||||
|
||||
getClientModules() {
|
||||
return isProd ? ['./gtag'] : [];
|
||||
return ['./gtag'];
|
||||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
if (!isProd) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
// Gtag includes GA by default, so we also preconnect to
|
||||
// google-analytics.
|
||||
|
|
|
@ -16,10 +16,12 @@ import type {PluginOptions, Options} from './options';
|
|||
export default function pluginGoogleAnalytics(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin {
|
||||
const {containerId} = options;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
): Plugin | null {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {containerId} = options;
|
||||
return {
|
||||
name: 'docusaurus-plugin-google-tag-manager',
|
||||
|
||||
|
@ -28,9 +30,6 @@ export default function pluginGoogleAnalytics(
|
|||
},
|
||||
|
||||
injectHtmlTags() {
|
||||
if (!isProd) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
preBodyTags: [
|
||||
{
|
||||
|
|
|
@ -19,8 +19,6 @@ import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
|||
|
||||
const PluginName = 'docusaurus-plugin-pwa';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
function getSWBabelLoader() {
|
||||
return {
|
||||
loader: 'babel-loader',
|
||||
|
@ -45,12 +43,21 @@ function getSWBabelLoader() {
|
|||
export default function pluginPWA(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
): Plugin<void> | null {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
if (context.siteConfig.future.experimental_router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
outDir,
|
||||
baseUrl,
|
||||
i18n: {currentLocale},
|
||||
siteConfig,
|
||||
} = context;
|
||||
const {
|
||||
debug,
|
||||
|
@ -61,13 +68,6 @@ export default function pluginPWA(
|
|||
swRegister,
|
||||
} = options;
|
||||
|
||||
if (siteConfig.future.experimental_router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
}
|
||||
|
||||
return {
|
||||
name: PluginName,
|
||||
|
||||
|
@ -79,7 +79,7 @@ export default function pluginPWA(
|
|||
},
|
||||
|
||||
getClientModules() {
|
||||
return isProd && swRegister ? [swRegister] : [];
|
||||
return swRegister ? [swRegister] : [];
|
||||
},
|
||||
|
||||
getDefaultCodeTranslationMessages() {
|
||||
|
@ -90,10 +90,6 @@ export default function pluginPWA(
|
|||
},
|
||||
|
||||
configureWebpack(config) {
|
||||
if (!isProd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
|
@ -111,7 +107,6 @@ export default function pluginPWA(
|
|||
|
||||
injectHtmlTags() {
|
||||
const headTags: HtmlTags = [];
|
||||
if (isProd) {
|
||||
pwaHead.forEach(({tagName, ...attributes}) => {
|
||||
(['href', 'content'] as const).forEach((attribute) => {
|
||||
const attributeValue = attributes[attribute];
|
||||
|
@ -133,15 +128,10 @@ export default function pluginPWA(
|
|||
attributes,
|
||||
});
|
||||
});
|
||||
}
|
||||
return {headTags};
|
||||
},
|
||||
|
||||
async postBuild(props) {
|
||||
if (!isProd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const swSourceFileTest = /\.m?js$/;
|
||||
|
||||
const swWebpackConfig: Configuration = {
|
||||
|
|
|
@ -17,12 +17,12 @@ const PluginName = 'docusaurus-plugin-sitemap';
|
|||
export default function pluginSitemap(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
): Plugin<void> | null {
|
||||
if (context.siteConfig.future.experimental_router === 'hash') {
|
||||
logger.warn(
|
||||
`${PluginName} does not support the Hash Router and will be disabled.`,
|
||||
);
|
||||
return {name: PluginName};
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -11,14 +11,15 @@ import type {PluginOptions, Options} from './options';
|
|||
export default function pluginVercelAnalytics(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin {
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
): Plugin | null {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: 'docusaurus-plugin-vercel-analytics',
|
||||
|
||||
getClientModules() {
|
||||
return isProd ? ['./analytics'] : [];
|
||||
return ['./analytics'];
|
||||
},
|
||||
|
||||
contentLoaded({actions}) {
|
||||
|
|
5
packages/docusaurus-types/src/plugin.d.ts
vendored
5
packages/docusaurus-types/src/plugin.d.ts
vendored
|
@ -191,7 +191,10 @@ export type LoadedPlugin = InitializedPlugin & {
|
|||
export type PluginModule<Content = unknown> = {
|
||||
(context: LoadContext, options: unknown):
|
||||
| Plugin<Content>
|
||||
| Promise<Plugin<Content>>;
|
||||
| Promise<Plugin<Content>>
|
||||
| null
|
||||
| Promise<null>;
|
||||
|
||||
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
|
||||
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
|
||||
|
||||
|
|
|
@ -6,22 +6,41 @@
|
|||
*/
|
||||
|
||||
import {loadContext} from '../../server/site';
|
||||
import {initPlugins} from '../../server/plugins/init';
|
||||
import {initPluginsConfigs} from '../../server/plugins/init';
|
||||
import {loadPluginConfigs} from '../../server/plugins/configs';
|
||||
import type {SwizzleCLIOptions, SwizzleContext} from './common';
|
||||
import type {SwizzleCLIOptions, SwizzleContext, SwizzlePlugin} from './common';
|
||||
import type {LoadContext} from '@docusaurus/types';
|
||||
|
||||
async function getSwizzlePlugins(
|
||||
context: LoadContext,
|
||||
): Promise<SwizzlePlugin[]> {
|
||||
const pluginConfigs = await loadPluginConfigs(context);
|
||||
const pluginConfigInitResults = await initPluginsConfigs(
|
||||
context,
|
||||
pluginConfigs,
|
||||
);
|
||||
|
||||
return pluginConfigInitResults.flatMap((initResult) => {
|
||||
// Ignore self-disabling plugins returning null
|
||||
if (initResult.plugin === null) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
// TODO this is a bit confusing, need refactor
|
||||
{
|
||||
plugin: initResult.config,
|
||||
instance: initResult.plugin,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
export async function initSwizzleContext(
|
||||
siteDir: string,
|
||||
options: SwizzleCLIOptions,
|
||||
): Promise<SwizzleContext> {
|
||||
const context = await loadContext({siteDir, config: options.config});
|
||||
const plugins = await initPlugins(context);
|
||||
const pluginConfigs = await loadPluginConfigs(context);
|
||||
|
||||
return {
|
||||
plugins: plugins.map((plugin, pluginIndex) => ({
|
||||
plugin: pluginConfigs[pluginIndex]!,
|
||||
instance: plugin,
|
||||
})),
|
||||
plugins: await getSwizzlePlugins(context),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ module.exports = {
|
|||
},
|
||||
{it: 'should work'},
|
||||
],
|
||||
function (context, options) {
|
||||
// it's ok for a plugin to self-disable
|
||||
return null;
|
||||
},
|
||||
'./plugin3.js',
|
||||
['./plugin4.js', {}],
|
||||
'./pluginEsm',
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 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-site.example.com',
|
||||
baseUrl: '/',
|
||||
favicon: 'img/favicon.ico',
|
||||
plugins: [
|
||||
function (context, options) {
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
};
|
|
@ -24,7 +24,7 @@ async function loadSite(
|
|||
describe('initPlugins', () => {
|
||||
it('parses plugins correctly and loads them in correct order', async () => {
|
||||
const {context, plugins} = await loadSite('site-with-plugin');
|
||||
expect(context.siteConfig.plugins).toHaveLength(6);
|
||||
expect(context.siteConfig.plugins).toHaveLength(7);
|
||||
expect(plugins).toHaveLength(10);
|
||||
|
||||
expect(plugins[0]!.name).toBe('preset-plugin1');
|
||||
|
@ -85,4 +85,13 @@ describe('initPlugins', () => {
|
|||
Note that even inline/anonymous plugin functions require a 'name' property."
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws user-friendly error message for plugins returning undefined', async () => {
|
||||
await expect(() => loadSite('site-with-undefined-plugin')).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"A Docusaurus plugin returned 'undefined', which is forbidden.
|
||||
A plugin is expected to return an object having at least a 'name' property.
|
||||
If you want a plugin to self-disable depending on context/options, you can explicitly return 'null' instead of 'undefined'"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -49,17 +49,32 @@ function getThemeValidationFunction(
|
|||
return normalizedPluginConfig.plugin.validateThemeConfig;
|
||||
}
|
||||
|
||||
type PluginConfigInitResult = {
|
||||
config: NormalizedPluginConfig;
|
||||
// Plugins might self-disable during initialization by returning null
|
||||
plugin: InitializedPlugin | null;
|
||||
};
|
||||
|
||||
// This filters self-disabling plugins and returns only the initialized ones
|
||||
function onlyInitializedPlugins(
|
||||
initPluginsConfigsResults: PluginConfigInitResult[],
|
||||
): InitializedPlugin[] {
|
||||
return initPluginsConfigsResults
|
||||
.map((results) => results.plugin)
|
||||
.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the plugin constructors and returns their return values. It would load
|
||||
* plugin configs from `plugins`, `themes`, and `presets`.
|
||||
*/
|
||||
export async function initPlugins(
|
||||
export async function initPluginsConfigs(
|
||||
context: LoadContext,
|
||||
): Promise<InitializedPlugin[]> {
|
||||
pluginConfigs: NormalizedPluginConfig[],
|
||||
): Promise<PluginConfigInitResult[]> {
|
||||
// We need to resolve plugins from the perspective of the site config, as if
|
||||
// we are using `require.resolve` on those module names.
|
||||
const pluginRequire = createRequire(context.siteConfigPath);
|
||||
const pluginConfigs = await loadPluginConfigs(context);
|
||||
|
||||
async function doLoadPluginVersion(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
|
@ -108,13 +123,15 @@ export async function initPlugins(
|
|||
|
||||
async function initializePlugin(
|
||||
normalizedPluginConfig: NormalizedPluginConfig,
|
||||
): Promise<InitializedPlugin> {
|
||||
): Promise<PluginConfigInitResult> {
|
||||
const pluginVersion: PluginVersionInformation = await doLoadPluginVersion(
|
||||
normalizedPluginConfig,
|
||||
);
|
||||
const pluginOptions = doValidatePluginOptions(normalizedPluginConfig);
|
||||
|
||||
// Side-effect: merge the normalized theme config in the original one
|
||||
// Note: it's important to do this before calling the plugin constructor
|
||||
// Example: the theme classic plugin will read siteConfig.themeConfig
|
||||
context.siteConfig.themeConfig = {
|
||||
...context.siteConfig.themeConfig,
|
||||
...doValidateThemeConfig(normalizedPluginConfig),
|
||||
|
@ -125,26 +142,61 @@ export async function initPlugins(
|
|||
pluginOptions,
|
||||
);
|
||||
|
||||
if (!pluginInstance.name) {
|
||||
// Returning null has been explicitly allowed
|
||||
// It's a way for plugins to self-disable depending on context
|
||||
// See https://github.com/facebook/docusaurus/pull/10286
|
||||
if (pluginInstance === null) {
|
||||
return {config: normalizedPluginConfig, plugin: null};
|
||||
}
|
||||
if (pluginInstance === undefined) {
|
||||
throw new Error(
|
||||
`A Docusaurus plugin returned 'undefined', which is forbidden.
|
||||
A plugin is expected to return an object having at least a 'name' property.
|
||||
If you want a plugin to self-disable depending on context/options, you can explicitly return 'null' instead of 'undefined'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!pluginInstance?.name) {
|
||||
throw new Error(
|
||||
`A Docusaurus plugin is missing a 'name' property.
|
||||
Note that even inline/anonymous plugin functions require a 'name' property.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
const plugin: InitializedPlugin = {
|
||||
...pluginInstance,
|
||||
options: pluginOptions,
|
||||
version: pluginVersion,
|
||||
path: path.dirname(normalizedPluginConfig.entryPath),
|
||||
};
|
||||
|
||||
return {
|
||||
config: normalizedPluginConfig,
|
||||
plugin,
|
||||
};
|
||||
}
|
||||
|
||||
const plugins: InitializedPlugin[] = await Promise.all(
|
||||
pluginConfigs.map(initializePlugin),
|
||||
);
|
||||
const plugins: PluginConfigInitResult[] = (
|
||||
await Promise.all(pluginConfigs.map(initializePlugin))
|
||||
).filter((p) => p !== null);
|
||||
|
||||
ensureUniquePluginInstanceIds(plugins);
|
||||
ensureUniquePluginInstanceIds(onlyInitializedPlugins(plugins));
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the plugin constructors and returns their return values
|
||||
* for all the site context plugins that do not return null to self-disable.
|
||||
*/
|
||||
export async function initPlugins(
|
||||
context: LoadContext,
|
||||
): Promise<InitializedPlugin[]> {
|
||||
const pluginConfigs = await loadPluginConfigs(context);
|
||||
const initPluginsConfigsResults = await initPluginsConfigs(
|
||||
context,
|
||||
pluginConfigs,
|
||||
);
|
||||
|
||||
return onlyInitializedPlugins(initPluginsConfigsResults);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue