mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-21 10:37:51 +02:00
Merge branch 'main' into slorber/fix-docs-category-index-translation-key-conflict
This commit is contained in:
commit
13828934b4
252 changed files with 10021 additions and 8162 deletions
|
@ -21,6 +21,7 @@
|
|||
],
|
||||
"ignorePaths": [
|
||||
"CHANGELOG.md",
|
||||
"CHANGELOG-v*.md",
|
||||
"patches",
|
||||
"packages/docusaurus-theme-translations/locales",
|
||||
"packages/docusaurus-plugin-ideal-image/src/theme/IdealImageLegacy",
|
||||
|
|
4
.github/workflows/lighthouse-report.yml
vendored
4
.github/workflows/lighthouse-report.yml
vendored
|
@ -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 }}
|
||||
|
|
2
.github/workflows/lint-autofix.yml
vendored
2
.github/workflows/lint-autofix.yml
vendored
|
@ -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'
|
||||
|
|
42
.github/workflows/tests-e2e.yml
vendored
42
.github/workflows/tests-e2e.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -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
6757
CHANGELOG-v2.md
Normal file
File diff suppressed because it is too large
Load diff
6836
CHANGELOG.md
6836
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "argos",
|
||||
"version": "3.8.0",
|
||||
"version": "3.8.1",
|
||||
"description": "Argos visual diff tests",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "3.8.0",
|
||||
"version": "3.8.1",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"useNx": false,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1 +0,0 @@
|
|||

|
|
@ -1 +0,0 @@
|
|||

|
|
@ -1 +0,0 @@
|
|||

|
|
@ -1 +0,0 @@
|
|||
![img]()
|
|
@ -1 +0,0 @@
|
|||

|
|
@ -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`] = `
|
||||
"
|
||||
"
|
||||
|
|
|
@ -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(
|
||||
``,
|
||||
);
|
||||
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(``);
|
||||
expect(result).toMatchSnapshot();
|
||||
expect(errorMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('onBrokenMarkdownImages', () => {
|
||||
const fixtures = {
|
||||
doesNotExistAbsolute: ``,
|
||||
doesNotExistRelative: ``,
|
||||
doesNotExistSiteAlias: ``,
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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(`
|
||||
"
|
||||
"
|
||||
`);
|
||||
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": "",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>[] = [];
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
[asset]()
|
|
@ -1 +0,0 @@
|
|||
[nonexistent](@site/foo.pdf)
|
|
@ -1 +0,0 @@
|
|||
[asset](pathname:///asset/unchecked.pdf)
|
|
@ -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>"
|
||||
`;
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>[] = [];
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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})` : ''}`;
|
||||
}
|
||||
|
|
|
@ -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": "*",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -22,5 +22,5 @@ export {
|
|||
getDefaultVersionBanner,
|
||||
getVersionBadge,
|
||||
getVersionBanner,
|
||||
} from './versions';
|
||||
} from './versions/version';
|
||||
export {readVersionNames} from './versions/files';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -31,6 +31,8 @@ describe('postProcess', () => {
|
|||
type: 'doc',
|
||||
id: 'doc ID',
|
||||
},
|
||||
className: 'category-className',
|
||||
customProps: {custom: true},
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
# no id but should conflict due to the name anyway
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,3 @@
|
|||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,3 @@
|
|||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,3 @@
|
|||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,3 @@
|
|||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc-1
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc-2
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
id: doc-3
|
||||
---
|
||||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -0,0 +1,3 @@
|
|||
[
|
||||
"with-id-conflicts"
|
||||
]
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Hello
|
||||
|
||||
World
|
|
@ -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",
|
||||
}
|
||||
`;
|
|
@ -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.
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 {
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue