mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-06 02:08:55 +02:00
refactor(core): improve dev perf, fine-grained site reloads - part2 (#9968)
This commit is contained in:
parent
91f93656d8
commit
93a09ea086
10 changed files with 707 additions and 402 deletions
2
packages/docusaurus-types/src/plugin.d.ts
vendored
2
packages/docusaurus-types/src/plugin.d.ts
vendored
|
@ -183,6 +183,8 @@ export type InitializedPlugin = Plugin & {
|
||||||
|
|
||||||
export type LoadedPlugin = InitializedPlugin & {
|
export type LoadedPlugin = InitializedPlugin & {
|
||||||
readonly content: unknown;
|
readonly content: unknown;
|
||||||
|
readonly globalData: unknown;
|
||||||
|
readonly routes: RouteConfig[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginModule<Content = unknown> = {
|
export type PluginModule<Content = unknown> = {
|
||||||
|
|
|
@ -37,12 +37,14 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
{
|
{
|
||||||
"content": undefined,
|
"content": undefined,
|
||||||
"getClientModules": [Function],
|
"getClientModules": [Function],
|
||||||
|
"globalData": undefined,
|
||||||
"injectHtmlTags": [Function],
|
"injectHtmlTags": [Function],
|
||||||
"name": "docusaurus-bootstrap-plugin",
|
"name": "docusaurus-bootstrap-plugin",
|
||||||
"options": {
|
"options": {
|
||||||
"id": "default",
|
"id": "default",
|
||||||
},
|
},
|
||||||
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site",
|
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site",
|
||||||
|
"routes": [],
|
||||||
"version": {
|
"version": {
|
||||||
"type": "synthetic",
|
"type": "synthetic",
|
||||||
},
|
},
|
||||||
|
@ -50,11 +52,13 @@ exports[`load loads props for site with custom i18n path 1`] = `
|
||||||
{
|
{
|
||||||
"configureWebpack": [Function],
|
"configureWebpack": [Function],
|
||||||
"content": undefined,
|
"content": undefined,
|
||||||
|
"globalData": undefined,
|
||||||
"name": "docusaurus-mdx-fallback-plugin",
|
"name": "docusaurus-mdx-fallback-plugin",
|
||||||
"options": {
|
"options": {
|
||||||
"id": "default",
|
"id": "default",
|
||||||
},
|
},
|
||||||
"path": ".",
|
"path": ".",
|
||||||
|
"routes": [],
|
||||||
"version": {
|
"version": {
|
||||||
"type": "synthetic",
|
"type": "synthetic",
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,15 +7,10 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {fromPartial} from '@total-typescript/shoehorn';
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
import {loadPlugins, mergeGlobalData} from '../plugins';
|
import {loadPlugins, reloadPlugin} from '../plugins';
|
||||||
import type {
|
import type {LoadContext, Plugin, PluginConfig} from '@docusaurus/types';
|
||||||
GlobalData,
|
|
||||||
LoadContext,
|
|
||||||
Plugin,
|
|
||||||
PluginConfig,
|
|
||||||
} from '@docusaurus/types';
|
|
||||||
|
|
||||||
function testLoad({
|
async function testLoad({
|
||||||
plugins,
|
plugins,
|
||||||
themes,
|
themes,
|
||||||
}: {
|
}: {
|
||||||
|
@ -39,7 +34,9 @@ function testLoad({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return loadPlugins(context);
|
const result = await loadPlugins(context);
|
||||||
|
|
||||||
|
return {context, ...result};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SyntheticPluginNames = [
|
const SyntheticPluginNames = [
|
||||||
|
@ -50,7 +47,7 @@ const SyntheticPluginNames = [
|
||||||
async function testPlugin<Content = unknown>(
|
async function testPlugin<Content = unknown>(
|
||||||
pluginConfig: PluginConfig<Content>,
|
pluginConfig: PluginConfig<Content>,
|
||||||
) {
|
) {
|
||||||
const {plugins, routes, globalData} = await testLoad({
|
const {context, plugins, routes, globalData} = await testLoad({
|
||||||
plugins: [pluginConfig],
|
plugins: [pluginConfig],
|
||||||
themes: [],
|
themes: [],
|
||||||
});
|
});
|
||||||
|
@ -62,204 +59,9 @@ async function testPlugin<Content = unknown>(
|
||||||
const plugin = nonSyntheticPlugins[0]!;
|
const plugin = nonSyntheticPlugins[0]!;
|
||||||
expect(plugin).toBeDefined();
|
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', () => {
|
describe('loadPlugins', () => {
|
||||||
it('registers default synthetic plugins', async () => {
|
it('registers default synthetic plugins', async () => {
|
||||||
const {plugins, routes, globalData} = await testLoad({
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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'},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,7 +9,7 @@ import path from 'path';
|
||||||
import {docuHash, generate} from '@docusaurus/utils';
|
import {docuHash, generate} from '@docusaurus/utils';
|
||||||
import {applyRouteTrailingSlash} from './routeConfig';
|
import {applyRouteTrailingSlash} from './routeConfig';
|
||||||
import type {
|
import type {
|
||||||
LoadedPlugin,
|
InitializedPlugin,
|
||||||
PluginContentLoadedActions,
|
PluginContentLoadedActions,
|
||||||
PluginRouteContext,
|
PluginRouteContext,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
|
@ -31,7 +31,7 @@ export async function createPluginActionsUtils({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
trailingSlash,
|
trailingSlash,
|
||||||
}: {
|
}: {
|
||||||
plugin: LoadedPlugin;
|
plugin: InitializedPlugin;
|
||||||
generatedFilesDir: string;
|
generatedFilesDir: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
trailingSlash: boolean | undefined;
|
trailingSlash: boolean | undefined;
|
||||||
|
|
|
@ -5,14 +5,19 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import logger from '@docusaurus/logger';
|
|
||||||
import {initPlugins} from './init';
|
import {initPlugins} from './init';
|
||||||
import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic';
|
import {createBootstrapPlugin, createMDXFallbackPlugin} from './synthetic';
|
||||||
import {localizePluginTranslationFile} from '../translations/translations';
|
import {localizePluginTranslationFile} from '../translations/translations';
|
||||||
import {sortRoutes} from './routeConfig';
|
import {sortRoutes} from './routeConfig';
|
||||||
import {PerfLogger} from '../../utils';
|
import {PerfLogger} from '../../utils';
|
||||||
import {createPluginActionsUtils} from './actions';
|
import {createPluginActionsUtils} from './actions';
|
||||||
|
import {
|
||||||
|
aggregateAllContent,
|
||||||
|
aggregateGlobalData,
|
||||||
|
aggregateRoutes,
|
||||||
|
getPluginByIdentifier,
|
||||||
|
mergeGlobalData,
|
||||||
|
} from './pluginsUtils';
|
||||||
import type {
|
import type {
|
||||||
LoadContext,
|
LoadContext,
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
|
@ -23,17 +28,17 @@ import type {
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
async function translatePlugin({
|
async function translatePluginContent({
|
||||||
plugin,
|
plugin,
|
||||||
|
content,
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
plugin: LoadedPlugin;
|
plugin: InitializedPlugin;
|
||||||
|
content: unknown;
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}): Promise<LoadedPlugin> {
|
}): Promise<unknown> {
|
||||||
const {content} = plugin;
|
|
||||||
|
|
||||||
const rawTranslationFiles =
|
const rawTranslationFiles =
|
||||||
(await plugin.getTranslationFiles?.({content: plugin.content})) ?? [];
|
(await plugin.getTranslationFiles?.({content})) ?? [];
|
||||||
|
|
||||||
const translationFiles = await Promise.all(
|
const translationFiles = await Promise.all(
|
||||||
rawTranslationFiles.map((translationFile) =>
|
rawTranslationFiles.map((translationFile) =>
|
||||||
|
@ -58,10 +63,10 @@ async function translatePlugin({
|
||||||
// translate its own slice of theme config and should make no assumptions
|
// translate its own slice of theme config and should make no assumptions
|
||||||
// about other plugins' keys, so this is safe to run in parallel.
|
// about other plugins' keys, so this is safe to run in parallel.
|
||||||
Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice);
|
Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice);
|
||||||
return {...plugin, content: translatedContent};
|
return translatedContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executePluginLoadContent({
|
async function executePluginContentLoading({
|
||||||
plugin,
|
plugin,
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
|
@ -69,65 +74,40 @@ async function executePluginLoadContent({
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}): Promise<LoadedPlugin> {
|
}): Promise<LoadedPlugin> {
|
||||||
return PerfLogger.async(
|
return PerfLogger.async(
|
||||||
`Plugin - loadContent - ${plugin.name}@${plugin.options.id}`,
|
`Plugins - single plugin content loading - ${plugin.name}@${plugin.options.id}`,
|
||||||
async () => {
|
async () => {
|
||||||
const content = await plugin.loadContent?.();
|
let content = await plugin.loadContent?.();
|
||||||
const loadedPlugin: LoadedPlugin = {...plugin, content};
|
|
||||||
return translatePlugin({plugin: loadedPlugin, context});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executePluginsLoadContent({
|
content = await translatePluginContent({
|
||||||
plugins,
|
plugin,
|
||||||
context,
|
content,
|
||||||
}: {
|
context,
|
||||||
plugins: InitializedPlugin[];
|
});
|
||||||
context: LoadContext;
|
|
||||||
}) {
|
|
||||||
return PerfLogger.async(`Plugins - loadContent`, () =>
|
|
||||||
Promise.all(
|
|
||||||
plugins.map((plugin) => executePluginLoadContent({plugin, 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) {
|
if (!plugin.contentLoaded) {
|
||||||
return {routes: [], globalData: undefined};
|
return {
|
||||||
|
...plugin,
|
||||||
|
content,
|
||||||
|
routes: [],
|
||||||
|
globalData: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginActionsUtils = await createPluginActionsUtils({
|
const pluginActionsUtils = await createPluginActionsUtils({
|
||||||
plugin,
|
plugin,
|
||||||
generatedFilesDir: context.generatedFilesDir,
|
generatedFilesDir: context.generatedFilesDir,
|
||||||
baseUrl: context.siteConfig.baseUrl,
|
baseUrl: context.siteConfig.baseUrl,
|
||||||
trailingSlash: context.siteConfig.trailingSlash,
|
trailingSlash: context.siteConfig.trailingSlash,
|
||||||
});
|
});
|
||||||
|
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded({
|
||||||
content: plugin.content,
|
content,
|
||||||
actions: pluginActionsUtils.getActions(),
|
actions: pluginActionsUtils.getActions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...plugin,
|
||||||
|
content,
|
||||||
routes: pluginActionsUtils.getRoutes(),
|
routes: pluginActionsUtils.getRoutes(),
|
||||||
globalData: pluginActionsUtils.getGlobalData(),
|
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({
|
async function executePluginAllContentLoaded({
|
||||||
plugin,
|
plugin,
|
||||||
context,
|
context,
|
||||||
|
@ -168,49 +162,15 @@ async function executePluginAllContentLoaded({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executePluginsContentLoaded({
|
type AllContentLoadedResult = {routes: RouteConfig[]; globalData: GlobalData};
|
||||||
|
|
||||||
|
async function executeAllPluginsAllContentLoaded({
|
||||||
plugins,
|
plugins,
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> {
|
}): Promise<AllContentLoadedResult> {
|
||||||
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}> {
|
|
||||||
return PerfLogger.async(`Plugins - allContentLoaded`, async () => {
|
return PerfLogger.async(`Plugins - allContentLoaded`, async () => {
|
||||||
const allContent = aggregateAllContent(plugins);
|
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};
|
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 = {
|
export type LoadPluginsResult = {
|
||||||
plugins: LoadedPlugin[];
|
plugins: LoadedPlugin[];
|
||||||
routes: RouteConfig[];
|
routes: RouteConfig[];
|
||||||
globalData: GlobalData;
|
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.
|
* Initializes the plugins and run their lifecycle functions.
|
||||||
*/
|
*/
|
||||||
|
@ -307,28 +238,24 @@ export async function loadPlugins(
|
||||||
() => initPlugins(context),
|
() => initPlugins(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO probably not the ideal place to hardcode those plugins
|
||||||
initializedPlugins.push(
|
initializedPlugins.push(
|
||||||
createBootstrapPlugin(context),
|
createBootstrapPlugin(context),
|
||||||
createMDXFallbackPlugin(context),
|
createMDXFallbackPlugin(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
const plugins = await executePluginsLoadContent({
|
const plugins = await executeAllPluginsContentLoading({
|
||||||
plugins: initializedPlugins,
|
plugins: initializedPlugins,
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contentLoadedResult = await executePluginsContentLoaded({
|
const allContentLoadedResult = await executeAllPluginsAllContentLoaded({
|
||||||
plugins,
|
|
||||||
context,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allContentLoadedResult = await executePluginsAllContentLoaded({
|
|
||||||
plugins,
|
plugins,
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {routes, globalData} = mergeResults({
|
const {routes, globalData} = mergeResults({
|
||||||
contentLoadedResult,
|
plugins,
|
||||||
allContentLoadedResult,
|
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({
|
export async function reloadPlugin({
|
||||||
pluginIdentifier,
|
pluginIdentifier,
|
||||||
plugins: previousPlugins,
|
plugins: previousPlugins,
|
||||||
|
@ -365,30 +273,32 @@ export async function reloadPlugin({
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}): Promise<LoadPluginsResult> {
|
}): Promise<LoadPluginsResult> {
|
||||||
return PerfLogger.async('Plugins - reloadPlugin', async () => {
|
return PerfLogger.async('Plugins - reloadPlugin', async () => {
|
||||||
const plugin = getPluginByIdentifier({
|
const previousPlugin = getPluginByIdentifier({
|
||||||
plugins: previousPlugins,
|
plugins: previousPlugins,
|
||||||
pluginIdentifier,
|
pluginIdentifier,
|
||||||
});
|
});
|
||||||
|
const plugin = await executePluginContentLoading({
|
||||||
const reloadedPlugin = await executePluginLoadContent({plugin, context});
|
plugin: previousPlugin,
|
||||||
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,
|
|
||||||
context,
|
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,
|
plugins,
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {routes, globalData} = mergeResults({
|
const {routes, globalData} = mergeResults({
|
||||||
contentLoadedResult,
|
plugins,
|
||||||
allContentLoadedResult,
|
allContentLoadedResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
87
packages/docusaurus/src/server/plugins/pluginsUtils.ts
Normal file
87
packages/docusaurus/src/server/plugins/pluginsUtils.ts
Normal 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;
|
||||||
|
}
|
|
@ -7,7 +7,11 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type {RuleSetRule} from 'webpack';
|
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';
|
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({
|
export function createBootstrapPlugin({
|
||||||
siteDir,
|
siteDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
}: LoadContext): LoadedPlugin {
|
}: LoadContext): InitializedPlugin {
|
||||||
const {
|
const {
|
||||||
stylesheets,
|
stylesheets,
|
||||||
scripts,
|
scripts,
|
||||||
|
@ -27,7 +31,6 @@ export function createBootstrapPlugin({
|
||||||
} = siteConfig;
|
} = siteConfig;
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-bootstrap-plugin',
|
name: 'docusaurus-bootstrap-plugin',
|
||||||
content: null,
|
|
||||||
options: {
|
options: {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
},
|
},
|
||||||
|
@ -75,10 +78,9 @@ export function createBootstrapPlugin({
|
||||||
export function createMDXFallbackPlugin({
|
export function createMDXFallbackPlugin({
|
||||||
siteDir,
|
siteDir,
|
||||||
siteConfig,
|
siteConfig,
|
||||||
}: LoadContext): LoadedPlugin {
|
}: LoadContext): InitializedPlugin {
|
||||||
return {
|
return {
|
||||||
name: 'docusaurus-mdx-fallback-plugin',
|
name: 'docusaurus-mdx-fallback-plugin',
|
||||||
content: null,
|
|
||||||
options: {
|
options: {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
},
|
},
|
||||||
|
|
|
@ -247,10 +247,6 @@ export async function reloadSitePlugin(
|
||||||
site: Site,
|
site: Site,
|
||||||
pluginIdentifier: PluginIdentifier,
|
pluginIdentifier: PluginIdentifier,
|
||||||
): Promise<Site> {
|
): Promise<Site> {
|
||||||
console.log(
|
|
||||||
`reloadSitePlugin ${pluginIdentifier.name}@${pluginIdentifier.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const {plugins, routes, globalData} = await reloadPlugin({
|
const {plugins, routes, globalData} = await reloadPlugin({
|
||||||
pluginIdentifier,
|
pluginIdentifier,
|
||||||
plugins: site.props.plugins,
|
plugins: site.props.plugins,
|
||||||
|
|
|
@ -11,6 +11,12 @@ import logger from '@docusaurus/logger';
|
||||||
export const PerfDebuggingEnabled: boolean =
|
export const PerfDebuggingEnabled: boolean =
|
||||||
!!process.env.DOCUSAURUS_PERF_LOGGER;
|
!!process.env.DOCUSAURUS_PERF_LOGGER;
|
||||||
|
|
||||||
|
const Thresholds = {
|
||||||
|
min: 5,
|
||||||
|
yellow: 100,
|
||||||
|
red: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
type PerfLoggerAPI = {
|
type PerfLoggerAPI = {
|
||||||
start: (label: string) => void;
|
start: (label: string) => void;
|
||||||
end: (label: string) => void;
|
end: (label: string) => void;
|
||||||
|
@ -34,17 +40,40 @@ function createPerfLogger(): PerfLoggerAPI {
|
||||||
|
|
||||||
const prefix = logger.yellow(`[PERF] `);
|
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) =>
|
const log: PerfLoggerAPI['log'] = (label: string) =>
|
||||||
console.log(prefix + label);
|
console.log(prefix + label);
|
||||||
|
|
||||||
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
const async: PerfLoggerAPI['async'] = async (label, asyncFn) => {
|
||||||
start(label);
|
start(label);
|
||||||
|
const before = performance.now();
|
||||||
const result = await asyncFn();
|
const result = await asyncFn();
|
||||||
end(label);
|
const duration = performance.now() - before;
|
||||||
|
logDuration(label, duration);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue