Merge branch 'main' into slorber/fix-docs-category-index-translation-key-conflict

This commit is contained in:
sebastien 2025-06-26 18:10:40 +02:00
commit 13828934b4
252 changed files with 10021 additions and 8162 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-docs",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docs plugin for Docusaurus.",
"main": "lib/index.js",
"sideEffects": false,
@ -35,15 +35,15 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "3.8.0",
"@docusaurus/mdx-loader": "3.8.0",
"@docusaurus/module-type-aliases": "3.8.0",
"@docusaurus/theme-common": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-common": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/logger": "3.8.1",
"@docusaurus/mdx-loader": "3.8.1",
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/theme-common": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-common": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",

View file

@ -2,6 +2,8 @@
id: hello-2
title: Hello 2
sidebar_label: Hello 2 From Doc
sidebar_class_name: front-matter-class-name
sidebar_custom_props: {custom: "from front matter"}
---
Hello World 2!

View file

@ -8,7 +8,9 @@
{
"id": "hello-2",
"type": "doc",
"label": "Hello Two"
"label": "Hello Two",
"className": "class-name-from-sidebars.json",
"customProps": {"test": "from sidebars.json"}
}
]
}

View file

@ -8,6 +8,10 @@ exports[`sidebar site with undefined sidebar 1`] = `
"type": "doc",
},
{
"className": "front-matter-class-name",
"customProps": {
"custom": "from front matter",
},
"id": "hello-2",
"label": "Hello 2 From Doc",
"type": "doc",

View file

@ -25,7 +25,7 @@ import {
type DocEnv,
} from '../docs';
import {loadSidebars} from '../sidebars';
import {readVersionsMetadata} from '../versions';
import {readVersionsMetadata} from '../versions/version';
import {DEFAULT_OPTIONS} from '../options';
import type {Sidebars} from '../sidebars/types';
import type {DocFile} from '../types';

View file

@ -582,14 +582,16 @@ describe('site with doc label', () => {
);
});
it('sidebar_label in doc has higher precedence over label in sidebar.json', async () => {
it('frontMatter.sidebar_* data in doc has higher precedence over sidebar.json data', async () => {
const {content} = await loadSite();
const loadedVersion = content.loadedVersions[0]!;
const sidebarProps = toSidebarsProp(loadedVersion);
expect((sidebarProps.docs![1] as PropSidebarItemLink).label).toBe(
'Hello 2 From Doc',
);
const item = sidebarProps.docs![1] as PropSidebarItemLink;
expect(item.label).toBe('Hello 2 From Doc');
expect(item.className).toBe('front-matter-class-name');
expect(item.customProps).toStrictEqual({custom: 'from front matter'});
});
});

View file

@ -7,8 +7,6 @@
import path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
normalizeUrl,
docuHash,
@ -17,30 +15,19 @@ import {
posixPath,
addTrailingPathSeparator,
createAbsoluteFilePathMatcher,
createSlugger,
resolveMarkdownLinkPathname,
DEFAULT_PLUGIN_ID,
type TagsFile,
} from '@docusaurus/utils';
import {
getTagsFile,
getTagsFilePathsToWatch,
} from '@docusaurus/utils-validation';
import {getTagsFilePathsToWatch} from '@docusaurus/utils-validation';
import {createMDXLoaderRule} from '@docusaurus/mdx-loader';
import {loadSidebars, resolveSidebarPathOption} from './sidebars';
import {resolveSidebarPathOption} from './sidebars';
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
import {
readVersionDocs,
processDocMetadata,
addDocNavigation,
type DocEnv,
createDocsByIdIndex,
} from './docs';
import {type DocEnv} from './docs';
import {
getVersionFromSourceFilePath,
readVersionsMetadata,
toFullVersion,
} from './versions';
} from './versions/version';
import cliDocs from './cli';
import {VERSIONS_JSON_FILE} from './constants';
import {toGlobalDataVersion} from './globalData';
@ -49,19 +36,17 @@ import {
getLoadedContentTranslationFiles,
} from './translations';
import {createAllRoutes} from './routes';
import {createSidebarsUtils} from './sidebars/utils';
import {createContentHelpers} from './contentHelpers';
import {loadVersion} from './versions/loadVersion';
import type {
PluginOptions,
DocMetadataBase,
VersionMetadata,
DocFrontMatter,
LoadedContent,
LoadedVersion,
} from '@docusaurus/plugin-content-docs';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {DocFile, FullVersion} from './types';
import type {FullVersion} from './types';
import type {RuleSetRule} from 'webpack';
// MDX loader is not 100% deterministic, leading to cache invalidation issue
@ -172,18 +157,12 @@ export default async function pluginContentDocs(
sourceFilePath,
versionsMetadata,
);
const permalink = resolveMarkdownLinkPathname(linkPathname, {
return resolveMarkdownLinkPathname(linkPathname, {
sourceFilePath,
sourceToPermalink: contentHelpers.sourceToPermalink,
siteDir,
contentPaths: version,
});
if (permalink === null) {
logger.report(
siteConfig.onBrokenMarkdownLinks,
)`Docs markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath} for version number=${version.versionName}`;
}
return permalink;
},
},
});
@ -243,102 +222,17 @@ export default async function pluginContentDocs(
},
async loadContent() {
async function loadVersionDocsBase(
versionMetadata: VersionMetadata,
tagsFile: TagsFile | null,
): Promise<DocMetadataBase[]> {
const docFiles = await readVersionDocs(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(
`Docs version "${
versionMetadata.versionName
}" has no docs! At least one doc should exist at "${path.relative(
siteDir,
versionMetadata.contentPath,
)}".`,
);
}
function processVersionDoc(docFile: DocFile) {
return processDocMetadata({
docFile,
versionMetadata,
context,
options,
env,
tagsFile,
});
}
return Promise.all(docFiles.map(processVersionDoc));
}
async function doLoadVersion(
versionMetadata: VersionMetadata,
): Promise<LoadedVersion> {
const tagsFile = await getTagsFile({
contentPaths: versionMetadata,
tags: options.tags,
});
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
versionMetadata,
tagsFile,
);
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = _.partition(docsBase, (doc) => doc.draft);
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: createSlugger(),
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const docsById = createDocsByIdIndex(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
return {
...versionMetadata,
docs: addDocNavigation({
docs,
sidebarsUtils,
}),
drafts,
sidebars,
};
}
async function loadVersion(versionMetadata: VersionMetadata) {
try {
return await doLoadVersion(versionMetadata);
} catch (err) {
logger.error`Loading of version failed for version name=${versionMetadata.versionName}`;
throw err;
}
}
return {
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
loadedVersions: await Promise.all(
versionsMetadata.map((versionMetadata) =>
loadVersion({
context,
options,
env,
versionMetadata,
}),
),
),
};
},

View file

@ -38,22 +38,14 @@ export function toSidebarDocItemLinkProp({
'id' | 'title' | 'permalink' | 'unlisted' | 'frontMatter'
>;
}): PropSidebarItemLink {
const {
id,
title,
permalink,
frontMatter: {
sidebar_label: sidebarLabel,
sidebar_custom_props: customProps,
},
unlisted,
} = doc;
const {id, title, permalink, frontMatter, unlisted} = doc;
return {
type: 'link',
label: sidebarLabel ?? item.label ?? title,
href: permalink,
className: item.className,
customProps: item.customProps ?? customProps,
// Front Matter data takes precedence over sidebars.json
label: frontMatter.sidebar_label ?? item.label ?? title,
className: frontMatter.sidebar_class_name ?? item.className,
customProps: frontMatter.sidebar_custom_props ?? item.customProps,
docId: id,
unlisted,
};

View file

@ -22,5 +22,5 @@ export {
getDefaultVersionBanner,
getVersionBadge,
getVersionBanner,
} from './versions';
} from './versions/version';
export {readVersionNames} from './versions/files';

View file

@ -76,6 +76,10 @@ exports[`postProcess transforms category without subitems 1`] = `
{
"sidebar": [
{
"className": "category-className",
"customProps": {
"custom": true,
},
"id": "doc ID",
"label": "Category 2",
"type": "doc",

View file

@ -31,6 +31,8 @@ describe('postProcess', () => {
type: 'doc',
id: 'doc ID',
},
className: 'category-className',
customProps: {custom: true},
items: [],
},
],

View file

@ -77,10 +77,13 @@ function postProcessSidebarItem(
) {
return null;
}
const {label, className, customProps} = category;
return {
type: 'doc',
label: category.label,
id: category.link.id,
label,
...(className && {className}),
...(customProps && {customProps}),
};
}
// A non-collapsible category can't be collapsed!

View file

@ -0,0 +1,7 @@
---
# no id but should conflict due to the name anyway
---
# Hello
World

View file

@ -0,0 +1,3 @@
[
"with-id-conflicts"
]

View file

@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loadVersion minimal site can load current version 1`] = `
{
"badge": false,
"banner": null,
"className": "docs-version-current",
"contentPath": "<PROJECT_ROOT>/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-minimal/docs",
"contentPathLocalized": "<PROJECT_ROOT>/packages/docusaurus-plugin-content-docs/src/versions/__tests__/__fixtures__/site-minimal/i18n/en/docusaurus-plugin-content-docs/current",
"docs": [
{
"description": "World",
"draft": false,
"editUrl": undefined,
"frontMatter": {},
"id": "hello",
"lastUpdatedAt": undefined,
"lastUpdatedBy": undefined,
"next": undefined,
"permalink": "/docs/hello",
"previous": undefined,
"sidebar": "defaultSidebar",
"sidebarPosition": undefined,
"slug": "/hello",
"source": "@site/docs/hello.md",
"sourceDirName": ".",
"tags": [],
"title": "Hello",
"unlisted": false,
"version": "current",
},
],
"drafts": [],
"editUrl": undefined,
"editUrlLocalized": undefined,
"isLast": true,
"label": "Next",
"noIndex": false,
"path": "/docs",
"routePriority": -1,
"sidebarFilePath": undefined,
"sidebars": {
"defaultSidebar": [
{
"id": "hello",
"type": "doc",
},
],
},
"tagsPath": "/docs/tags",
"versionName": "current",
}
`;

View file

@ -0,0 +1,117 @@
/**
* 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.
*/
import path from 'path';
import {fromPartial} from '@total-typescript/shoehorn';
import {DEFAULT_PARSE_FRONT_MATTER} from '@docusaurus/utils/src';
import {readVersionsMetadata} from '../version';
import {DEFAULT_OPTIONS} from '../../options';
import {loadVersion} from '../loadVersion';
import type {I18n, LoadContext} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
const DefaultI18N: I18n = {
path: 'i18n',
currentLocale: 'en',
locales: ['en'],
defaultLocale: 'en',
localeConfigs: {},
};
async function siteFixture(fixture: string) {
const siteDir = path.resolve(path.join(__dirname, './__fixtures__', fixture));
const options: PluginOptions = fromPartial<PluginOptions>({
id: 'default',
...DEFAULT_OPTIONS,
});
const context = fromPartial<LoadContext>({
siteDir,
baseUrl: '/',
i18n: DefaultI18N,
localizationDir: path.join(siteDir, 'i18n/en'),
siteConfig: {
markdown: {
parseFrontMatter: DEFAULT_PARSE_FRONT_MATTER,
},
},
});
const versions = await readVersionsMetadata({
options,
context,
});
return {
siteDir,
options,
context,
versions,
};
}
describe('loadVersion', () => {
describe('minimal site', () => {
it('can load current version', async () => {
const {options, context, versions} = await siteFixture('site-minimal');
const version = versions[0];
expect(version).toBeDefined();
expect(version.versionName).toBe('current');
const loadedVersion = loadVersion({
context,
options,
versionMetadata: version,
env: 'production',
});
await expect(loadedVersion).resolves.toMatchSnapshot();
});
});
describe('site with broken versions', () => {
async function loadTestVersion(versionName: string) {
const {options, context, versions} = await siteFixture(
'site-broken-versions',
);
const version = versions.find((v) => v.versionName === versionName);
if (!version) {
throw new Error(`Version '${versionName}' should exist`);
}
return loadVersion({
context,
options,
versionMetadata: version,
env: 'production',
});
}
it('rejects version with doc id conflict', async () => {
await expect(() => loadTestVersion('with-id-conflicts')).rejects
.toThrowErrorMatchingInlineSnapshot(`
"The docs plugin found docs sharing the same id:
- \`frontMatter/doc\` found in 3 docs:
- versioned_docs/version-with-id-conflicts/frontMatter/doc.md
- versioned_docs/version-with-id-conflicts/frontMatter/doc1.md
- versioned_docs/version-with-id-conflicts/frontMatter/doc2.md
- \`number-prefix/doc\` found in 2 docs:
- versioned_docs/version-with-id-conflicts/number-prefix/1-doc.md
- versioned_docs/version-with-id-conflicts/number-prefix/2-doc.md
- \`number-prefix/deeply/nested/doc\` found in 2 docs:
- versioned_docs/version-with-id-conflicts/number-prefix/deeply/nested/2-doc.md
- versioned_docs/version-with-id-conflicts/number-prefix/deeply/nested/3-doc.md
Docs should have distinct ids.
In case of conflict, you can rename the docs file, or use the \`id\` front matter to assign an explicit distinct id to each doc.
"
`);
});
});
});

View file

@ -8,7 +8,7 @@
import {jest} from '@jest/globals';
import path from 'path';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {readVersionsMetadata} from '../index';
import {readVersionsMetadata} from '../version';
import {DEFAULT_OPTIONS} from '../../options';
import type {I18n, LoadContext} from '@docusaurus/types';
import type {

View file

@ -19,7 +19,7 @@ import type {
PluginOptions,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {VersionContext} from './index';
import type {VersionContext} from './version';
/** Add a prefix like `community_version-1.0.0`. No-op for default instance. */
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {

View file

@ -0,0 +1,179 @@
/**
* 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.
*/
import path from 'path';
import _ from 'lodash';
import {aliasedSitePathToRelativePath, createSlugger} from '@docusaurus/utils';
import {getTagsFile} from '@docusaurus/utils-validation';
import logger from '@docusaurus/logger';
import {
addDocNavigation,
createDocsByIdIndex,
type DocEnv,
processDocMetadata,
readVersionDocs,
} from '../docs';
import {loadSidebars} from '../sidebars';
import {createSidebarsUtils} from '../sidebars/utils';
import type {TagsFile} from '@docusaurus/utils';
import type {
DocMetadataBase,
LoadedVersion,
PluginOptions,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {DocFile} from '../types';
import type {LoadContext} from '@docusaurus/types';
type LoadVersionParams = {
context: LoadContext;
options: PluginOptions;
versionMetadata: VersionMetadata;
env: DocEnv;
};
function ensureNoDuplicateDocId(docs: DocMetadataBase[]): void {
const duplicatesById = _.chain(docs)
.groupBy((d) => d.id)
.pickBy((group) => group.length > 1)
.value();
const duplicateIdEntries = Object.entries(duplicatesById);
if (duplicateIdEntries.length) {
const idMessages = duplicateIdEntries
.map(([id, duplicateDocs]) => {
return logger.interpolate`- code=${id} found in number=${
duplicateDocs.length
} docs:
- ${duplicateDocs
.map((d) => aliasedSitePathToRelativePath(d.source))
.join('\n - ')}`;
})
.join('\n\n');
const message = `The docs plugin found docs sharing the same id:
\n${idMessages}\n
Docs should have distinct ids.
In case of conflict, you can rename the docs file, or use the ${logger.code(
'id',
)} front matter to assign an explicit distinct id to each doc.
`;
throw new Error(message);
}
}
async function loadVersionDocsBase({
tagsFile,
context,
options,
versionMetadata,
env,
}: LoadVersionParams & {
tagsFile: TagsFile | null;
}): Promise<DocMetadataBase[]> {
const docFiles = await readVersionDocs(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(
`Docs version "${
versionMetadata.versionName
}" has no docs! At least one doc should exist at "${path.relative(
context.siteDir,
versionMetadata.contentPath,
)}".`,
);
}
function processVersionDoc(docFile: DocFile) {
return processDocMetadata({
docFile,
versionMetadata,
context,
options,
env,
tagsFile,
});
}
const docs = await Promise.all(docFiles.map(processVersionDoc));
ensureNoDuplicateDocId(docs);
return docs;
}
async function doLoadVersion({
context,
options,
versionMetadata,
env,
}: LoadVersionParams): Promise<LoadedVersion> {
const tagsFile = await getTagsFile({
contentPaths: versionMetadata,
tags: options.tags,
});
const docsBase: DocMetadataBase[] = await loadVersionDocsBase({
tagsFile,
context,
options,
versionMetadata,
env,
});
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = _.partition(docsBase, (doc) => doc.draft);
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: createSlugger(),
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const docsById = createDocsByIdIndex(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
return {
...versionMetadata,
docs: addDocNavigation({
docs,
sidebarsUtils,
}),
drafts,
sidebars,
};
}
export async function loadVersion(
params: LoadVersionParams,
): Promise<LoadedVersion> {
try {
return await doLoadVersion(params);
} catch (err) {
// TODO use error cause (but need to refactor many tests)
logger.error`Loading of version failed for version name=${params.versionMetadata.versionName}`;
throw err;
}
}

View file

@ -243,7 +243,7 @@ export async function readVersionsMetadata({
validateVersionsOptions(allVersionNames, options);
const versionNames = filterVersions(allVersionNames, options);
const lastVersionName = getLastVersionName({versionNames, options});
const versionsMetadata = await Promise.all(
return Promise.all(
versionNames.map((versionName) =>
createVersionMetadata({
versionName,
@ -254,7 +254,6 @@ export async function readVersionsMetadata({
}),
),
);
return versionsMetadata;
}
export function toFullVersion(version: LoadedVersion): FullVersion {