in packages/docusaurus/src/server/plugins/index.ts [33:173]
export async function loadPlugins(context: LoadContext): Promise<{
plugins: LoadedPlugin[];
pluginsRouteConfigs: RouteConfig[];
globalData: GlobalData;
}> {
// 1. Plugin Lifecycle - Initialization/Constructor.
const plugins: InitializedPlugin[] = await initPlugins(context);
plugins.push(
createBootstrapPlugin(context),
createMDXFallbackPlugin(context),
);
// 2. Plugin Lifecycle - loadContent.
// Currently plugins run lifecycle methods in parallel and are not
// order-dependent. We could change this in future if there are plugins which
// need to run in certain order or depend on others for data.
// This would also translate theme config and content upfront, given the
// translation files that the plugin declares.
const loadedPlugins: LoadedPlugin[] = await Promise.all(
plugins.map(async (plugin) => {
const content = await plugin.loadContent?.();
const rawTranslationFiles =
(await plugin?.getTranslationFiles?.({content})) ?? [];
const translationFiles = await Promise.all(
rawTranslationFiles.map((translationFile) =>
localizePluginTranslationFile({
locale: context.i18n.currentLocale,
siteDir: context.siteDir,
translationFile,
plugin,
}),
),
);
const translatedContent =
plugin.translateContent?.({content, translationFiles}) ?? content;
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
themeConfig: context.siteConfig.themeConfig,
translationFiles,
});
// Side-effect to merge theme config translations. A plugin should only
// translate its own slice of theme config and should make no assumptions
// about other plugins' keys, so this is safe to run in parallel.
Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice);
return {...plugin, content: translatedContent};
}),
);
const allContent: AllContent = _.chain(loadedPlugins)
.groupBy((item) => item.name)
.mapValues((nameItems) =>
_.chain(nameItems)
.groupBy((item) => item.options.id)
.mapValues((idItems) => idItems[0]!.content)
.value(),
)
.value();
// 3. Plugin Lifecycle - contentLoaded.
const pluginsRouteConfigs: RouteConfig[] = [];
const globalData: GlobalData = {};
await Promise.all(
loadedPlugins.map(async ({content, ...plugin}) => {
if (!plugin.contentLoaded) {
return;
}
const pluginId = plugin.options.id;
// plugins data files are namespaced by pluginName/pluginId
const dataDir = path.join(
context.generatedFilesDir,
plugin.name,
pluginId,
);
// TODO this would be better to do all that in the codegen phase
// TODO handle context for nested routes
const pluginRouteContext: PluginRouteContext = {
plugin: {name: plugin.name, id: pluginId},
data: undefined, // TODO allow plugins to provide context data
};
const pluginRouteContextModulePath = path.join(
dataDir,
`${docuHash('pluginRouteContextModule')}.json`,
);
await generate(
'/',
pluginRouteContextModulePath,
JSON.stringify(pluginRouteContext, null, 2),
);
const actions: PluginContentLoadedActions = {
addRoute(initialRouteConfig) {
// Trailing slash behavior is handled generically for all plugins
const finalRouteConfig = applyRouteTrailingSlash(
initialRouteConfig,
context.siteConfig,
);
pluginsRouteConfigs.push({
...finalRouteConfig,
modules: {
...finalRouteConfig.modules,
__context: pluginRouteContextModulePath,
},
});
},
async createData(name, data) {
const modulePath = path.join(dataDir, name);
await generate(dataDir, name, data);
return modulePath;
},
setGlobalData(data) {
globalData[plugin.name] ??= {};
globalData[plugin.name]![pluginId] = data;
},
};
await plugin.contentLoaded({content, actions, allContent});
}),
);
// 4. Plugin Lifecycle - routesLoaded.
await Promise.all(
loadedPlugins.map(async (plugin) => {
if (!plugin.routesLoaded) {
return;
}
// TODO alpha-60: remove this deprecated lifecycle soon
// 1 user reported usage of this lifecycle: https://github.com/facebook/docusaurus/issues/3918
logger.error`Plugin code=${'routesLoaded'} lifecycle is deprecated. If you think we should keep this lifecycle, please report here: url=${'https://github.com/facebook/docusaurus/issues/3918'}`;
await plugin.routesLoaded(pluginsRouteConfigs);
}),
);
// Sort the route config. This ensures that route with nested
// routes are always placed last.
sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl);
return {plugins: loadedPlugins, pluginsRouteConfigs, globalData};
}