mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-25 04:28:07 +02:00
feat(core): add new plugin allContentLoaded lifecycle (#9931)
This commit is contained in:
parent
d02b96f7f5
commit
8d115a9e0d
7 changed files with 791 additions and 228 deletions
|
@ -30,7 +30,7 @@ export default function pluginDebug({
|
||||||
return '../src/theme';
|
return '../src/theme';
|
||||||
},
|
},
|
||||||
|
|
||||||
async contentLoaded({actions: {createData, addRoute}, allContent}) {
|
async allContentLoaded({actions: {createData, addRoute}, allContent}) {
|
||||||
const allContentPath = await createData(
|
const allContentPath = await createData(
|
||||||
// Note that this created data path must be in sync with
|
// Note that this created data path must be in sync with
|
||||||
// metadataPath provided to mdx-loader.
|
// metadataPath provided to mdx-loader.
|
||||||
|
|
16
packages/docusaurus-types/src/plugin.d.ts
vendored
16
packages/docusaurus-types/src/plugin.d.ts
vendored
|
@ -18,11 +18,11 @@ import type {RouteConfig} from './routing';
|
||||||
|
|
||||||
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
export type PluginOptions = {id?: string} & {[key: string]: unknown};
|
||||||
|
|
||||||
export type PluginConfig =
|
export type PluginConfig<Content = unknown> =
|
||||||
| string
|
| string
|
||||||
| [string, PluginOptions]
|
| [string, PluginOptions]
|
||||||
| [PluginModule, PluginOptions]
|
| [PluginModule<Content>, PluginOptions]
|
||||||
| PluginModule
|
| PluginModule<Content>
|
||||||
| false
|
| false
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
|
@ -110,7 +110,9 @@ export type Plugin<Content = unknown> = {
|
||||||
contentLoaded?: (args: {
|
contentLoaded?: (args: {
|
||||||
/** The content loaded by this plugin instance */
|
/** The content loaded by this plugin instance */
|
||||||
content: Content; //
|
content: Content; //
|
||||||
/** Content loaded by ALL the plugins */
|
actions: PluginContentLoadedActions;
|
||||||
|
}) => Promise<void> | void;
|
||||||
|
allContentLoaded?: (args: {
|
||||||
allContent: AllContent;
|
allContent: AllContent;
|
||||||
actions: PluginContentLoadedActions;
|
actions: PluginContentLoadedActions;
|
||||||
}) => Promise<void> | void;
|
}) => Promise<void> | void;
|
||||||
|
@ -183,8 +185,10 @@ export type LoadedPlugin = InitializedPlugin & {
|
||||||
readonly content: unknown;
|
readonly content: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginModule = {
|
export type PluginModule<Content = unknown> = {
|
||||||
(context: LoadContext, options: unknown): Plugin | Promise<Plugin>;
|
(context: LoadContext, options: unknown):
|
||||||
|
| Plugin<Content>
|
||||||
|
| Promise<Plugin<Content>>;
|
||||||
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
|
validateOptions?: <T, U>(data: OptionValidationContext<T, U>) => U;
|
||||||
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
|
validateThemeConfig?: <T>(data: ThemeConfigValidationContext<T>) => T;
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "3.0.0",
|
"@docusaurus/module-type-aliases": "3.0.0",
|
||||||
"@docusaurus/types": "3.0.0",
|
"@docusaurus/types": "3.0.0",
|
||||||
|
"@total-typescript/shoehorn": "^0.1.2",
|
||||||
"@types/detect-port": "^1.3.3",
|
"@types/detect-port": "^1.3.3",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-router-config": "^5.0.7",
|
"@types/react-router-config": "^5.0.7",
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`loadPlugins loads plugins 1`] = `
|
|
||||||
{
|
|
||||||
"globalData": {
|
|
||||||
"test1": {
|
|
||||||
"default": {
|
|
||||||
"content": "a",
|
|
||||||
"prop": "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"content": "a",
|
|
||||||
"contentLoaded": [Function],
|
|
||||||
"loadContent": [Function],
|
|
||||||
"name": "test1",
|
|
||||||
"options": {
|
|
||||||
"id": "default",
|
|
||||||
},
|
|
||||||
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin",
|
|
||||||
"prop": "a",
|
|
||||||
"version": {
|
|
||||||
"type": "local",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"configureWebpack": [Function],
|
|
||||||
"content": undefined,
|
|
||||||
"name": "test2",
|
|
||||||
"options": {
|
|
||||||
"id": "default",
|
|
||||||
},
|
|
||||||
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin",
|
|
||||||
"version": {
|
|
||||||
"type": "local",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": undefined,
|
|
||||||
"getClientModules": [Function],
|
|
||||||
"injectHtmlTags": [Function],
|
|
||||||
"name": "docusaurus-bootstrap-plugin",
|
|
||||||
"options": {
|
|
||||||
"id": "default",
|
|
||||||
},
|
|
||||||
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin",
|
|
||||||
"version": {
|
|
||||||
"type": "synthetic",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"configureWebpack": [Function],
|
|
||||||
"content": undefined,
|
|
||||||
"name": "docusaurus-mdx-fallback-plugin",
|
|
||||||
"options": {
|
|
||||||
"id": "default",
|
|
||||||
},
|
|
||||||
"path": ".",
|
|
||||||
"version": {
|
|
||||||
"type": "synthetic",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"component": "Comp",
|
|
||||||
"context": {
|
|
||||||
"data": {
|
|
||||||
"content": "path",
|
|
||||||
},
|
|
||||||
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/test1/default/plugin-route-context-module-100.json",
|
|
||||||
},
|
|
||||||
"modules": {
|
|
||||||
"content": "path",
|
|
||||||
},
|
|
||||||
"path": "foo/",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`;
|
|
|
@ -6,53 +6,523 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {loadPlugins} from '../plugins';
|
import {fromPartial} from '@total-typescript/shoehorn';
|
||||||
import type {Plugin, Props} from '@docusaurus/types';
|
import {loadPlugins, mergeGlobalData} from '../plugins';
|
||||||
|
import type {
|
||||||
|
GlobalData,
|
||||||
|
LoadContext,
|
||||||
|
Plugin,
|
||||||
|
PluginConfig,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
describe('loadPlugins', () => {
|
function testLoad({
|
||||||
it('loads plugins', async () => {
|
plugins,
|
||||||
const siteDir = path.join(__dirname, '__fixtures__/site-with-plugin');
|
themes,
|
||||||
await expect(
|
}: {
|
||||||
loadPlugins({
|
plugins: PluginConfig<any>[];
|
||||||
siteDir,
|
themes: PluginConfig<any>[];
|
||||||
generatedFilesDir: path.join(siteDir, '.docusaurus'),
|
}) {
|
||||||
outDir: path.join(siteDir, 'build'),
|
const siteDir = path.join(__dirname, '__fixtures__/site-with-plugin');
|
||||||
siteConfig: {
|
|
||||||
baseUrl: '/',
|
const context = fromPartial<LoadContext>({
|
||||||
trailingSlash: true,
|
siteDir,
|
||||||
themeConfig: {},
|
siteConfigPath: path.join(siteDir, 'docusaurus.config.js'),
|
||||||
presets: [],
|
generatedFilesDir: path.join(siteDir, '.docusaurus'),
|
||||||
plugins: [
|
outDir: path.join(siteDir, 'build'),
|
||||||
() =>
|
siteConfig: {
|
||||||
({
|
baseUrl: '/',
|
||||||
name: 'test1',
|
trailingSlash: true,
|
||||||
prop: 'a',
|
themeConfig: {},
|
||||||
async loadContent() {
|
presets: [],
|
||||||
// Testing that plugin lifecycle is bound to the instance
|
plugins,
|
||||||
return this.prop;
|
themes,
|
||||||
},
|
},
|
||||||
async contentLoaded({content, actions}) {
|
});
|
||||||
actions.addRoute({
|
|
||||||
path: 'foo',
|
return loadPlugins(context);
|
||||||
component: 'Comp',
|
}
|
||||||
modules: {content: 'path'},
|
|
||||||
context: {content: 'path'},
|
const SyntheticPluginNames = [
|
||||||
});
|
'docusaurus-bootstrap-plugin',
|
||||||
actions.setGlobalData({content, prop: this.prop});
|
'docusaurus-mdx-fallback-plugin',
|
||||||
},
|
];
|
||||||
} as Plugin & ThisType<{prop: 'a'}>),
|
|
||||||
],
|
async function testPlugin<Content = unknown>(
|
||||||
themes: [
|
pluginConfig: PluginConfig<Content>,
|
||||||
() => ({
|
) {
|
||||||
name: 'test2',
|
const {plugins, routes, globalData} = await testLoad({
|
||||||
configureWebpack() {
|
plugins: [pluginConfig],
|
||||||
return {};
|
themes: [],
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
],
|
const nonSyntheticPlugins = plugins.filter(
|
||||||
|
(p) => !SyntheticPluginNames.includes(p.name),
|
||||||
|
);
|
||||||
|
expect(nonSyntheticPlugins).toHaveLength(1);
|
||||||
|
const plugin = nonSyntheticPlugins[0]!;
|
||||||
|
expect(plugin).toBeDefined();
|
||||||
|
|
||||||
|
return {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',
|
||||||
},
|
},
|
||||||
siteConfigPath: path.join(siteDir, 'docusaurus.config.js'),
|
},
|
||||||
} as unknown as Props),
|
});
|
||||||
).resolves.toMatchSnapshot();
|
});
|
||||||
|
|
||||||
|
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({
|
||||||
|
plugins: [],
|
||||||
|
themes: [],
|
||||||
|
});
|
||||||
|
// This adds some default synthetic plugins by default
|
||||||
|
expect(plugins.map((p) => p.name)).toEqual(SyntheticPluginNames);
|
||||||
|
expect(routes).toEqual([]);
|
||||||
|
expect(globalData).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('simplest plugin', async () => {
|
||||||
|
const {plugin, routes, globalData} = await testPlugin(() => ({
|
||||||
|
name: 'plugin-name',
|
||||||
|
}));
|
||||||
|
expect(plugin.name).toBe('plugin-name');
|
||||||
|
expect(routes).toEqual([]);
|
||||||
|
expect(globalData).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('typical plugin', async () => {
|
||||||
|
const {plugin, routes, globalData} = await testPlugin(() => ({
|
||||||
|
name: 'plugin-name',
|
||||||
|
loadContent: () => ({name: 'Toto', age: 42}),
|
||||||
|
translateContent: ({content}) => ({
|
||||||
|
...content,
|
||||||
|
name: `${content.name} (translated)`,
|
||||||
|
}),
|
||||||
|
contentLoaded({content, actions}) {
|
||||||
|
actions.addRoute({
|
||||||
|
path: '/foo',
|
||||||
|
component: 'Comp',
|
||||||
|
modules: {someModule: 'someModulePath'},
|
||||||
|
context: {someContext: 'someContextPath'},
|
||||||
|
});
|
||||||
|
actions.setGlobalData({
|
||||||
|
globalName: content.name,
|
||||||
|
globalAge: content.age,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(plugin.content).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"age": 42,
|
||||||
|
"name": "Toto (translated)",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(routes).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"component": "Comp",
|
||||||
|
"context": {
|
||||||
|
"data": {
|
||||||
|
"someContext": "someContextPath",
|
||||||
|
},
|
||||||
|
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/default/plugin-route-context-module-100.json",
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
"someModule": "someModulePath",
|
||||||
|
},
|
||||||
|
"path": "/foo/",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(globalData).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"plugin-name": {
|
||||||
|
"default": {
|
||||||
|
"globalAge": 42,
|
||||||
|
"globalName": "Toto (translated)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plugin with options', async () => {
|
||||||
|
const pluginOptions = {id: 'plugin-id', someOption: 42};
|
||||||
|
|
||||||
|
const {plugin, routes, globalData} = await testPlugin([
|
||||||
|
(_context, options) => ({
|
||||||
|
name: 'plugin-name',
|
||||||
|
loadContent: () => ({options, name: 'Toto'}),
|
||||||
|
contentLoaded({content, actions}) {
|
||||||
|
actions.addRoute({
|
||||||
|
path: '/foo',
|
||||||
|
component: 'Comp',
|
||||||
|
});
|
||||||
|
actions.setGlobalData({
|
||||||
|
// @ts-expect-error: TODO fix plugin/option type inference issue
|
||||||
|
globalName: content.name,
|
||||||
|
// @ts-expect-error: TODO fix plugin/option type inference issue
|
||||||
|
globalSomeOption: content.options.someOption,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
pluginOptions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(plugin.name).toBe('plugin-name');
|
||||||
|
expect(plugin.options).toEqual(pluginOptions);
|
||||||
|
expect(plugin.content).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"name": "Toto",
|
||||||
|
"options": {
|
||||||
|
"id": "plugin-id",
|
||||||
|
"someOption": 42,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(routes).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"component": "Comp",
|
||||||
|
"context": {
|
||||||
|
"plugin": "<PROJECT_ROOT>/packages/docusaurus/src/server/plugins/__tests__/__fixtures__/site-with-plugin/.docusaurus/plugin-name/plugin-id/plugin-route-context-module-100.json",
|
||||||
|
},
|
||||||
|
"path": "/foo/",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(globalData).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"plugin-name": {
|
||||||
|
"plugin-id": {
|
||||||
|
"globalName": "Toto",
|
||||||
|
"globalSomeOption": 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plugin with This binding', async () => {
|
||||||
|
const {plugin, routes, globalData} = await testPlugin(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
name: 'plugin-name',
|
||||||
|
someAttribute: 'val',
|
||||||
|
async loadContent() {
|
||||||
|
return this.someAttribute;
|
||||||
|
},
|
||||||
|
async contentLoaded({content, actions}) {
|
||||||
|
actions.setGlobalData({
|
||||||
|
content,
|
||||||
|
someAttributeGlobal: this.someAttribute,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as Plugin & ThisType<{someAttribute: string}>),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(plugin.content).toMatchInlineSnapshot(`"val"`);
|
||||||
|
expect(routes).toMatchInlineSnapshot(`[]`);
|
||||||
|
expect(globalData).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"plugin-name": {
|
||||||
|
"default": {
|
||||||
|
"content": "val",
|
||||||
|
"someAttributeGlobal": "val",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plugin with contentLoaded + allContentLoaded lifecycle', async () => {
|
||||||
|
const {routes, globalData} = await testPlugin(() => ({
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Routes of both lifecycles are appropriately sorted
|
||||||
|
expect(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(globalData).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"plugin-name": {
|
||||||
|
"default": {
|
||||||
|
"globalAllContentLoaded": "val2",
|
||||||
|
"globalContentLoaded": "val1",
|
||||||
|
"globalOverridden": "override-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
91
packages/docusaurus/src/server/plugins/actions.ts
Normal file
91
packages/docusaurus/src/server/plugins/actions.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import {docuHash, generate} from '@docusaurus/utils';
|
||||||
|
import {applyRouteTrailingSlash} from './routeConfig';
|
||||||
|
import type {
|
||||||
|
LoadedPlugin,
|
||||||
|
PluginContentLoadedActions,
|
||||||
|
PluginRouteContext,
|
||||||
|
RouteConfig,
|
||||||
|
} from '@docusaurus/types';
|
||||||
|
|
||||||
|
type PluginActionUtils = {
|
||||||
|
getRoutes: () => RouteConfig[];
|
||||||
|
getGlobalData: () => unknown;
|
||||||
|
getActions: () => PluginContentLoadedActions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO refactor historical action system and make this side-effect-free
|
||||||
|
// If the function were pure, we could more easily compare previous/next values
|
||||||
|
// on site reloads, and bail-out of the reload process earlier
|
||||||
|
// Particularly, createData() modules should rather be declarative
|
||||||
|
export async function createPluginActionsUtils({
|
||||||
|
plugin,
|
||||||
|
generatedFilesDir,
|
||||||
|
baseUrl,
|
||||||
|
trailingSlash,
|
||||||
|
}: {
|
||||||
|
plugin: LoadedPlugin;
|
||||||
|
generatedFilesDir: string;
|
||||||
|
baseUrl: string;
|
||||||
|
trailingSlash: boolean | undefined;
|
||||||
|
}): Promise<PluginActionUtils> {
|
||||||
|
const pluginId = plugin.options.id;
|
||||||
|
// Plugins data files are namespaced by pluginName/pluginId
|
||||||
|
const dataDir = path.join(generatedFilesDir, plugin.name, pluginId);
|
||||||
|
|
||||||
|
const pluginRouteContext: PluginRouteContext['plugin'] = {
|
||||||
|
name: plugin.name,
|
||||||
|
id: pluginId,
|
||||||
|
};
|
||||||
|
const pluginRouteContextModulePath = path.join(
|
||||||
|
dataDir,
|
||||||
|
`${docuHash('pluginRouteContextModule')}.json`,
|
||||||
|
);
|
||||||
|
await generate(
|
||||||
|
'/',
|
||||||
|
pluginRouteContextModulePath,
|
||||||
|
JSON.stringify(pluginRouteContext, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
const routes: RouteConfig[] = [];
|
||||||
|
let globalData: unknown;
|
||||||
|
|
||||||
|
const actions: PluginContentLoadedActions = {
|
||||||
|
addRoute(initialRouteConfig) {
|
||||||
|
// Trailing slash behavior is handled generically for all plugins
|
||||||
|
const finalRouteConfig = applyRouteTrailingSlash(initialRouteConfig, {
|
||||||
|
baseUrl,
|
||||||
|
trailingSlash,
|
||||||
|
});
|
||||||
|
routes.push({
|
||||||
|
...finalRouteConfig,
|
||||||
|
context: {
|
||||||
|
...(finalRouteConfig.context && {data: finalRouteConfig.context}),
|
||||||
|
plugin: pluginRouteContextModulePath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async createData(name, data) {
|
||||||
|
const modulePath = path.join(dataDir, name);
|
||||||
|
await generate(dataDir, name, data);
|
||||||
|
return modulePath;
|
||||||
|
},
|
||||||
|
setGlobalData(data) {
|
||||||
|
globalData = data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Some variables are mutable, so we expose a getter instead of the value
|
||||||
|
getRoutes: () => routes,
|
||||||
|
getGlobalData: () => globalData,
|
||||||
|
getActions: () => actions,
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,24 +5,21 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {docuHash, generate} from '@docusaurus/utils';
|
|
||||||
import logger from '@docusaurus/logger';
|
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 {applyRouteTrailingSlash, sortRoutes} from './routeConfig';
|
import {sortRoutes} from './routeConfig';
|
||||||
import {PerfLogger} from '../../utils';
|
import {PerfLogger} from '../../utils';
|
||||||
|
import {createPluginActionsUtils} from './actions';
|
||||||
import type {
|
import type {
|
||||||
LoadContext,
|
LoadContext,
|
||||||
PluginContentLoadedActions,
|
|
||||||
RouteConfig,
|
RouteConfig,
|
||||||
AllContent,
|
AllContent,
|
||||||
GlobalData,
|
GlobalData,
|
||||||
LoadedPlugin,
|
LoadedPlugin,
|
||||||
InitializedPlugin,
|
InitializedPlugin,
|
||||||
PluginRouteContext,
|
|
||||||
} from '@docusaurus/types';
|
} from '@docusaurus/types';
|
||||||
import type {PluginIdentifier} from '@docusaurus/types/src/plugin';
|
import type {PluginIdentifier} from '@docusaurus/types/src/plugin';
|
||||||
|
|
||||||
|
@ -107,24 +104,12 @@ function aggregateAllContent(loadedPlugins: LoadedPlugin[]): AllContent {
|
||||||
.value();
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor and make this side-effect-free
|
|
||||||
// If the function was pure, we could more easily compare previous/next values
|
|
||||||
// on site reloads, and bail-out of the reload process earlier
|
|
||||||
// createData() modules should rather be declarative
|
|
||||||
async function executePluginContentLoaded({
|
async function executePluginContentLoaded({
|
||||||
plugin,
|
plugin,
|
||||||
context,
|
context,
|
||||||
allContent,
|
|
||||||
}: {
|
}: {
|
||||||
plugin: LoadedPlugin;
|
plugin: LoadedPlugin;
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
// TODO AllContent was injected to this lifecycle for the debug plugin
|
|
||||||
// This is what permits to create the debug routes for all other plugins
|
|
||||||
// This was likely a bad idea and prevents to start executing contentLoaded()
|
|
||||||
// until all plugins have finished loading all the data
|
|
||||||
// we'd rather remove this and find another way to implement the debug plugin
|
|
||||||
// A possible solution: make it a core feature instead of a plugin?
|
|
||||||
allContent: AllContent;
|
|
||||||
}): Promise<{routes: RouteConfig[]; globalData: unknown}> {
|
}): Promise<{routes: RouteConfig[]; globalData: unknown}> {
|
||||||
return PerfLogger.async(
|
return PerfLogger.async(
|
||||||
`Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`,
|
`Plugins - contentLoaded - ${plugin.name}@${plugin.options.id}`,
|
||||||
|
@ -132,63 +117,53 @@ async function executePluginContentLoaded({
|
||||||
if (!plugin.contentLoaded) {
|
if (!plugin.contentLoaded) {
|
||||||
return {routes: [], globalData: undefined};
|
return {routes: [], globalData: undefined};
|
||||||
}
|
}
|
||||||
|
const pluginActionsUtils = await createPluginActionsUtils({
|
||||||
const pluginId = plugin.options.id;
|
plugin,
|
||||||
// Plugins data files are namespaced by pluginName/pluginId
|
generatedFilesDir: context.generatedFilesDir,
|
||||||
const dataDir = path.join(
|
baseUrl: context.siteConfig.baseUrl,
|
||||||
context.generatedFilesDir,
|
trailingSlash: context.siteConfig.trailingSlash,
|
||||||
plugin.name,
|
});
|
||||||
pluginId,
|
|
||||||
);
|
|
||||||
const pluginRouteContextModulePath = path.join(
|
|
||||||
dataDir,
|
|
||||||
`${docuHash('pluginRouteContextModule')}.json`,
|
|
||||||
);
|
|
||||||
const pluginRouteContext: PluginRouteContext['plugin'] = {
|
|
||||||
name: plugin.name,
|
|
||||||
id: pluginId,
|
|
||||||
};
|
|
||||||
await generate(
|
|
||||||
'/',
|
|
||||||
pluginRouteContextModulePath,
|
|
||||||
JSON.stringify(pluginRouteContext, null, 2),
|
|
||||||
);
|
|
||||||
|
|
||||||
const routes: RouteConfig[] = [];
|
|
||||||
let globalData: unknown;
|
|
||||||
|
|
||||||
const actions: PluginContentLoadedActions = {
|
|
||||||
addRoute(initialRouteConfig) {
|
|
||||||
// Trailing slash behavior is handled generically for all plugins
|
|
||||||
const finalRouteConfig = applyRouteTrailingSlash(
|
|
||||||
initialRouteConfig,
|
|
||||||
context.siteConfig,
|
|
||||||
);
|
|
||||||
routes.push({
|
|
||||||
...finalRouteConfig,
|
|
||||||
context: {
|
|
||||||
...(finalRouteConfig.context && {data: finalRouteConfig.context}),
|
|
||||||
plugin: pluginRouteContextModulePath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async createData(name, data) {
|
|
||||||
const modulePath = path.join(dataDir, name);
|
|
||||||
await generate(dataDir, name, data);
|
|
||||||
return modulePath;
|
|
||||||
},
|
|
||||||
setGlobalData(data) {
|
|
||||||
globalData = data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await plugin.contentLoaded({
|
await plugin.contentLoaded({
|
||||||
content: plugin.content,
|
content: plugin.content,
|
||||||
actions,
|
actions: pluginActionsUtils.getActions(),
|
||||||
allContent,
|
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
routes: pluginActionsUtils.getRoutes(),
|
||||||
|
globalData: pluginActionsUtils.getGlobalData(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {routes, globalData};
|
async function executePluginAllContentLoaded({
|
||||||
|
plugin,
|
||||||
|
context,
|
||||||
|
allContent,
|
||||||
|
}: {
|
||||||
|
plugin: LoadedPlugin;
|
||||||
|
context: LoadContext;
|
||||||
|
allContent: AllContent;
|
||||||
|
}): Promise<{routes: RouteConfig[]; globalData: unknown}> {
|
||||||
|
return PerfLogger.async(
|
||||||
|
`Plugins - allContentLoaded - ${plugin.name}@${plugin.options.id}`,
|
||||||
|
async () => {
|
||||||
|
if (!plugin.allContentLoaded) {
|
||||||
|
return {routes: [], globalData: undefined};
|
||||||
|
}
|
||||||
|
const pluginActionsUtils = await createPluginActionsUtils({
|
||||||
|
plugin,
|
||||||
|
generatedFilesDir: context.generatedFilesDir,
|
||||||
|
baseUrl: context.siteConfig.baseUrl,
|
||||||
|
trailingSlash: context.siteConfig.trailingSlash,
|
||||||
|
});
|
||||||
|
await plugin.allContentLoaded({
|
||||||
|
allContent,
|
||||||
|
actions: pluginActionsUtils.getActions(),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
routes: pluginActionsUtils.getRoutes(),
|
||||||
|
globalData: pluginActionsUtils.getGlobalData(),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -201,6 +176,42 @@ async function executePluginsContentLoaded({
|
||||||
context: LoadContext;
|
context: LoadContext;
|
||||||
}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> {
|
}): Promise<{routes: RouteConfig[]; globalData: GlobalData}> {
|
||||||
return PerfLogger.async(`Plugins - contentLoaded`, async () => {
|
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 () => {
|
||||||
const allContent = aggregateAllContent(plugins);
|
const allContent = aggregateAllContent(plugins);
|
||||||
|
|
||||||
const routes: RouteConfig[] = [];
|
const routes: RouteConfig[] = [];
|
||||||
|
@ -209,7 +220,7 @@ async function executePluginsContentLoaded({
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
plugins.map(async (plugin) => {
|
plugins.map(async (plugin) => {
|
||||||
const {routes: pluginRoutes, globalData: pluginGlobalData} =
|
const {routes: pluginRoutes, globalData: pluginGlobalData} =
|
||||||
await executePluginContentLoaded({
|
await executePluginAllContentLoaded({
|
||||||
plugin,
|
plugin,
|
||||||
context,
|
context,
|
||||||
allContent,
|
allContent,
|
||||||
|
@ -238,37 +249,90 @@ export type LoadPluginsResult = {
|
||||||
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, runs `loadContent`, `translateContent`,
|
* Initializes the plugins and run their lifecycle functions.
|
||||||
* `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is
|
|
||||||
* side-effect-ful (it generates temp files), so is this function. This function
|
|
||||||
* would also mutate `context.siteConfig.themeConfig` to translate it.
|
|
||||||
*/
|
*/
|
||||||
export async function loadPlugins(
|
export async function loadPlugins(
|
||||||
context: LoadContext,
|
context: LoadContext,
|
||||||
): Promise<LoadPluginsResult> {
|
): Promise<LoadPluginsResult> {
|
||||||
return PerfLogger.async('Plugins - loadPlugins', async () => {
|
return PerfLogger.async('Plugins - loadPlugins', async () => {
|
||||||
// 1. Plugin Lifecycle - Initialization/Constructor.
|
const initializedPlugins: InitializedPlugin[] = await PerfLogger.async(
|
||||||
const plugins: InitializedPlugin[] = await PerfLogger.async(
|
|
||||||
'Plugins - initPlugins',
|
'Plugins - initPlugins',
|
||||||
() => initPlugins(context),
|
() => initPlugins(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
plugins.push(
|
initializedPlugins.push(
|
||||||
createBootstrapPlugin(context),
|
createBootstrapPlugin(context),
|
||||||
createMDXFallbackPlugin(context),
|
createMDXFallbackPlugin(context),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Plugin Lifecycle - loadContent.
|
const plugins = await executePluginsLoadContent({
|
||||||
const loadedPlugins = await executePluginsLoadContent({plugins, context});
|
plugins: initializedPlugins,
|
||||||
|
|
||||||
// 3. Plugin Lifecycle - contentLoaded.
|
|
||||||
const {routes, globalData} = await executePluginsContentLoaded({
|
|
||||||
plugins: loadedPlugins,
|
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {plugins: loadedPlugins, routes, globalData};
|
const contentLoadedResult = await executePluginsContentLoaded({
|
||||||
|
plugins,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allContentLoadedResult = await executePluginsAllContentLoaded({
|
||||||
|
plugins,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {routes, globalData} = mergeResults({
|
||||||
|
contentLoadedResult,
|
||||||
|
allContentLoadedResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {plugins, routes, globalData};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +357,7 @@ export function getPluginByIdentifier({
|
||||||
|
|
||||||
export async function reloadPlugin({
|
export async function reloadPlugin({
|
||||||
pluginIdentifier,
|
pluginIdentifier,
|
||||||
plugins,
|
plugins: previousPlugins,
|
||||||
context,
|
context,
|
||||||
}: {
|
}: {
|
||||||
pluginIdentifier: PluginIdentifier;
|
pluginIdentifier: PluginIdentifier;
|
||||||
|
@ -301,18 +365,33 @@ 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({plugins, pluginIdentifier});
|
const plugin = getPluginByIdentifier({
|
||||||
|
plugins: previousPlugins,
|
||||||
|
pluginIdentifier,
|
||||||
|
});
|
||||||
|
|
||||||
const reloadedPlugin = await executePluginLoadContent({plugin, context});
|
const reloadedPlugin = await executePluginLoadContent({plugin, context});
|
||||||
const newPlugins = plugins.with(plugins.indexOf(plugin), reloadedPlugin);
|
const plugins = previousPlugins.with(
|
||||||
|
previousPlugins.indexOf(plugin),
|
||||||
|
reloadedPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
// Unfortunately, due to the "AllContent" data we have to re-execute this
|
// TODO optimize this, we shouldn't need to re-run this lifecycle
|
||||||
// for all plugins, not just the one to reload...
|
const contentLoadedResult = await executePluginsContentLoaded({
|
||||||
const {routes, globalData} = await executePluginsContentLoaded({
|
plugins,
|
||||||
plugins: newPlugins,
|
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {plugins: newPlugins, routes, globalData};
|
const allContentLoadedResult = await executePluginsAllContentLoaded({
|
||||||
|
plugins,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {routes, globalData} = mergeResults({
|
||||||
|
contentLoadedResult,
|
||||||
|
allContentLoadedResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {plugins, routes, globalData};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue