mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-27 23:17:56 +02:00
Merge branch 'main' into ozaki/execa
This commit is contained in:
commit
5fa32e3950
1179 changed files with 91436 additions and 15880 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@docusaurus/plugin-content-docs",
|
||||
"version": "3.4.0",
|
||||
"version": "3.7.0",
|
||||
"description": "Docs plugin for Docusaurus.",
|
||||
"main": "lib/index.js",
|
||||
"sideEffects": false,
|
||||
|
@ -35,20 +35,21 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.4.0",
|
||||
"@docusaurus/logger": "3.4.0",
|
||||
"@docusaurus/mdx-loader": "3.4.0",
|
||||
"@docusaurus/module-type-aliases": "3.4.0",
|
||||
"@docusaurus/theme-common": "3.4.0",
|
||||
"@docusaurus/types": "3.4.0",
|
||||
"@docusaurus/utils": "3.4.0",
|
||||
"@docusaurus/utils-common": "3.4.0",
|
||||
"@docusaurus/utils-validation": "3.4.0",
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/logger": "3.7.0",
|
||||
"@docusaurus/mdx-loader": "3.7.0",
|
||||
"@docusaurus/module-type-aliases": "3.7.0",
|
||||
"@docusaurus/theme-common": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/utils": "3.7.0",
|
||||
"@docusaurus/utils-common": "3.7.0",
|
||||
"@docusaurus/utils-validation": "3.7.0",
|
||||
"@types/react-router-config": "^5.0.7",
|
||||
"combine-promises": "^1.1.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"schema-dts": "^1.1.2",
|
||||
"tslib": "^2.6.0",
|
||||
"utility-types": "^3.10.0",
|
||||
"webpack": "^5.88.1"
|
||||
|
@ -60,8 +61,8 @@
|
|||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"some.key": "some translation"
|
||||
}
|
Binary file not shown.
|
@ -13,16 +13,20 @@ import {isMatch} from 'picomatch';
|
|||
import commander from 'commander';
|
||||
import webpack from 'webpack';
|
||||
import {loadContext} from '@docusaurus/core/src/server/site';
|
||||
import {applyConfigureWebpack} from '@docusaurus/core/src/webpack/configure';
|
||||
import {
|
||||
applyConfigureWebpack,
|
||||
createConfigureWebpackUtils,
|
||||
} from '@docusaurus/core/src/webpack/configure';
|
||||
import {sortRoutes} from '@docusaurus/core/src/server/plugins/routeConfig';
|
||||
import {posixPath} from '@docusaurus/utils';
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
|
||||
import {fromPartial} from '@total-typescript/shoehorn';
|
||||
import pluginContentDocs from '../index';
|
||||
import {toSidebarsProp} from '../props';
|
||||
import {DefaultSidebarItemsGenerator} from '../sidebars/generator';
|
||||
import {DisabledSidebars} from '../sidebars';
|
||||
import * as cliDocs from '../cli';
|
||||
import cliDocs from '../cli';
|
||||
import {validateOptions} from '../options';
|
||||
|
||||
import type {RouteConfig, Validate, Plugin} from '@docusaurus/types';
|
||||
|
@ -273,19 +277,26 @@ describe('simple website', () => {
|
|||
|
||||
const content = await plugin.loadContent?.();
|
||||
|
||||
const config = applyConfigureWebpack(
|
||||
plugin.configureWebpack as NonNullable<Plugin['configureWebpack']>,
|
||||
{
|
||||
const config = applyConfigureWebpack({
|
||||
configureWebpack: plugin.configureWebpack as NonNullable<
|
||||
Plugin['configureWebpack']
|
||||
>,
|
||||
config: {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
filename: 'main.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
},
|
||||
false,
|
||||
undefined,
|
||||
isServer: false,
|
||||
configureWebpackUtils: await createConfigureWebpackUtils({
|
||||
siteConfig: {
|
||||
webpack: {jsLoader: 'babel'},
|
||||
future: {experimental_faster: fromPartial({})},
|
||||
},
|
||||
}),
|
||||
content,
|
||||
);
|
||||
});
|
||||
const errors = webpack.validate(config);
|
||||
expect(errors).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
getVersionDocsDirPath,
|
||||
getVersionSidebarsPath,
|
||||
getDocsDirPathLocalized,
|
||||
getPluginDirPathLocalized,
|
||||
readVersionsFile,
|
||||
} from './versions/files';
|
||||
import {validateVersionName} from './versions/validation';
|
||||
|
@ -53,7 +54,7 @@ async function createVersionedSidebarFile({
|
|||
}
|
||||
|
||||
// Tests depend on non-default export for mocking.
|
||||
export async function cliDocsVersionCommand(
|
||||
async function cliDocsVersionCommand(
|
||||
version: unknown,
|
||||
{id: pluginId, path: docsPath, sidebarPath}: PluginOptions,
|
||||
{siteDir, i18n}: LoadContext,
|
||||
|
@ -123,6 +124,23 @@ export async function cliDocsVersionCommand(
|
|||
versionName: version,
|
||||
});
|
||||
await fs.copy(docsDir, newVersionDir);
|
||||
|
||||
// Copy version JSON translation file for this locale
|
||||
// i18n/<l>/docusaurus-plugin-content-docs/current.json => version-v1.json
|
||||
// See https://docusaurus.io/docs/next/api/plugins/@docusaurus/plugin-content-docs#translation-files-location
|
||||
if (locale !== i18n.defaultLocale) {
|
||||
const dir = getPluginDirPathLocalized({
|
||||
localizationDir,
|
||||
pluginId,
|
||||
});
|
||||
const sourceFile = path.join(dir, 'current.json');
|
||||
const dest = path.join(dir, `version-${version}.json`);
|
||||
if (await fs.pathExists(sourceFile)) {
|
||||
await fs.copy(sourceFile, dest);
|
||||
} else {
|
||||
logger.warn`${pluginIdLogPrefix}: i18n translation file does not exist in path=${sourceFile}. Skipping.`;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -142,3 +160,17 @@ export async function cliDocsVersionCommand(
|
|||
|
||||
logger.success`name=${pluginIdLogPrefix}: version name=${version} created!`;
|
||||
}
|
||||
|
||||
// TODO try to remove this workaround
|
||||
// Why use a default export instead of named exports here?
|
||||
// This is only to make Jest mocking happy
|
||||
// After upgrading Jest/SWC we got this weird mocking error in extendCli tests
|
||||
// "spyOn: Cannot redefine property cliDocsVersionCommand"
|
||||
// I tried various workarounds, and it's the only one that worked :/
|
||||
// See also:
|
||||
// - https://pyk.sh/fixing-typeerror-cannot-redefine-property-x-error-in-jest-tests#heading-solution-2-using-barrel-imports
|
||||
// - https://github.com/aelbore/esbuild-jest/issues/26
|
||||
// - https://stackoverflow.com/questions/67872622/jest-spyon-not-working-on-index-file-cannot-redefine-property/69951703#69951703
|
||||
export default {
|
||||
cliDocsVersionCommand,
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
useDocById,
|
||||
findSidebarCategory,
|
||||
useCurrentSidebarCategory,
|
||||
useCurrentSidebarSiblings,
|
||||
useSidebarBreadcrumbs,
|
||||
isVisibleSidebarItem,
|
||||
} from '../docsUtils';
|
||||
|
@ -440,6 +441,7 @@ describe('isVisibleSidebarItem', () => {
|
|||
it('works with category', () => {
|
||||
const subCategoryAllUnlisted = testCategory({
|
||||
href: '/sub-category-path',
|
||||
linkUnlisted: true,
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
|
@ -455,6 +457,7 @@ describe('isVisibleSidebarItem', () => {
|
|||
},
|
||||
testCategory({
|
||||
href: '/sub-sub-category-path',
|
||||
linkUnlisted: true,
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
|
@ -500,6 +503,22 @@ describe('isVisibleSidebarItem', () => {
|
|||
expect(
|
||||
isVisibleSidebarItem(categorySomeUnlisted, categorySomeUnlisted.href!),
|
||||
).toBe(true);
|
||||
|
||||
const categoryOnlyIndexListed = testCategory({
|
||||
href: '/category-only-index-listed',
|
||||
items: [
|
||||
{
|
||||
type: 'link',
|
||||
href: '/sub-link-path',
|
||||
label: 'Label',
|
||||
unlisted: true,
|
||||
},
|
||||
subCategoryAllUnlisted,
|
||||
],
|
||||
});
|
||||
expect(
|
||||
isVisibleSidebarItem(categoryOnlyIndexListed, '/nonexistentPath'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -762,3 +781,128 @@ describe('useCurrentSidebarCategory', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCurrentSidebarSiblings', () => {
|
||||
const createUseCurrentSidebarSiblingsMock =
|
||||
(sidebar?: PropSidebar) => (location: string) =>
|
||||
renderHook(() => useCurrentSidebarSiblings(), {
|
||||
wrapper: ({children}) => (
|
||||
<DocsSidebarProvider name="sidebarName" items={sidebar}>
|
||||
<StaticRouter location={location}>{children}</StaticRouter>
|
||||
</DocsSidebarProvider>
|
||||
),
|
||||
}).result.current;
|
||||
|
||||
it('works for sidebar category', () => {
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat',
|
||||
items: [testLink(), testLink()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/cat')).toEqual(category.items);
|
||||
});
|
||||
|
||||
it('works for sidebar root', () => {
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat',
|
||||
items: [testLink(), testLink()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink({href: '/rootLink'}),
|
||||
testLink(),
|
||||
category,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/rootLink')).toEqual(sidebar);
|
||||
});
|
||||
|
||||
it('works for nested sidebar category', () => {
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
items: [testLink(), testCategory()],
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category1,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/cat2')).toEqual(category2.items);
|
||||
});
|
||||
|
||||
it('works for category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const category: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(
|
||||
category.items,
|
||||
);
|
||||
});
|
||||
|
||||
it('works for nested category link item', () => {
|
||||
const link = testLink({href: '/my/link/path'});
|
||||
const category2: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat2',
|
||||
items: [testLink(), testLink(), link, testCategory()],
|
||||
});
|
||||
const category1: PropSidebarItemCategory = testCategory({
|
||||
href: '/cat1',
|
||||
items: [testLink(), testLink(), category2, testCategory()],
|
||||
});
|
||||
const sidebar: PropSidebar = [
|
||||
testLink(),
|
||||
testLink(),
|
||||
category1,
|
||||
testCategory(),
|
||||
];
|
||||
|
||||
const mockUseCurrentSidebarCategory =
|
||||
createUseCurrentSidebarSiblingsMock(sidebar);
|
||||
|
||||
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(
|
||||
category2.items,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sidebar is missing', () => {
|
||||
const mockUseCurrentSidebarCategory = createUseCurrentSidebarSiblingsMock();
|
||||
expect(() =>
|
||||
mockUseCurrentSidebarCategory('/cat'),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected: cant find current sidebar in context"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ export function DocProvider({
|
|||
}: {
|
||||
children: ReactNode;
|
||||
content: PropDocContent;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
const contextValue = useContextValue(content);
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export function DocSidebarItemsExpandedStateProvider({
|
|||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
const [expandedItem, setExpandedItem] = useState<number | null>(null);
|
||||
const contextValue = useMemo(
|
||||
() => ({expandedItem, setExpandedItem}),
|
||||
|
|
|
@ -163,7 +163,7 @@ function DocsPreferredVersionContextProviderUnsafe({
|
|||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
const value = useContextValue();
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ export function DocsPreferredVersionContextProvider({
|
|||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
return (
|
||||
<DocsPreferredVersionContextProviderUnsafe>
|
||||
{children}
|
||||
|
|
|
@ -30,7 +30,7 @@ export function DocsSidebarProvider({
|
|||
children: ReactNode;
|
||||
name: string | undefined;
|
||||
items: PropSidebar | undefined;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
const stableValue: ContextValue | null = useMemo(
|
||||
() => (name && items ? {name, items} : null),
|
||||
[name, items],
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {useMemo} from 'react';
|
||||
import {type ReactNode, useMemo} from 'react';
|
||||
import {matchPath, useLocation} from '@docusaurus/router';
|
||||
import renderRoutes from '@docusaurus/renderRoutes';
|
||||
import {
|
||||
|
@ -132,6 +132,25 @@ export function useCurrentSidebarCategory(): PropSidebarItemCategory {
|
|||
return deepestCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category associated with the current location. Should only be used
|
||||
* on category index pages.
|
||||
*/
|
||||
export function useCurrentSidebarSiblings(): PropSidebarItem[] {
|
||||
const {pathname} = useLocation();
|
||||
const sidebar = useDocsSidebar();
|
||||
if (!sidebar) {
|
||||
throw new Error('Unexpected: cant find current sidebar in context');
|
||||
}
|
||||
const categoryBreadcrumbs = getSidebarBreadcrumbs({
|
||||
sidebarItems: sidebar.items,
|
||||
pathname,
|
||||
onlyCategories: true,
|
||||
});
|
||||
const deepestCategory = categoryBreadcrumbs.slice(-1)[0];
|
||||
return deepestCategory?.items ?? sidebar.items;
|
||||
}
|
||||
|
||||
const isActive = (testedPath: string | undefined, activePath: string) =>
|
||||
typeof testedPath !== 'undefined' && isSamePath(testedPath, activePath);
|
||||
const containsActiveSidebarItem = (
|
||||
|
@ -168,6 +187,7 @@ export function isVisibleSidebarItem(
|
|||
case 'category':
|
||||
return (
|
||||
isActiveSidebarItem(item, activePath) ||
|
||||
(typeof item.href !== 'undefined' && !item.linkUnlisted) ||
|
||||
item.items.some((subItem) => isVisibleSidebarItem(subItem, activePath))
|
||||
);
|
||||
case 'link':
|
||||
|
@ -363,7 +383,7 @@ Available doc ids are:
|
|||
*/
|
||||
export function useDocRootMetadata({route}: DocRootProps): null | {
|
||||
/** The element that should be rendered at the current location. */
|
||||
docElement: JSX.Element;
|
||||
docElement: ReactNode;
|
||||
/**
|
||||
* The name of the sidebar associated with the current doc. `sidebarName` and
|
||||
* `sidebarItems` correspond to the value of {@link useDocsSidebar}.
|
||||
|
|
|
@ -20,7 +20,7 @@ export function DocsVersionProvider({
|
|||
}: {
|
||||
children: ReactNode;
|
||||
version: PropVersionMetadata | null;
|
||||
}): JSX.Element {
|
||||
}): ReactNode {
|
||||
return <Context.Provider value={version}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ export {
|
|||
useLayoutDocsSidebar,
|
||||
useDocRootMetadata,
|
||||
useCurrentSidebarCategory,
|
||||
useCurrentSidebarSiblings,
|
||||
filterDocCardListItems,
|
||||
} from './docsUtils';
|
||||
|
||||
|
@ -59,6 +60,8 @@ export {
|
|||
getDocsVersionSearchTag,
|
||||
} from './docsSearch';
|
||||
|
||||
export {useBreadcrumbsStructuredData} from './structuredDataUtils';
|
||||
|
||||
export type ActivePlugin = {
|
||||
pluginId: string;
|
||||
pluginData: GlobalPluginData;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
|
||||
import type {WithContext, BreadcrumbList} from 'schema-dts';
|
||||
|
||||
export function useBreadcrumbsStructuredData({
|
||||
breadcrumbs,
|
||||
}: {
|
||||
breadcrumbs: PropSidebarBreadcrumbsItem[];
|
||||
}): WithContext<BreadcrumbList> {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: breadcrumbs
|
||||
// We filter breadcrumb items without links, they are not allowed
|
||||
// See also https://github.com/facebook/docusaurus/issues/9319#issuecomment-2643560845
|
||||
.filter((breadcrumb) => breadcrumb.href)
|
||||
.map((breadcrumb, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: breadcrumb.label,
|
||||
item: `${siteConfig.url}${breadcrumb.href}`,
|
||||
})),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 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 type {DocMetadata, LoadedContent} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
function indexDocsBySource(content: LoadedContent): Map<string, DocMetadata> {
|
||||
const allDocs = content.loadedVersions.flatMap((v) => v.docs);
|
||||
return new Map(allDocs.map((doc) => [doc.source, doc]));
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const sourceToDoc = new Map<string, DocMetadata>();
|
||||
const sourceToPermalink = new Map<string, string>();
|
||||
|
||||
// Mutable map update :/
|
||||
function updateContent(content: LoadedContent): void {
|
||||
sourceToDoc.clear();
|
||||
sourceToPermalink.clear();
|
||||
indexDocsBySource(content).forEach((value, key) => {
|
||||
sourceToDoc.set(key, value);
|
||||
sourceToPermalink.set(key, value.permalink);
|
||||
});
|
||||
}
|
||||
|
||||
return {updateContent, sourceToDoc, sourceToPermalink};
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {
|
||||
|
@ -19,13 +20,13 @@ import {
|
|||
createSlugger,
|
||||
resolveMarkdownLinkPathname,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
type SourceToPermalink,
|
||||
type TagsFile,
|
||||
} from '@docusaurus/utils';
|
||||
import {
|
||||
getTagsFile,
|
||||
getTagsFilePathsToWatch,
|
||||
} from '@docusaurus/utils-validation';
|
||||
import {createMDXLoaderRule} from '@docusaurus/mdx-loader';
|
||||
import {loadSidebars, resolveSidebarPathOption} from './sidebars';
|
||||
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
|
||||
import {
|
||||
|
@ -40,7 +41,7 @@ import {
|
|||
readVersionsMetadata,
|
||||
toFullVersion,
|
||||
} from './versions';
|
||||
import {cliDocsVersionCommand} from './cli';
|
||||
import cliDocs from './cli';
|
||||
import {VERSIONS_JSON_FILE} from './constants';
|
||||
import {toGlobalDataVersion} from './globalData';
|
||||
import {
|
||||
|
@ -49,8 +50,8 @@ import {
|
|||
} from './translations';
|
||||
import {createAllRoutes} from './routes';
|
||||
import {createSidebarsUtils} from './sidebars/utils';
|
||||
import type {Options as MDXLoaderOptions} from '@docusaurus/mdx-loader';
|
||||
|
||||
import {createContentHelpers} from './contentHelpers';
|
||||
import type {
|
||||
PluginOptions,
|
||||
DocMetadataBase,
|
||||
|
@ -61,29 +62,37 @@ import type {
|
|||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {DocFile, FullVersion} from './types';
|
||||
import type {RuleSetUseItem} from 'webpack';
|
||||
import type {RuleSetRule} 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]));
|
||||
// MDX loader is not 100% deterministic, leading to cache invalidation issue
|
||||
// This permits to invalidate the MDX loader cache entries when content changes
|
||||
// Problem documented here: https://github.com/facebook/docusaurus/pull/10934
|
||||
// TODO this is not a perfect solution, find better?
|
||||
async function createMdxLoaderDependencyFile({
|
||||
dataDir,
|
||||
options,
|
||||
versionsMetadata,
|
||||
}: {
|
||||
dataDir: string;
|
||||
options: PluginOptions;
|
||||
versionsMetadata: VersionMetadata[];
|
||||
}): Promise<string | undefined> {
|
||||
// TODO this has been temporarily made opt-in until Rspack cache bug is fixed
|
||||
// See https://github.com/facebook/docusaurus/pull/10931
|
||||
// See https://github.com/facebook/docusaurus/pull/10934#issuecomment-2672253145
|
||||
if (!process.env.DOCUSAURUS_ENABLE_MDX_DEPENDENCY_FILE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Mutable map update :/
|
||||
function update(content: LoadedContent): void {
|
||||
sourceToPermalink.clear();
|
||||
computeSourceToPermalink(content).forEach((value, key) => {
|
||||
sourceToPermalink.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return {get: () => sourceToPermalink, update};
|
||||
const filePath = path.join(dataDir, '__mdx-loader-dependency.json');
|
||||
// the cache is invalidated whenever this file content changes
|
||||
const fileContent = {
|
||||
options,
|
||||
versionsMetadata,
|
||||
};
|
||||
await fs.ensureDir(dataDir);
|
||||
await fs.writeFile(filePath, JSON.stringify(fileContent));
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export default async function pluginContentDocs(
|
||||
|
@ -112,7 +121,82 @@ export default async function pluginContentDocs(
|
|||
// TODO env should be injected into all plugins
|
||||
const env = process.env.NODE_ENV as DocEnv;
|
||||
|
||||
const sourceToPermalinkHelper = createSourceToPermalinkHelper();
|
||||
const contentHelpers = createContentHelpers();
|
||||
|
||||
async function createDocsMDXLoaderRule(): Promise<RuleSetRule> {
|
||||
const {
|
||||
rehypePlugins,
|
||||
remarkPlugins,
|
||||
recmaPlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
beforeDefaultRemarkPlugins,
|
||||
} = options;
|
||||
const contentDirs = versionsMetadata
|
||||
.flatMap(getContentPathList)
|
||||
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
|
||||
.map(addTrailingPathSeparator);
|
||||
|
||||
return createMDXLoaderRule({
|
||||
include: contentDirs,
|
||||
options: {
|
||||
dependencies: [
|
||||
await createMdxLoaderDependencyFile({
|
||||
dataDir,
|
||||
options,
|
||||
versionsMetadata,
|
||||
}),
|
||||
].filter((d): d is string => typeof d === 'string'),
|
||||
|
||||
useCrossCompilerCache:
|
||||
siteConfig.future.experimental_faster.mdxCrossCompilerCache,
|
||||
admonitions: options.admonitions,
|
||||
remarkPlugins,
|
||||
rehypePlugins,
|
||||
recmaPlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
beforeDefaultRemarkPlugins,
|
||||
staticDirs: siteConfig.staticDirectories.map((dir) =>
|
||||
path.resolve(siteDir, dir),
|
||||
),
|
||||
siteDir,
|
||||
isMDXPartial: createAbsoluteFilePathMatcher(
|
||||
options.exclude,
|
||||
contentDirs,
|
||||
),
|
||||
metadataPath: (mdxPath: string) => {
|
||||
// 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`);
|
||||
},
|
||||
// createAssets converts relative paths to require() calls
|
||||
createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({
|
||||
image: frontMatter.image,
|
||||
}),
|
||||
markdownConfig: siteConfig.markdown,
|
||||
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
|
||||
const version = getVersionFromSourceFilePath(
|
||||
sourceFilePath,
|
||||
versionsMetadata,
|
||||
);
|
||||
const permalink = 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;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const docsMDXLoaderRule = await createDocsMDXLoaderRule();
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-content-docs',
|
||||
|
@ -134,7 +218,7 @@ export default async function pluginContentDocs(
|
|||
.arguments('<version>')
|
||||
.description(commandDescription)
|
||||
.action((version: unknown) =>
|
||||
cliDocsVersionCommand(version, options, context),
|
||||
cliDocs.cliDocsVersionCommand(version, options, context),
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -270,7 +354,7 @@ export default async function pluginContentDocs(
|
|||
},
|
||||
|
||||
async contentLoaded({content, actions}) {
|
||||
sourceToPermalinkHelper.update(content);
|
||||
contentHelpers.updateContent(content);
|
||||
|
||||
const versions: FullVersion[] = content.loadedVersions.map(toFullVersion);
|
||||
|
||||
|
@ -289,74 +373,7 @@ export default async function pluginContentDocs(
|
|||
});
|
||||
},
|
||||
|
||||
configureWebpack(_config, isServer, utils, content) {
|
||||
const {
|
||||
rehypePlugins,
|
||||
remarkPlugins,
|
||||
recmaPlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
beforeDefaultRemarkPlugins,
|
||||
} = options;
|
||||
|
||||
const contentDirs = versionsMetadata
|
||||
.flatMap(getContentPathList)
|
||||
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
|
||||
.map(addTrailingPathSeparator);
|
||||
|
||||
function createMDXLoader(): RuleSetUseItem {
|
||||
const loaderOptions: MDXLoaderOptions = {
|
||||
admonitions: options.admonitions,
|
||||
remarkPlugins,
|
||||
rehypePlugins,
|
||||
recmaPlugins,
|
||||
beforeDefaultRehypePlugins,
|
||||
beforeDefaultRemarkPlugins,
|
||||
staticDirs: siteConfig.staticDirectories.map((dir) =>
|
||||
path.resolve(siteDir, dir),
|
||||
),
|
||||
siteDir,
|
||||
isMDXPartial: createAbsoluteFilePathMatcher(
|
||||
options.exclude,
|
||||
contentDirs,
|
||||
),
|
||||
metadataPath: (mdxPath: string) => {
|
||||
// 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`);
|
||||
},
|
||||
// Assets allow to convert some relative images paths to
|
||||
// require(...) calls
|
||||
createAssets: ({frontMatter}: {frontMatter: DocFrontMatter}) => ({
|
||||
image: frontMatter.image,
|
||||
}),
|
||||
markdownConfig: siteConfig.markdown,
|
||||
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
|
||||
const version = getVersionFromSourceFilePath(
|
||||
sourceFilePath,
|
||||
content.loadedVersions,
|
||||
);
|
||||
const permalink = resolveMarkdownLinkPathname(linkPathname, {
|
||||
sourceFilePath,
|
||||
sourceToPermalink: sourceToPermalinkHelper.get(),
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
loader: require.resolve('@docusaurus/mdx-loader'),
|
||||
options: loaderOptions,
|
||||
};
|
||||
}
|
||||
|
||||
configureWebpack() {
|
||||
return {
|
||||
ignoreWarnings: [
|
||||
// Suppress warnings about non-existing of versions file.
|
||||
|
@ -370,13 +387,7 @@ export default async function pluginContentDocs(
|
|||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.mdx?$/i,
|
||||
include: contentDirs,
|
||||
use: [createMDXLoader()],
|
||||
},
|
||||
],
|
||||
rules: [docsMDXLoaderRule],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -20,7 +20,11 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
TagMetadata,
|
||||
TagsPluginOptions,
|
||||
} from '@docusaurus/utils';
|
||||
import type {Plugin, LoadContext} from '@docusaurus/types';
|
||||
import type {
|
||||
Plugin,
|
||||
LoadContext,
|
||||
OptionValidationContext,
|
||||
} from '@docusaurus/types';
|
||||
import type {Overwrite, Required} from 'utility-types';
|
||||
|
||||
export type Assets = {
|
||||
|
@ -559,13 +563,18 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Promise<Plugin<LoadedContent>>;
|
||||
|
||||
export function validateOptions(
|
||||
args: OptionValidationContext<Options | undefined, PluginOptions>,
|
||||
): PluginOptions;
|
||||
}
|
||||
|
||||
declare module '@theme/DocItem' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PropDocContent} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export type DocumentRoute = {
|
||||
readonly component: () => JSX.Element;
|
||||
readonly component: () => ReactNode;
|
||||
readonly exact: boolean;
|
||||
readonly path: string;
|
||||
readonly sidebar?: string;
|
||||
|
@ -576,10 +585,11 @@ declare module '@theme/DocItem' {
|
|||
readonly content: PropDocContent;
|
||||
}
|
||||
|
||||
export default function DocItem(props: Props): JSX.Element;
|
||||
export default function DocItem(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocCategoryGeneratedIndexPage' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
|
@ -588,39 +598,45 @@ declare module '@theme/DocCategoryGeneratedIndexPage' {
|
|||
|
||||
export default function DocCategoryGeneratedIndexPage(
|
||||
props: Props,
|
||||
): JSX.Element;
|
||||
): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocTagsListPage' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PropTagsListPage} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props extends PropTagsListPage {}
|
||||
export default function DocTagsListPage(props: Props): JSX.Element;
|
||||
export default function DocTagsListPage(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocTagDocListPage' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PropTagDocList} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
export interface Props {
|
||||
readonly tag: PropTagDocList;
|
||||
}
|
||||
export default function DocTagDocListPage(props: Props): JSX.Element;
|
||||
export default function DocTagDocListPage(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocBreadcrumbs' {
|
||||
export default function DocBreadcrumbs(): JSX.Element;
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export default function DocBreadcrumbs(): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocsRoot' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {RouteConfigComponentProps} from 'react-router-config';
|
||||
import type {Required} from 'utility-types';
|
||||
|
||||
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
|
||||
|
||||
export default function DocsRoot(props: Props): JSX.Element;
|
||||
export default function DocsRoot(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocVersionRoot' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
|
||||
import type {RouteConfigComponentProps} from 'react-router-config';
|
||||
import type {Required} from 'utility-types';
|
||||
|
@ -629,14 +645,15 @@ declare module '@theme/DocVersionRoot' {
|
|||
readonly version: PropVersionMetadata;
|
||||
}
|
||||
|
||||
export default function DocVersionRoot(props: Props): JSX.Element;
|
||||
export default function DocVersionRoot(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocRoot' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {RouteConfigComponentProps} from 'react-router-config';
|
||||
import type {Required} from 'utility-types';
|
||||
|
||||
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
|
||||
|
||||
export default function DocRoot(props: Props): JSX.Element;
|
||||
export default function DocRoot(props: Props): ReactNode;
|
||||
}
|
||||
|
|
|
@ -75,6 +75,21 @@ export function getDocsDirPathLocalized({
|
|||
});
|
||||
}
|
||||
|
||||
export function getPluginDirPathLocalized({
|
||||
localizationDir,
|
||||
pluginId,
|
||||
}: {
|
||||
localizationDir: string;
|
||||
pluginId: string;
|
||||
}): string {
|
||||
return getPluginI18nPath({
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-docs',
|
||||
pluginId,
|
||||
subPaths: [],
|
||||
});
|
||||
}
|
||||
|
||||
/** `community` => `[siteDir]/community_versions.json` */
|
||||
export function getVersionsFilePath(siteDir: string, pluginId: string): string {
|
||||
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue