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

@ -21,6 +21,7 @@
],
"ignorePaths": [
"CHANGELOG.md",
"CHANGELOG-v*.md",
"patches",
"packages/docusaurus-theme-translations/locales",
"packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy",

View file

@ -37,7 +37,7 @@ jobs:
- name: Audit URLs using Lighthouse
id: lighthouse_audit
uses: treosh/lighthouse-ci-action@2f8dda6cf4de7d73b29853c3f29e73a01e297bd8 # 12.1.0
uses: treosh/lighthouse-ci-action@fcd65974f7c4c2bf0ee9d09b84d2489183c29726 # 12.6.1
with:
urls: |
http://localhost:3000
@ -65,7 +65,7 @@ jobs:
- name: Add Lighthouse stats as comment
id: comment_to_pr
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # 2.9.2
uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # 2.9.3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}

View file

@ -42,6 +42,6 @@ jobs:
- name: Print Diff
run: git diff
- uses: stefanzweifel/git-auto-commit-action@v5
- uses: stefanzweifel/git-auto-commit-action@v6
with:
commit_message: 'refactor: apply lint autofix'

View file

@ -72,6 +72,48 @@ jobs:
DOCUSAURUS_PERF_LOGGER: 'true'
working-directory: ../test-website
yarn-v1-windows:
name: E2E — Yarn v1 Windows
timeout-minutes: 30
runs-on: windows-8-core
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Use Node.js LTS
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: yarn
- name: Installation
run: yarn || yarn || yarn
- name: Generate test-website project against main branch
# Not using test-release.sh => no verdaccio docker image on Windows
# run: bash ./admin/scripts/test-release.sh -s
run: yarn create-docusaurus test-website-in-workspace classic --typescript
- name: Install test-website project with Yarn v1
run: yarn || yarn || yarn
working-directory: test-website-in-workspace
- name: Start test-website project
run: yarn start --no-open
working-directory: test-website-in-workspace
env:
E2E_TEST: true
- name: Build test-website project
# We build 2 locales to ensure a localized site doesn't leak memory
# See https://github.com/facebook/docusaurus/pull/10599
run: yarn build --locale en --locale fr
env:
# Our website should build even with limited memory
# See https://github.com/facebook/docusaurus/pull/10590
NODE_OPTIONS: '--max-old-space-size=300'
DOCUSAURUS_PERF_LOGGER: 'true'
working-directory: test-website-in-workspace
- name: Upload Website artifact
uses: actions/upload-artifact@v4
with:
name: website-e2e-windows
path: test-website-in-workspace/build
yarn-berry:
name: E2E — Yarn Berry
timeout-minutes: 30

1
.gitignore vendored
View file

@ -47,6 +47,7 @@ website/i18n/**/*
.netlify
website/rspack-tracing.json
website/rspack-tracing.pftrace
website/bundler-cpu-profile.json
website/profile.json.gz

6757
CHANGELOG-v2.md Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "new.docusaurus.io",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"scripts": {
"start": "npx --package netlify-cli netlify dev"

View file

@ -53,6 +53,8 @@ git diff --name-only -- '*.json' | sed 's, ,\\&,g' | xargs git checkout --
# The website is generated outside the repo to minimize chances of yarn resolving the wrong version
cd ..
echo Generating test-website in `pwd`
# Build skeleton website with new version
npm_config_registry="$CUSTOM_REGISTRY_URL" npx --yes --loglevel silly create-docusaurus@"$NEW_VERSION" test-website classic --javascript $EXTRA_OPTS

View file

@ -1,6 +1,6 @@
{
"name": "test-bad-package",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"dependencies": {
"@mdx-js/react": "1.0.1",

View file

@ -1,6 +1,6 @@
{
"name": "argos",
"version": "3.8.0",
"version": "3.8.1",
"description": "Argos visual diff tests",
"license": "MIT",
"private": true,

View file

@ -134,11 +134,6 @@ function throwOnConsole(page: Page) {
// it's already happening in main branch
'Failed to load resource: the server responded with a status of 404 (Not Found)',
// TODO legit hydration bugs to fix on embeds of /docs/styling-layout
// useLocation() returns window.search/hash immediately :s
'/docs/configuration?docusaurus-theme=light',
'/docs/configuration?docusaurus-theme=dark',
// Warning because react-live not supporting React automatic JSX runtime
// See https://github.com/FormidableLabs/react-live/issues/405
'Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance',

View file

@ -1,5 +1,5 @@
{
"version": "3.8.0",
"version": "3.8.1",
"npmClient": "yarn",
"useWorkspaces": true,
"useNx": false,

View file

@ -1,6 +1,6 @@
{
"name": "create-docusaurus",
"version": "3.8.0",
"version": "3.8.1",
"description": "Create Docusaurus apps easily.",
"type": "module",
"repository": {
@ -22,8 +22,8 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/logger": "3.8.1",
"@docusaurus/utils": "3.8.1",
"commander": "^5.1.0",
"execa": "5.1.1",
"fs-extra": "^11.1.1",

View file

@ -26,7 +26,6 @@ const config: Config = {
projectName: 'docusaurus', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
@ -72,6 +71,9 @@ const config: Config = {
themeConfig: {
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: 'My Site',
logo: {

View file

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-typescript-template",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -15,8 +15,8 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/preset-classic": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -24,9 +24,9 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.0",
"@docusaurus/tsconfig": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/tsconfig": "3.8.1",
"@docusaurus/types": "3.8.1",
"typescript": "~5.6.2"
},
"browserslist": {

View file

@ -31,7 +31,6 @@ const config = {
projectName: 'docusaurus', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
@ -80,6 +79,9 @@ const config = {
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: 'My Site',
logo: {

View file

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-template",
"version": "3.8.0",
"version": "3.8.1",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -14,8 +14,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/preset-classic": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
@ -23,8 +23,8 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.0",
"@docusaurus/types": "3.8.0"
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/types": "3.8.1"
},
"browserslist": {
"production": [

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/babel",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus package for Babel-related utils.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@ -38,8 +38,8 @@
"@babel/runtime": "^7.25.9",
"@babel/runtime-corejs3": "^7.25.9",
"@babel/traverse": "^7.25.9",
"@docusaurus/logger": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/logger": "3.8.1",
"@docusaurus/utils": "3.8.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/bundler",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus util package to abstract the current bundler.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@ -19,24 +19,24 @@
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.9",
"@docusaurus/babel": "3.8.0",
"@docusaurus/cssnano-preset": "3.8.0",
"@docusaurus/logger": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/babel": "3.8.1",
"@docusaurus/cssnano-preset": "3.8.1",
"@docusaurus/logger": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"babel-loader": "^9.2.1",
"clean-css": "^5.3.2",
"clean-css": "^5.3.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cssnano": "^6.1.2",
"file-loader": "^6.2.0",
"html-minifier-terser": "^7.2.0",
"mini-css-extract-plugin": "^2.9.1",
"mini-css-extract-plugin": "^2.9.2",
"null-loader": "^4.0.1",
"postcss": "^8.4.26",
"postcss-loader": "^7.3.3",
"postcss-preset-env": "^10.1.0",
"postcss": "^8.5.4",
"postcss-loader": "^7.3.4",
"postcss-preset-env": "^10.2.1",
"terser-webpack-plugin": "^5.3.9",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",

View file

@ -129,8 +129,8 @@ export async function registerBundlerTracing({
await Rspack.experiments.globalTrace.register(
filter,
'chrome',
'./rspack-tracing.json',
'perfetto',
'./rspack-tracing.pftrace',
);
console.info(`Rspack tracing registered, filter=${filter}`);

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/cssnano-preset",
"version": "3.8.0",
"version": "3.8.1",
"description": "Advanced cssnano preset for maximum optimization.",
"main": "lib/index.js",
"license": "MIT",
@ -18,7 +18,7 @@
},
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.4.38",
"postcss": "^8.5.4",
"postcss-sort-media-queries": "^5.2.0",
"tslib": "^2.6.0"
},

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/faster",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus experimental package exposing new modern dependencies to make the build faster.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@ -18,8 +18,8 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/types": "3.8.0",
"@rspack/core": "^1.3.10",
"@docusaurus/types": "3.8.1",
"@rspack/core": "^1.4.0",
"@swc/core": "^1.7.39",
"@swc/html": "^1.7.39",
"browserslist": "^4.24.2",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/logger",
"version": "3.8.0",
"version": "3.8.1",
"description": "An encapsulated logger for semantically formatting console messages.",
"main": "./lib/index.js",
"repository": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/mdx-loader",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus Loader for MDX",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/logger": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@ -44,7 +44,7 @@
"webpack": "^5.88.1"
},
"devDependencies": {
"@docusaurus/types": "3.8.0",
"@docusaurus/types": "3.8.1",
"@types/escape-html": "^1.0.2",
"@types/mdast": "^4.0.2",
"@types/stringify-object": "^3.3.1",

View file

@ -22,6 +22,9 @@ import type {WebpackCompilerName} from '@docusaurus/utils';
import type {MDXFrontMatter} from './frontMatter';
import type {Options} from './options';
import type {AdmonitionOptions} from './remark/admonitions';
import type {PluginOptions as ResolveMarkdownLinksOptions} from './remark/resolveMarkdownLinks';
import type {PluginOptions as TransformLinksOptions} from './remark/transformLinks';
import type {PluginOptions as TransformImageOptions} from './remark/transformImage';
import type {ProcessorOptions} from '@mdx-js/mdx';
// TODO as of April 2023, no way to import/re-export this ESM type easily :/
@ -92,7 +95,7 @@ async function createProcessorFactory() {
headings,
{anchorsMaintainCase: options.markdownConfig.anchors.maintainCase},
],
emoji,
...(options.markdownConfig.emoji ? [emoji] : []),
toc,
];
}
@ -121,13 +124,19 @@ async function createProcessorFactory() {
{
staticDirs: options.staticDirs,
siteDir: options.siteDir,
},
onBrokenMarkdownImages:
options.markdownConfig.hooks.onBrokenMarkdownImages,
} satisfies TransformImageOptions,
],
// TODO merge this with transformLinks?
options.resolveMarkdownLink
? [
resolveMarkdownLinks,
{resolveMarkdownLink: options.resolveMarkdownLink},
{
resolveMarkdownLink: options.resolveMarkdownLink,
onBrokenMarkdownLinks:
options.markdownConfig.hooks.onBrokenMarkdownLinks,
} satisfies ResolveMarkdownLinksOptions,
]
: undefined,
[
@ -135,7 +144,9 @@ async function createProcessorFactory() {
{
staticDirs: options.staticDirs,
siteDir: options.siteDir,
},
onBrokenMarkdownLinks:
options.markdownConfig.hooks.onBrokenMarkdownLinks,
} satisfies TransformLinksOptions,
],
gfm,
options.markdownConfig.mdx1Compat.comments ? comment : null,

View file

@ -5,22 +5,47 @@
* LICENSE file in the root directory of this source tree.
*/
import {jest} from '@jest/globals';
import * as path from 'path';
import plugin from '..';
import type {PluginOptions} from '../index';
async function process(content: string) {
const {remark} = await import('remark');
const siteDir = __dirname;
const options: PluginOptions = {
const DefaultTestOptions: PluginOptions = {
resolveMarkdownLink: ({linkPathname}) => `/RESOLVED---${linkPathname}`,
onBrokenMarkdownLinks: 'throw',
};
async function process(content: string, optionsInput?: Partial<PluginOptions>) {
const options = {
...DefaultTestOptions,
...optionsInput,
};
const result = await remark().use(plugin, options).process(content);
const {remark} = await import('remark');
const result = await remark()
.use(plugin, options)
.process({
value: content,
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
});
return result.value;
}
describe('resolveMarkdownLinks remark plugin', () => {
it('accepts non-md link', async () => {
/* language=markdown */
const content = `[link1](link1)`;
const result = await process(content);
expect(result).toMatchInlineSnapshot(`
"[link1](link1)
"
`);
});
it('resolves Markdown and MDX links', async () => {
/* language=markdown */
const content = `[link1](link1.mdx)
@ -157,4 +182,212 @@ this is a code block
"
`);
});
describe('onBrokenMarkdownLinks', () => {
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
beforeEach(() => {
warnMock.mockClear();
});
async function processResolutionErrors(
content: string,
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'] = 'throw',
) {
return process(content, {
resolveMarkdownLink: () => null,
onBrokenMarkdownLinks,
});
}
describe('throws', () => {
it('for unresolvable mdx link', async () => {
/* language=markdown */
const content = `[link1](link1.mdx)`;
await expect(() => processResolutionErrors(content)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with URL \`link1.mdx\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});
it('for unresolvable md link', async () => {
/* language=markdown */
const content = `[link1](link1.md)`;
await expect(() => processResolutionErrors(content)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with URL \`link1.md\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});
});
describe('warns', () => {
it('for unresolvable md and mdx link', async () => {
/* language=markdown */
const content = `
[link1](link1.mdx)
[link2](link2)
[link3](dir/link3.md)
[link 4](/link/4)
`;
const result = await processResolutionErrors(content, 'warn');
expect(result).toMatchInlineSnapshot(`
"[link1](link1.mdx)
[link2](link2)
[link3](dir/link3.md)
[link 4](/link/4)
"
`);
expect(warnMock).toHaveBeenCalledTimes(2);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown link with URL \`link1.mdx\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (2:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.",
],
[
"[WARNING] Markdown link with URL \`dir/link3.md\` in source file "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx" (6:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.",
],
]
`);
});
it('for unresolvable md and mdx link - with recovery', async () => {
/* language=markdown */
const content = `
[link1](link1.mdx)
[link2](link2)
[link3](dir/link3.md?query#hash)
[link 4](/link/4)
`;
const result = await processResolutionErrors(content, (params) => {
console.warn(`onBrokenMarkdownLinks called with`, params);
// We can alter the AST Node
params.node.title = 'fixed link title';
params.node.url = 'ignored, less important than returned value';
// Or return a new URL
return `/recovered-link`;
});
expect(result).toMatchInlineSnapshot(`
"[link1](/recovered-link "fixed link title")
[link2](link2)
[link3](/recovered-link "fixed link title")
[link 4](/link/4)
"
`);
expect(warnMock).toHaveBeenCalledTimes(2);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 7,
"line": 2,
"offset": 7,
},
"start": {
"column": 2,
"line": 2,
"offset": 2,
},
},
"type": "text",
"value": "link1",
},
],
"position": {
"end": {
"column": 19,
"line": 2,
"offset": 19,
},
"start": {
"column": 1,
"line": 2,
"offset": 1,
},
},
"title": "fixed link title",
"type": "link",
"url": "/recovered-link",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx",
"url": "link1.mdx",
},
],
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 7,
"line": 6,
"offset": 43,
},
"start": {
"column": 2,
"line": 6,
"offset": 38,
},
},
"type": "text",
"value": "link3",
},
],
"position": {
"end": {
"column": 33,
"line": 6,
"offset": 69,
},
"start": {
"column": 1,
"line": 6,
"offset": 37,
},
},
"title": "fixed link title",
"type": "link",
"url": "/recovered-link",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/resolveMarkdownLinks/__tests__/docs/myFile.mdx",
"url": "dir/link3.md?query#hash",
},
],
]
`);
});
});
});
});

View file

@ -8,11 +8,18 @@
import {
parseLocalURLPath,
serializeURLPath,
toMessageRelativeFilePath,
type URLPath,
} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
import {formatNodePositionExtraMessage} from '../utils';
import type {Plugin, Transformer} from 'unified';
import type {Definition, Link, Root} from 'mdast';
import type {
MarkdownConfig,
OnBrokenMarkdownLinksFunction,
} from '@docusaurus/types';
type ResolveMarkdownLinkParams = {
/**
@ -32,6 +39,33 @@ export type ResolveMarkdownLink = (
export interface PluginOptions {
resolveMarkdownLink: ResolveMarkdownLink;
onBrokenMarkdownLinks: MarkdownConfig['hooks']['onBrokenMarkdownLinks'];
}
function asFunction(
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'],
): OnBrokenMarkdownLinksFunction {
if (typeof onBrokenMarkdownLinks === 'string') {
const extraHelp =
onBrokenMarkdownLinks === 'throw'
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} option, or apply the code=${'pathname://'} protocol to the broken link URLs.`
: '';
return ({sourceFilePath, url: linkUrl, node}) => {
const relativePath = toMessageRelativeFilePath(sourceFilePath);
logger.report(
onBrokenMarkdownLinks,
)`Markdown link with URL code=${linkUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
node,
)} couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.${extraHelp}`;
};
} else {
return (params) =>
onBrokenMarkdownLinks({
...params,
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
});
}
}
const HAS_MARKDOWN_EXTENSION = /\.mdx?$/i;
@ -57,10 +91,15 @@ function parseMarkdownLinkURLPath(link: string): URLPath | null {
* This is exposed as "data.contentTitle" to the processed vfile
* Also gives the ability to strip that content title (used for the blog plugin)
*/
// TODO merge this plugin with "transformLinks"
// in general we'd want to avoid traversing multiple times the same AST
const plugin: Plugin<PluginOptions[], Root> = function plugin(
options,
): Transformer<Root> {
const {resolveMarkdownLink} = options;
const onBrokenMarkdownLinks = asFunction(options.onBrokenMarkdownLinks);
return async (root, file) => {
const {visit} = await import('unist-util-visit');
@ -71,18 +110,26 @@ const plugin: Plugin<PluginOptions[], Root> = function plugin(
return;
}
const sourceFilePath = file.path;
const permalink = resolveMarkdownLink({
sourceFilePath: file.path,
sourceFilePath,
linkPathname: linkURLPath.pathname,
});
if (permalink) {
// This reapplies the link ?qs#hash part to the resolved pathname
const resolvedUrl = serializeURLPath({
link.url = serializeURLPath({
...linkURLPath,
pathname: permalink,
});
link.url = resolvedUrl;
} else {
link.url =
onBrokenMarkdownLinks({
url: link.url,
sourceFilePath,
node: link,
}) ?? link.url;
}
});
};

View file

@ -1 +0,0 @@
![img](/img/doesNotExist.png)

View file

@ -1 +0,0 @@
![img](./notFound.png)

View file

@ -1 +0,0 @@
![invalid image](/invalid.png)

View file

@ -1 +0,0 @@
![img](pathname:///img/unchecked.png)

View file

@ -1,16 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformImage plugin does not choke on invalid image 1`] = `
"<img alt="invalid image" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./static/invalid.png").default} />
"<img alt="invalid image" src={require("!<PROJECT_ROOT>/node_modules/url-loader/dist/cjs.js?limit=10000&name=assets/images/[name]-[contenthash].[ext]&fallback=<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js!./../static/invalid.png").default} />
"
`;
exports[`transformImage plugin fail if image does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/img/doesNotExist.png or packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static2/img/doesNotExist.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/fail.md not found."`;
exports[`transformImage plugin fail if image relative path does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/notFound.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/fail2.md not found."`;
exports[`transformImage plugin fail if image url is absent 1`] = `"Markdown image URL is mandatory in "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/noUrl.md" file"`;
exports[`transformImage plugin pathname protocol 1`] = `
"![img](/img/unchecked.png)
"

View file

@ -6,65 +6,361 @@
*/
import {jest} from '@jest/globals';
import path from 'path';
import * as path from 'path';
import vfile from 'to-vfile';
import plugin, {type PluginOptions} from '../index';
const processFixture = async (
name: string,
options: Partial<PluginOptions>,
) => {
const {remark} = await import('remark');
const {default: mdx} = await import('remark-mdx');
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
const file = await vfile.read(filePath);
const result = await remark()
.use(mdx)
.use(plugin, {siteDir: __dirname, staticDirs: [], ...options})
.process(file);
return result.value;
};
const siteDir = path.join(__dirname, '__fixtures__');
const staticDirs = [
path.join(__dirname, '__fixtures__/static'),
path.join(__dirname, '__fixtures__/static2'),
];
const siteDir = path.join(__dirname, '__fixtures__');
const getProcessor = async (options?: Partial<PluginOptions>) => {
const {remark} = await import('remark');
const {default: mdx} = await import('remark-mdx');
return remark()
.use(mdx)
.use(plugin, {
siteDir,
staticDirs,
onBrokenMarkdownImages: 'throw',
...options,
});
};
const processFixture = async (
name: string,
options?: Partial<PluginOptions>,
) => {
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
const file = await vfile.read(filePath);
const processor = await getProcessor(options);
const result = await processor.process(file);
return result.value;
};
const processContent = async (
content: string,
options?: Partial<PluginOptions>,
) => {
const processor = await getProcessor(options);
const result = await processor.process({
value: content,
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
});
return result.value.toString();
};
describe('transformImage plugin', () => {
it('fail if image does not exist', async () => {
await expect(
processFixture('fail', {staticDirs}),
).rejects.toThrowErrorMatchingSnapshot();
});
it('fail if image relative path does not exist', async () => {
await expect(
processFixture('fail2', {staticDirs}),
).rejects.toThrowErrorMatchingSnapshot();
});
it('fail if image url is absent', async () => {
await expect(
processFixture('noUrl', {staticDirs}),
).rejects.toThrowErrorMatchingSnapshot();
});
it('transform md images to <img />', async () => {
const result = await processFixture('img', {staticDirs, siteDir});
// TODO split that large fixture into many smaller test cases?
const result = await processFixture('img');
expect(result).toMatchSnapshot();
});
it('pathname protocol', async () => {
const result = await processFixture('pathname', {staticDirs});
const result = await processContent(
`![img](pathname:///img/unchecked.png)`,
);
expect(result).toMatchSnapshot();
});
it('does not choke on invalid image', async () => {
const errorMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
const result = await processFixture('invalid-img', {staticDirs});
const result = await processContent(`![invalid image](/invalid.png)`);
expect(result).toMatchSnapshot();
expect(errorMock).toHaveBeenCalledTimes(1);
});
describe('onBrokenMarkdownImages', () => {
const fixtures = {
doesNotExistAbsolute: `![img](/img/doesNotExist.png)`,
doesNotExistRelative: `![img](./doesNotExist.png)`,
doesNotExistSiteAlias: `![img](@site/doesNotExist.png)`,
urlEmpty: `![img]()`,
};
describe('throws', () => {
it('if image absolute path does not exist', async () => {
await expect(processContent(fixtures.doesNotExistAbsolute)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown image with URL \`/img/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
`);
});
it('if image relative path does not exist', async () => {
await expect(processContent(fixtures.doesNotExistRelative)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown image with URL \`./doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
`);
});
it('if image @site path does not exist', async () => {
await expect(processContent(fixtures.doesNotExistSiteAlias)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown image with URL \`@site/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
`);
});
it('if image url empty', async () => {
await expect(processContent(fixtures.urlEmpty)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown image with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1).
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownImages\` option, or apply the \`pathname://\` protocol to the broken image URLs."
`);
});
});
describe('warns', () => {
function processWarn(content: string) {
return processContent(content, {onBrokenMarkdownImages: 'warn'});
}
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
beforeEach(() => {
warnMock.mockClear();
});
it('if image absolute path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistAbsolute);
expect(result).toMatchInlineSnapshot(`
"![img](/img/doesNotExist.png)
"
`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown image with URL \`/img/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
],
]
`);
});
it('if image relative path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistRelative);
expect(result).toMatchInlineSnapshot(`
"![img](./doesNotExist.png)
"
`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown image with URL \`./doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
],
]
`);
});
it('if image @site path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistSiteAlias);
expect(result).toMatchInlineSnapshot(`
"![img](@site/doesNotExist.png)
"
`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown image with URL \`@site/doesNotExist.png\` in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved to an existing local image file.",
],
]
`);
});
it('if image url empty', async () => {
const result = await processWarn(fixtures.urlEmpty);
expect(result).toMatchInlineSnapshot(`
"![img]()
"
`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown image with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx" (1:1).",
],
]
`);
});
});
describe('function form', () => {
function processWarn(content: string) {
return processContent(content, {
onBrokenMarkdownImages: (params) => {
console.log('onBrokenMarkdownImages called for ', params);
// We can alter the AST Node
params.node.alt = 'new 404 alt';
params.node.url = 'ignored, less important than returned value';
// Or return a new URL
return '/404.png';
},
});
}
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
beforeEach(() => {
logMock.mockClear();
});
it('if image absolute path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistAbsolute);
expect(result).toMatchInlineSnapshot(`
"![new 404 alt](/404.png)
"
`);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownImages called for ",
{
"node": {
"alt": "new 404 alt",
"position": {
"end": {
"column": 30,
"line": 1,
"offset": 29,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": null,
"type": "image",
"url": "/404.png",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
"url": "/img/doesNotExist.png",
},
],
]
`);
});
it('if image relative path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistRelative);
expect(result).toMatchInlineSnapshot(`
"![new 404 alt](/404.png)
"
`);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownImages called for ",
{
"node": {
"alt": "new 404 alt",
"position": {
"end": {
"column": 27,
"line": 1,
"offset": 26,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": null,
"type": "image",
"url": "/404.png",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
"url": "./doesNotExist.png",
},
],
]
`);
});
it('if image @site path does not exist', async () => {
const result = await processWarn(fixtures.doesNotExistSiteAlias);
expect(result).toMatchInlineSnapshot(`
"![new 404 alt](/404.png)
"
`);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownImages called for ",
{
"node": {
"alt": "new 404 alt",
"position": {
"end": {
"column": 31,
"line": 1,
"offset": 30,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": null,
"type": "image",
"url": "/404.png",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
"url": "@site/doesNotExist.png",
},
],
]
`);
});
it('if image url empty', async () => {
const result = await processWarn(fixtures.urlEmpty);
expect(result).toMatchInlineSnapshot(`
"![new 404 alt](/404.png)
"
`);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownImages called for ",
{
"node": {
"alt": "new 404 alt",
"position": {
"end": {
"column": 9,
"line": 1,
"offset": 8,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": null,
"type": "image",
"url": "/404.png",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/docs/myFile.mdx",
"url": "",
},
],
]
`);
});
});
});
});

View file

@ -19,22 +19,67 @@ import {
import escapeHtml from 'escape-html';
import {imageSizeFromFile} from 'image-size/fromFile';
import logger from '@docusaurus/logger';
import {assetRequireAttributeValue, transformNode} from '../utils';
import {
assetRequireAttributeValue,
formatNodePositionExtraMessage,
transformNode,
} from '../utils';
import type {Plugin, Transformer} from 'unified';
import type {MdxJsxTextElement} from 'mdast-util-mdx';
import type {Image, Root} from 'mdast';
import type {Parent} from 'unist';
import type {
MarkdownConfig,
OnBrokenMarkdownImagesFunction,
} from '@docusaurus/types';
type PluginOptions = {
export type PluginOptions = {
staticDirs: string[];
siteDir: string;
onBrokenMarkdownImages: MarkdownConfig['hooks']['onBrokenMarkdownImages'];
};
type Context = PluginOptions & {
type Context = {
staticDirs: PluginOptions['staticDirs'];
siteDir: PluginOptions['siteDir'];
onBrokenMarkdownImages: OnBrokenMarkdownImagesFunction;
filePath: string;
inlineMarkdownImageFileLoader: string;
};
function asFunction(
onBrokenMarkdownImages: PluginOptions['onBrokenMarkdownImages'],
): OnBrokenMarkdownImagesFunction {
if (typeof onBrokenMarkdownImages === 'string') {
const extraHelp =
onBrokenMarkdownImages === 'throw'
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownImages'} option, or apply the code=${'pathname://'} protocol to the broken image URLs.`
: '';
return ({sourceFilePath, url: imageUrl, node}) => {
const relativePath = toMessageRelativeFilePath(sourceFilePath);
if (imageUrl) {
logger.report(
onBrokenMarkdownImages,
)`Markdown image with URL code=${imageUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
node,
)} couldn't be resolved to an existing local image file.${extraHelp}`;
} else {
logger.report(
onBrokenMarkdownImages,
)`Markdown image with empty URL found in source file path=${relativePath}${formatNodePositionExtraMessage(
node,
)}.${extraHelp}`;
}
};
} else {
return (params) =>
onBrokenMarkdownImages({
...params,
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
});
}
}
type Target = [node: Image, index: number, parent: Parent];
async function toImageRequireNode(
@ -51,7 +96,7 @@ async function toImageRequireNode(
);
relativeImagePath = `./${relativeImagePath}`;
const parsedUrl = parseURLOrPath(node.url, 'https://example.com');
const parsedUrl = parseURLOrPath(node.url);
const hash = parsedUrl.hash ?? '';
const search = parsedUrl.search ?? '';
const requireString = `${context.inlineMarkdownImageFileLoader}${
@ -113,57 +158,53 @@ ${(err as Error).message}`;
});
}
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
const imageExists = await fs.pathExists(imagePath);
if (!imageExists) {
throw new Error(
`Image ${toMessageRelativeFilePath(
imagePath,
)} used in ${toMessageRelativeFilePath(sourceFilePath)} not found.`,
);
}
}
async function getImageAbsolutePath(
imagePath: string,
async function getLocalImageAbsolutePath(
originalImagePath: string,
{siteDir, filePath, staticDirs}: Context,
) {
if (imagePath.startsWith('@site/')) {
const imageFilePath = path.join(siteDir, imagePath.replace('@site/', ''));
await ensureImageFileExist(imageFilePath, filePath);
if (originalImagePath.startsWith('@site/')) {
const imageFilePath = path.join(
siteDir,
originalImagePath.replace('@site/', ''),
);
if (!(await fs.pathExists(imageFilePath))) {
return null;
}
return imageFilePath;
} else if (path.isAbsolute(imagePath)) {
} else if (path.isAbsolute(originalImagePath)) {
// Absolute paths are expected to exist in the static folder.
const possiblePaths = staticDirs.map((dir) => path.join(dir, imagePath));
const possiblePaths = staticDirs.map((dir) =>
path.join(dir, originalImagePath),
);
const imageFilePath = await findAsyncSequential(
possiblePaths,
fs.pathExists,
);
if (!imageFilePath) {
throw new Error(
`Image ${possiblePaths
.map((p) => toMessageRelativeFilePath(p))
.join(' or ')} used in ${toMessageRelativeFilePath(
filePath,
)} not found.`,
);
return null;
}
return imageFilePath;
}
} else {
// relative paths are resolved against the source file's folder
const imageFilePath = path.join(path.dirname(filePath), imagePath);
await ensureImageFileExist(imageFilePath, filePath);
const imageFilePath = path.join(path.dirname(filePath), originalImagePath);
if (!(await fs.pathExists(imageFilePath))) {
return null;
}
return imageFilePath;
}
}
async function processImageNode(target: Target, context: Context) {
const [node] = target;
if (!node.url) {
throw new Error(
`Markdown image URL is mandatory in "${toMessageRelativeFilePath(
context.filePath,
)}" file`,
);
node.url =
context.onBrokenMarkdownImages({
url: node.url,
sourceFilePath: context.filePath,
node,
}) ?? node.url;
return;
}
const parsedUrl = url.parse(node.url);
@ -183,13 +224,27 @@ async function processImageNode(target: Target, context: Context) {
// We try to convert image urls without protocol to images with require calls
// going through webpack ensures that image assets exist at build time
const imagePath = await getImageAbsolutePath(decodedPathname, context);
await toImageRequireNode(target, imagePath, context);
const localImagePath = await getLocalImageAbsolutePath(
decodedPathname,
context,
);
if (localImagePath === null) {
node.url =
context.onBrokenMarkdownImages({
url: node.url,
sourceFilePath: context.filePath,
node,
}) ?? node.url;
} else {
await toImageRequireNode(target, localImagePath, context);
}
}
const plugin: Plugin<PluginOptions[], Root> = function plugin(
options,
): Transformer<Root> {
const onBrokenMarkdownImages = asFunction(options.onBrokenMarkdownImages);
return async (root, vfile) => {
const {visit} = await import('unist-util-visit');
@ -201,6 +256,7 @@ const plugin: Plugin<PluginOptions[], Root> = function plugin(
filePath: vfile.path!,
inlineMarkdownImageFileLoader:
fileLoaderUtils.loaders.inlineMarkdownImageFileLoader,
onBrokenMarkdownImages,
};
const promises: Promise<void>[] = [];

View file

@ -1 +0,0 @@
[asset](pathname:///asset/unchecked.pdf)

View file

@ -1,15 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link URL is mandatory in "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/noUrl.md" file (title: asset, line: 1)."`;
exports[`transformAsset plugin fail if asset with site alias does not exist 1`] = `"Asset packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/foo.pdf used in packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/nonexistentSiteAlias.md not found."`;
exports[`transformAsset plugin pathname protocol 1`] = `
"[asset](pathname:///asset/unchecked.pdf)
"
`;
exports[`transformAsset plugin transform md links to <a /> 1`] = `
exports[`transformLinks plugin transform md links to <a /> 1`] = `
"[asset](https://example.com/asset.pdf)
<a target="_blank" data-noBrokenLinkCheck={true} href={require("!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./asset.pdf").default} />
@ -54,6 +45,5 @@ in paragraph <a target="_blank" data-noBrokenLinkCheck={true} href={require("!<P
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./data.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./data.json").default}>JSON</a>
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>
"
<a target="_blank" data-noBrokenLinkCheck={true} href={require("./static/static-json.raw!=!<PROJECT_ROOT>/node_modules/file-loader/dist/cjs.js?name=assets/files/[name]-[contenthash].[ext]!./static/static-json.json").default}>static JSON</a>"
`;

View file

@ -5,53 +5,270 @@
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import {jest} from '@jest/globals';
import * as path from 'path';
import vfile from 'to-vfile';
import plugin from '..';
import transformImage, {type PluginOptions} from '../../transformImage';
import plugin, {type PluginOptions} from '..';
import transformImage from '../../transformImage';
const processFixture = async (name: string, options?: PluginOptions) => {
const {remark} = await import('remark');
const {default: mdx} = await import('remark-mdx');
const siteDir = path.join(__dirname, `__fixtures__`);
const staticDirs = [
const siteDir = path.join(__dirname, `__fixtures__`);
const staticDirs = [
path.join(siteDir, 'static'),
path.join(siteDir, 'static2'),
];
const file = await vfile.read(path.join(siteDir, `${name}.md`));
const result = await remark()
.use(mdx)
.use(transformImage, {...options, siteDir, staticDirs})
.use(plugin, {
...options,
staticDirs,
siteDir: path.join(__dirname, '__fixtures__'),
})
.process(file);
];
return result.value;
const getProcessor = async (options?: Partial<PluginOptions>) => {
const {remark} = await import('remark');
const {default: mdx} = await import('remark-mdx');
return remark()
.use(mdx)
.use(transformImage, {
siteDir,
staticDirs,
onBrokenMarkdownImages: 'throw',
})
.use(plugin, {
staticDirs,
siteDir,
onBrokenMarkdownLinks: 'throw',
...options,
});
};
describe('transformAsset plugin', () => {
it('fail if asset url is absent', async () => {
await expect(
processFixture('noUrl'),
).rejects.toThrowErrorMatchingSnapshot();
});
const processFixture = async (
name: string,
options?: Partial<PluginOptions>,
) => {
const processor = await getProcessor(options);
const file = await vfile.read(path.join(siteDir, `${name}.md`));
const result = await processor.process(file);
return result.value.toString().trim();
};
it('fail if asset with site alias does not exist', async () => {
await expect(
processFixture('nonexistentSiteAlias'),
).rejects.toThrowErrorMatchingSnapshot();
const processContent = async (
content: string,
options?: Partial<PluginOptions>,
) => {
const processor = await getProcessor(options);
const result = await processor.process({
value: content,
path: path.posix.join(siteDir, 'docs', 'myFile.mdx'),
});
return result.value.toString().trim();
};
describe('transformLinks plugin', () => {
it('transform md links to <a />', async () => {
// TODO split fixture in many smaller test cases
const result = await processFixture('asset');
expect(result).toMatchSnapshot();
});
it('pathname protocol', async () => {
const result = await processFixture('pathname');
expect(result).toMatchSnapshot();
const result = await processContent(`pathname:///unchecked.pdf)`);
expect(result).toMatchInlineSnapshot(`"pathname:///unchecked.pdf)"`);
});
it('accepts absolute file that does not exist', async () => {
const result = await processContent(`[file](/dir/file.zip)`);
expect(result).toMatchInlineSnapshot(`"[file](/dir/file.zip)"`);
});
it('accepts relative file that does not exist', async () => {
const result = await processContent(`[file](dir/file.zip)`);
expect(result).toMatchInlineSnapshot(`"[file](dir/file.zip)"`);
});
describe('onBrokenMarkdownLinks', () => {
const fixtures = {
urlEmpty: `[empty]()`,
fileDoesNotExistSiteAlias: `[file](@site/file.zip)`,
};
describe('throws', () => {
it('if url is empty', async () => {
await expect(processContent(fixtures.urlEmpty)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1).
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});
it('if file with site alias does not exist', async () => {
await expect(processContent(fixtures.fileDoesNotExistSiteAlias)).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Markdown link with URL \`@site/file.zip\` in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.
To ignore this error, use the \`siteConfig.markdown.hooks.onBrokenMarkdownLinks\` option, or apply the \`pathname://\` protocol to the broken link URLs."
`);
});
});
describe('warns', () => {
function processWarn(content: string) {
return processContent(content, {onBrokenMarkdownLinks: 'warn'});
}
const warnMock = jest.spyOn(console, 'warn').mockImplementation(() => {});
beforeEach(() => {
warnMock.mockClear();
});
it('if url is empty', async () => {
const result = await processWarn(fixtures.urlEmpty);
expect(result).toMatchInlineSnapshot(`"[empty]()"`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown link with empty URL found in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1).",
],
]
`);
});
it('if file with site alias does not exist', async () => {
const result = await processWarn(fixtures.fileDoesNotExistSiteAlias);
expect(result).toMatchInlineSnapshot(`"[file](@site/file.zip)"`);
expect(warnMock).toHaveBeenCalledTimes(1);
expect(warnMock.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Markdown link with URL \`@site/file.zip\` in source file "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx" (1:1) couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.",
],
]
`);
});
});
describe('function form', () => {
function processWarn(content: string) {
return processContent(content, {
onBrokenMarkdownLinks: (params) => {
console.log('onBrokenMarkdownLinks called with', params);
// We can alter the AST Node
params.node.title = 'fixed link title';
params.node.url = 'ignored, less important than returned value';
// Or return a new URL
return '/404';
},
});
}
const logMock = jest.spyOn(console, 'log').mockImplementation(() => {});
beforeEach(() => {
logMock.mockClear();
});
it('if url is empty', async () => {
const result = await processWarn(fixtures.urlEmpty);
expect(result).toMatchInlineSnapshot(
`"[empty](/404 "fixed link title")"`,
);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 7,
"line": 1,
"offset": 6,
},
"start": {
"column": 2,
"line": 1,
"offset": 1,
},
},
"type": "text",
"value": "empty",
},
],
"position": {
"end": {
"column": 10,
"line": 1,
"offset": 9,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": "fixed link title",
"type": "link",
"url": "/404",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx",
"url": "",
},
],
]
`);
});
it('if file with site alias does not exist', async () => {
const result = await processWarn(fixtures.fileDoesNotExistSiteAlias);
expect(result).toMatchInlineSnapshot(
`"[file](/404 "fixed link title")"`,
);
expect(logMock).toHaveBeenCalledTimes(1);
expect(logMock.mock.calls).toMatchInlineSnapshot(`
[
[
"onBrokenMarkdownLinks called with",
{
"node": {
"children": [
{
"position": {
"end": {
"column": 6,
"line": 1,
"offset": 5,
},
"start": {
"column": 2,
"line": 1,
"offset": 1,
},
},
"type": "text",
"value": "file",
},
],
"position": {
"end": {
"column": 23,
"line": 1,
"offset": 22,
},
"start": {
"column": 1,
"line": 1,
"offset": 0,
},
},
"title": "fixed link title",
"type": "link",
"url": "/404",
},
"sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/docs/myFile.mdx",
"url": "@site/file.zip",
},
],
]
`);
});
});
});
});

View file

@ -17,24 +17,72 @@ import {
parseURLOrPath,
} from '@docusaurus/utils';
import escapeHtml from 'escape-html';
import {assetRequireAttributeValue, transformNode} from '../utils';
import logger from '@docusaurus/logger';
import {
assetRequireAttributeValue,
formatNodePositionExtraMessage,
transformNode,
} from '../utils';
import type {Plugin, Transformer} from 'unified';
import type {MdxJsxTextElement} from 'mdast-util-mdx';
import type {Parent} from 'unist';
import type {Link, Literal, Root} from 'mdast';
import type {Link, Root} from 'mdast';
import type {
MarkdownConfig,
OnBrokenMarkdownLinksFunction,
} from '@docusaurus/types';
type PluginOptions = {
export type PluginOptions = {
staticDirs: string[];
siteDir: string;
onBrokenMarkdownLinks: MarkdownConfig['hooks']['onBrokenMarkdownLinks'];
};
type Context = PluginOptions & {
staticDirs: string[];
siteDir: string;
onBrokenMarkdownLinks: OnBrokenMarkdownLinksFunction;
filePath: string;
inlineMarkdownLinkFileLoader: string;
};
type Target = [node: Link, index: number, parent: Parent];
function asFunction(
onBrokenMarkdownLinks: PluginOptions['onBrokenMarkdownLinks'],
): OnBrokenMarkdownLinksFunction {
if (typeof onBrokenMarkdownLinks === 'string') {
const extraHelp =
onBrokenMarkdownLinks === 'throw'
? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onBrokenMarkdownLinks'} option, or apply the code=${'pathname://'} protocol to the broken link URLs.`
: '';
return ({sourceFilePath, url: linkUrl, node}) => {
const relativePath = toMessageRelativeFilePath(sourceFilePath);
if (linkUrl) {
logger.report(
onBrokenMarkdownLinks,
)`Markdown link with URL code=${linkUrl} in source file path=${relativePath}${formatNodePositionExtraMessage(
node,
)} couldn't be resolved.
Make sure it references a local Markdown file that exists within the current plugin.${extraHelp}`;
} else {
logger.report(
onBrokenMarkdownLinks,
)`Markdown link with empty URL found in source file path=${relativePath}${formatNodePositionExtraMessage(
node,
)}.${extraHelp}`;
}
};
} else {
return (params) =>
onBrokenMarkdownLinks({
...params,
sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath),
});
}
}
/**
* Transforms the link node to a JSX `<a>` element with a `require()` call.
*/
@ -123,27 +171,15 @@ async function toAssetRequireNode(
});
}
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
const assetExists = await fs.pathExists(assetPath);
if (!assetExists) {
throw new Error(
`Asset ${toMessageRelativeFilePath(
assetPath,
)} used in ${toMessageRelativeFilePath(sourceFilePath)} not found.`,
);
}
}
async function getAssetAbsolutePath(
async function getLocalFileAbsolutePath(
assetPath: string,
{siteDir, filePath, staticDirs}: Context,
) {
if (assetPath.startsWith('@site/')) {
const assetFilePath = path.join(siteDir, assetPath.replace('@site/', ''));
// The @site alias is the only way to believe that the user wants an asset.
// Everything else can just be a link URL
await ensureAssetFileExist(assetFilePath, filePath);
if (await fs.pathExists(assetFilePath)) {
return assetFilePath;
}
} else if (path.isAbsolute(assetPath)) {
const assetFilePath = await findAsyncSequential(
staticDirs.map((dir) => path.join(dir, assetPath)),
@ -164,16 +200,13 @@ async function getAssetAbsolutePath(
async function processLinkNode(target: Target, context: Context) {
const [node] = target;
if (!node.url) {
// Try to improve error feedback
// see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675
const title =
node.title ?? (node.children[0] as Literal | undefined)?.value ?? '?';
const line = node.position?.start.line ?? '?';
throw new Error(
`Markdown link URL is mandatory in "${toMessageRelativeFilePath(
context.filePath,
)}" file (title: ${title}, line: ${line}).`,
);
node.url =
context.onBrokenMarkdownLinks({
url: node.url,
sourceFilePath: context.filePath,
node,
}) ?? node.url;
return;
}
const parsedUrl = url.parse(node.url);
@ -189,29 +222,48 @@ async function processLinkNode(target: Target, context: Context) {
return;
}
const assetPath = await getAssetAbsolutePath(
const localFilePath = await getLocalFileAbsolutePath(
decodeURIComponent(parsedUrl.pathname),
context,
);
if (assetPath) {
await toAssetRequireNode(target, assetPath, context);
if (localFilePath) {
await toAssetRequireNode(target, localFilePath, context);
} else {
// The @site alias is the only way to believe that the user wants an asset.
if (hasSiteAlias) {
node.url =
context.onBrokenMarkdownLinks({
url: node.url,
sourceFilePath: context.filePath,
node,
}) ?? node.url;
} else {
// Even if the url has a dot, and it looks like a file extension
// it can be risky to throw and fail fast by default
// It's perfectly valid for a route path segment to look like a filename
}
}
}
const plugin: Plugin<PluginOptions[], Root> = function plugin(
options,
): Transformer<Root> {
const onBrokenMarkdownLinks = asFunction(options.onBrokenMarkdownLinks);
return async (root, vfile) => {
const {visit} = await import('unist-util-visit');
const fileLoaderUtils = getFileLoaderUtils(
vfile.data.compilerName === 'server',
);
const context: Context = {
...options,
filePath: vfile.path!,
inlineMarkdownLinkFileLoader:
fileLoaderUtils.loaders.inlineMarkdownLinkFileLoader,
onBrokenMarkdownLinks,
};
const promises: Promise<void>[] = [];

View file

@ -8,7 +8,7 @@ import path from 'path';
import process from 'process';
import logger from '@docusaurus/logger';
import {posixPath} from '@docusaurus/utils';
import {transformNode} from '../utils';
import {formatNodePositionExtraMessage, transformNode} from '../utils';
import type {Root} from 'mdast';
import type {Parent} from 'unist';
import type {Transformer, Processor, Plugin} from 'unified';
@ -39,17 +39,9 @@ function formatDirectiveName(directive: Directives) {
return `${prefix}${directive.name}`;
}
function formatDirectivePosition(directive: Directives): string | undefined {
return directive.position?.start
? logger.interpolate`number=${directive.position.start.line}:number=${directive.position.start.column}`
: undefined;
}
function formatUnusedDirectiveMessage(directive: Directives) {
const name = formatDirectiveName(directive);
const position = formatDirectivePosition(directive);
return `- ${name} ${position ? `(${position})` : ''}`;
return `- ${name}${formatNodePositionExtraMessage(directive)}`;
}
function formatUnusedDirectivesMessage({

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import logger from '@docusaurus/logger';
import type {Node} from 'unist';
import type {MdxJsxAttributeValueExpression} from 'mdast-util-mdx';
@ -83,3 +84,16 @@ export function assetRequireAttributeValue(
},
};
}
function formatNodePosition(node: Node): string | undefined {
return node.position?.start
? logger.interpolate`number=${node.position.start.line}:number=${node.position.start.column}`
: undefined;
}
// Returns " (line:column)" when position info is available
// The initial space is useful to append easily to any existing message
export function formatNodePositionExtraMessage(node: Node): string {
const position = formatNodePosition(node);
return `${position ? ` (${position})` : ''}`;
}

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/module-type-aliases",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus module type aliases.",
"types": "./src/index.d.ts",
"publishConfig": {
@ -12,7 +12,7 @@
"directory": "packages/docusaurus-module-type-aliases"
},
"dependencies": {
"@docusaurus/types": "3.8.0",
"@docusaurus/types": "3.8.1",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-client-redirects",
"version": "3.8.0",
"version": "3.8.1",
"description": "Client redirects plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,18 +18,18 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "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/utils": "3.8.1",
"@docusaurus/utils-common": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"eta": "^2.2.0",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"devDependencies": {
"@docusaurus/types": "3.8.0"
"@docusaurus/types": "3.8.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-blog",
"version": "3.8.0",
"version": "3.8.1",
"description": "Blog plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-blog.d.ts",
@ -31,14 +31,14 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "3.8.0",
"@docusaurus/mdx-loader": "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/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",
"cheerio": "1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",

View file

@ -71,7 +71,7 @@ export default async function pluginContentBlog(
);
}
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
const {baseUrl} = siteConfig;
const contentPaths: BlogContentPaths = {
contentPath: path.resolve(siteDir, options.path),
@ -154,18 +154,12 @@ export default async function pluginContentBlog(
},
markdownConfig: siteConfig.markdown,
resolveMarkdownLink: ({linkPathname, sourceFilePath}) => {
const permalink = resolveMarkdownLinkPathname(linkPathname, {
return resolveMarkdownLinkPathname(linkPathname, {
sourceFilePath,
sourceToPermalink: contentHelpers.sourceToPermalink,
siteDir,
contentPaths,
});
if (permalink === null) {
logger.report(
onBrokenMarkdownLinks,
)`Blog markdown link couldn't be resolved: (url=${linkPathname}) in source file path=${sourceFilePath}`;
}
return permalink;
},
});

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,
return {
loadedVersions: await Promise.all(
versionsMetadata.map((versionMetadata) =>
loadVersion({
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)),
),
),
};
},

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 {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-pages",
"version": "3.8.0",
"version": "3.8.1",
"description": "Pages plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-pages.d.ts",
@ -18,11 +18,11 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/mdx-loader": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/mdx-loader": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-css-cascade-layers",
"version": "3.8.0",
"version": "3.8.1",
"description": "CSS Cascade Layer plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,10 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"tslib": "^2.6.0"
},
"engines": {

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {Joi} from '@docusaurus/utils-validation';
import {posixPath} from '@docusaurus/utils';
import {isValidLayerName} from './layers';
import type {OptionValidationContext} from '@docusaurus/types';
@ -20,7 +21,10 @@ export type Options = {
// Not ideal to compute layers using "filePath.includes()"
// But this is mostly temporary until we add first-class layers everywhere
function layerFor(...params: string[]) {
return (filePath: string) => params.some((p) => filePath.includes(p));
return (filePath: string) => {
const posixFilePath = posixPath(filePath);
return params.some((p) => posixFilePath.includes(p));
};
}
// Object order matters, it defines the layer order

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-debug",
"version": "3.8.0",
"version": "3.8.1",
"description": "Debug plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-debug.d.ts",
@ -20,9 +20,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^2.3.0",
"tslib": "^2.6.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-google-analytics",
"version": "3.8.0",
"version": "3.8.1",
"description": "Global analytics (analytics.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"tslib": "^2.6.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-google-gtag",
"version": "3.8.0",
"version": "3.8.1",
"description": "Global Site Tag (gtag.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-google-tag-manager",
"version": "3.8.0",
"version": "3.8.1",
"description": "Google Tag Manager (gtm.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"tslib": "^2.6.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-ideal-image",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus Plugin to generate an almost ideal image (responsive, lazy-loading, and low quality placeholder).",
"main": "lib/index.js",
"types": "src/plugin-ideal-image.d.ts",
@ -20,18 +20,18 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/lqip-loader": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/lqip-loader": "3.8.1",
"@docusaurus/responsive-loader": "^1.7.0",
"@docusaurus/theme-translations": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/theme-translations": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"sharp": "^0.32.3",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.0",
"@docusaurus/module-type-aliases": "3.8.1",
"fs-extra": "^11.1.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-pwa",
"version": "3.8.0",
"version": "3.8.1",
"description": "Docusaurus Plugin to add PWA support.",
"main": "lib/index.js",
"types": "src/plugin-pwa.d.ts",
@ -22,14 +22,14 @@
"dependencies": {
"@babel/core": "^7.25.9",
"@babel/preset-env": "^7.25.9",
"@docusaurus/bundler": "3.8.0",
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "3.8.0",
"@docusaurus/theme-common": "3.8.0",
"@docusaurus/theme-translations": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/bundler": "3.8.1",
"@docusaurus/core": "3.8.1",
"@docusaurus/logger": "3.8.1",
"@docusaurus/theme-common": "3.8.1",
"@docusaurus/theme-translations": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"babel-loader": "^9.2.1",
"clsx": "^2.0.0",
"core-js": "^3.31.1",
@ -41,7 +41,7 @@
"workbox-window": "^7.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.0",
"@docusaurus/module-type-aliases": "3.8.1",
"fs-extra": "^11.1.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-rsdoctor",
"version": "3.8.0",
"version": "3.8.1",
"description": "Rsdoctor plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@rsdoctor/rspack-plugin": "^0.4.6",
"@rsdoctor/webpack-plugin": "^0.4.6",
"tslib": "^2.6.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-sitemap",
"version": "3.8.0",
"version": "3.8.1",
"description": "Simple sitemap generation plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,12 +18,12 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "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/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-common": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-svgr",
"version": "3.8.0",
"version": "3.8.1",
"description": "SVGR plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,10 +18,10 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@svgr/core": "8.1.0",
"@svgr/webpack": "^8.1.0",
"tslib": "^2.6.0",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-vercel-analytics",
"version": "3.8.0",
"version": "3.8.1",
"description": "Global vercel analytics plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,11 +18,11 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/logger": "3.8.0",
"@docusaurus/types": "3.8.0",
"@docusaurus/utils": "3.8.0",
"@docusaurus/utils-validation": "3.8.0",
"@docusaurus/core": "3.8.1",
"@docusaurus/logger": "3.8.1",
"@docusaurus/types": "3.8.1",
"@docusaurus/utils": "3.8.1",
"@docusaurus/utils-validation": "3.8.1",
"@vercel/analytics": "^1.1.1",
"tslib": "^2.6.0"
},

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/preset-classic",
"version": "3.8.0",
"version": "3.8.1",
"description": "Classic preset for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,21 +18,21 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.8.0",
"@docusaurus/plugin-content-blog": "3.8.0",
"@docusaurus/plugin-content-docs": "3.8.0",
"@docusaurus/plugin-content-pages": "3.8.0",
"@docusaurus/plugin-css-cascade-layers": "3.8.0",
"@docusaurus/plugin-debug": "3.8.0",
"@docusaurus/plugin-google-analytics": "3.8.0",
"@docusaurus/plugin-google-gtag": "3.8.0",
"@docusaurus/plugin-google-tag-manager": "3.8.0",
"@docusaurus/plugin-sitemap": "3.8.0",
"@docusaurus/plugin-svgr": "3.8.0",
"@docusaurus/theme-classic": "3.8.0",
"@docusaurus/theme-common": "3.8.0",
"@docusaurus/theme-search-algolia": "3.8.0",
"@docusaurus/types": "3.8.0"
"@docusaurus/core": "3.8.1",
"@docusaurus/plugin-content-blog": "3.8.1",
"@docusaurus/plugin-content-docs": "3.8.1",
"@docusaurus/plugin-content-pages": "3.8.1",
"@docusaurus/plugin-css-cascade-layers": "3.8.1",
"@docusaurus/plugin-debug": "3.8.1",
"@docusaurus/plugin-google-analytics": "3.8.1",
"@docusaurus/plugin-google-gtag": "3.8.1",
"@docusaurus/plugin-google-tag-manager": "3.8.1",
"@docusaurus/plugin-sitemap": "3.8.1",
"@docusaurus/plugin-svgr": "3.8.1",
"@docusaurus/theme-classic": "3.8.1",
"@docusaurus/theme-common": "3.8.1",
"@docusaurus/theme-search-algolia": "3.8.1",
"@docusaurus/types": "3.8.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/remark-plugin-npm2yarn",
"version": "3.8.0",
"version": "3.8.1",
"description": "Remark plugin for converting npm commands to Yarn commands as tabs.",
"main": "lib/index.js",
"publishConfig": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-classic",
"version": "3.8.0",
"version": "3.8.1",
"description": "Classic theme for Docusaurus",
"main": "lib/index.js",
"types": "src/theme-classic.d.ts",
@ -20,26 +20,26 @@
"copy:watch": "node ../../admin/scripts/copyUntypedFiles.js --watch"
},
"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/plugin-content-blog": "3.8.0",
"@docusaurus/plugin-content-docs": "3.8.0",
"@docusaurus/plugin-content-pages": "3.8.0",
"@docusaurus/theme-common": "3.8.0",
"@docusaurus/theme-translations": "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/plugin-content-blog": "3.8.1",
"@docusaurus/plugin-content-docs": "3.8.1",
"@docusaurus/plugin-content-pages": "3.8.1",
"@docusaurus/theme-common": "3.8.1",
"@docusaurus/theme-translations": "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",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
"infima": "0.2.0-alpha.45",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"postcss": "^8.4.26",
"postcss": "^8.5.4",
"prism-react-renderer": "^2.3.0",
"prismjs": "^1.29.0",
"react-router-dom": "^5.3.4",

View file

@ -1040,7 +1040,9 @@ declare module '@theme/SkipToContent' {
declare module '@theme/MDXComponents/A' {
import type {ComponentProps, ReactNode} from 'react';
export interface Props extends ComponentProps<'a'> {}
export interface Props extends ComponentProps<'a'> {
'data-footnote-ref'?: true;
}
export default function MDXA(props: Props): ReactNode;
}

View file

@ -188,7 +188,11 @@ export default function DocSidebarItemCategory({
? (e) => {
onItemClick?.(item);
if (href) {
if (isActive) {
// When already on the category's page, we collapse it
// We don't use "isActive" because it would collapse the
// category even when we browse a children element
// See https://github.com/facebook/docusaurus/issues/11213
if (isCurrentPage) {
e.preventDefault();
updateCollapsed();
} else {

Some files were not shown because too many files have changed in this diff Show more