fix(docs, blog): Markdown link resolution does not support hot reload (#10185)

This commit is contained in:
Sébastien Lorber 2024-05-31 17:47:36 +02:00 committed by GitHub
parent 0eb7b64aac
commit a7afd9cc87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 93 additions and 44 deletions

View file

@ -45,14 +45,6 @@ export function truncate(fileString: string, truncateMarker: RegExp): string {
return fileString.split(truncateMarker, 1).shift()!; 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({ export function paginateBlogPosts({
blogPosts, blogPosts,
basePageUrl, basePageUrl,

View file

@ -19,10 +19,10 @@ import {
getDataFilePath, getDataFilePath,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
resolveMarkdownLinkPathname, resolveMarkdownLinkPathname,
type SourceToPermalink,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation'; import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation';
import { import {
getSourceToPermalink,
getBlogTags, getBlogTags,
paginateBlogPosts, paginateBlogPosts,
shouldBeListed, shouldBeListed,
@ -50,6 +50,33 @@ import type {RuleSetUseItem} from 'webpack';
const PluginName = 'docusaurus-plugin-content-blog'; 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( export default async function pluginContentBlog(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
@ -96,6 +123,8 @@ export default async function pluginContentBlog(
contentPaths, contentPaths,
}); });
const sourceToPermalinkHelper = createSourceToPermalinkHelper();
return { return {
name: PluginName, name: PluginName,
@ -201,6 +230,8 @@ export default async function pluginContentBlog(
}, },
async contentLoaded({content, actions}) { async contentLoaded({content, actions}) {
sourceToPermalinkHelper.update(content);
await createAllRoutes({ await createAllRoutes({
baseUrl, baseUrl,
content, content,
@ -214,7 +245,7 @@ export default async function pluginContentBlog(
return translateContent(content, translationFiles); return translateContent(content, translationFiles);
}, },
configureWebpack(_config, isServer, utils, content) { configureWebpack() {
const { const {
admonitions, admonitions,
rehypePlugins, rehypePlugins,
@ -224,7 +255,6 @@ export default async function pluginContentBlog(
beforeDefaultRehypePlugins, beforeDefaultRehypePlugins,
} = options; } = options;
const sourceToPermalink = getSourceToPermalink(content.blogPosts);
const contentDirs = getContentPathList(contentPaths); const contentDirs = getContentPathList(contentPaths);
function createMDXLoader(): RuleSetUseItem { function createMDXLoader(): RuleSetUseItem {
@ -271,7 +301,7 @@ export default async function pluginContentBlog(
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => { resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
const permalink = resolveMarkdownLinkPathname(linkPathname, { const permalink = resolveMarkdownLinkPathname(linkPathname, {
sourceFilePath, sourceFilePath,
sourceToPermalink, sourceToPermalink: sourceToPermalinkHelper.get(),
siteDir, siteDir,
contentPaths, contentPaths,
}); });

View file

@ -19,6 +19,8 @@ import {
createSlugger, createSlugger,
resolveMarkdownLinkPathname, resolveMarkdownLinkPathname,
DEFAULT_PLUGIN_ID, DEFAULT_PLUGIN_ID,
type SourceToPermalink,
type TagsFile,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import { import {
getTagsFile, getTagsFile,
@ -47,7 +49,6 @@ import {
} from './translations'; } from './translations';
import {createAllRoutes} from './routes'; import {createAllRoutes} from './routes';
import {createSidebarsUtils} from './sidebars/utils'; import {createSidebarsUtils} from './sidebars/utils';
import type {TagsFile} from '@docusaurus/utils';
import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader'; import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader';
import type { import type {
@ -59,9 +60,32 @@ import type {
LoadedVersion, LoadedVersion,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import type {LoadContext, Plugin} from '@docusaurus/types'; 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'; 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( export default async function pluginContentDocs(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
@ -88,6 +112,8 @@ export default async function pluginContentDocs(
// TODO env should be injected into all plugins // TODO env should be injected into all plugins
const env = process.env.NODE_ENV as DocEnv; const env = process.env.NODE_ENV as DocEnv;
const sourceToPermalinkHelper = createSourceToPermalinkHelper();
return { return {
name: 'docusaurus-plugin-content-docs', name: 'docusaurus-plugin-content-docs',
@ -244,6 +270,8 @@ export default async function pluginContentDocs(
}, },
async contentLoaded({content, actions}) { async contentLoaded({content, actions}) {
sourceToPermalinkHelper.update(content);
const versions: FullVersion[] = content.loadedVersions.map(toFullVersion); const versions: FullVersion[] = content.loadedVersions.map(toFullVersion);
await createAllRoutes({ await createAllRoutes({
@ -274,16 +302,6 @@ export default async function pluginContentDocs(
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970 // Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator); .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 { function createMDXLoader(): RuleSetUseItem {
const loaderOptions: MDXLoaderOptions = { const loaderOptions: MDXLoaderOptions = {
admonitions: options.admonitions, admonitions: options.admonitions,
@ -318,7 +336,7 @@ export default async function pluginContentDocs(
); );
const permalink = resolveMarkdownLinkPathname(linkPathname, { const permalink = resolveMarkdownLinkPathname(linkPathname, {
sourceFilePath, sourceFilePath,
sourceToPermalink, sourceToPermalink: sourceToPermalinkHelper.get(),
siteDir, siteDir,
contentPaths: version, contentPaths: version,
}); });

View file

@ -19,10 +19,6 @@ export type DocFile = {
content: string; content: string;
}; };
export type SourceToPermalink = {
[source: string]: string;
};
export type VersionTag = TagMetadata & { export type VersionTag = TagMetadata & {
/** All doc ids having this tag. */ /** All doc ids having this tag. */
docIds: string[]; docIds: string[];

View file

@ -18,12 +18,14 @@ describe('resolveMarkdownLinkPathname', () => {
contentPath: 'docs', contentPath: 'docs',
contentPathLocalized: 'i18n/docs-localized', contentPathLocalized: 'i18n/docs-localized',
}, },
sourceToPermalink: { sourceToPermalink: new Map(
Object.entries({
'@site/docs/intro.md': '/docs/intro', '@site/docs/intro.md': '/docs/intro',
'@site/docs/foo.md': '/doc/foo', '@site/docs/foo.md': '/doc/foo',
'@site/docs/bar/baz.md': '/doc/baz', '@site/docs/bar/baz.md': '/doc/baz',
'@site/docs/http.foo.md': '/doc/http', '@site/docs/http.foo.md': '/doc/http',
}, }),
),
}; };
function test(linkPathname: string, expectedOutput: string) { function test(linkPathname: string, expectedOutput: string) {
@ -50,11 +52,13 @@ describe('resolveMarkdownLinkPathname', () => {
contentPathLocalized: 'i18n/docs-localized', contentPathLocalized: 'i18n/docs-localized',
}, },
sourceToPermalink: { sourceToPermalink: new Map(
Object.entries({
'@site/docs/intro/intro.md': '/docs/intro', '@site/docs/intro/intro.md': '/docs/intro',
'@site/docs/intro/another.md': '/docs/another', '@site/docs/intro/another.md': '/docs/another',
'@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri', '@site/docs/api/classes/divine_uri.URI.md': '/docs/api/classes/uri',
}, }),
),
}; };
function test(linkPathname: string, expectedOutput: string) { function test(linkPathname: string, expectedOutput: string) {

View file

@ -77,7 +77,11 @@ export {
writeMarkdownHeadingId, writeMarkdownHeadingId,
type WriteHeadingIDOptions, type WriteHeadingIDOptions,
} from './markdownUtils'; } 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 {type SluggerOptions, type Slugger, createSlugger} from './slugger';
export { export {
isNameTooLong, isNameTooLong,

View file

@ -40,6 +40,11 @@ export type BrokenMarkdownLink<T extends ContentPaths> = {
link: string; 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 // Note this is historical logic extracted during a 2024 refactor
// The algo has been kept exactly as before for retro compatibility // The algo has been kept exactly as before for retro compatibility
// See also https://github.com/facebook/docusaurus/pull/10168 // See also https://github.com/facebook/docusaurus/pull/10168
@ -47,7 +52,7 @@ export function resolveMarkdownLinkPathname(
linkPathname: string, linkPathname: string,
context: { context: {
sourceFilePath: string; sourceFilePath: string;
sourceToPermalink: {[aliasedFilePath: string]: string}; sourceToPermalink: SourceToPermalink;
contentPaths: ContentPaths; contentPaths: ContentPaths;
siteDir: string; siteDir: string;
}, },
@ -66,9 +71,9 @@ export function resolveMarkdownLinkPathname(
const aliasedSourceMatch = sourceDirsToTry const aliasedSourceMatch = sourceDirsToTry
.map((sourceDir) => path.join(sourceDir, decodeURIComponent(linkPathname))) .map((sourceDir) => path.join(sourceDir, decodeURIComponent(linkPathname)))
.map((source) => aliasedSitePath(source, siteDir)) .map((source) => aliasedSitePath(source, siteDir))
.find((source) => sourceToPermalink[source]); .find((source) => sourceToPermalink.has(source));
return aliasedSourceMatch return aliasedSourceMatch
? sourceToPermalink[aliasedSourceMatch] ?? null ? sourceToPermalink.get(aliasedSourceMatch) ?? null
: null; : null;
} }