refactor(core): improve dev perf, fine-grained site reloads - part2 (#9968)

This commit is contained in:
Sébastien Lorber 2024-03-21 13:05:19 +01:00 committed by GitHub
parent 91f93656d8
commit 93a09ea086
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 707 additions and 402 deletions

View file

@ -183,6 +183,8 @@ export type InitializedPlugin = Plugin & {
export type LoadedPlugin = InitializedPlugin & {
readonly content: unknown;
readonly globalData: unknown;
readonly routes: RouteConfig[];
};
export type PluginModule<Content = unknown> = {

View file

@ -37,12 +37,14 @@ exports[`load loads props for site with custom i18n path 1`] = `
{
"content": undefined,
"getClientModules": [Function],
"globalData": undefined,
"injectHtmlTags": [Function],
"name": "docusaurus-bootstrap-plugin",
"options": {
"id": "default",
},
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site",
"routes": [],
"version": {
"type": "synthetic",
},
@ -50,11 +52,13 @@ exports[`load loads props for site with custom i18n path 1`] = `
{
"configureWebpack": [Function],
"content": undefined,
"globalData": undefined,
"name": "docusaurus-mdx-fallback-plugin",
"options": {
"id": "default",
},
"path": ".",
"routes": [],
"version": {
"type": "synthetic",
},

View file

@ -7,15 +7,10 @@
import path from 'path';
import {fromPartial} from '@total-typescript/shoehorn';
import {loadPlugins, mergeGlobalData} from '../plugins';
import type {
GlobalData,
LoadContext,
Plugin,
PluginConfig,
} from '@docusaurus/types';
import {loadPlugins, reloadPlugin} from '../plugins';
import type {LoadContext, Plugin, PluginConfig} from '@docusaurus/types';
function testLoad({
async function testLoad({
plugins,
themes,
}: {
@ -39,7 +34,9 @@ function testLoad({
},
});
return loadPlugins(context);
const result = await loadPlugins(context);
return {context, ...result};
}
const SyntheticPluginNames = [
@ -50,7 +47,7 @@ const SyntheticPluginNames = [
async function testPlugin<Content = unknown>(
pluginConfig: PluginConfig<Content>,
) {
const {plugins, routes, globalData} = await testLoad({
const {context, plugins, routes, globalData} = await testLoad({
plugins: [pluginConfig],
themes: [],
});
@ -62,204 +59,9 @@ async function testPlugin<Content = unknown>(
const plugin = nonSyntheticPlugins[0]!;
expect(plugin).toBeDefined();
return {plugin, routes, globalData};
return {context, plugin, routes, globalData};
}
describe('mergeGlobalData', () => {
it('no global data', () => {
expect(mergeGlobalData()).toEqual({});
});
it('1 global data', () => {
const globalData: GlobalData = {
plugin: {
default: {someData: 'val'},
},
};
expect(mergeGlobalData(globalData)).toEqual(globalData);
});
it('1 global data - primitive value', () => {
// For retro-compatibility we allow primitive values to be kept as is
// Not sure anyone is using primitive global data though...
const globalData: GlobalData = {
plugin: {
default: 42,
},
};
expect(mergeGlobalData(globalData)).toEqual(globalData);
});
it('3 distinct plugins global data', () => {
const globalData1: GlobalData = {
plugin1: {
default: {someData1: 'val1'},
},
};
const globalData2: GlobalData = {
plugin2: {
default: {someData2: 'val2'},
},
};
const globalData3: GlobalData = {
plugin3: {
default: {someData3: 'val3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin1: {
default: {someData1: 'val1'},
},
plugin2: {
default: {someData2: 'val2'},
},
plugin3: {
default: {someData3: 'val3'},
},
});
});
it('3 plugin instances of same plugin', () => {
const globalData1: GlobalData = {
plugin: {
id1: {someData1: 'val1'},
},
};
const globalData2: GlobalData = {
plugin: {
id2: {someData2: 'val2'},
},
};
const globalData3: GlobalData = {
plugin: {
id3: {someData3: 'val3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
id1: {someData1: 'val1'},
id2: {someData2: 'val2'},
id3: {someData3: 'val3'},
},
});
});
it('3 times the same plugin', () => {
const globalData1: GlobalData = {
plugin: {
id: {someData1: 'val1', shared: 'shared1'},
},
};
const globalData2: GlobalData = {
plugin: {
id: {someData2: 'val2', shared: 'shared2'},
},
};
const globalData3: GlobalData = {
plugin: {
id: {someData3: 'val3', shared: 'shared3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
id: {
someData1: 'val1',
someData2: 'val2',
someData3: 'val3',
shared: 'shared3',
},
},
});
});
it('3 times same plugin - including primitive values', () => {
// Very unlikely to happen, but we can't merge primitive values together
// Since we use Object.assign(), the primitive values are simply ignored
const globalData1: GlobalData = {
plugin: {
default: 42,
},
};
const globalData2: GlobalData = {
plugin: {
default: {hey: 'val'},
},
};
const globalData3: GlobalData = {
plugin: {
default: 84,
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
default: {hey: 'val'},
},
});
});
it('real world case', () => {
const globalData1: GlobalData = {
plugin1: {
id1: {someData1: 'val1', shared: 'globalData1'},
},
};
const globalData2: GlobalData = {
plugin1: {
id1: {someData2: 'val2', shared: 'globalData2'},
},
};
const globalData3: GlobalData = {
plugin1: {
id2: {someData3: 'val3', shared: 'globalData3'},
},
};
const globalData4: GlobalData = {
plugin2: {
id1: {someData1: 'val1', shared: 'globalData4'},
},
};
const globalData5: GlobalData = {
plugin2: {
id2: {someData1: 'val1', shared: 'globalData5'},
},
};
const globalData6: GlobalData = {
plugin3: {
id1: {someData1: 'val1', shared: 'globalData6'},
},
};
expect(
mergeGlobalData(
globalData1,
globalData2,
globalData3,
globalData4,
globalData5,
globalData6,
),
).toEqual({
plugin1: {
id1: {someData1: 'val1', someData2: 'val2', shared: 'globalData2'},
id2: {someData3: 'val3', shared: 'globalData3'},
},
plugin2: {
id1: {someData1: 'val1', shared: 'globalData4'},
id2: {someData1: 'val1', shared: 'globalData5'},
},
plugin3: {
id1: {someData1: 'val1', shared: 'globalData6'},
},
});
});
});
describe('loadPlugins', () => {
it('registers default synthetic plugins', async () => {
const {plugins, routes, globalData} = await testLoad({
@ -526,3 +328,272 @@ describe('loadPlugins', () => {
`);
});
});
describe('reloadPlugin', () => {
it('can reload a single complex plugin with same content', async () => {
const plugin: PluginConfig = () => ({
name: 'plugin-name',
contentLoaded({actions}) {
actions.addRoute({
path: '/contentLoadedRouteParent',
component: 'Comp',
routes: [
{path: '/contentLoadedRouteParent/child', component: 'Comp'},
],
});
actions.addRoute({
path: '/contentLoadedRouteSingle',
component: 'Comp',
});
actions.setGlobalData({
globalContentLoaded: 'val1',
globalOverridden: 'initial-value',
});
},
allContentLoaded({actions}) {
actions.addRoute({
path: '/allContentLoadedRouteParent',
component: 'Comp',
routes: [
{path: '/allContentLoadedRouteParent/child', component: 'Comp'},
],
});
actions.addRoute({
path: '/allContentLoadedRouteSingle',
component: 'Comp',
});
actions.setGlobalData({
globalAllContentLoaded: 'val2',
globalOverridden: 'override-value',
});
},
});
const loadResult = await testLoad({
plugins: [plugin],
themes: [],
});
const reloadResult = await reloadPlugin({
context: loadResult.context,
plugins: loadResult.plugins,
pluginIdentifier: {name: 'plugin-name', id: 'default'},
});
expect(loadResult.routes).toEqual(reloadResult.routes);
expect(loadResult.globalData).toEqual(reloadResult.globalData);
expect(reloadResult.routes).toMatchInlineSnapshot(`
[
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
},
"path": "/allContentLoadedRouteSingle/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
},
"path": "/contentLoadedRouteSingle/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
},
"path": "/allContentLoadedRouteParent/",
"routes": [
{
"component": "Comp",
"path": "/allContentLoadedRouteParent/child/",
},
],
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
},
"path": "/contentLoadedRouteParent/",
"routes": [
{
"component": "Comp",
"path": "/contentLoadedRouteParent/child/",
},
],
},
]
`);
expect(reloadResult.globalData).toMatchInlineSnapshot(`
{
"plugin-name": {
"default": {
"globalAllContentLoaded": "val2",
"globalContentLoaded": "val1",
"globalOverridden": "override-value",
},
},
}
`);
});
it('can reload plugins in real-world setup', async () => {
let isPlugin1Reload = false;
const plugin1: PluginConfig = () => ({
name: 'plugin-name-1',
contentLoaded({actions}) {
actions.addRoute({
path: isPlugin1Reload
? '/contentLoaded-route-reload'
: '/contentLoaded-route-initial',
component: 'Comp',
});
actions.setGlobalData({
contentLoadedVal: isPlugin1Reload
? 'contentLoaded-val-reload'
: 'contentLoaded-val-initial',
});
},
allContentLoaded({actions}) {
actions.addRoute({
path: isPlugin1Reload
? '/allContentLoaded-route-reload'
: '/allContentLoaded-route-initial',
component: 'Comp',
});
actions.setGlobalData({
allContentLoadedVal: isPlugin1Reload
? 'allContentLoaded-val-reload'
: 'allContentLoaded-val-initial',
});
},
});
const plugin2: PluginConfig = () => ({
name: 'plugin-name-2',
contentLoaded({actions}) {
actions.addRoute({
path: '/plugin-2-route',
component: 'Comp',
});
actions.setGlobalData({plugin2Val: 'val'});
},
});
const loadResult = await testLoad({
plugins: [plugin1, plugin2],
themes: [],
});
isPlugin1Reload = true;
const reloadResult = await reloadPlugin({
context: loadResult.context,
plugins: loadResult.plugins,
pluginIdentifier: {name: 'plugin-name-1', id: 'default'},
});
expect(loadResult.routes).not.toEqual(reloadResult.routes);
expect(loadResult.globalData).not.toEqual(reloadResult.globalData);
expect(loadResult.routes).toMatchInlineSnapshot(`
[
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
},
"path": "/allContentLoaded-route-initial/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
},
"path": "/contentLoaded-route-initial/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-2/default/plugin-route-context-module-100.json",
},
"path": "/plugin-2-route/",
},
]
`);
expect(loadResult.globalData).toMatchInlineSnapshot(`
{
"plugin-name-1": {
"default": {
"allContentLoadedVal": "allContentLoaded-val-initial",
"contentLoadedVal": "contentLoaded-val-initial",
},
},
"plugin-name-2": {
"default": {
"plugin2Val": "val",
},
},
}
`);
expect(reloadResult.routes).toMatchInlineSnapshot(`
[
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
},
"path": "/allContentLoaded-route-reload/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-1/default/plugin-route-context-module-100.json",
},
"path": "/contentLoaded-route-reload/",
},
{
"component": "Comp",
"context": {
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name-2/default/plugin-route-context-module-100.json",
},
"path": "/plugin-2-route/",
},
]
`);
expect(reloadResult.globalData).toMatchInlineSnapshot(`
{
"plugin-name-1": {
"default": {
"allContentLoadedVal": "allContentLoaded-val-reload",
"contentLoadedVal": "contentLoaded-val-reload",
},
},
"plugin-name-2": {
"default": {
"plugin2Val": "val",
},
},
}
`);
// Trying to reload again one plugin or the other should give
// the same result because the plugin content doesn't change
const reloadResult2 = await reloadPlugin({
context: loadResult.context,
plugins: reloadResult.plugins,
pluginIdentifier: {name: 'plugin-name-1', id: 'default'},
});
expect(reloadResult2.routes).toEqual(reloadResult.routes);
expect(reloadResult2.globalData).toEqual(reloadResult.globalData);
const reloadResult3 = await reloadPlugin({
context: loadResult.context,
plugins: reloadResult2.plugins,
pluginIdentifier: {name: 'plugin-name-2', id: 'default'},
});
expect(reloadResult3.routes).toEqual(reloadResult.routes);
expect(reloadResult3.globalData).toEqual(reloadResult.globalData);
});
});

View file

@ -0,0 +1,204 @@
/**
* 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 {mergeGlobalData} from '../pluginsUtils';
import type {GlobalData} from '@docusaurus/types';
describe('mergeGlobalData', () => {
it('no global data', () => {
expect(mergeGlobalData()).toEqual({});
});
it('1 global data', () => {
const globalData: GlobalData = {
plugin: {
default: {someData: 'val'},
},
};
expect(mergeGlobalData(globalData)).toEqual(globalData);
});
it('1 global data - primitive value', () => {
// For retro-compatibility we allow primitive values to be kept as is
// Not sure anyone is using primitive global data though...
const globalData: GlobalData = {
plugin: {
default: 42,
},
};
expect(mergeGlobalData(globalData)).toEqual(globalData);
});
it('3 distinct plugins global data', () => {
const globalData1: GlobalData = {
plugin1: {
default: {someData1: 'val1'},
},
};
const globalData2: GlobalData = {
plugin2: {
default: {someData2: 'val2'},
},
};
const globalData3: GlobalData = {
plugin3: {
default: {someData3: 'val3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin1: {
default: {someData1: 'val1'},
},
plugin2: {
default: {someData2: 'val2'},
},
plugin3: {
default: {someData3: 'val3'},
},
});
});
it('3 plugin instances of same plugin', () => {
const globalData1: GlobalData = {
plugin: {
id1: {someData1: 'val1'},
},
};
const globalData2: GlobalData = {
plugin: {
id2: {someData2: 'val2'},
},
};
const globalData3: GlobalData = {
plugin: {
id3: {someData3: 'val3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
id1: {someData1: 'val1'},
id2: {someData2: 'val2'},
id3: {someData3: 'val3'},
},
});
});
it('3 times the same plugin', () => {
const globalData1: GlobalData = {
plugin: {
id: {someData1: 'val1', shared: 'shared1'},
},
};
const globalData2: GlobalData = {
plugin: {
id: {someData2: 'val2', shared: 'shared2'},
},
};
const globalData3: GlobalData = {
plugin: {
id: {someData3: 'val3', shared: 'shared3'},
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
id: {
someData1: 'val1',
someData2: 'val2',
someData3: 'val3',
shared: 'shared3',
},
},
});
});
it('3 times same plugin - including primitive values', () => {
// Very unlikely to happen, but we can't merge primitive values together
// Since we use Object.assign(), the primitive values are simply ignored
const globalData1: GlobalData = {
plugin: {
default: 42,
},
};
const globalData2: GlobalData = {
plugin: {
default: {hey: 'val'},
},
};
const globalData3: GlobalData = {
plugin: {
default: 84,
},
};
expect(mergeGlobalData(globalData1, globalData2, globalData3)).toEqual({
plugin: {
default: {hey: 'val'},
},
});
});
it('real world case', () => {
const globalData1: GlobalData = {
plugin1: {
id1: {someData1: 'val1', shared: 'globalData1'},
},
};
const globalData2: GlobalData = {
plugin1: {
id1: {someData2: 'val2', shared: 'globalData2'},
},
};
const globalData3: GlobalData = {
plugin1: {
id2: {someData3: 'val3', shared: 'globalData3'},
},
};
const globalData4: GlobalData = {
plugin2: {
id1: {someData1: 'val1', shared: 'globalData4'},
},
};
const globalData5: GlobalData = {
plugin2: {
id2: {someData1: 'val1', shared: 'globalData5'},
},
};
const globalData6: GlobalData = {
plugin3: {
id1: {someData1: 'val1', shared: 'globalData6'},
},
};
expect(
mergeGlobalData(
globalData1,
globalData2,
globalData3,
globalData4,
globalData5,
globalData6,
),
).toEqual({
plugin1: {
id1: {someData1: 'val1', someData2: 'val2', shared: 'globalData2'},
id2: {someData3: 'val3', shared: 'globalData3'},
},
plugin2: {
id1: {someData1: 'val1', shared: 'globalData4'},
id2: {someData1: 'val1', shared: 'globalData5'},
},
plugin3: {
id1: {someData1: 'val1', shared: 'globalData6'},
},
});
});
});

View file

@ -9,7 +9,7 @@ import path from 'path';
import {docuHash, generate} from '@docusaurus/utils';
import {applyRouteTrailingSlash} from './routeConfig';
import type {
LoadedPlugin,
InitializedPlugin,
PluginContentLoadedActions,
PluginRouteContext,
RouteConfig,
@ -31,7 +31,7 @@ export async function createPluginActionsUtils({
baseUrl,
trailingSlash,
}: {
plugin: LoadedPlugin;
plugin: InitializedPlugin;
generatedFilesDir: string;
baseUrl: string;
trailingSlash: boolean | undefined;

View file

@ -5,14 +5,19 @@
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {initPlugins} from './init';
import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic';
import {localizePluginTranslationFile} from '../translations/translations';
import {sortRoutes} from './routeConfig';
import {PerfLogger} from '../../utils';
import {createPluginActionsUtils} from './actions';
import {
aggregateAllContent,
aggregateGlobalData,
aggregateRoutes,
getPluginByIdentifier,
mergeGlobalData,
} from './pluginsUtils';
import type {
LoadContext,
RouteConfig,
@ -23,17 +28,17 @@ import type {
InitializedPlugin,
} from '@docusaurus/types';
async function translatePlugin({
async function translatePluginContent({
plugin,
content,
context,
}: {
plugin: LoadedPlugin;
plugin: InitializedPlugin;
content: unknown;
context: LoadContext;
}): Promise<LoadedPlugin> {
const {content} = plugin;
}): Promise<unknown> {
const rawTranslationFiles =
(await plugin.getTranslationFiles?.({content: plugin.content})) ?? [];
(await plugin.getTranslationFiles?.({content})) ?? [];
const translationFiles = await Promise.all(
rawTranslationFiles.map((translationFile) =>
@ -58,10 +63,10 @@ async function translatePlugin({
// translate its own slice of theme config and should make no assumptions
// about other plugins' keys, so this is safe to run in parallel.
Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice);
return {...plugin, content: translatedContent};
return translatedContent;
}
async function executePluginLoadContent({
async function executePluginContentLoading({
plugin,
context,
}: {
@ -69,65 +74,40 @@ async function executePluginLoadContent({
context: LoadContext;
}): Promise<LoadedPlugin> {
return PerfLogger.async(
`Plugin - loadContent - ${plugin.name}@${plugin.options.id}`,
`Plugins - single plugin content loading - ${plugin.name}@${plugin.options.id}`,
async () => {
const content = await plugin.loadContent?.();
const loadedPlugin: LoadedPlugin = {...plugin, content};
return translatePlugin({plugin: loadedPlugin, context});
},
);
}
let content = await plugin.loadContent?.();
async function executePluginsLoadContent({
plugins,
context,
}: {
plugins: InitializedPlugin[];
context: LoadContext;
}) {
return PerfLogger.async(`Plugins - loadContent`, () =>
Promise.all(
plugins.map((plugin) => executePluginLoadContent({plugin, context})),
),
);
}
content = await translatePluginContent({
plugin,
content,
context,
});
function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent {
return _.chain(loadedPlugins)
.groupBy((item) => item.name)
.mapValues((nameItems) =>
_.chain(nameItems)
.groupBy((item) => item.options.id)
.mapValues((idItems) => idItems[0]!.content)
.value(),
)
.value();
}
async function executePluginContentLoaded({
plugin,
context,
}: {
plugin: LoadedPlugin;
context: LoadContext;
}): Promise<{routes: RouteConfig[]; globalData: unknown}> {
return PerfLogger.async(
`Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`,
async () => {
if (!plugin.contentLoaded) {
return {routes: [], globalData: undefined};
return {
...plugin,
content,
routes: [],
globalData: undefined,
};
}
const pluginActionsUtils = await createPluginActionsUtils({
plugin,
generatedFilesDir: context.generatedFilesDir,
baseUrl: context.siteConfig.baseUrl,
trailingSlash: context.siteConfig.trailingSlash,
});
await plugin.contentLoaded({
content: plugin.content,
content,
actions: pluginActionsUtils.getActions(),
});
return {
...plugin,
content,
routes: pluginActionsUtils.getRoutes(),
globalData: pluginActionsUtils.getGlobalData(),
};
@ -135,6 +115,20 @@ async function executePluginContentLoaded({
);
}
async function executeAllPluginsContentLoading({
plugins,
context,
}: {
plugins: InitializedPlugin[];
context: LoadContext;
}): Promise<LoadedPlugin[]> {
return PerfLogger.async(`Plugins - all plugins content loading`, () => {
return Promise.all(
plugins.map((plugin) => executePluginContentLoading({plugin, context})),
);
});
}
async function executePluginAllContentLoaded({
plugin,
context,
@ -168,49 +162,15 @@ async function executePluginAllContentLoaded({
);
}
async function executePluginsContentLoaded({
type AllContentLoadedResult = {routes: RouteConfig[]; globalData: GlobalData};
async function executeAllPluginsAllContentLoaded({
plugins,
context,
}: {
plugins: LoadedPlugin[];
context: LoadContext;
}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> {
return PerfLogger.async(`Plugins - contentLoaded`, async () => {
const routes: RouteConfig[] = [];
const globalData: GlobalData = {};
await Promise.all(
plugins.map(async (plugin) => {
const {routes: pluginRoutes, globalData: pluginGlobalData} =
await executePluginContentLoaded({
plugin,
context,
});
routes.push(...pluginRoutes);
if (pluginGlobalData !== undefined) {
globalData[plugin.name] ??= {};
globalData[plugin.name]![plugin.options.id] = pluginGlobalData;
}
}),
);
// Sort the route config.
// This ensures that route with sub routes are always placed last.
sortRoutes(routes, context.siteConfig.baseUrl);
return {routes, globalData};
});
}
async function executePluginsAllContentLoaded({
plugins,
context,
}: {
plugins: LoadedPlugin[];
context: LoadContext;
}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> {
}): Promise<AllContentLoadedResult> {
return PerfLogger.async(`Plugins - allContentLoaded`, async () => {
const allContent = aggregateAllContent(plugins);
@ -235,66 +195,37 @@ async function executePluginsAllContentLoaded({
}),
);
// Sort the route config.
// This ensures that route with sub routes are always placed last.
sortRoutes(routes, context.siteConfig.baseUrl);
return {routes, globalData};
});
}
function mergeResults({
plugins,
allContentLoadedResult,
}: {
plugins: LoadedPlugin[];
allContentLoadedResult: AllContentLoadedResult;
}) {
const routes: RouteConfig[] = [
...aggregateRoutes(plugins),
...allContentLoadedResult.routes,
];
sortRoutes(routes);
const globalData: GlobalData = mergeGlobalData(
aggregateGlobalData(plugins),
allContentLoadedResult.globalData,
);
return {routes, globalData};
}
export type LoadPluginsResult = {
plugins: LoadedPlugin[];
routes: RouteConfig[];
globalData: GlobalData;
};
type ContentLoadedResult = {routes: RouteConfig[]; globalData: GlobalData};
export function mergeGlobalData(...globalDataList: GlobalData[]): GlobalData {
const result: GlobalData = {};
const allPluginIdentifiers: PluginIdentifier[] = globalDataList.flatMap(
(gd) =>
Object.keys(gd).flatMap((name) =>
Object.keys(gd[name]!).map((id) => ({name, id})),
),
);
allPluginIdentifiers.forEach(({name, id}) => {
const allData = globalDataList
.map((gd) => gd?.[name]?.[id])
.filter((d) => typeof d !== 'undefined');
const mergedData =
allData.length === 1 ? allData[0] : Object.assign({}, ...allData);
result[name] ??= {};
result[name]![id] = mergedData;
});
return result;
}
function mergeResults({
contentLoadedResult,
allContentLoadedResult,
}: {
contentLoadedResult: ContentLoadedResult;
allContentLoadedResult: ContentLoadedResult;
}): ContentLoadedResult {
const routes = [
...contentLoadedResult.routes,
...allContentLoadedResult.routes,
];
sortRoutes(routes);
const globalData = mergeGlobalData(
contentLoadedResult.globalData,
allContentLoadedResult.globalData,
);
return {routes, globalData};
}
/**
* Initializes the plugins and run their lifecycle functions.
*/
@ -307,28 +238,24 @@ export async function loadPlugins(
() => initPlugins(context),
);
// TODO probably not the ideal place to hardcode those plugins
initializedPlugins.push(
createBootstrapPlugin(context),
createMDXFallbackPlugin(context),
);
const plugins = await executePluginsLoadContent({
const plugins = await executeAllPluginsContentLoading({
plugins: initializedPlugins,
context,
});
const contentLoadedResult = await executePluginsContentLoaded({
plugins,
context,
});
const allContentLoadedResult = await executePluginsAllContentLoaded({
const allContentLoadedResult = await executeAllPluginsAllContentLoaded({
plugins,
context,
});
const {routes, globalData} = mergeResults({
contentLoadedResult,
plugins,
allContentLoadedResult,
});
@ -336,25 +263,6 @@ export async function loadPlugins(
});
}
export function getPluginByIdentifier({
plugins,
pluginIdentifier,
}: {
pluginIdentifier: PluginIdentifier;
plugins: LoadedPlugin[];
}): LoadedPlugin {
const plugin = plugins.find(
(p) =>
p.name === pluginIdentifier.name && p.options.id === pluginIdentifier.id,
);
if (!plugin) {
throw new Error(
logger.interpolate`Plugin not found for identifier ${pluginIdentifier.name}@${pluginIdentifier.id}`,
);
}
return plugin;
}
export async function reloadPlugin({
pluginIdentifier,
plugins: previousPlugins,
@ -365,30 +273,32 @@ export async function reloadPlugin({
context: LoadContext;
}): Promise<LoadPluginsResult> {
return PerfLogger.async('Plugins - reloadPlugin', async () => {
const plugin = getPluginByIdentifier({
const previousPlugin = getPluginByIdentifier({
plugins: previousPlugins,
pluginIdentifier,
});
const reloadedPlugin = await executePluginLoadContent({plugin, context});
const plugins = previousPlugins.with(
previousPlugins.indexOf(plugin),
reloadedPlugin,
);
// TODO optimize this, we shouldn't need to re-run this lifecycle
const contentLoadedResult = await executePluginsContentLoaded({
plugins,
const plugin = await executePluginContentLoading({
plugin: previousPlugin,
context,
});
const allContentLoadedResult = await executePluginsAllContentLoaded({
/*
// TODO Docusaurus v4 - upgrade to Node 20, use array.with()
const plugins = previousPlugins.with(
previousPlugins.indexOf(previousPlugin),
plugin,
);
*/
const plugins = [...previousPlugins];
plugins[previousPlugins.indexOf(previousPlugin)] = plugin;
const allContentLoadedResult = await executeAllPluginsAllContentLoaded({
plugins,
context,
});
const {routes, globalData} = mergeResults({
contentLoadedResult,
plugins,
allContentLoadedResult,
});

View file

@ -0,0 +1,87 @@
/**
* 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 _ from 'lodash';
import logger from '@docusaurus/logger';
import type {
AllContent,
GlobalData,
InitializedPlugin,
LoadedPlugin,
PluginIdentifier,
RouteConfig,
} from '@docusaurus/types';
export function getPluginByIdentifier<P extends InitializedPlugin>({
plugins,
pluginIdentifier,
}: {
pluginIdentifier: PluginIdentifier;
plugins: P[];
}): P {
const plugin = plugins.find(
(p) =>
p.name === pluginIdentifier.name && p.options.id === pluginIdentifier.id,
);
if (!plugin) {
throw new Error(
logger.interpolate`Plugin not found for identifier ${pluginIdentifier.name}@${pluginIdentifier.id}`,
);
}
return plugin;
}
export function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent {
return _.chain(loadedPlugins)
.groupBy((item) => item.name)
.mapValues((nameItems) =>
_.chain(nameItems)
.groupBy((item) => item.options.id)
.mapValues((idItems) => idItems[0]!.content)
.value(),
)
.value();
}
export function aggregateRoutes(loadedPlugins: LoadedPlugin[]): RouteConfig[] {
return loadedPlugins.flatMap((p) => p.routes);
}
export function aggregateGlobalData(loadedPlugins: LoadedPlugin[]): GlobalData {
const globalData: GlobalData = {};
loadedPlugins.forEach((plugin) => {
if (plugin.globalData !== undefined) {
globalData[plugin.name] ??= {};
globalData[plugin.name]![plugin.options.id] = plugin.globalData;
}
});
return globalData;
}
export function mergeGlobalData(...globalDataList: GlobalData[]): GlobalData {
const result: GlobalData = {};
const allPluginIdentifiers: PluginIdentifier[] = globalDataList.flatMap(
(gd) =>
Object.keys(gd).flatMap((name) =>
Object.keys(gd[name]!).map((id) => ({name, id})),
),
);
allPluginIdentifiers.forEach(({name, id}) => {
const allData = globalDataList
.map((gd) => gd?.[name]?.[id])
.filter((d) => typeof d !== 'undefined');
const mergedData =
allData.length === 1 ? allData[0] : Object.assign({}, ...allData);
result[name] ??= {};
result[name]![id] = mergedData;
});
return result;
}

View file

@ -7,7 +7,11 @@
import path from 'path';
import type {RuleSetRule} from 'webpack';
import type {HtmlTagObject, LoadedPlugin, LoadContext} from '@docusaurus/types';
import type {
HtmlTagObject,
LoadContext,
InitializedPlugin,
} from '@docusaurus/types';
import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader';
/**
@ -18,7 +22,7 @@ import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader';
export function createBootstrapPlugin({
siteDir,
siteConfig,
}: LoadContext): LoadedPlugin {
}: LoadContext): InitializedPlugin {
const {
stylesheets,
scripts,
@ -27,7 +31,6 @@ export function createBootstrapPlugin({
} = siteConfig;
return {
name: 'docusaurus-bootstrap-plugin',
content: null,
options: {
id: 'default',
},
@ -75,10 +78,9 @@ export function createBootstrapPlugin({
export function createMDXFallbackPlugin({
siteDir,
siteConfig,
}: LoadContext): LoadedPlugin {
}: LoadContext): InitializedPlugin {
return {
name: 'docusaurus-mdx-fallback-plugin',
content: null,
options: {
id: 'default',
},

View file

@ -247,10 +247,6 @@ export async function reloadSitePlugin(
site: Site,
pluginIdentifier: PluginIdentifier,
): Promise<Site> {
console.log(
`reloadSitePlugin ${pluginIdentifier.name}@${pluginIdentifier.id}`,
);
const {plugins, routes, globalData} = await reloadPlugin({
pluginIdentifier,
plugins: site.props.plugins,

View file

@ -11,6 +11,12 @@ import logger from '@docusaurus/logger';
export const PerfDebuggingEnabled: boolean =
!!process.env.DOCUSAURUS_PERF_LOGGER;
const Thresholds = {
min: 5,
yellow: 100,
red: 1000,
};
type PerfLoggerAPI = {
start: (label: string) => void;
end: (label: string) => void;
@ -34,17 +40,40 @@ function createPerfLogger(): PerfLoggerAPI {
const prefix = logger.yellow(`[PERF] `);
const start: PerfLoggerAPI['start'] = (label) => console.time(prefix + label);
const formatDuration = (duration: number): string => {
if (duration > Thresholds.red) {
return logger.red(`${(duration / 1000).toFixed(2)} seconds!`);
} else if (duration > Thresholds.yellow) {
return logger.yellow(`${duration.toFixed(2)} ms`);
} else {
return logger.green(`${duration.toFixed(2)} ms`);
}
};
const end: PerfLoggerAPI['end'] = (label) => console.timeEnd(prefix + label);
const logDuration = (label: string, duration: number) => {
if (duration < Thresholds.min) {
return;
}
console.log(`${prefix + label} - ${formatDuration(duration)}`);
};
const start: PerfLoggerAPI['start'] = (label) => performance.mark(label);
const end: PerfLoggerAPI['end'] = (label) => {
const {duration} = performance.measure(label);
performance.clearMarks(label);
logDuration(label, duration);
};
const log: PerfLoggerAPI['log'] = (label: string) =>
console.log(prefix + label);
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
start(label);
const before = performance.now();
const result = await asyncFn();
end(label);
const duration = performance.now() - before;
logDuration(label, duration);
return result;
};