diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 0238e6c8a6..5a95aeb9cc 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -55,7 +55,7 @@ import { export default function pluginContentBlog( context: LoadContext, options: PluginOptions, -): Plugin { +): Plugin { if (options.admonitions) { options.remarkPlugins = options.remarkPlugins.concat([ [admonitions, options.admonitions], @@ -88,8 +88,6 @@ export default function pluginContentBlog( const aliasedSource = (source: string) => `~blog/${posixPath(path.relative(pluginDataDirRoot, source))}`; - let blogPosts: BlogPost[] = []; - return { name: 'docusaurus-plugin-content-blog', @@ -116,10 +114,19 @@ export default function pluginContentBlog( async loadContent() { const {postsPerPage, routeBasePath} = options; - blogPosts = await generateBlogPosts(contentPaths, context, options); + const blogPosts: BlogPost[] = await generateBlogPosts( + contentPaths, + context, + options, + ); if (!blogPosts.length) { - return null; + return { + blogPosts: [], + blogListPaginated: [], + blogTags: {}, + blogTagsListPath: null, + }; } // Colocate next and prev metadata. @@ -242,7 +249,7 @@ export default function pluginContentBlog( const {addRoute, createData} = actions; const { - blogPosts: loadedBlogPosts, + blogPosts, blogListPaginated, blogTags, blogTagsListPath, @@ -275,7 +282,7 @@ export default function pluginContentBlog( // Create routes for blog entries. await Promise.all( - loadedBlogPosts.map(async (blogPost) => { + blogPosts.map(async (blogPost) => { const {id, metadata} = blogPost; await createData( // Note that this created data path must be in sync with @@ -403,6 +410,7 @@ export default function pluginContentBlog( _config: Configuration, isServer: boolean, {getJSLoader}: ConfigureWebpackUtils, + content, ) { const { rehypePlugins, @@ -416,7 +424,7 @@ export default function pluginContentBlog( siteDir, contentPaths, truncateMarker, - sourceToPermalink: getSourceToPermalink(blogPosts), + sourceToPermalink: getSourceToPermalink(content.blogPosts), onBrokenMarkdownLink: (brokenMarkdownLink) => { if (onBrokenMarkdownLinks === 'ignore') { return; @@ -506,8 +514,8 @@ export default function pluginContentBlog( ); }, - injectHtmlTags() { - if (!blogPosts.length) { + injectHtmlTags({content}) { + if (!content.blogPosts.length) { return {}; } diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index 9fb7d4bc16..fce3a99c03 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -309,6 +309,8 @@ describe('simple website', () => { test('configureWebpack', async () => { const {plugin} = await loadSite(); + const content = await plugin.loadContent?.(); + const config = applyConfigureWebpack( plugin.configureWebpack, { @@ -319,6 +321,8 @@ describe('simple website', () => { }, }, false, + undefined, + content, ); const errors = validate(config); expect(errors).toBeUndefined(); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index e836ca3026..8617e57a69 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -41,7 +41,7 @@ import {PermalinkToSidebar} from '@docusaurus/plugin-content-docs-types'; import {RuleSetRule} from 'webpack'; import {cliDocsVersionCommand} from './cli'; import {VERSIONS_JSON_FILE} from './constants'; -import {flatten, keyBy, compact} from 'lodash'; +import {flatten, keyBy, compact, mapValues} from 'lodash'; import {toGlobalDataVersion} from './globalData'; import {toVersionMetadataProp} from './props'; import { @@ -59,7 +59,6 @@ export default function pluginContentDocs( const versionsMetadata = readVersionsMetadata({context, options}); - const sourceToPermalink: SourceToPermalink = {}; const pluginId = options.id ?? DEFAULT_PLUGIN_ID; const pluginDataDirRoot = path.join( @@ -225,12 +224,6 @@ export default function pluginContentDocs( // sort to ensure consistent output for tests docs.sort((a, b) => a.id.localeCompare(b.id)); - // TODO annoying side effect! - Object.values(docs).forEach((loadedDoc) => { - const {source, permalink} = loadedDoc; - sourceToPermalink[source] = permalink; - }); - // TODO really useful? replace with global state logic? const permalinkToSidebar: PermalinkToSidebar = {}; Object.values(docs).forEach((doc) => { @@ -369,7 +362,7 @@ export default function pluginContentDocs( }); }, - configureWebpack(_config, isServer, utils) { + configureWebpack(_config, isServer, utils, content) { const {getJSLoader} = utils; const { rehypePlugins, @@ -378,9 +371,17 @@ export default function pluginContentDocs( beforeDefaultRemarkPlugins, } = options; + function getSourceToPermalink(): SourceToPermalink { + const allDocs = flatten(content.loadedVersions.map((v) => v.docs)); + return mapValues( + keyBy(allDocs, (d) => d.source), + (d) => d.permalink, + ); + } + const docsMarkdownOptions: DocsMarkdownOption = { siteDir, - sourceToPermalink, + sourceToPermalink: getSourceToPermalink(), versionsMetadata, onBrokenMarkdownLink: (brokenMarkdownLink) => { if (siteConfig.onBrokenMarkdownLinks === 'ignore') { diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 1ba356a86e..cd67b3c8d0 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -198,7 +198,7 @@ export interface Props extends LoadContext, InjectedHtmlTags { siteMetadata: DocusaurusSiteMetadata; routes: RouteConfig[]; routesPaths: string[]; - plugins: Plugin[]; + plugins: LoadedPlugin[]; } export interface PluginContentLoadedActions { @@ -233,10 +233,12 @@ export interface Plugin { routesLoaded?(routes: RouteConfig[]): void; // TODO remove soon, deprecated (alpha-60) postBuild?(props: Props): void; postStart?(props: Props): void; + // TODO refactor the configureWebpack API surface: use an object instead of multiple params (requires breaking change) configureWebpack?( config: Configuration, isServer: boolean, utils: ConfigureWebpackUtils, + content: Content, ): Configuration & {mergeStrategy?: ConfigureWebpackFnMergeStrategy}; configurePostCss?(options: PostCssOptions): PostCssOptions; getThemePath?(): string; @@ -244,7 +246,9 @@ export interface Plugin { getPathsToWatch?(): string[]; getClientModules?(): string[]; extendCli?(cli: Command): void; - injectHtmlTags?(): { + injectHtmlTags?({ + content: Content, + }): { headTags?: HtmlTags; preBodyTags?: HtmlTags; postBodyTags?: HtmlTags; diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 746904b603..4f0891ede0 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -184,17 +184,19 @@ async function buildLocale({ if (configureWebpack) { clientConfig = applyConfigureWebpack( - configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. + configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. // TODO remove this implicit api: inject in callback instead clientConfig, false, props.siteConfig.webpack?.jsLoader, + plugin.content, ); serverConfig = applyConfigureWebpack( - configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. + configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. // TODO remove this implicit api: inject in callback instead serverConfig, true, props.siteConfig.webpack?.jsLoader, + plugin.content, ); } }); diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index 893293661d..96f8cc5fd7 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -156,10 +156,11 @@ export default async function start( if (configureWebpack) { config = applyConfigureWebpack( - configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. + configureWebpack.bind(plugin), // The plugin lifecycle may reference `this`. // TODO remove this implicit api: inject in callback instead config, false, props.siteConfig.webpack?.jsLoader, + plugin.content, ); } }); diff --git a/packages/docusaurus/src/server/html-tags/index.ts b/packages/docusaurus/src/server/html-tags/index.ts index fd4e5c2036..76540e51fb 100644 --- a/packages/docusaurus/src/server/html-tags/index.ts +++ b/packages/docusaurus/src/server/html-tags/index.ts @@ -6,12 +6,8 @@ */ import htmlTagObjectToString from './htmlTags'; -import { - Plugin, - InjectedHtmlTags, - HtmlTagObject, - HtmlTags, -} from '@docusaurus/types'; +import {InjectedHtmlTags, HtmlTagObject, HtmlTags} from '@docusaurus/types'; +import {LoadedPlugin} from '../plugins'; function toString(val: string | HtmlTagObject): string { return typeof val === 'string' ? val : htmlTagObjectToString(val); @@ -21,14 +17,14 @@ export function createHtmlTagsString(tags: HtmlTags): string { return Array.isArray(tags) ? tags.map(toString).join('\n') : toString(tags); } -export function loadHtmlTags(plugins: Plugin[]): InjectedHtmlTags { +export function loadHtmlTags(plugins: LoadedPlugin[]): InjectedHtmlTags { const htmlTags = plugins.reduce( (acc, plugin) => { if (!plugin.injectHtmlTags) { return acc; } const {headTags, preBodyTags, postBodyTags} = - plugin.injectHtmlTags() || {}; + plugin.injectHtmlTags({content: plugin.content}) || {}; return { headTags: headTags ? `${acc.headTags}\n${createHtmlTagsString(headTags)}` diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 7a0dce4ddb..45edecf3ca 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -196,6 +196,7 @@ export async function load( } = siteConfig; plugins.push({ name: 'docusaurus-bootstrap-plugin', + content: null, options: {}, version: {type: 'synthetic'}, getClientModules() { diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 0a1f8764c4..37305e36b8 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -53,6 +53,8 @@ export function sortConfig(routeConfigs: RouteConfig[]): void { }); } +export type LoadedPlugin = InitPlugin & {content: unknown}; + export async function loadPlugins({ pluginConfigs, context, @@ -60,7 +62,7 @@ export async function loadPlugins({ pluginConfigs: PluginConfig[]; context: LoadContext; }): Promise<{ - plugins: InitPlugin[]; + plugins: LoadedPlugin[]; pluginsRouteConfigs: RouteConfig[]; globalData: unknown; themeConfigTranslated: ThemeConfig; @@ -75,21 +77,20 @@ export async function loadPlugins({ // 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. - type ContentLoadedPlugin = {plugin: InitPlugin; content: unknown}; - const contentLoadedPlugins: ContentLoadedPlugin[] = await Promise.all( + const loadedPlugins: LoadedPlugin[] = await Promise.all( plugins.map(async (plugin) => { const content = plugin.loadContent ? await plugin.loadContent() : null; - return {plugin, content}; + return {...plugin, content}; }), ); - type ContentLoadedTranslatedPlugin = ContentLoadedPlugin & { + type ContentLoadedTranslatedPlugin = LoadedPlugin & { translationFiles: TranslationFiles; }; const contentLoadedTranslatedPlugins: ContentLoadedTranslatedPlugin[] = await Promise.all( - contentLoadedPlugins.map(async (contentLoadedPlugin) => { + loadedPlugins.map(async (contentLoadedPlugin) => { const translationFiles = - (await contentLoadedPlugin.plugin?.getTranslationFiles?.({ + (await contentLoadedPlugin?.getTranslationFiles?.({ content: contentLoadedPlugin.content, })) ?? []; const localizedTranslationFiles = await Promise.all( @@ -98,7 +99,7 @@ export async function loadPlugins({ locale: context.i18n.currentLocale, siteDir: context.siteDir, translationFile, - plugin: contentLoadedPlugin.plugin, + plugin: contentLoadedPlugin, }), ), ); @@ -109,11 +110,11 @@ export async function loadPlugins({ }), ); - const allContent: AllContent = chain(contentLoadedPlugins) - .groupBy((item) => item.plugin.name) + const allContent: AllContent = chain(loadedPlugins) + .groupBy((item) => item.name) .mapValues((nameItems) => { return chain(nameItems) - .groupBy((item) => item.plugin.options.id ?? DEFAULT_PLUGIN_ID) + .groupBy((item) => item.options.id ?? DEFAULT_PLUGIN_ID) .mapValues((idItems) => idItems[0].content) .value(); }) @@ -126,7 +127,7 @@ export async function loadPlugins({ await Promise.all( contentLoadedTranslatedPlugins.map( - async ({plugin, content, translationFiles}) => { + async ({content, translationFiles, ...plugin}) => { if (!plugin.contentLoaded) { return; } @@ -191,7 +192,7 @@ export async function loadPlugins({ // We could change this in future if there are plugins which need to // run in certain order or depend on others for data. await Promise.all( - contentLoadedTranslatedPlugins.map(async ({plugin}) => { + contentLoadedTranslatedPlugins.map(async (plugin) => { if (!plugin.routesLoaded) { return null; } @@ -218,10 +219,10 @@ export async function loadPlugins({ untranslatedThemeConfig: ThemeConfig, ): ThemeConfig { return contentLoadedTranslatedPlugins.reduce( - (currentThemeConfig, {plugin, translationFiles}) => { + (currentThemeConfig, plugin) => { const translatedThemeConfigSlice = plugin.translateThemeConfig?.({ themeConfig: currentThemeConfig, - translationFiles, + translationFiles: plugin.translationFiles, }); return { ...currentThemeConfig, @@ -233,7 +234,7 @@ export async function loadPlugins({ } return { - plugins, + plugins: loadedPlugins, pluginsRouteConfigs, globalData, themeConfigTranslated: translateThemeConfig(context.siteConfig.themeConfig), diff --git a/packages/docusaurus/src/webpack/__tests__/utils.test.ts b/packages/docusaurus/src/webpack/__tests__/utils.test.ts index 1dbde42171..4dc700fb0a 100644 --- a/packages/docusaurus/src/webpack/__tests__/utils.test.ts +++ b/packages/docusaurus/src/webpack/__tests__/utils.test.ts @@ -77,7 +77,9 @@ describe('extending generated webpack config', () => { return {}; }; - config = applyConfigureWebpack(configureWebpack, config, false); + config = applyConfigureWebpack(configureWebpack, config, false, undefined, { + content: 42, + }); expect(config).toEqual({ entry: 'entry.js', output: { @@ -105,7 +107,9 @@ describe('extending generated webpack config', () => { }, }); - config = applyConfigureWebpack(configureWebpack, config, false); + config = applyConfigureWebpack(configureWebpack, config, false, undefined, { + content: 42, + }); expect(config).toEqual({ entry: 'entry.js', output: { @@ -137,6 +141,8 @@ describe('extending generated webpack config', () => { createConfigureWebpack(), config, false, + undefined, + {content: 42}, ); expect(defaultStrategyMergeConfig).toEqual({ module: { @@ -148,6 +154,8 @@ describe('extending generated webpack config', () => { createConfigureWebpack({'module.rules': 'prepend'}), config, false, + undefined, + {content: 42}, ); expect(prependRulesStrategyConfig).toEqual({ module: { @@ -159,6 +167,8 @@ describe('extending generated webpack config', () => { createConfigureWebpack({uselessAttributeName: 'append'}), config, false, + undefined, + {content: 42}, ); expect(uselessMergeStrategyConfig).toEqual({ module: { diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index e760fbcb36..b4566b9e2c 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -198,13 +198,15 @@ function getCacheLoaderDeprecated() { * @param config initial webpack config * @param isServer indicates if this is a server webpack configuration * @param jsLoader custom js loader config + * @param content content loaded by the plugin * @returns final/ modified webpack config */ export function applyConfigureWebpack( configureWebpack: ConfigureWebpackFn, config: Configuration, isServer: boolean, - jsLoader?: 'babel' | ((isServer: boolean) => RuleSetRule), + jsLoader: 'babel' | ((isServer: boolean) => RuleSetRule) | undefined, + content: unknown, ): Configuration { // Export some utility functions const utils: ConfigureWebpackUtils = { @@ -214,7 +216,12 @@ export function applyConfigureWebpack( getCacheLoader: getCacheLoaderDeprecated, }; if (typeof configureWebpack === 'function') { - const {mergeStrategy, ...res} = configureWebpack(config, isServer, utils); + const {mergeStrategy, ...res} = configureWebpack( + config, + isServer, + utils, + content, + ); if (res && typeof res === 'object') { // @ts-expect-error: annoying error due to enums: https://github.com/survivejs/webpack-merge/issues/179 const customizeRules: Record = mergeStrategy ?? {}; diff --git a/website/docs/lifecycle-apis.md b/website/docs/lifecycle-apis.md index 6a1466907f..a0cbc889eb 100644 --- a/website/docs/lifecycle-apis.md +++ b/website/docs/lifecycle-apis.md @@ -279,10 +279,16 @@ export default function friendsPlugin(context, options) { } ``` -## `configureWebpack(config, isServer, utils)` {#configurewebpackconfig-isserver-utils} +## `configureWebpack(config, isServer, utils, content)` {#configurewebpackconfig-isserver-utils} Modifies the internal webpack config. If the return value is a JavaScript object, it will be merged into the final config using [`webpack-merge`](https://github.com/survivejs/webpack-merge). If it is a function, it will be called and receive `config` as the first argument and an `isServer` flag as the argument argument. +:::caution + +The API of `configureWebpack` will be modified in the future to accept an object (`configureWebpack({config, isServer, utils, content})`) + +::: + ### `config` {#config} `configureWebpack` is called with `config` generated according to client/server build. You may treat this as the base config to be merged with. @@ -293,11 +299,10 @@ Modifies the internal webpack config. If the return value is a JavaScript object ### `utils` {#utils} -The initial call to `configureWebpack` also receives a util object consists of three functions: +`configureWebpack` also receives an util object: - `getStyleLoaders(isServer: boolean, cssOptions: {[key: string]: any}): Loader[]` -- `getCacheLoader(isServer: boolean, cacheOptions?: {}): Loader | null` -- `getBabelLoader(isServer: boolean, babelOptions?: {}): Loader` +- `getJSLoader(isServer: boolean, cacheOptions?: {}): Loader | null` You may use them to return your webpack configures conditionally. @@ -326,6 +331,10 @@ module.exports = function (context, options) { }; ``` +### `content` {#content} + +`configureWebpack` will be called both with the content loaded by the plugin. + ### Merge strategy {#merge-strategy} We merge the Webpack configuration parts of plugins into the global Webpack config using [webpack-merge](https://github.com/survivejs/webpack-merge). @@ -439,10 +448,12 @@ module.exports = function (context, options) { }; ``` -## `injectHtmlTags()` {#injecthtmltags} +## `injectHtmlTags({content})` {#injecthtmltags} Inject head and/or body HTML tags to Docusaurus generated HTML. +`injectHtmlTags` will be called both with the content loaded by the plugin. + ```typescript function injectHtmlTags(): { headTags?: HtmlTags; @@ -477,8 +488,11 @@ Example: module.exports = function (context, options) { return { name: 'docusaurus-plugin', + loadContent: async () => { + return {remoteHeadTags: await fetchHeadTagsFromAPI()}; + }, // highlight-start - injectHtmlTags() { + injectHtmlTags({content}) { return { headTags: [ { @@ -488,6 +502,7 @@ module.exports = function (context, options) { href: 'https://www.github.com', }, }, + ...content.remoteHeadTags, ], preBodyTags: [ { @@ -765,7 +780,7 @@ module.exports = function (context, opts) { // https://webpack.js.org/configuration/dev-server/#devserverafter }, - configureWebpack(config, isServer) { + configureWebpack(config, isServer, utils, content) { // Modify internal webpack config. If returned value is an Object, it // will be merged into the final config using webpack-merge; // If the returned value is a function, it will receive the config as the 1st argument and an isServer flag as the 2nd argument. @@ -790,11 +805,11 @@ module.exports = function (context, opts) { // Register an extra command to enhance the CLI of Docusaurus }, - injectHtmlTags() { + injectHtmlTags({content}) { // Inject head and/or body HTML tags. }, - async getTranslationFiles() { + async getTranslationFiles({content}) { // Return translation files },