From a7afd9cc87859b76c3f0c4f4eed959a8c92800f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 31 May 2024 17:47:36 +0200 Subject: [PATCH] fix(docs, blog): Markdown link resolution does not support hot reload (#10185) --- .../src/blogUtils.ts | 8 ---- .../src/index.ts | 38 ++++++++++++++-- .../src/index.ts | 44 +++++++++++++------ .../src/types.ts | 4 -- .../src/__tests__/markdownLinks.test.ts | 26 ++++++----- packages/docusaurus-utils/src/index.ts | 6 ++- .../docusaurus-utils/src/markdownLinks.ts | 11 +++-- 7 files changed, 93 insertions(+), 44 deletions(-) diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index d4a9f275bc87..d7a777ff77b6 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -45,14 +45,6 @@ export function truncate(fileString: string, truncateMarker: RegExp): string { return fileString.split(truncateMarker, 1).shift()!; } -export function getSourceToPermalink(blogPosts: BlogPost[]): { - [aliasedPath: string]: string; -} { - return Object.fromEntries( - blogPosts.map(({metadata: {source, permalink}}) => [source, permalink]), - ); -} - export function paginateBlogPosts({ blogPosts, basePageUrl, diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 19dfb1a5d2ad..6d40bea301df 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -19,10 +19,10 @@ import { getDataFilePath, DEFAULT_PLUGIN_ID, resolveMarkdownLinkPathname, + type SourceToPermalink, } from '@docusaurus/utils'; import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation'; import { - getSourceToPermalink, getBlogTags, paginateBlogPosts, shouldBeListed, @@ -50,6 +50,33 @@ import type {RuleSetUseItem} from 'webpack'; const PluginName = 'docusaurus-plugin-content-blog'; +// TODO this is bad, we should have a better way to do this (new lifecycle?) +// The source to permalink is currently a mutable map passed to the mdx loader +// for link resolution +// see https://github.com/facebook/docusaurus/pull/10185 +function createSourceToPermalinkHelper() { + const sourceToPermalink: SourceToPermalink = new Map(); + + function computeSourceToPermalink(content: BlogContent): SourceToPermalink { + return new Map( + content.blogPosts.map(({metadata: {source, permalink}}) => [ + source, + permalink, + ]), + ); + } + + // Mutable map update :/ + function update(content: BlogContent): void { + sourceToPermalink.clear(); + computeSourceToPermalink(content).forEach((value, key) => { + sourceToPermalink.set(key, value); + }); + } + + return {get: () => sourceToPermalink, update}; +} + export default async function pluginContentBlog( context: LoadContext, options: PluginOptions, @@ -96,6 +123,8 @@ export default async function pluginContentBlog( contentPaths, }); + const sourceToPermalinkHelper = createSourceToPermalinkHelper(); + return { name: PluginName, @@ -201,6 +230,8 @@ export default async function pluginContentBlog( }, async contentLoaded({content, actions}) { + sourceToPermalinkHelper.update(content); + await createAllRoutes({ baseUrl, content, @@ -214,7 +245,7 @@ export default async function pluginContentBlog( return translateContent(content, translationFiles); }, - configureWebpack(_config, isServer, utils, content) { + configureWebpack() { const { admonitions, rehypePlugins, @@ -224,7 +255,6 @@ export default async function pluginContentBlog( beforeDefaultRehypePlugins, } = options; - const sourceToPermalink = getSourceToPermalink(content.blogPosts); const contentDirs = getContentPathList(contentPaths); function createMDXLoader(): RuleSetUseItem { @@ -271,7 +301,7 @@ export default async function pluginContentBlog( resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { const permalink = resolveMarkdownLinkPathname(linkPathname, { sourceFilePath, - sourceToPermalink, + sourceToPermalink: sourceToPermalinkHelper.get(), siteDir, contentPaths, }); diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 0f0ae32458ee..937f7f7fa9d0 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -19,6 +19,8 @@ import { createSlugger, resolveMarkdownLinkPathname, DEFAULT_PLUGIN_ID, + type SourceToPermalink, + type TagsFile, } from '@docusaurus/utils'; import { getTagsFile, @@ -47,7 +49,6 @@ import { } from './translations'; import {createAllRoutes} from './routes'; import {createSidebarsUtils} from './sidebars/utils'; -import type {TagsFile} from '@docusaurus/utils'; import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type { @@ -59,9 +60,32 @@ import type { LoadedVersion, } from '@docusaurus/plugin-content-docs'; import type {LoadContext, Plugin} from '@docusaurus/types'; -import type {SourceToPermalink, DocFile, FullVersion} from './types'; +import type {DocFile, FullVersion} from './types'; import type {RuleSetUseItem} from 'webpack'; +// TODO this is bad, we should have a better way to do this (new lifecycle?) +// The source to permalink is currently a mutable map passed to the mdx loader +// for link resolution +// see https://github.com/facebook/docusaurus/pull/10185 +function createSourceToPermalinkHelper() { + const sourceToPermalink: SourceToPermalink = new Map(); + + function computeSourceToPermalink(content: LoadedContent): SourceToPermalink { + const allDocs = content.loadedVersions.flatMap((v) => v.docs); + return new Map(allDocs.map(({source, permalink}) => [source, permalink])); + } + + // Mutable map update :/ + function update(content: LoadedContent): void { + sourceToPermalink.clear(); + computeSourceToPermalink(content).forEach((value, key) => { + sourceToPermalink.set(key, value); + }); + } + + return {get: () => sourceToPermalink, update}; +} + export default async function pluginContentDocs( context: LoadContext, options: PluginOptions, @@ -88,6 +112,8 @@ export default async function pluginContentDocs( // TODO env should be injected into all plugins const env = process.env.NODE_ENV as DocEnv; + const sourceToPermalinkHelper = createSourceToPermalinkHelper(); + return { name: 'docusaurus-plugin-content-docs', @@ -244,6 +270,8 @@ export default async function pluginContentDocs( }, async contentLoaded({content, actions}) { + sourceToPermalinkHelper.update(content); + const versions: FullVersion[] = content.loadedVersions.map(toFullVersion); await createAllRoutes({ @@ -274,16 +302,6 @@ export default async function pluginContentDocs( // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 .map(addTrailingPathSeparator); - // TODO this does not re-run when content gets updated in dev! - // it's probably better to restore a mutable cache in the plugin - function getSourceToPermalink(): SourceToPermalink { - const allDocs = content.loadedVersions.flatMap((v) => v.docs); - return Object.fromEntries( - allDocs.map(({source, permalink}) => [source, permalink]), - ); - } - const sourceToPermalink = getSourceToPermalink(); - function createMDXLoader(): RuleSetUseItem { const loaderOptions: MDXLoaderOptions = { admonitions: options.admonitions, @@ -318,7 +336,7 @@ export default async function pluginContentDocs( ); const permalink = resolveMarkdownLinkPathname(linkPathname, { sourceFilePath, - sourceToPermalink, + sourceToPermalink: sourceToPermalinkHelper.get(), siteDir, contentPaths: version, }); diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 208675c72adc..c0c9227a42a4 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -19,10 +19,6 @@ export type DocFile = { content: string; }; -export type SourceToPermalink = { - [source: string]: string; -}; - export type VersionTag = TagMetadata & { /** All doc ids having this tag. */ docIds: string[]; diff --git a/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts b/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts index c9526c12dcd3..9a3e0ddfaa77 100644 --- a/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts +++ b/packages/docusaurus-utils/src/__tests__/markdownLinks.test.ts @@ -18,12 +18,14 @@ describe('resolveMarkdownLinkPathname', () => { contentPath: 'docs', contentPathLocalized: 'i18n/docs-localized', }, - sourceToPermalink: { - '@site/docs/intro.md': '/docs/intro', - '@site/docs/foo.md': '/doc/foo', - '@site/docs/bar/baz.md': '/doc/baz', - '@site/docs/http.foo.md': '/doc/http', - }, + sourceToPermalink: new Map( + Object.entries({ + '@site/docs/intro.md': '/docs/intro', + '@site/docs/foo.md': '/doc/foo', + '@site/docs/bar/baz.md': '/doc/baz', + '@site/docs/http.foo.md': '/doc/http', + }), + ), }; function test(linkPathname: string, expectedOutput: string) { @@ -50,11 +52,13 @@ describe('resolveMarkdownLinkPathname', () => { contentPathLocalized: 'i18n/docs-localized', }, - sourceToPermalink: { - '@site/docs/intro/intro.md': '/docs/intro', - '@site/docs/intro/another.md': '/docs/another', - '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', - }, + sourceToPermalink: new Map( + Object.entries({ + '@site/docs/intro/intro.md': '/docs/intro', + '@site/docs/intro/another.md': '/docs/another', + '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', + }), + ), }; function test(linkPathname: string, expectedOutput: string) { diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 3dd23137f536..7201f8eec1a8 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -77,7 +77,11 @@ export { writeMarkdownHeadingId, type WriteHeadingIDOptions, } from './markdownUtils'; -export {type ContentPaths, resolveMarkdownLinkPathname} from './markdownLinks'; +export { + type ContentPaths, + type SourceToPermalink, + resolveMarkdownLinkPathname, +} from './markdownLinks'; export {type SluggerOptions, type Slugger, createSlugger} from './slugger'; export { isNameTooLong, diff --git a/packages/docusaurus-utils/src/markdownLinks.ts b/packages/docusaurus-utils/src/markdownLinks.ts index 1b65776187f8..1183656aa182 100644 --- a/packages/docusaurus-utils/src/markdownLinks.ts +++ b/packages/docusaurus-utils/src/markdownLinks.ts @@ -40,6 +40,11 @@ export type BrokenMarkdownLink = { link: string; }; +export type SourceToPermalink = Map< + string, // Aliased source path: "@site/docs/content.mdx" + string // Permalink: "/docs/content" +>; + // Note this is historical logic extracted during a 2024 refactor // The algo has been kept exactly as before for retro compatibility // See also https://github.com/facebook/docusaurus/pull/10168 @@ -47,7 +52,7 @@ export function resolveMarkdownLinkPathname( linkPathname: string, context: { sourceFilePath: string; - sourceToPermalink: {[aliasedFilePath: string]: string}; + sourceToPermalink: SourceToPermalink; contentPaths: ContentPaths; siteDir: string; }, @@ -66,9 +71,9 @@ export function resolveMarkdownLinkPathname( const aliasedSourceMatch = sourceDirsToTry .map((sourceDir) => path.join(sourceDir, decodeURIComponent(linkPathname))) .map((source) => aliasedSitePath(source, siteDir)) - .find((source) => sourceToPermalink[source]); + .find((source) => sourceToPermalink.has(source)); return aliasedSourceMatch - ? sourceToPermalink[aliasedSourceMatch] ?? null + ? sourceToPermalink.get(aliasedSourceMatch) ?? null : null; }