From 044b0a5091b088bd127e4f1f4b186fb41e341cf7 Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Thu, 21 May 2026 05:01:57 +0500 Subject: [PATCH] fix(docs): resolve metadata paths with zero-width spaces --- .../src/contentHelpers.ts | 32 +++++++++++-- .../src/index.ts | 3 +- packages/docusaurus-utils/src/index.ts | 1 + .../docusaurus-utils/src/metadataUtils.ts | 47 +++++++++++++++++++ .../end\342\200\213/index.mdx" | 1 + .../mi\342\200\213ddle/index.mdx" | 1 + .../\342\200\213start/index.mdx" | 1 + 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 packages/docusaurus-utils/src/metadataUtils.ts create mode 100644 "website/_dogfooding/_docs tests/tests/zero-width-spaces/end\342\200\213/index.mdx" create mode 100644 "website/_dogfooding/_docs tests/tests/zero-width-spaces/mi\342\200\213ddle/index.mdx" create mode 100644 "website/_dogfooding/_docs tests/tests/zero-width-spaces/\342\200\213start/index.mdx" diff --git a/packages/docusaurus-plugin-content-docs/src/contentHelpers.ts b/packages/docusaurus-plugin-content-docs/src/contentHelpers.ts index 75eddef4cc31..8a4bfbd03dc3 100644 --- a/packages/docusaurus-plugin-content-docs/src/contentHelpers.ts +++ b/packages/docusaurus-plugin-content-docs/src/contentHelpers.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {createMetadataSourceResolver} from '@docusaurus/utils'; import type {DocMetadata, LoadedContent} from '@docusaurus/plugin-content-docs'; function indexDocsBySource(content: LoadedContent): Map { @@ -12,23 +13,48 @@ function indexDocsBySource(content: LoadedContent): Map { return new Map(allDocs.map((doc) => [doc.source, doc])); } +type ContentHelpers = { + updateContent: (content: LoadedContent) => void; + sourceToDoc: Map; + sourceToPermalink: Map; + getMetadataSource: (source: string) => string; +}; + // TODO this is bad, we should have a better way to do this (new lifecycle?) // The source to doc/permalink is a mutable map passed to the mdx loader // See https://github.com/facebook/docusaurus/pull/10457 // See https://github.com/facebook/docusaurus/pull/10185 -export function createContentHelpers() { +export function createContentHelpers(): ContentHelpers { const sourceToDoc = new Map(); const sourceToPermalink = new Map(); + let metadataSourceResolver = createMetadataSourceResolver({ + sources: sourceToDoc.keys(), + createAmbiguousSourceError: createAmbiguousMetadataSourceError, + }); // Mutable map update :/ function updateContent(content: LoadedContent): void { sourceToDoc.clear(); sourceToPermalink.clear(); - indexDocsBySource(content).forEach((value, key) => { + const docsBySource = indexDocsBySource(content); + docsBySource.forEach((value, key) => { sourceToDoc.set(key, value); sourceToPermalink.set(key, value.permalink); }); + metadataSourceResolver = createMetadataSourceResolver({ + sources: docsBySource.keys(), + createAmbiguousSourceError: createAmbiguousMetadataSourceError, + }); + } + + function createAmbiguousMetadataSourceError(source: string): Error { + return new Error(`Docusaurus could not safely resolve the docs metadata path for "${source}" because multiple docs paths only differ by U+200B ZERO WIDTH SPACE characters. +Please rename the affected docs files or folders to remove the invisible zero-width spaces.`); + } + + function getMetadataSource(source: string): string { + return metadataSourceResolver(source); } - return {updateContent, sourceToDoc, sourceToPermalink}; + return {updateContent, sourceToDoc, sourceToPermalink, getMetadataSource}; } diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 90975eff9860..9c952e816736 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -150,7 +150,8 @@ export default async function pluginContentDocs( // Note that metadataPath must be the same/in-sync as // the path from createData for each MDX. const aliasedPath = aliasedSitePath(mdxPath, siteDir); - return path.join(dataDir, `${docuHash(aliasedPath)}.json`); + const metadataSource = contentHelpers.getMetadataSource(aliasedPath); + return path.join(dataDir, `${docuHash(metadataSource)}.json`); }, // createAssets converts relative paths to require() calls createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({ diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 815b4bbc9070..0fafbd876589 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -99,6 +99,7 @@ export { addTrailingPathSeparator, } from './pathUtils'; export {md5Hash, simpleHash, docuHash} from './hashUtils'; +export {createMetadataSourceResolver} from './metadataUtils'; export { Globby, GlobExcludeDefault, diff --git a/packages/docusaurus-utils/src/metadataUtils.ts b/packages/docusaurus-utils/src/metadataUtils.ts new file mode 100644 index 000000000000..75810d0e611f --- /dev/null +++ b/packages/docusaurus-utils/src/metadataUtils.ts @@ -0,0 +1,47 @@ +/** + * 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. + */ + +const ZeroWidthSpace = '\u200B'; + +function getMetadataSourceKey(source: string): string { + return source.replaceAll(ZeroWidthSpace, ''); +} + +export function createMetadataSourceResolver({ + sources, + createAmbiguousSourceError, +}: { + sources: Iterable; + createAmbiguousSourceError?: (source: string) => Error; +}): (source: string) => string { + const sourceKeyToSource = new Map(); + const ambiguousSourceKeys = new Set(); + + for (const source of sources) { + const sourceKey = getMetadataSourceKey(source); + const existingSource = sourceKeyToSource.get(sourceKey); + if (existingSource && existingSource !== source) { + ambiguousSourceKeys.add(sourceKey); + sourceKeyToSource.delete(sourceKey); + } else if (!ambiguousSourceKeys.has(sourceKey)) { + sourceKeyToSource.set(sourceKey, source); + } + } + + return (source) => { + const sourceKey = getMetadataSourceKey(source); + if (ambiguousSourceKeys.has(sourceKey)) { + throw ( + createAmbiguousSourceError?.(source) ?? + new Error( + `Docusaurus could not safely resolve the metadata path for "${source}" because multiple source paths only differ by U+200B ZERO WIDTH SPACE characters.`, + ) + ); + } + return sourceKeyToSource.get(sourceKey) ?? source; + }; +} diff --git "a/website/_dogfooding/_docs tests/tests/zero-width-spaces/end\342\200\213/index.mdx" "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/end\342\200\213/index.mdx" new file mode 100644 index 000000000000..388d30736382 --- /dev/null +++ "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/end\342\200\213/index.mdx" @@ -0,0 +1 @@ +See https://github.com/facebook/docusaurus/issues/12005 diff --git "a/website/_dogfooding/_docs tests/tests/zero-width-spaces/mi\342\200\213ddle/index.mdx" "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/mi\342\200\213ddle/index.mdx" new file mode 100644 index 000000000000..388d30736382 --- /dev/null +++ "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/mi\342\200\213ddle/index.mdx" @@ -0,0 +1 @@ +See https://github.com/facebook/docusaurus/issues/12005 diff --git "a/website/_dogfooding/_docs tests/tests/zero-width-spaces/\342\200\213start/index.mdx" "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/\342\200\213start/index.mdx" new file mode 100644 index 000000000000..388d30736382 --- /dev/null +++ "b/website/_dogfooding/_docs tests/tests/zero-width-spaces/\342\200\213start/index.mdx" @@ -0,0 +1 @@ +See https://github.com/facebook/docusaurus/issues/12005